"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.resizeDoc = exports.Dropper = exports.scheduleCallback = exports.updateTraining = exports.updateTrainingGraphs = exports.Cursor = exports.Entity = exports.freeCamera = exports.resetMouseListeners = exports.removeMouseListener = exports.mouseListeners = exports.Pointer = exports.Effects = exports.PhysicsParticle = exports.sync = exports.addCameraImpulse = exports.getFrames = exports.getPollTicks = exports.resetTime = exports.loseFrames = exports.setMaxBuffer = exports.getMaxBuffer = void 0;
const gl_matrix_1 = require("gl-matrix");
const open = require("open");
const perf_hooks_1 = require("perf_hooks");
const animation_1 = require("./animation");
const audio_1 = require("./audio");
const collision_1 = require("./collision");
const color_1 = require("./color");
const controllers_1 = require("./controllers");
const easing_1 = require("./easing");
const engine_1 = require("./engine");
const fsutils_1 = require("./fsutils");
const GFX = require("./gfx");
const i18n_1 = require("./i18n");
const math_1 = require("./math");
const model_1 = require("./model");
const Native = require("./native");
const networking_1 = require("./networking");
const PCB = require("./pcb");
const stage_1 = require("./stage");
const terminal_1 = require("./terminal");
const utils_1 = require("./utils");
let constants = null;
const characters = ['Silicon', 'Iron', 'Xenon', 'Helium', 'Carbon', 'Rhodium'];
let ctx = null;
let gfx = null;
let screenWidth = 0;
let screenHeight = 0;
const windowSize = gl_matrix_1.vec2.create();
let gamePaused = false;
let ticks = 0;
let frames = 0;
let pollTicks = 0;
let renderTime = 0.0;
let lostFrames = 0;
let maxBuffer = 1;
const setActiveMode = (mode, data) => {
    activeMode && exports.scheduleCallback(() => {
        initCamera();
        stageNeutralCamera();
    });
    savedState.entities.length = 0;
    engine_1.setActiveMode(mode, data);
};
const getMaxBuffer = () => maxBuffer;
exports.getMaxBuffer = getMaxBuffer;
const setMaxBuffer = (polls) => {
    maxBuffer = polls;
};
exports.setMaxBuffer = setMaxBuffer;
const loseFrames = (f) => {
    lostFrames = lostFrames + f;
};
exports.loseFrames = loseFrames;
let exitGame = false;
let activeUI = null;
let activeNode = null;
const resetTime = () => {
    if (pollTicks - frames > 0) {
        exports.loseFrames(pollTicks - frames);
    }
    pollTicks = frames;
    controllers_1.resetConnected();
};
exports.resetTime = resetTime;
const getPollTicks = () => pollTicks;
exports.getPollTicks = getPollTicks;
const getFrames = () => frames;
exports.getFrames = getFrames;
const menuPrefabData = {
    models: [
        {
            "name": "I_Icosphere",
            "size": [1500, 1500, 1500],
            "position": [0, 200, 0],
            "rotation": { "pitch": 0, "yaw": 0.5, "roll": -0.15 },
            "material": {
                "file": "longboat_shell.mtl",
                "name": "Material",
                "recolor": {
                    "name": "orange",
                    "rgba": [0.5, 0.25, 0.05, 1]
                }
            }
        }
    ]
};
const stageNeutralCamera = () => {
    cameraPan[0] = 0;
    cameraPan[1] = -100;
    cameraPan[2] = 500;
    gl_matrix_1.mat4.targetTo(view, gl_matrix_1.vec3.fromValues(cameraPan[0], cameraPan[1], cameraPan[2]), gl_matrix_1.vec3.fromValues(0, -200, 0), upVector);
    gl_matrix_1.mat4.invert(iview, view);
    gl_matrix_1.vec3.set(cameraVector, view[8], view[9], view[10]);
    gl_matrix_1.vec3.set(cameraPosition, iview[12], iview[13], iview[14]);
    audio_1.setPosition(cameraPan[0], cameraPan[1], cameraPan[2]);
    gl_matrix_1.mat4.multiply(combined, perspective, view);
};
const setActiveNode = (node) => {
    if (activeNode !== null) {
        activeNode.box.class.delete('is:active');
        activeNode.box.resolveStyle();
    }
    activeNode = node;
    node.box.class.add('is:active');
    node.box.resolveStyle();
    node.events.emit('active');
};
utils_1.Sync.loading.promise.then(() => {
    engine_1.getConstants(c => {
        constants = c;
    });
    initGame();
    Native.quitting.then(() => {
        exitGame = true;
    });
});
const perspective = gl_matrix_1.mat4.create();
const view = gl_matrix_1.mat4.create();
const combined = gl_matrix_1.mat4.create();
const screenToUI = gl_matrix_1.mat4.create();
let worldTransform = combined;
const cameraPan = gl_matrix_1.vec3.create();
const upVector = gl_matrix_1.vec3.fromValues(0, -1, 0);
const inVector = gl_matrix_1.vec3.fromValues(0, 0, -1);
const uiView = gl_matrix_1.mat4.create();
const degree = Math.PI / 180;
const cameraVelocity = gl_matrix_1.vec3.create();
const cameraShake = gl_matrix_1.vec3.create();
const cameraSpringiness = gl_matrix_1.vec3.fromValues(-0.4, -0.4, -0.4);
const cameraFriction = gl_matrix_1.vec3.fromValues(0.7, 0.7, 0.7);
const cameraMaxSpeed = gl_matrix_1.vec3.fromValues(100, 100, 100);
const cameraMinSpeed = gl_matrix_1.vec3.negate(gl_matrix_1.vec3.create(), cameraMaxSpeed);
const cameraPosition = gl_matrix_1.vec3.create();
const cameraVector = gl_matrix_1.vec3.create();
const addCameraImpulse = (x, y, z) => {
    cameraVelocity[0] = cameraVelocity[0] + x;
    cameraVelocity[1] = cameraVelocity[1] + y;
    cameraVelocity[2] = cameraVelocity[2] + z;
};
exports.addCameraImpulse = addCameraImpulse;
const initCamera = () => {
    cameraPan[0] = 0;
    cameraPan[1] = 100;
    cameraPan[2] = 500;
    cameraShake[0] = 0;
    cameraShake[1] = 0;
    cameraShake[2] = 0;
    gl_matrix_1.mat4.targetTo(view, gl_matrix_1.vec3.fromValues(0, 100, 500), gl_matrix_1.vec3.fromValues(0, 0, 0), upVector);
    resizeCamera();
    updateCamera();
};
const cameraMath = gl_matrix_1.vec3.create();
const cameraPhysics = () => {
    gl_matrix_1.vec3.mul(cameraMath, cameraShake, cameraSpringiness);
    gl_matrix_1.vec3.add(cameraVelocity, cameraVelocity, cameraMath);
    gl_matrix_1.vec3.mul(cameraVelocity, cameraVelocity, cameraFriction);
    gl_matrix_1.vec3.min(cameraVelocity, cameraVelocity, cameraMaxSpeed);
    gl_matrix_1.vec3.max(cameraVelocity, cameraVelocity, cameraMinSpeed);
    gl_matrix_1.vec3.add(cameraShake, cameraShake, cameraVelocity);
};
const resizeCamera = () => {
    gl_matrix_1.mat4.identity(uiView);
    gl_matrix_1.mat4.translate(uiView, uiView, gl_matrix_1.vec3.fromValues(-screenWidth / 2, -screenHeight / 2, 0));
    gl_matrix_1.mat4.invert(screenToUI, uiView);
    gl_matrix_1.mat4.identity(uiView);
    gl_matrix_1.mat4.scale(uiView, uiView, gl_matrix_1.vec3.fromValues((1 / screenWidth) * 2, (-1 / screenHeight) * 2, 0.0001));
    gl_matrix_1.mat4.translate(uiView, uiView, gl_matrix_1.vec3.fromValues(-screenWidth / 2, -screenHeight / 2, 0));
    gl_matrix_1.mat4.perspective(perspective, degree * 60, screenWidth / screenHeight, 0.01, 1000);
    gl_matrix_1.mat4.scale(perspective, perspective, gl_matrix_1.vec3.fromValues(-2 / screenHeight, 2 / screenHeight, -2 / screenHeight));
    exports.resizeDoc(false);
};
const updateCamera = () => {
    view[12] = cameraPan[0] + cameraShake[0] * cameraPan[2] * -0.004;
    view[13] = cameraPan[1] + cameraShake[1] * cameraPan[2] * -0.004;
    view[14] = cameraPan[2] + cameraShake[2] * cameraPan[2] * -0.004;
    gl_matrix_1.mat4.invert(iview, view);
    gl_matrix_1.vec3.set(cameraVector, view[8], view[9], view[10]);
    gl_matrix_1.vec3.set(cameraPosition, iview[12], iview[13], iview[14]);
    audio_1.setPosition(cameraPan[0], cameraPan[1], cameraPan[2]);
    gl_matrix_1.mat4.multiply(combined, perspective, view);
};
let stage = null;
let activeMode = null;
let menuDoc = null;
const sync = (_stage, _activeMode) => {
    stage = _stage;
    activeMode = _activeMode;
    activeUI = null;
    activeNode = null;
    docKey[0] = 0;
    docKey[1] = 0;
    lastMove = -1;
    startedMoving = -1;
    gamePaused = false;
};
exports.sync = sync;
const sphereModel = model_1.Model.build({
    name: 'Icosphere',
});
const lightData = gl_matrix_1.vec4.create();
const drawPointLight = (pos, c, fadeIn = 0.8, fadeOut = 1.0, shine = 1.0, noiseRatio = 1.0, pipeline = gfx.pointLights) => {
    const radius = Math.sqrt(c[3] * 254);
    gl_matrix_1.vec4.set(lightData, pos[0], pos[1], pos[2], radius);
    pipeline.lights.vec4(lightData);
    gl_matrix_1.vec4.set(lightData, fadeIn, fadeOut, shine, noiseRatio);
    pipeline.props.vec4(lightData);
    pipeline.colors(c, c, sphereModel.material.specularColor);
};
const gemPrefab = model_1.Prefab.build({
    models: [
        {
            name: 'Center',
            alias: 'center',
        },
        {
            name: 'Side',
            alias: 'side1',
        },
        {
            name: 'Side',
            alias: 'side2',
        },
    ],
});
const gemMaterial = new model_1.Material();
gemMaterial.ambientColor = gemMaterial.diffuseColor;
for (let i = 0; i < gemPrefab.models.length; i++) {
    gemPrefab.models[i].material = gemMaterial;
}
const footPrefab = model_1.Prefab.build({
    models: [
        {
            name: 'Foot',
        },
    ],
});
footPrefab.models[0].material = gemMaterial;
const pi = Math.PI;
const globalQuat = gl_matrix_1.quat.create();
const identityQuat = gl_matrix_1.quat.create();
const transVec = gl_matrix_1.vec3.create();
const scaleVec = gl_matrix_1.vec3.create();
const drawGem = (x, y, x2, y2, r, z, left, invert, color1, _color2, prefab = gemPrefab, rotate = true) => {
    const rad = math_1.computeRadians(x2 - x, y2 - y);
    if (prefab.names.has('side1')) {
        const gemSide1 = prefab.names.get('side1');
        gemSide1.dirty = true;
        gl_matrix_1.vec4.copy(gemSide1.material.ambientColor, color1);
        gl_matrix_1.vec3.set(transVec, x2, y2, z * left);
        if (rotate) {
            gl_matrix_1.quat.rotateZ(globalQuat, identityQuat, 0.5 * pi + rad);
        }
        else {
            gl_matrix_1.quat.identity(globalQuat);
        }
        gl_matrix_1.vec3.set(scaleVec, -r * invert, r, r * invert);
        gl_matrix_1.mat4.fromRotationTranslationScale(gemSide1.transform, globalQuat, transVec, scaleVec);
        gl_matrix_1.mat4.mul(gemSide1.transform, gemSide1.transform, gemSide1.rootTransform);
    }
    if (prefab.names.has('side2')) {
        const gemSide2 = prefab.names.get('side2');
        gemSide2.dirty = true;
        gl_matrix_1.vec4.copy(gemSide2.material.ambientColor, color1);
        if (rotate) {
            gl_matrix_1.quat.rotateZ(globalQuat, identityQuat, 1.5 * pi + rad);
        }
        else {
            gl_matrix_1.quat.rotateZ(globalQuat, identityQuat, 1 * pi);
        }
        gl_matrix_1.vec3.set(transVec, x, y, z * left);
        gl_matrix_1.vec3.set(scaleVec, -r * invert, r, r * invert);
        gl_matrix_1.mat4.fromRotationTranslationScale(gemSide2.transform, globalQuat, transVec, scaleVec);
        gl_matrix_1.mat4.mul(gemSide2.transform, gemSide2.transform, gemSide2.rootTransform);
    }
    if (prefab.names.has('center')) {
        const gemCenter = prefab.names.get('center');
        gemCenter.dirty = true;
        if (rotate) {
            gl_matrix_1.quat.rotateZ(globalQuat, identityQuat, 0.5 * pi + rad);
        }
        else {
            gl_matrix_1.quat.identity(globalQuat);
        }
        gl_matrix_1.vec3.set(transVec, x + (x2 - x) * 0.5, y + (y2 - y) * 0.5, z * left);
        gl_matrix_1.vec3.set(scaleVec, -r * invert, -0.5 * Math.sqrt((x2 - x) ** 2 + (y2 - y) ** 2), -r * invert);
        gl_matrix_1.vec4.copy(gemCenter.material.ambientColor, color1);
        gl_matrix_1.mat4.fromRotationTranslationScale(gemCenter.transform, globalQuat, transVec, scaleVec);
        gl_matrix_1.mat4.mul(gemCenter.transform, gemCenter.transform, gemCenter.rootTransform);
    }
    renderPrefab(prefab, color1[3] >= 1 ? gfx.solid : gfx.transparent);
};
const setQuatToRadians = (q, p, y, r) => {
    gl_matrix_1.quat.identity(q);
    gl_matrix_1.quat.rotateX(q, q, p);
    gl_matrix_1.quat.rotateY(q, q, y);
    gl_matrix_1.quat.rotateZ(q, q, r);
    return q;
};
const working = gl_matrix_1.vec3.create();
const screenScale = gl_matrix_1.vec3.create();
const screenScale4 = gl_matrix_1.vec4.create();
const point = (x, y) => {
    working[0] = x;
    working[1] = y;
    working[2] = 0;
    gl_matrix_1.vec3.transformMat4(working, working, worldTransform);
    gl_matrix_1.vec3.multiply(working, working, screenScale);
};
const pointV = (out, v) => {
    v[2] = 0;
    gl_matrix_1.vec3.transformMat4(v, v, worldTransform);
    gl_matrix_1.vec3.multiply(v, v, screenScale);
    return gl_matrix_1.vec2.set(out, v[0], v[1]);
};
const point3D = (x, y, z) => {
    gl_matrix_1.vec3.set(working, x, y, z);
    gl_matrix_1.vec3.transformMat4(working, working, worldTransform);
    gl_matrix_1.vec3.multiply(working, working, screenScale);
};
const pathCapsule = (x, y, x2, y2, r, angles = 4) => {
    const rads = 2 * Math.PI - Math.atan2(x2 - x, y2 - y);
    let perp = rads - Math.PI;
    const step = (1 / angles) * Math.PI;
    point(x + Math.cos(perp) * r, y + Math.sin(perp) * r);
    ctx.moveTo(working[0], working[1]);
    for (let i = 0; i < angles; i++) {
        perp = perp + step;
        point(x + Math.cos(perp) * r, y + Math.sin(perp) * r);
        ctx.lineTo(working[0], working[1]);
    }
    perp = rads + Math.PI * 2;
    for (let i = 0; i < angles + 1; i++) {
        point(x2 + Math.cos(perp) * r, y2 + Math.sin(perp) * r);
        ctx.lineTo(working[0], working[1]);
        perp = perp + step;
    }
    ctx.closePath();
};
const diamond = [
    gl_matrix_1.vec4.fromValues(0, 1, 0, 0),
    gl_matrix_1.vec4.fromValues(1, 0, 0, 0),
    gl_matrix_1.vec4.fromValues(0, -1, 0, 0),
    gl_matrix_1.vec4.fromValues(-1, 0, 0, 0),
];
const transformedDiamond = [
    gl_matrix_1.vec4.create(),
    gl_matrix_1.vec4.create(),
    gl_matrix_1.vec4.create(),
    gl_matrix_1.vec4.create(),
];
const diamondPos = gl_matrix_1.vec4.create();
const diamondNormal = gl_matrix_1.vec3.create();
const diamondVelocity = gl_matrix_1.vec4.create();
const drawDiamond = (x, y, z, w, h, dx, dy, dz, color, q) => {
    let min = Infinity;
    let max = -Infinity;
    gl_matrix_1.vec4.set(diamondPos, x, y, z, 0);
    gl_matrix_1.vec4.set(diamondVelocity, -dx, -dy, -dz, 0);
    diamond[0][1] = h;
    diamond[1][0] = w;
    diamond[2][1] = -h;
    diamond[3][0] = -w;
    for (let i = 0; i < 4; i++) {
        const vec = diamond[i];
        const out = transformedDiamond[i];
        const qx = q[0];
        const qy = q[1];
        const qz = q[2];
        const w2 = q[3] * 2;
        const ax = vec[0];
        const ay = vec[1];
        const az = vec[2];
        const uvx = qy * az - qz * ay;
        const uvy = qz * ax - qx * az;
        const uvz = qx * ay - qy * ax;
        const uuvx = (qy * uvz - qz * uvy) * 2;
        const uuvy = (qz * uvx - qx * uvz) * 2;
        const uuvz = (qx * uvy - qy * uvx) * 2;
        let d = 0;
        out[0] = ax + uvx * w2 + uuvx;
        out[1] = ay + uvy * w2 + uuvy;
        out[2] = az + uvz * w2 + uuvz;
        d = gl_matrix_1.vec4.dot(out, diamondVelocity);
        if (d < min) {
            min = d;
        }
        if (d > max) {
            max = d;
        }
    }
    max = 1 / (max - min);
    for (let i = 0; i < transformedDiamond.length; i++) {
        const d = gl_matrix_1.vec4.dot(transformedDiamond[i], diamondVelocity) * max;
        transformedDiamond[i][0]
            = transformedDiamond[i][0] + diamondVelocity[0] * d + diamondPos[0];
        transformedDiamond[i][1]
            = transformedDiamond[i][1] + diamondVelocity[1] * d + diamondPos[1];
        transformedDiamond[i][2]
            = transformedDiamond[i][2] + diamondVelocity[2] * d + diamondPos[2];
    }
    gfx.unculled.tris.vec3of4(transformedDiamond[0]);
    gfx.unculled.tris.vec3of4(transformedDiamond[1]);
    gfx.unculled.tris.vec3of4(transformedDiamond[2]);
    gfx.unculled.tris.vec3of4(transformedDiamond[2]);
    gfx.unculled.tris.vec3of4(transformedDiamond[3]);
    gfx.unculled.tris.vec3of4(transformedDiamond[0]);
    gfx.unculled.color.vec4x3(color);
    gfx.unculled.color.vec4x3(color);
    gl_matrix_1.vec3.transformQuat(diamondNormal, inVector, q);
    gfx.unculled.normals.vec3x3(diamondNormal);
    gfx.unculled.normals.vec3x3(diamondNormal);
};
const strokeCapsule = (x, y, x2, y2, r) => {
    pathCapsule(x, y, x2, y2, r);
    ctx.stroke();
};
const pathCircle = (x, y, r, angles = 6, perp = -Math.PI) => {
    const step = (1 / angles) * Math.PI * 2;
    point(x + Math.cos(perp) * r, y + Math.sin(perp) * r);
    ctx.moveTo(working[0], working[1]);
    for (let i = 0; i < angles; i++) {
        perp = perp + step;
        point(x + Math.cos(perp) * r, y + Math.sin(perp) * r);
        ctx.lineTo(working[0], working[1]);
    }
    ctx.closePath();
};
const strokeCircle = (x, y, radius) => {
    pathCircle(x, y, radius);
    ctx.stroke();
};
const fillCircle = (x, y, radius) => {
    pathCircle(x, y, radius);
    ctx.fill();
};
const drawCircle = (x, y, radius) => {
    pathCircle(x, y, radius);
    ctx.fill();
    ctx.stroke();
};
const v1 = gl_matrix_1.vec4.fromValues(0, 0, 0, 1);
const v2 = gl_matrix_1.vec4.fromValues(0, 0, 0, 1);
const v3 = gl_matrix_1.vec4.fromValues(0, 0, 0, 1);
const iview = gl_matrix_1.mat4.create();
const itT = gl_matrix_1.mat4.create();
const normal = gl_matrix_1.vec3.create();
const renderModel = (model, pipeline, t = model.transform, _material = model.material) => {
    const tris = model.tris;
    const verts = model.verts;
    if (!model.dirty && model.index === pipeline.index()) {
        pipeline.skip(tris.length);
        return;
    }
    model.index = pipeline.index();
    model.dirty = false;
    gl_matrix_1.mat4.invert(itT, t);
    gl_matrix_1.mat4.transpose(itT, itT);
    for (let i = 0; i < tris.length; i = i + 3) {
        const n = model.surfaces[(i / 3) | 0] * 3;
        const m = model.material;
        let tri = 0;
        normal[0] = model.normals[n];
        normal[1] = model.normals[n + 1];
        normal[2] = model.normals[n + 2];
        tri = tris[i] * 3;
        v1[0] = verts[tri];
        v1[1] = verts[tri + 1];
        v1[2] = verts[tri + 2];
        tri = tris[i + 1] * 3;
        v2[0] = verts[tri];
        v2[1] = verts[tri + 1];
        v2[2] = verts[tri + 2];
        tri = tris[i + 2] * 3;
        v3[0] = verts[tri];
        v3[1] = verts[tri + 1];
        v3[2] = verts[tri + 2];
        pipeline.transformTri(v1, v2, v3, normal, t, itT);
        pipeline.colors(m.diffuseColor, m.ambientColor, m.specularColor);
    }
};
const renderPrefab = (prefab, pipeline) => {
    for (let i = 0; i < prefab.models.length; i++) {
        renderModel(prefab.models[i], pipeline);
    }
};
const drawLine = (x, y, x2, y2) => {
    point(x, y);
    ctx.moveTo(working[0], working[1]);
    point(x2, y2);
    ctx.lineTo(working[0], working[1]);
    ctx.stroke();
};
const drawLineZ = (x, y, z, x2, y2, z2) => {
    point3D(x, y, z);
    ctx.moveTo(working[0], working[1]);
    point3D(x2, y2, z2);
    ctx.lineTo(working[0], working[1]);
    ctx.stroke();
};
const drawPlus = (x, y, z, radius) => {
    point3D(x - radius, y, z);
    ctx.moveTo(working[0], working[1]);
    point3D(x + radius, y, z);
    ctx.lineTo(working[0], working[1]);
    point3D(x, y - radius, z);
    ctx.stroke();
    ctx.moveTo(working[0], working[1]);
    point3D(x, y + radius, z);
    ctx.lineTo(working[0], working[1]);
    ctx.stroke();
};
const pathLines = (v) => {
    point(v[0], v[1]);
    ctx.moveTo(working[0], working[1]);
    for (let i = 2; i < v.length; i = i + 2) {
        point(v[i], v[i + 1]);
        ctx.lineTo(working[0], working[1]);
    }
};
const pathPolygonZ = (v, z) => {
    point3D(v[v.length - 2], v[v.length - 1], z);
    ctx.moveTo(working[0], working[1]);
    for (let i = 0; i < v.length; i = i + 2) {
        point3D(v[i], v[i + 1], z);
        ctx.lineTo(working[0], working[1]);
    }
};
const pathPolygon = (...v) => {
    const z = 0;
    point3D(v[v.length - 2], v[v.length - 1], z);
    ctx.moveTo(working[0], working[1]);
    for (let i = 0; i < v.length; i = i + 2) {
        point3D(v[i], v[i + 1], z);
        ctx.lineTo(working[0], working[1]);
    }
};
const rotPolygon = (n, ...v) => {
    const z = 0;
    point3D(v[v.length - 2] * n[0] - v[v.length - 1] * n[0], v[v.length - 1] * n[1] - v[v.length - 2] * n[1], z);
    ctx.moveTo(working[0], working[1]);
    for (let i = 0; i < v.length; i = i + 2) {
        point3D(v[i] * n[0] - v[i] * n[1], v[i + 1], z);
        ctx.lineTo(working[0], working[1]);
    }
};
const pathRect = (x, y, w, h) => {
    point(x, y);
    ctx.moveTo(working[0], working[1]);
    point(x + w, y);
    ctx.lineTo(working[0], working[1]);
    point(x + w, y + h);
    ctx.lineTo(working[0], working[1]);
    point(x, y + h);
    ctx.lineTo(working[0], working[1]);
    ctx.closePath();
};
const pv3 = gl_matrix_1.vec3.create();
const pv3b = gl_matrix_1.vec3.create();
const pv2 = gl_matrix_1.vec2.create();
const pv2b = gl_matrix_1.vec2.create();
const strokeRect = (x, y, w, h) => {
    const z = 0;
    ctx.strokeLine(pointV(pv2, gl_matrix_1.vec3.set(pv3, x - ctx._lineWidth, y, z)), pointV(pv2b, gl_matrix_1.vec3.set(pv3b, x + w + ctx._lineWidth, y, z)), 0);
    ctx.strokeLine(pointV(pv2b, gl_matrix_1.vec3.set(pv3b, x + w, y, z)), pointV(pv2, gl_matrix_1.vec3.set(pv3, x + w, y + h, z)), 0);
    ctx.strokeLine(pointV(pv2, gl_matrix_1.vec3.set(pv3, x + w + ctx._lineWidth, y + h, z)), pointV(pv2b, gl_matrix_1.vec3.set(pv3b, x - ctx._lineWidth, y + h, z)), 0);
    ctx.strokeLine(pointV(pv2b, gl_matrix_1.vec3.set(pv3b, x, y + h, z)), pointV(pv2, gl_matrix_1.vec3.set(pv3, x, y, z)), 0);
};
const fillRect = (x, y, w, h) => {
    point(x, y);
    ctx.moveTo(working[0], working[1]);
    point(x + w, y);
    ctx.lineTo(working[0], working[1]);
    point(x + w, y + h);
    ctx.lineTo(working[0], working[1]);
    point(x, y + h);
    ctx.lineTo(working[0], working[1]);
    ctx.closePath();
    ctx.fill();
};
const baseFill = gl_matrix_1.vec4.create();
const fillEmboss = (x, y, w, h, b) => {
    gl_matrix_1.vec4.copy(baseFill, ctx._fillRGBA);
    fillRect(x + b, y + b, w - b * 2, h - b * 2);
    point(x + b, y + b);
    ctx.moveTo(working[0], working[1]);
    point(x + w, y);
    ctx.lineTo(working[0], working[1]);
    point(x + w, y + h);
    ctx.lineTo(working[0], working[1]);
    point(x, y + h);
    ctx.lineTo(working[0], working[1]);
    point(x, y);
    ctx.lineTo(working[0], working[1]);
    ctx.fill();
    ctx.fillVec4(baseFill);
};
const fillText = (text, x, y, w = 0) => {
    point(x, y);
    ctx.fillText(text, working[0], working[1], w);
};
const strokeText = (text, x, y, w = 0) => {
    point(x, y);
    ctx.strokeText(text, working[0], working[1], w);
};
const paintText = (text, x, y, w = 0) => {
    point(x, y);
    ctx.drawText(text, working[0], working[1], w);
};
let fontSize = 10;
const setFontSize = (size) => {
    fontSize = size;
    ctx.fontSize(size);
};
const setMonoSize = (size) => {
    fontSize = size;
    ctx.style(1);
    ctx.fontSize(size);
};
const setSansSize = (size) => {
    fontSize = size;
    ctx.style(2);
    ctx.fontSize(size);
};
const timeString = (() => {
    let cacheFrame = -1;
    let cache = '';
    return (frames) => {
        if (cacheFrame !== ((frames / 60) | 0)) {
            cacheFrame = (frames / 60) | 0;
            cache = `${(frames / 3600) | 0}${frames % 3600 >= 600 ? ':' : ':0'}${((frames % 3600) / 60) | 0}`;
        }
        return cache;
    };
})();
const hitSparkColor = gl_matrix_1.vec4.fromValues(1.0, 0.95, 0.25, 0.8);
const dustColor = new color_1.Color(gl_matrix_1.vec4.fromValues(0, 0.3, 1, 0.4));
const smokeColor = new color_1.Color(gl_matrix_1.vec4.fromValues(0, 0.5, 1, 0.2));
const uiTextColor = new color_1.Color(gl_matrix_1.vec4.fromValues(0, 0, 1, 1));
class PhysicsParticle {
    constructor() {
        this.removed = false;
        this.startFrame = 0;
        this.duration = 0;
        this.gravity = 0;
        this.mass = 1;
        this.x = 0;
        this.y = 0;
        this.z = 0;
        this.w = 0;
        this.h = 0;
        this.dx = 0;
        this.dy = 0;
        this.dz = 0;
        this.r = 0;
        this.c = gl_matrix_1.vec4.create();
        this.a = 0;
        this.glow = 1;
        this.id = 0;
        this.rotation = gl_matrix_1.quat.create();
        this.spin = gl_matrix_1.quat.create();
        this.draw = 1;
        this.fadeIn = 0.85;
        this.fadeOut = 1.0;
        this.shine = 1.5;
        this.noiseRatio = 0.7;
    }
}
exports.PhysicsParticle = PhysicsParticle;
const parCol = gl_matrix_1.vec4.create();
const parPos = gl_matrix_1.vec3.create();
const upQuat = gl_matrix_1.quat.create();
const frontQuat = gl_matrix_1.quat.rotateX(gl_matrix_1.quat.create(), upQuat, Math.PI * 0.5);
exports.Effects = (() => {
    const physicsParticles = [];
    let physicsParticlesCount = 0;
    const physicsParticlesRemoved = [];
    let physicsParticlesRemovedCount = 0;
    let particleID = 0;
    const airResistance = 0.98;
    const updraft = -0.01;
    const spinAmt = 0.1;
    const forcePos = gl_matrix_1.vec3.create();
    const particlePos = gl_matrix_1.vec3.create();
    const distance = gl_matrix_1.vec3.create();
    const vector = gl_matrix_1.vec3.create();
    const physicsImpulse = (x, y, z, r, force) => {
        for (let i = 0; i < physicsParticles.length; i++) {
            const p = physicsParticles[i];
            let dist = 0;
            let applied = 0;
            gl_matrix_1.vec3.set(particlePos, p.x, p.y, p.z);
            gl_matrix_1.vec3.set(forcePos, x, y, z);
            gl_matrix_1.vec3.sub(distance, particlePos, forcePos);
            dist = Math.sqrt(gl_matrix_1.vec3.dot(distance, distance));
            if (dist <= r + 1) {
                applied = force / 2 / (p.mass + 1);
            }
            else {
                applied = force / (dist - r + 2) / (p.mass + 1);
            }
            gl_matrix_1.vec3.normalize(vector, distance);
            if (applied > 0.001) {
                p.dx = p.dx + vector[0] * applied;
                p.dy = p.dy + vector[1] * applied;
                p.dz = p.dz + vector[2] * applied;
            }
        }
    };
    const addSparkParticle = (x, y, z, dx, dy, dz, r, mass, rgba, duration) => {
        const p = addPhysicsParticle(x, y, z, dx, dy, dz, r, mass, rgba, duration, 0);
        p.draw = math_1.lqrandom() < 0.25 ? 0b10 : 0b1;
        return p;
    };
    const nextPhysicsParticle = () => {
        let p = null;
        if (physicsParticlesRemovedCount > 0) {
            physicsParticlesRemovedCount = physicsParticlesRemovedCount - 1;
            p = physicsParticlesRemoved[physicsParticlesRemovedCount];
        }
        else {
            p = new PhysicsParticle();
        }
        p.id = particleID;
        physicsParticles[physicsParticlesCount] = p;
        physicsParticlesCount++;
        particleID++;
        p.draw = 1;
        p.glow = 1;
        p.fadeIn = 0.85;
        p.fadeOut = 1.0;
        p.shine = 1.5;
        p.noiseRatio = 0.7;
        return p;
    };
    const addPhysicsParticle = (x, y, z, dx, dy, dz, r, mass, rgba, duration, gravity) => {
        const p = nextPhysicsParticle();
        p.x = x;
        p.y = y;
        p.z = z;
        p.a = rgba[3];
        p.dx = dx;
        p.dy = dy;
        p.dz = dz;
        p.r = r;
        p.mass = mass;
        gl_matrix_1.vec4.copy(p.c, rgba);
        p.w = 0.5 + 3 * math_1.lqrandom() * r;
        p.h = 0.5 + 3 * math_1.lqrandom() * r;
        p.startFrame = ticks;
        p.gravity = -gravity;
        p.duration = duration;
        setQuatToRadians(p.rotation, math_1.lqrandom() * pi, math_1.lqrandom() * pi, math_1.lqrandom() * pi);
        setQuatToRadians(p.spin, math_1.lqrandom() * spinAmt, math_1.lqrandom() * spinAmt, math_1.lqrandom() * spinAmt);
        return p;
    };
    const addCharacterParticle = (x, y, z, dx, dy, dz, r, mass, durationScale, rgba) => {
        const gravity = updraft;
        const duration = ((44 + math_1.lqrandom() * 30) * durationScale) | 0;
        return addPhysicsParticle(x, y, z, dx, dy, dz, r, mass, rgba, duration, gravity);
    };
    const renderPhysicsParticles = () => {
        const startRender = perf_hooks_1.performance.now();
        for (let i = 0; i < physicsParticlesCount; i++) {
            const p = physicsParticles[i];
            const c = p.c;
            const f = (ticks + p.id * 27) % (28 + (p.id & 0x8f));
            gl_matrix_1.vec4.set(parCol, c[0], c[1], c[2], c[3]);
            if (f <= 11) {
                const ff = Math.abs(f - 6) / 6;
                parCol[3] = parCol[3] * ff;
            }
            if ((p.draw & 0b1) === 0b1) {
                drawDiamond(p.x, p.y, p.z, p.w, p.h, p.dx, p.dy, p.dz, parCol, p.rotation);
            }
            if ((p.draw & 0b10) === 0b10) {
                parCol[0] = parCol[0] * parCol[3];
                parCol[1] = parCol[1] * parCol[3];
                parCol[2] = parCol[2] * parCol[3];
                parCol[3] = parCol[3] * p.glow * 25;
                drawPointLight(gl_matrix_1.vec3.set(parPos, p.x, p.y, p.z), parCol, p.fadeIn, p.fadeOut, p.shine, p.noiseRatio);
            }
        }
        particleDrawTimes.push((perf_hooks_1.performance.now() - startRender) / physicsParticlesCount);
    };
    const v2out = gl_matrix_1.vec2.create();
    const direction = gl_matrix_1.vec2.create();
    const momentum = gl_matrix_1.vec2.create();
    const tickPhysicsParticles = () => {
        const startRender = perf_hooks_1.performance.now();
        let compact = 0;
        if (engine_1.dbg.freezePhysicsParticles) {
            return;
        }
        for (let i = 0; i < physicsParticlesCount; i++) {
            const p = physicsParticles[i];
            if (ticks - p.startFrame > p.duration) {
                physicsParticlesRemoved[physicsParticlesRemovedCount] = p;
                physicsParticlesRemovedCount++;
                compact++;
                continue;
            }
            if (compact > 0) {
                physicsParticles[i - compact] = p;
            }
            gl_matrix_1.quat.multiply(p.rotation, p.rotation, p.spin);
            gl_matrix_1.vec2.set(momentum, p.dx, p.dy);
            gl_matrix_1.vec2.normalize(direction, momentum);
            {
                let x2 = p.x + p.dx;
                let y2 = p.y + p.dy;
                const z2 = p.z + p.dz;
                const rad = ((1 - (ticks - p.startFrame) / p.duration) * 0.5 + 0.5) * p.r;
                const intersect = Math.abs(p.z) > 50
                    ? null
                    : stage.intersect(v2out, p.x, p.y, x2 + rad * direction[0] + momentum[0], y2 + rad * direction[1] + momentum[1], direction);
                const a = easing_1.Ease.quadInOut((ticks - p.startFrame) / p.duration);
                p.c[3] = p.a * (1 - a * a);
                if (intersect !== null) {
                    const mass = p.mass;
                    x2 = v2out[0] - intersect[0] * (rad + 0.01);
                    y2 = v2out[1] - intersect[1] * (rad + 0.01);
                    gl_matrix_1.vec2.sub(momentum, momentum, gl_matrix_1.vec2.scale(v2out, intersect, 2 * gl_matrix_1.vec2.dot(momentum, intersect)));
                    p.dx = momentum[0] / mass;
                    p.dy = momentum[1] / mass;
                    p.dz = p.dz / mass;
                    if (p.dx * p.dx + p.dy * p.dy < 2) {
                        setQuatToRadians(p.spin, 0, 0, 0);
                    }
                }
                p.x = x2;
                p.y = y2;
                p.z = z2;
                p.dx = p.dx * airResistance;
                p.dy = p.dy - p.gravity;
                p.dy = p.dy * airResistance;
                p.dz = p.dz * airResistance;
            }
        }
        physicsParticlesCount = physicsParticlesCount - compact;
        particleTickTimes.push((perf_hooks_1.performance.now() - startRender) / physicsParticlesCount);
    };
    const effects = [renderPhysicsParticles];
    const effectTicks = [tickPhysicsParticles];
    const top = [];
    const ui = [];
    const removed = [];
    const removeTicks = [];
    const removetop = [];
    const removeui = [];
    const vec2Zero = gl_matrix_1.vec2.fromValues(0, 0);
    const n2 = gl_matrix_1.vec2.create();
    const effectFunctions = {
        nextPhysicsParticle,
        addPhysicsParticle,
        addCharacterParticle,
        addSparkParticle,
        peek: () => effects[effects.length - 1],
        remove: (fn) => removed.push(fn),
        render: () => {
            if (removed.length > 0) {
                let index = 0;
                let r = 0;
                while (index < effects.length - r) {
                    if (r > 0) {
                        effects[index] = effects[index + r];
                    }
                    if (removed.includes(effects[index])) {
                        r++;
                    }
                    else {
                        index++;
                    }
                }
                effects.length = effects.length - r;
                removed.length = 0;
            }
            for (let i = effects.length - 1; i >= 0; i--) {
                effects[i]();
            }
        },
        tick: () => {
            for (let i = effectTicks.length - 1; i >= 0; i--) {
                effectTicks[i]();
            }
            if (removeTicks.length > 0) {
                let index = 0;
                let r = 0;
                while (index < effectTicks.length - r) {
                    if (r > 0) {
                        effectTicks[index] = effectTicks[index + r];
                    }
                    if (removeTicks.includes(effectTicks[index])) {
                        r++;
                    }
                    else {
                        index++;
                    }
                }
                effectTicks.length = effectTicks.length - r;
                removeTicks.length = 0;
            }
        },
        renderTop: () => {
            for (let i = top.length - 1; i >= 0; i--) {
                top[i]();
            }
            if (removetop.length > 0) {
                let index = 0;
                let r = 0;
                while (index < top.length - r) {
                    if (r > 0) {
                        top[index] = top[index + r];
                    }
                    if (removetop.includes(top[index])) {
                        r++;
                    }
                    else {
                        index++;
                    }
                }
                top.length = top.length - r;
                removetop.length = 0;
            }
        },
        renderUI: () => {
            for (let i = ui.length - 1; i >= 0; i--) {
                ui[i]();
            }
            if (removeui.length > 0) {
                let index = 0;
                let r = 0;
                while (index < ui.length - r) {
                    if (r > 0) {
                        ui[index] = ui[index + r];
                    }
                    if (removeui.includes(ui[index])) {
                        r++;
                    }
                    else {
                        index++;
                    }
                }
                ui.length = ui.length - r;
                removeui.length = 0;
            }
        },
        reset: () => {
            effects.length = 0;
            effects.push(renderPhysicsParticles);
            effectTicks.length = 0;
            effectTicks.push(tickPhysicsParticles);
            removeTicks.length = 0;
            removed.length = 0;
            top.length = 0;
            top.push();
            ui.length = 0;
            removetop.length = 0;
            removeui.length = 0;
            physicsParticlesCount = 0;
        },
        fadingText: (str, size, r, g, b, owner, y) => {
            const start = ticks;
            const duration = 40;
            const dist = 10;
            const f = () => {
                let life = 0;
                if (ticks - start > duration) {
                    removetop.push(f);
                    return;
                }
                life = ((ticks - start) / duration) ** 2;
                ctx.fillRGBA(r, g, b, 1 - life);
                setFontSize(size);
                ctx.strokeRGBA(0.13, 0.13, 0.13, 1 - life);
                paintText(str, owner.x - life * dist * 0.1, owner.y + y - life * dist);
            };
            top.push(f);
        },
        powershield: (entity, _x, _y, _r, gro) => {
            const duration = 6;
            const color = entity.palette.lighter[0];
            const start = ticks;
            const grow = gro / duration;
            const f = () => {
                if (ticks - start > duration) {
                    removed.push(f);
                }
                else {
                    ctx.strokeHSLA(color.hsla[0], color.hsla[1], color.hsla[2], 1 - (ticks - start) / duration);
                    strokeCapsule(entity.x + entity.shield.x, entity.y - entity.shield.y, entity.x + entity.shield.x2, entity.y - entity.shield.y2, entity.shield.powershield + grow * (ticks - start));
                }
            };
            effects.push(f);
        },
        countdown: (_size, _shrink, duration, endText) => {
            let startFrame = ticks;
            let n = duration;
            let t = n.toString(10);
            const f = () => {
                if (ticks - startFrame > 60) {
                    startFrame = ticks;
                    if (t === endText) {
                        removeui.push(f);
                    }
                    else {
                        n--;
                        if (n === 0) {
                            t = endText;
                            audio_1.playAudio('countdownend');
                        }
                        else {
                            t = n.toString(10);
                            audio_1.playAudio('countdownmid');
                        }
                    }
                }
                else {
                    ctx.strokeRGBA(0.2, 0.2, 0.2, 1);
                    ctx.fillRGBA(0.8, 0.8, 0.8, 1);
                    setFontSize(60);
                    ctx.textAlign(GFX.TextAlign.center);
                    paintText(t, screenWidth / 2, screenHeight / 2 + fontSize - 5);
                    ctx.textAlign(GFX.TextAlign.left);
                }
            };
            audio_1.playAudio('countdownmid');
            ui.push(f);
        },
        message: (size, shrink, duration, msg) => {
            const startFrame = ticks;
            const f = () => {
                if (ticks - startFrame > duration) {
                    removeui.push(f);
                }
                else {
                    ctx.strokeRGBA(0.2, 0.2, 0.2, 1);
                    ctx.fillRGBA(0.8, 0.8, 0.8, 1);
                    setFontSize(size - ((ticks - startFrame) / duration) * shrink);
                    ctx.textAlign(GFX.TextAlign.center);
                    paintText(msg, screenWidth / 2, screenHeight / 2 + fontSize - 5);
                    ctx.textAlign(GFX.TextAlign.left);
                }
            };
            ui.push(f);
        },
        damageShatter: (e) => {
            const dmg = Math.ceil(e.damage).toString(10);
            const damageSize = Math.min(e.damage * 0.1, 5);
            const textSize = Math.floor(40 + damageSize);
            const damageColor = (Math.max(0, e.damage - 40) * 3) / 255;
            const w = 90;
            const x = (screenWidth / engine_1.players.length) * (engine_1.players.indexOf(e) + 0.5)
                - w * 0.5
                + 40
                + 62;
            const y = screenHeight * 0.95 - damageSize * 0.5 - 20;
            const damageBrightness = 230;
            const r = (damageBrightness - damageColor * 0.5) | 0;
            const g = (damageBrightness - damageColor) | 0;
            const b = (damageBrightness - damageColor) | 0;
            let offset = 0;
            setFontSize(textSize);
            for (let i = dmg.length - 1; i >= 0; i--) {
                exports.Effects.textParticle(x - offset, y, 40 * math_1.lqrandom() - 20, 100 + math_1.lqrandom() * 30, -90 - math_1.lqrandom() * 30, textSize, textSize * 0.5 * math_1.lqrandom(), 30, dmg[i], r, g, b);
                offset = offset + ctx.measureText(dmg[i]);
            }
        },
        textParticle: (x, y, dx, ya, yb, size, shrink, duration, msg, r, g, b) => {
            const startFrame = ticks;
            const f = () => {
                if (ticks - startFrame > duration) {
                    removeui.push(f);
                }
                else {
                    const pct = (ticks - startFrame) / duration;
                    const ipct = 1 - pct;
                    const tx = x + dx * pct;
                    const ty = y + ya * (pct * pct) + yb * pct;
                    const oldFontSize = fontSize;
                    setFontSize(size - pct * shrink);
                    ctx.strokeRGBA(0.07, 0.07, 0.07, ipct);
                    ctx.fillRGBA(0.07, 0.07, 0.07, ipct * ipct);
                    ctx.textAlign(GFX.TextAlign.right);
                    paintText(msg, tx + 3, ty + 3);
                    ctx.fillRGBA(r, g, b, ipct);
                    paintText(msg, tx, ty);
                    ctx.textAlign(GFX.TextAlign.left);
                    setFontSize(oldFontSize);
                }
            };
            ui.push(f);
        },
        respawn: (entity) => {
            let startFrame = ticks;
            const duration = 90;
            const fadeTime = (duration * 0.5) | 0;
            const c = entity.palette.base;
            const animation = entity.animation;
            const f = () => {
                if (entity.animation !== animation || entity.removed) {
                    removed.push(f);
                }
                else {
                    const x = entity.x;
                    const y = entity.y;
                    const fr = ticks - startFrame;
                    const ease = fr < fadeTime ? fr / fadeTime : 1 - (fr - fadeTime) / fadeTime;
                    if (ticks - startFrame >= duration) {
                        startFrame = ticks;
                    }
                    drawPointLight(gl_matrix_1.vec3.set(parPos, x, y, 0), gl_matrix_1.vec4.set(parCol, c.rgba[0], c.rgba[1], c.rgba[2], ease * 40 + 10), 1.0, 1.0, 2.0, 0.9);
                    gl_matrix_1.quat.rotateZ(globalQuat, frontQuat, (fr / duration) * Math.PI);
                    parCol[3] = 1;
                    drawDiamond(x, y, 0, 30, 30, 0, 0, 0, parCol, globalQuat);
                }
            };
            effects.push(f);
        },
        combo: (x, y, dx, dy, r, angle, lag, color) => {
            const startFrame = ticks + lag;
            const duration = 9;
            const f = () => {
                if (ticks - startFrame > duration) {
                    removed.push(f);
                }
                else {
                    const t = easing_1.Ease.quadOut(Math.min(1, 1 - (ticks - startFrame) / duration));
                    const radius = (Math.abs(dx) + Math.abs(dy)) * 0.25 + 10;
                    ctx.strokeRGBA(1, 1, 0.5, 1);
                    pathCircle(x, y, t * radius, 6, Math.PI * t);
                    ctx._strokeRGBA[3] = t;
                    strokeCircle(x, y, radius);
                }
            };
            exports.Effects.hit(x, y, dx, dy, r, angle, lag, color);
            effects.push(f);
        },
        hit: (x, y, dx, dy, r, _angle, lag, color) => {
            const startFrame = ticks;
            const duration = 30;
            const sqdist = dx ** 2 + dy ** 2;
            const strLen = Math.sqrt(sqdist);
            const f = () => {
                const fr = ticks - startFrame;
                if (fr > duration) {
                    removed.push(f);
                }
                else {
                    const t = easing_1.Ease.quadOut(1 - fr / duration);
                    if (fr < lag) {
                        const pct = easing_1.Ease.quadOut(1.0 - fr / lag);
                        ctx.fillRGBA(1, 1, 1, ((fr + 4) / (lag + 4)) * 0.1);
                        pathCircle(x, y, t * r * 3, 6, (Math.PI * fr) / duration);
                        ctx.fill();
                        if (sqdist > 4) {
                            const len = Math.pow(sqdist, 0.5) * pct;
                            const srlen = Math.sqrt(len);
                            gl_matrix_1.vec2.normalize(n2, gl_matrix_1.vec2.set(n2, dx, dy));
                            ctx.fillRGBA(1, 1, 0.8, pct);
                            drawDiamond(x, y, 0, srlen, srlen, dx, dy, 0, ctx._fillRGBA, identityQuat);
                        }
                        if (sqdist > 11) {
                            const len = Math.pow(sqdist, 0.75) * pct;
                            const srlen = Math.pow(len, 0.35);
                            ctx.fillRGBA(1, 1, 0.8, pct);
                            pathPolygon(x - len, y, x, y - srlen, x + len, y, x, y + srlen);
                            ctx.fill();
                            ctx.pathRect(x, y, 100, 100);
                        }
                        gl_matrix_1.vec4.copy(parCol, color.rgba);
                        parCol[3] = 6 * pct;
                        drawPointLight(gl_matrix_1.vec3.set(parPos, x, y, 0), parCol, 0.1, 1.0, 2.0, 0.9);
                    }
                    else {
                        gl_matrix_1.vec4.scale(parCol, color.rgba, 0.2);
                        parCol[3] = 5 * t;
                        drawPointLight(gl_matrix_1.vec3.set(parPos, x, y, 0), parCol, 0.1, 1.7, 1.5, 0.8);
                    }
                }
            };
            physicsImpulse(x, y, 0, strLen, strLen * 2);
            for (let i = 0; i < sqdist * 0.15 + 2; i++) {
                const pdx = dx * 0.2 + dx * math_1.lqrandom();
                const pdy = -dy * 0.2 - dy * math_1.lqrandom();
                const rand = math_1.lqrandom() * (pi * 0.5 - pi * 0.25);
                const cos = Math.cos(rand);
                const sin = Math.sin(rand);
                const useColor = math_1.lqrandom() > 0.3;
                const c = gl_matrix_1.vec4.copy(parCol, useColor ? color.rgba : hitSparkColor);
                const p = addPhysicsParticle(x + r * cos, y + r * sin, 0, pdx * cos + pdy * sin, pdx * sin + pdy * cos, (math_1.lqrandom() * 0.5 - 0.25) * strLen, 2, 2, c, (15 + math_1.lqrandom() * 60) | 0, 0.1);
                p.fadeIn = 0.9;
                p.glow = (0.25 + math_1.lqrandom()) * 0.005 * strLen;
                p.draw = math_1.lqrandom() < 0.25 || useColor ? 0b10 : 0b01;
            }
            if (sqdist > 21) {
                for (let i = 0; i < strLen * 1.25; i++) {
                    const leftRight = ((math_1.lqrandom() * 2) | 0) * 2 - 1;
                    const upDown = ((math_1.lqrandom() * 2) | 0) * 2 - 1;
                    const pdy = (1 + math_1.lqrandom() * 0.1) * upDown;
                    const pdx = strLen * (0.34 + math_1.lqrandom() * 0.66) * leftRight;
                    const rand = (math_1.lqrandom() - 0.5) * (pi * 0.3);
                    const cos = Math.cos(rand);
                    const sin = Math.sin(rand);
                    const c = math_1.lqrandom() > 0.3 ? color.rgba : hitSparkColor;
                    const p = addPhysicsParticle(x + r * cos, y + r * sin, 0, pdx * cos + pdy * sin, pdx * sin + pdy * cos, Math.sqrt(strLen) * ((math_1.lqrandom() - 0.5) * 0.5), 2, 2, c, (15 + math_1.lqrandom() * 60) | 0, 0.1);
                    p.glow = (0.75 + math_1.lqrandom()) * 0.005 * strLen;
                    p.draw = math_1.lqrandom() < 0.25 ? 0b10 : 0b1;
                }
            }
            effects.push(f);
        },
        ledgegrab: (x, y, color) => {
            const startFrame = ticks;
            const duration = 10;
            const f = () => {
                if (ticks - startFrame > duration) {
                    removed.push(f);
                }
                else {
                    gl_matrix_1.vec4.set(parCol, color.rgba[0], color.rgba[1], color.rgba[2], (1 - (ticks - startFrame) / duration) * 5);
                    drawPointLight(gl_matrix_1.vec3.set(parPos, x, y, 0), parCol, 0.7, 1.0, 1.0, 0.3);
                }
            };
            effects.push(f);
        },
        light: (duration, x, y, z, r, g, b, a, fadeIn, fadeOut, shine, noiseRatio) => {
            const startFrame = ticks;
            const f = () => {
                if (ticks - startFrame > duration) {
                    removed.push(f);
                }
                else {
                    const pct = 1.0 - (ticks - startFrame) / duration;
                    const useA = a * easing_1.Ease.quartIn(pct);
                    drawPointLight(gl_matrix_1.vec3.set(parPos, x, y, z), gl_matrix_1.vec4.set(parCol, r, g, b, useA), fadeIn, fadeOut, shine, noiseRatio);
                }
            };
            effects.push(f);
        },
        hitbubble: (x, y, x2, y2, _angle, _tkb, _d, r, c) => {
            const dx = x2 - x;
            const dy = y2 - y;
            const ct = (Math.sqrt(dx * dx + dy * dy) / 2 + 1) | 0;
            for (let i = 0; i < ct; i++) {
                const a = Math.PI * 2 * math_1.lqrandom();
                const d1 = math_1.lqrandom();
                const d2 = math_1.lqrandom();
                addSparkParticle(x + dx * d1 + Math.sin(a) * r * d2 * 0.8, y + dy * d1 + Math.cos(a) * r * d2 * 0.8, 0, 0, 0, math_1.lqrandom() - 0.5, r * 0.1, 3, c.rgba, (30 + 10 * math_1.lqrandom()) | 0);
            }
        },
        physicsImpulse: physicsImpulse,
        hurtbubble: (x, y, z, dx, dy, dz, r, duration, c1, _c2) => {
            return addCharacterParticle(x, y, z * 0.25, dx, dy, dz, r * 0.5, 1.25 + math_1.lqrandom() * 3, duration, c1.rgba);
        },
        burst: (x, y, r, speed, color, impulse = vec2Zero) => {
            const duration = 12;
            const angles = 16;
            const step = (1 / angles) * Math.PI * 2;
            const offset = step * math_1.lqrandom();
            for (let i = 0; i < angles; i++) {
                const a = step * i + offset;
                const dx = Math.cos(a);
                const dy = Math.sin(a);
                const r2 = math_1.lqrandom() * 0.75 + 1.5;
                const p = nextPhysicsParticle();
                p.x = x + dx * r;
                p.y = y + dy * r;
                p.z = 0;
                p.a = 0.9;
                p.dx = dx * speed + impulse[0];
                p.dy = dy * speed + impulse[1];
                p.dz = 0;
                p.r = r2;
                p.mass = 1;
                gl_matrix_1.vec4.copy(p.c, color.rgba);
                p.w = 0.5 + 3 * math_1.lqrandom() * r2;
                p.h = 0.5 + 3 * math_1.lqrandom() * r2;
                p.startFrame = ticks;
                p.duration = duration;
                p.draw = math_1.lqrandom() < 0.25 ? 0b10 : 0b1;
            }
        },
        airjump: (x, y) => {
            const duration = 12;
            const angles = 64;
            const step = (1 / angles) * Math.PI * 2;
            const offset = step * math_1.lqrandom();
            const size = 5;
            physicsImpulse(x, y, 0, 10, 20);
            for (let i = 0; i < angles; i++) {
                const a = step * i + offset;
                const dx = Math.cos(a) * size;
                const dz = Math.sin(a) * size;
                const r = math_1.lqrandom() * 1 + 1;
                const p = nextPhysicsParticle();
                p.x = x + dx * 0.25;
                p.y = y;
                p.z = dz * 0.25;
                p.a = 0.9;
                p.dx = dx * 0.3;
                p.dy = 0;
                p.dz = dz * 0.3;
                p.r = r;
                p.mass = 1;
                gl_matrix_1.vec4.copy(p.c, dustColor.rgba);
                p.w = 0.5 + 3 * math_1.lqrandom() * r;
                p.h = 0.5 + 3 * math_1.lqrandom() * r;
                p.startFrame = ticks;
                p.gravity = 0;
                p.duration = duration;
                setQuatToRadians(p.rotation, pi * 1.5, 0, 0);
                gl_matrix_1.quat.identity(p.spin);
            }
        },
        skid: (x, y, dx) => {
            const adx = Math.abs(dx);
            const dy = Math.sqrt(adx);
            const n = (4 + dy) | 0;
            for (let i = 0; i < n; i++) {
                const duration = (16 * dy * easing_1.Ease.quartIn(math_1.lqrandom()) + 4) | 0;
                const pdx = dx * 0.5 + dx * 0.5 * math_1.lqrandom();
                const pdy = dy * math_1.lqrandom() + adx * 0.1;
                const pdz = dy * (math_1.lqrandom() - 0.5);
                const r = math_1.lqrandom() * 0.5 + 0.5;
                const p = addPhysicsParticle(x, y, 0, pdx, -pdy, pdz, r, 1, dustColor.rgba, duration, 0.5);
                p.glow = 0.5;
                if (math_1.lqrandom() > 0.5) {
                    p.draw = 0b10;
                }
            }
        },
        thump: (x, y, dx, dy, size, axis) => {
            const duration = 20;
            const angles = 32;
            const step = (1 / angles) * Math.PI * 2;
            const offset = step * math_1.lqrandom();
            physicsImpulse(x, y + 4, 0, 10, 20);
            for (let i = 0; i < angles; i++) {
                const r = math_1.lqrandom() * 1 + 1;
                const a = step * i + offset;
                const cos = Math.cos(a);
                const sin = Math.sin(a);
                const ox = cos * size * -axis[1];
                const oy = cos * size * axis[0] + (r * 2 + 2) * -axis[1];
                const oz = sin * size;
                const h = size * math_1.lqrandom() * 0.3 + 1;
                const pdx = ox * 0.1 + h * axis[0] + dx;
                const pdy = cos * size * axis[0] * 0.1 + h * -axis[1] + dy;
                const dz = sin * size * 0.1;
                const p = addPhysicsParticle(x + ox, y + oy, oz, pdx, pdy, dz, r, 1, smokeColor.rgba, duration, 0.5);
                if (math_1.lqrandom() > 0.75) {
                    p.draw = 0b10;
                }
            }
        },
        rectSpawner: ({ offset = gl_matrix_1.vec3.create(), width = 0, height = 0, depth = 0, transform = gl_matrix_1.mat4.create(), spawnProgress = (f, _progress, _frame) => f * 0.01, dx = (f) => f, dy = (f) => f, dz = (f) => f, duration = (f) => (easing_1.Ease.quadInOut(f) * 45) | 0, size: radius = (f) => 3 * f + 3, mass = (f) => 3 + f, color = (out, f) => gl_matrix_1.vec4.set(out, 1, 1, 1, f * 3), gravity = (f) => f, draw = (_f) => 0b1, }) => {
            const cv = gl_matrix_1.vec4.create();
            let progress = 0;
            let last = ticks;
            const spawn = () => {
                gl_matrix_1.vec3.set(working, offset[0] + (math_1.lqrandom() * 2.0 - 1.0) * width, offset[1] + (math_1.lqrandom() * 2.0 - 1.0) * height, offset[2] + (math_1.lqrandom() * 2.0 - 1.0) * depth);
                gl_matrix_1.vec3.transformMat4(working, working, transform);
                const p = addPhysicsParticle(working[0], working[1], working[2], dx(math_1.lqrandom()), dy(math_1.lqrandom()), dz(math_1.lqrandom()), radius(math_1.lqrandom()), mass(math_1.lqrandom()), color(cv, math_1.lqrandom()), duration(math_1.lqrandom()), gravity(math_1.lqrandom()));
                p.draw = draw(math_1.lqrandom());
            };
            effects.push(() => {
                for (; last < ticks; last++) {
                    progress = progress + spawnProgress(math_1.lqrandom(), progress, ticks);
                    for (; progress > 1; progress--) {
                        spawn();
                    }
                }
            });
        },
        sphereSpawner: ({ offset = gl_matrix_1.vec3.create(), width = 0, height = 0, depth = 0, transform = gl_matrix_1.mat4.create(), spawnProgress = (f, _progress, _frame) => f * 0.01, dx = (f) => f, dy = (f) => f, dz = (f) => f, duration = (f) => (easing_1.Ease.quadInOut(f) * 45) | 0, size = (f) => 3 * f + 3, mass = (f) => 3 + f, color = (out, f) => gl_matrix_1.vec4.set(out, 1, 1, 1, f * 3), gravity = (f) => f, draw = (_f) => 0b1, }) => {
            const cv = gl_matrix_1.vec4.create();
            let progress = 0;
            let last = ticks;
            const spawn = () => {
                math_1.spherePoint(working);
                gl_matrix_1.vec3.set(working, offset[0] + working[0] * width, offset[1] + working[1] * height, offset[2] + working[2] * depth);
                gl_matrix_1.vec3.transformMat4(working, working, transform);
                const p = addPhysicsParticle(working[0], working[1], working[2], dx(math_1.lqrandom()), dy(math_1.lqrandom()), dz(math_1.lqrandom()), size(math_1.lqrandom()), mass(math_1.lqrandom()), color(cv, math_1.lqrandom()), duration(math_1.lqrandom()), gravity(math_1.lqrandom()));
                p.draw = draw(math_1.lqrandom());
            };
            effects.push(() => {
                for (; last < ticks; last++) {
                    progress = progress + spawnProgress(math_1.lqrandom(), progress, ticks);
                    for (; progress > 1; progress--) {
                        spawn();
                    }
                }
            });
        },
    };
    return effectFunctions;
})();
class Pointer {
    constructor() {
        this.x = 0;
        this.y = 0;
        this.lx = 0;
        this.ly = 0;
        this.state = new utils_1.StateEmitter(this);
        this.event = new utils_1.EventEmitter();
    }
}
exports.Pointer = Pointer;
const mousePointer = new Pointer();
let mx = 0;
let my = 0;
let mwdx = 0;
let mwdy = 0;
let mp = 0;
let began = -1;
let countTo = -1;
exports.mouseListeners = [];
const removeMouseListener = (o) => {
    const i = exports.mouseListeners.indexOf(o);
    if (i === -1) {
        return;
    }
    exports.mouseListeners.splice(i, 1);
};
exports.removeMouseListener = removeMouseListener;
const resetMouseListeners = () => {
    exports.mouseListeners.length = 0;
};
exports.resetMouseListeners = resetMouseListeners;
const fixedScreen = (x, y, w, h) => {
    const run = () => { };
    const set = (nx, ny, nw, nh) => {
        x = nx;
        y = ny;
        w = nw;
        h = nh;
        refresh();
    };
    const refresh = () => {
        cameraPan[0] = x - w / 2;
        cameraPan[1] = y + h / 2;
        cameraPan[2] = 100;
        updateCamera();
    };
    refresh();
    return { run, set, refresh };
};
const freeCamera = (controller) => {
    const oldCamera = cameraType;
    let moving = true;
    const cam = {
        run: controller === null
            ? () => { }
            : () => {
                if (moving) {
                    let updated = false;
                    if (Math.abs(controller.hmove) > 0.2) {
                        cameraPan[0]
                            = cameraPan[0]
                                + controller.hmove * 10 * (1 - controller.shield * 0.99);
                        updated = true;
                    }
                    if (Math.abs(controller.vmove) > 0.2) {
                        cameraPan[1]
                            = cameraPan[1]
                                + controller.vmove * 10 * (1 - controller.shield * 0.99);
                        updated = true;
                    }
                    if (controller.dup) {
                        const speed = 6 * (1 - controller.shield * 0.99);
                        cameraPan[0] = cameraPan[0] + speed * cameraVector[0];
                        cameraPan[1] = cameraPan[1] + speed * cameraVector[1];
                        cameraPan[2] = cameraPan[2] - speed * cameraVector[2];
                        updated = true;
                    }
                    if (controller.ddown) {
                        const speed = -6 * (1 - controller.shield * 0.99);
                        cameraPan[0] = cameraPan[0] + speed * cameraVector[0];
                        cameraPan[1] = cameraPan[1] + speed * cameraVector[1];
                        cameraPan[2] = cameraPan[2] - speed * cameraVector[2];
                        updated = true;
                    }
                    if (Math.abs(controller.hright) > 0.2) {
                        gl_matrix_1.mat4.rotateY(view, view, Math.PI
                            * 0.01
                            * (1 - controller.shield * 0.99)
                            * controller.hright);
                        updated = true;
                    }
                    if (Math.abs(controller.vright) > 0.2) {
                        gl_matrix_1.mat4.rotateX(view, view, Math.PI
                            * -0.01
                            * (1 - controller.shield * 0.99)
                            * controller.vright);
                        updated = true;
                    }
                    if (controller.drightPress
                        && !controller.dup
                        && !controller.ddown) {
                        moving = false;
                        engine_1.dbg.log('Freezing camera');
                    }
                    if (updated) {
                        updateCamera();
                    }
                }
                else {
                    if (controller.drightPress) {
                        moving = true;
                        engine_1.dbg.log('Unfreezing camera');
                    }
                }
                if (controller.dleft && !controller.dup && controller.jump) {
                    cameraType = oldCamera;
                    initCamera();
                    engine_1.dbg.log('disabling free camera');
                }
            },
    };
    updateCamera();
    engine_1.dbg.log(controller !== null ? 'enabling free camera' : 'locked camera position');
    cameraType = cam;
};
exports.freeCamera = freeCamera;
const fitOnScreen = (() => {
    const smoothness = 18;
    const pos = gl_matrix_1.vec3.create();
    const minZ = 60;
    const maxZ = 600;
    let lastSpeed = 1000;
    const run = (smooth) => {
        let minX = Infinity;
        let maxX = -Infinity;
        let minY = Infinity;
        let maxY = -Infinity;
        let important = 0;
        for (let i = 0; i < engine_1.entities.length; i++) {
            const entity = engine_1.entities[i];
            if (!entity.removed && entity.follow) {
                const x1 = entity.x
                    + (entity.face === 1 ? -60 : -150)
                    + (entity.kbx < 0 ? entity.kbx * 3 : 0);
                const y1 = entity.y + entity.height - 60 + (entity.kby > 0 ? -entity.kby * 3 : 0);
                const x2 = entity.x
                    + (entity.face === 1 ? 150 : 60)
                    + (entity.kbx > 0 ? entity.kbx * 3 : 0);
                const y2 = entity.y + 60 + (entity.kby < 0 ? -entity.kby * 3 : 0);
                if (x1 < minX) {
                    minX = x1;
                }
                if (x2 > maxX) {
                    maxX = x2;
                }
                if (y1 < minY) {
                    minY = y1;
                }
                if (y2 > maxY) {
                    maxY = y2;
                }
                important++;
            }
        }
        if (stage) {
            if (!Number.isFinite(minX) && stage.anchors.length > 0) {
                minX = stage.anchors[0].x;
                maxX = minX;
                minY = stage.anchors[0].y;
                maxY = minY;
            }
            for (let i = 0; i < stage.anchors.length; i++) {
                const a = stage.anchors[i];
                const x = a.x;
                const y = a.y;
                const strength = important !== 0 ? a.weight : 1;
                if (x < minX) {
                    minX = minX + (x - minX) * strength;
                }
                if (x > maxX) {
                    maxX = maxX + (x - maxX) * strength;
                }
                if (y < minY) {
                    minY = minY + (y - minY) * strength;
                }
                if (y > maxY) {
                    maxY = maxY + (y - maxY) * strength;
                }
            }
        }
        for (let i = 0; i < engine_1.entities.length; i++) {
            const entity = engine_1.entities[i];
            if (entity.follow && entity.lastFall.frame > ticks - 120) {
                const x = entity.lastFall.x;
                const y = entity.lastFall.y;
                const strength = 1 - ((ticks - entity.lastFall.frame) / 120) ** 2;
                if (x < minX) {
                    minX = minX + (x - minX) * strength;
                }
                if (x > maxX) {
                    maxX = maxX + (x - maxX) * strength;
                }
                if (y - entity.height < minY) {
                    minY = minY + (y - entity.height - minY) * strength;
                }
                if (y + entity.height * 2 > maxY) {
                    maxY = maxY + (y + entity.height * 2 - maxY) * strength;
                }
            }
        }
        if (!smooth) {
            smooth = smoothness;
        }
        {
            const x = minX + (maxX - minX) / 2;
            const z = Math.min(Math.max(minZ, (maxX - minX) * (screenHeight / screenWidth), maxY - minY), maxZ);
            const y = minY + (maxY - minY) / 2 - z * 0.05;
            const dx = x - cameraPan[0];
            const dy = y - cameraPan[1];
            const bestSpeed = Math.min(Math.sqrt(dx * dx + dy * dy * 3) / 100 / z, lastSpeed * 1.1 + 0.005);
            pos[0] = x;
            pos[1] = y;
            pos[2] = z;
            lastSpeed = bestSpeed;
            gl_matrix_1.vec3.lerp(cameraPan, cameraPan, pos, 16 * bestSpeed);
        }
        cameraPhysics();
        updateCamera();
    };
    return { run };
})();
let cameraType = fitOnScreen;
class Entity {
    constructor() {
        this.removed = false;
        this.disable = false;
        this.hide = false;
    }
    mouseListener() { }
    act() { }
    paint() { }
    remove() {
        this.removed = true;
        this.mouseListener && exports.removeMouseListener(this);
    }
    place() {
        this.removed = false;
        this.mouseListener && exports.mouseListeners.push(this);
    }
}
exports.Entity = Entity;
Entity.exclusions = ['name', 'x', 'y', 'dx'];
class Cursor extends Entity {
    constructor(controller, x, y, data) {
        super();
        this.entity = null;
        this.color = null;
        this.restrict = null;
        this.spring = null;
        this.back = null;
        this.select = null;
        this.selected = '';
        this.dropper = null;
        this.character = null;
        this.colorSelectable = true;
        this.tick = null;
        this.seed = gl_matrix_1.vec4.create();
        this.x = x;
        this.y = y;
        for (const key of Object.getOwnPropertyNames(data)) {
            if (!utils_1.objHas(this, key)) {
                console.warn('setting an unknown cursor property', key);
            }
            if (data[key] instanceof Function) {
                ;
                this[key] = data[key].bind(this);
            }
            else {
                ;
                this[key] = data[key];
            }
        }
        this.controller = controller;
        controller.hook = this;
        this.place();
    }
    act() {
        let speed = 8;
        if (!this.controller || this.hide) {
            if (this.tick !== null && !this.disable) {
                this.tick();
            }
            return;
        }
        if (this.disable) {
            return;
        }
        if (this.controller.hmove !== 0 || this.controller.vmove !== 0) {
            if (this.controller.hright ** 2 + this.controller.vright ** 2 > 0.1) {
                speed
                    = speed
                        + speed
                            * Math.sqrt(this.controller.hright ** 2 + this.controller.vright ** 2);
            }
            this.x = this.x + this.controller.hmove * speed;
            this.y = this.y + this.controller.vmove * speed;
            if (this.restrict) {
                if (this.x > this.restrict[2]) {
                    this.x = this.restrict[2];
                }
                if (this.y > this.restrict[3]) {
                    this.y = this.restrict[3];
                }
                if (this.x < this.restrict[0]) {
                    this.x = this.restrict[0];
                }
                if (this.y < this.restrict[1]) {
                    this.y = this.restrict[1];
                }
            }
            for (let i = 0; i < engine_1.uiEntities.length; i++) {
                const entity = engine_1.uiEntities[i];
                if (!(entity instanceof Button || entity instanceof Selection)) {
                    continue;
                }
                if (entity.disallowNetwork && this.controller.name === 'network') {
                    continue;
                }
                if (entity.disable || entity.hide) {
                    continue;
                }
                if (entity.owner !== null && entity.owner !== this) {
                    continue;
                }
                if ((entity.press === null && entity.click === null)) {
                    continue;
                }
                if (entity.contains(this.x, this.y)) {
                    if (!entity.hovered) {
                        entity.hovered = true;
                        entity.hover?.(this, true);
                        audio_1.playAudio('ui_hover');
                    }
                    entity.hoverList.add(this);
                }
                else if (entity.hovered && entity.hoverList.has(this)) {
                    entity.hovered = false;
                    entity.hoverList.delete(this);
                    entity.hoverList.size === 0 && entity.hover?.(this, false);
                }
            }
        }
        if (this.spring !== null) {
            if (this.x > this.spring[2]) {
                this.x = this.x - (this.x - this.spring[2]) / speed;
            }
            if (this.y > this.spring[3]) {
                this.y = this.y - (this.y - this.spring[3]) / speed;
            }
            if (this.x < this.spring[0]) {
                this.x = this.x - (this.x - this.spring[0]) / speed;
            }
            if (this.y < this.spring[1]) {
                this.y = this.y - (this.y - this.spring[1]) / speed;
            }
        }
        if (this.controller.specialPress && this.back !== null) {
            this.back();
        }
        if (this.controller.attackPress) {
            for (let i = 0; i < engine_1.uiEntities.length; i++) {
                const entity = engine_1.uiEntities[i];
                if (!(entity instanceof Button || entity instanceof Selection)) {
                    continue;
                }
                if (entity.disallowNetwork && this.controller.name === 'network') {
                    continue;
                }
                if (entity.disable || entity.hide) {
                    continue;
                }
                if (entity.owner !== null && entity.owner !== this) {
                    continue;
                }
                if ((entity.press === null && entity.click === null)
                    || !entity.contains(this.x, this.y)) {
                    continue;
                }
                audio_1.playAudio('ui_trigger');
                if (entity.press !== null) {
                    entity.press(this);
                }
                else {
                    entity.click(entity.x - this.x, entity.y - this.y);
                }
            }
        }
        if (this.tick !== null) {
            this.tick();
        }
    }
    paint() {
        if (this.disable) {
            return;
        }
        ctx.lineWidth(2);
        ctx.strokeRGBA(0, 0, 0, 1);
        strokeCircle(this.x, this.y, 18);
        ctx.strokeRGBA(1, 1, 1, 1);
        ctx.fillStyle(this.color);
        drawCircle(this.x, this.y, 16);
        ctx.strokeRGBA(0, 0, 0, 1);
        ctx.fillStyle(uiTextColor);
        ctx.textAlign(GFX.TextAlign.center);
        if (!this.dropper || !this.dropper.holding) {
            setFontSize(30);
            paintText(this.controller.portNumber + 1 + '', this.x, this.y + 10);
        }
        else {
            setFontSize(25);
            paintText(this.controller.portNumber + 1 + '', this.x, this.y + 10);
        }
        ctx.textAlign(GFX.TextAlign.left);
    }
}
exports.Cursor = Cursor;
class CharacterCursor extends Cursor {
    constructor(controller, x, y, data) {
        super(controller, x, y, data);
    }
    setColor(color) {
        const entity = this.entity;
        if (entity !== null) {
            entity.setPalette(color_1.ColorPalette.fromColorSeed(this.color, this.controller.seed, entity.styles[entity.style]));
        }
        this.controller.color = color;
        this.color = color;
    }
    swapCharacter(name) {
        const entity = new engine_1.Animatable({ type: 0, name: name, important: true });
        this.entity.replace(entity);
    }
    act() {
        super.act();
    }
}
class RectangleUI extends Entity {
    constructor(x, y, w, h) {
        super();
        this.disable = false;
        this.x = 0;
        this.y = 0;
        this.w = 0;
        this.h = 0;
        this.x2 = 0;
        this.y2 = 0;
        this.x = x;
        this.y = y;
        this.w = w;
        this.h = h;
        this.x2 = x + w;
        this.y2 = y + h;
    }
    checkMouse() {
        return mx > this.x && my > this.y && mx < this.x2 && my < this.y2;
    }
    contains(x, y) {
        return x > this.x && y > this.y && x < this.x2 && y < this.y2;
    }
}
class Button extends RectangleUI {
    constructor(text, data, x, y, w, h, handlers = null, disallowNetwork = false) {
        super(x, y, w, h);
        this.hovered = false;
        this.pressed = false;
        this.disable = false;
        this.owner = null;
        this.disallowNetwork = false;
        this.xOffset = 0;
        this.yOffset = 0;
        this.drag = null;
        this.click = null;
        this.press = null;
        this.hover = null;
        this.hoverList = new Set();
        this.colors = {
            bg: color_1.Color.fromRGBA(0, 0, 0, 0.88),
            outline: color_1.Color.fromRGBA(0.95, 0.95, 0.95, 1),
            color: uiTextColor.clone(),
            stroke: color_1.Color.fromRGBA(0, 0, 0, 1),
        };
        if (typeof handlers === 'function') {
            this.click = handlers;
            this.press = handlers;
            this.drag = null;
        }
        else if (handlers !== null) {
            this.drag = utils_1.objHas(handlers, 'drag') ? handlers.drag : null;
            this.click = utils_1.objHas(handlers, 'click') ? handlers.click : null;
            this.press = utils_1.objHas(handlers, 'press') ? handlers.press : null;
            this.hover = utils_1.objHas(handlers, 'hover') ? handlers.hover : null;
            this.owner = utils_1.objHas(handlers, 'owner') ? handlers.owner : null;
        }
        this.translator = i18n_1.makeTranslator(text, data);
        this.translator.free();
        this.x2 = this.x + this.w;
        this.setText(text);
        this.disallowNetwork = disallowNetwork;
        this.place();
    }
    text(_s) {
        return false;
    }
    keyUp(_k) {
        return false;
    }
    keyDown(_k) {
        return false;
    }
    setText(t, data = null) {
        this.translator.set(t, data);
        setFontSize((this.h * 0.88) | 0);
    }
    paint() {
        ctx.lineWidth(2);
        setFontSize((this.h * 0.88) | 0);
        if (this !== activeUI && !this.hovered) {
            ctx.fillStyle(this.colors.bg);
            ctx.strokeStyle(this.colors.outline);
        }
        else {
            ctx.fillRGBA(0.4, 0.4, 0.3, 0.88);
            ctx.strokeRGBA(1, 1, 0.6, 1);
        }
        strokeRect(this.x, this.y, this.w, this.h);
        fillRect(this.x, this.y, this.w, this.h);
        ctx.strokeStyle(this.colors.stroke);
        ctx.fillStyle(this.colors.color);
        paintText(this.translator.value, this.x + 2, this.y2 - this.h * 0.2, this.w - 4);
    }
    mouseListener() {
        if (this.disable
            || this.hide
            || (!this.disallowNetwork && networking_1.isOnline())
            || (!this.click && !this.drag)) {
            return;
        }
        if (this.pressed) {
            if (mp & 1) {
                if (this.drag !== null) {
                    this.drag(this.xOffset, this.yOffset);
                }
            }
            else {
                if (this.checkMouse()) {
                    if (this.click !== null) {
                        audio_1.playAudio('ui_trigger');
                        this.click(this.x - mx, this.y - my);
                    }
                }
                this.pressed = false;
            }
        }
        else {
            if (this.checkMouse()) {
                if (!this.hovered) {
                    this.hovered = true;
                    this.hover?.(null, true);
                    audio_1.playAudio('ui_hover');
                }
                this.hoverList.add(null);
                if (mp & 2) {
                    this.pressed = true;
                    this.xOffset = this.x - mx;
                    this.yOffset = this.y - my;
                    audio_1.playAudio('ui_press');
                }
                if (mp & 8) {
                }
                if (mp & 32) {
                }
            }
            else if (this.hoverList.has(null)) {
                this.hovered = false;
                this.hoverList.delete(null);
                this.hoverList.size === 0 && this.hover?.(null, false);
            }
        }
    }
}
class ObjectButton extends Button {
    constructor(obj, prop, propType, text, data, x, y, w, h, changed = null, disallowNetwork = false, owner = null) {
        super(text, data, x, y, w, h);
        this.obj = obj;
        this.prop = prop;
        this.propType = propType;
        this.changed = changed;
        this.press = (c) => {
            if (this.disallowNetwork
                && c !== null
                && c.controller
                && c.controller.name === 'network') {
                return;
            }
            const v = this.obj[this.prop];
            switch (this.propType) {
                case 'boolean':
                case '!boolean':
                    this.obj[this.prop] = !v;
                    break;
            }
            if (this.changed) {
                this.changed(this.obj[this.prop], v);
            }
        };
        this.click = () => this.press(null);
        this.disallowNetwork = disallowNetwork;
        this.owner = owner;
    }
    paint() {
        super.paint();
        switch (this.propType) {
            case 'boolean':
                if (!this.obj[this.prop]) {
                    ctx.strokeRGBA(0.95, 0.8, 0.8, 1);
                    drawLine(this.x, this.y + this.h * 0.5, this.x + this.w, this.y + this.h * 0.5);
                }
                break;
            case '!boolean':
                if (this.obj[this.prop]) {
                    ctx.strokeRGBA(0.95, 0.8, 0.8, 1);
                    drawLine(this.x, this.y + this.h * 0.5, this.x + this.w, this.y + this.h * 0.5);
                }
                break;
        }
    }
}
class Selection extends RectangleUI {
    constructor(defaultText, data, x, y, w, rowHeight, showRows, items, handlers) {
        super(x, y, w, rowHeight);
        this.selectedIndex = -1;
        this.hovered = false;
        this.pressed = false;
        this.expandedRows = 0;
        this.eh = 0;
        this.cursor = null;
        this.expanded = false;
        this.defaultText = '';
        this.selected = '';
        this.typedIn = '';
        this.lastScrolled = 0;
        this.scrolling = false;
        this.scrollHover = false;
        this.changedLast = false;
        this.cursorPressed = false;
        this.xOffset = 0;
        this.yOffset = 0;
        this.scrollIndex = 0;
        this.scrollOffset = 0;
        this.showRows = 4;
        this.items = [];
        this.lastRetracted = -1000;
        this.owner = null;
        this.disallowNetwork = false;
        this.hoverList = new Set();
        this.drag = utils_1.objHas(handlers, 'drag') ? handlers.drag : null;
        this.click = utils_1.objHas(handlers, 'click') ? handlers.click : null;
        this.hover = utils_1.objHas(handlers, 'hover') ? handlers.hover : null;
        this.select = utils_1.objHas(handlers, 'select') ? handlers.select : null;
        this.back = utils_1.objHas(handlers, 'back') ? handlers.back : null;
        this.owner = utils_1.objHas(handlers, 'owner') ? handlers.owner : null;
        this.translator = i18n_1.makeTranslator(defaultText, data);
        this.translators = items.map(o => i18n_1.makeTranslator(o));
        this.rowHeight = rowHeight;
        this.defaultText = defaultText;
        this.selected = defaultText;
        this.xOffset = 0;
        this.yOffset = 0;
        this.showRows = showRows;
        this.items = items;
        this.update();
        this.place();
    }
    setMessage(text, data = null) {
        this.selected = text;
        this.translator.set(text, data);
    }
    setText(text) {
        this.selected = text;
        this.translator.value = text;
    }
    update() {
        this.expandedRows = Math.min(this.showRows, this.items.length);
        this.eh
            = this.rowHeight * (Math.min(this.expandedRows, this.items.length) + 1);
    }
    expand(c) {
        if (this.lastRetracted > ticks - 10) {
            return;
        }
        this.expanded = true;
        this.h = this.eh;
        this.y2 = this.y + this.h;
        this.cursorPressed = false;
        this.cursor = c;
        this.lastScrolled = ticks;
        if (c !== null) {
            this.cursor.hide = true;
            this.cursorPressed = true;
            if (c.controller.name !== 'network'
                && !controllers_1.isKeyboardConnected()
                && activeUI === null) {
                activeUI = this;
            }
        }
        else {
            activeUI = this;
        }
        if (this.selectedIndex === -1) {
            this.selectedIndex = 0;
            this.setMessage(this.items[this.selectedIndex]);
        }
    }
    retract() {
        this.expanded = false;
        this.lastRetracted = ticks;
        this.h = this.rowHeight;
        this.y2 = this.y + this.h;
        if (this.cursor !== null) {
            this.cursor.hide = false;
            this.cursor = null;
        }
        if (activeUI === this) {
            activeUI = null;
        }
    }
    toggle() {
        if (this.expanded) {
            this.retract();
        }
        else {
            this.expand(null);
        }
    }
    checkScroll() {
        return (this.expandedRows < this.items.length
            && mx > this.x2 - 12
            && my > this.y + this.scrollOffset + this.rowHeight
            && mx < this.x2 + 4
            && my < this.y + this.scrollOffset + this.rowHeight * 2);
    }
    text(s) {
        this.typedIn = this.typedIn + s;
        this.setText(this.typedIn);
        this.selectedIndex = this.items.indexOf(this.items.filter(utils_1.startsWith(this.typedIn.toLowerCase()))[0]);
        this.scrollIndex = Math.min(Math.max(0, this.selectedIndex), this.items.length - this.expandedRows);
        this.scrollOffset
            = (this.scrollIndex / (this.items.length - this.expandedRows))
                * (this.eh - this.rowHeight * 2);
        return true;
    }
    keyUp(_e) {
        return true;
    }
    keyDown(e) {
        switch (e.key) {
            case 'TAB':
                if (this.cursor.controller.name === 'keyboard') {
                    activeUI = this;
                }
                return true;
            case 'RETURN':
                if (this.selectedIndex >= 0) {
                    this.setMessage(this.items[this.selectedIndex]);
                    if (this.select !== null) {
                        this.select(this.selected, this.selectedIndex, null);
                    }
                }
                else {
                    this.setMessage(this.defaultText);
                }
                this.typedIn = '';
                this.retract();
                return true;
            case 'BACKSPACE':
                this.typedIn = this.typedIn.substr(0, this.typedIn.length - 1);
                this.setText(this.typedIn);
                return true;
            case 'ESCAPE':
                if (this.cursor === null) {
                    this.retract();
                }
                return true;
            case 'UP':
                if (this.selectedIndex === -1) {
                    this.selectedIndex = this.items.length;
                }
                this.selectedIndex--;
                if (this.selectedIndex < 0) {
                    this.selectedIndex = this.selectedIndex + this.items.length;
                }
                return true;
            case 'DOWN':
                this.selectedIndex = (this.selectedIndex + 1) % this.items.length;
                return true;
        }
        return true;
    }
    mouseListener() {
        if (this.disable || this.hide) {
            return;
        }
        if (mwdy !== 0) {
            this.scrollIndex
                = Math.min(Math.max(0, this.scrollIndex - mwdy / 40), this.items.length - this.expandedRows) | 0;
            this.scrollOffset
                = (this.scrollIndex / (this.items.length - this.expandedRows))
                    * (this.eh - this.rowHeight * 2);
        }
        if (this.scrolling) {
            if (mp & 1) {
                this.scrollOffset = Math.min(Math.max(0, my - this.yOffset - this.y), this.eh - this.rowHeight * 2);
                this.scrollIndex = Math.floor((this.scrollOffset / (this.eh - this.rowHeight * 2))
                    * (this.items.length - this.expandedRows));
            }
            else {
                this.scrolling = false;
            }
        }
        if (this.pressed) {
            if (mp & 1) {
                if (this.drag !== null) {
                    this.drag(this.xOffset, this.yOffset);
                }
            }
            else {
                if (this.checkMouse()) {
                    if (this.click !== null) {
                        this.click(this.x - mx, this.y - my);
                    }
                    if (my - this.y > this.rowHeight) {
                        audio_1.playAudio('ui_trigger');
                        this.selectedIndex
                            = Math.floor((my - this.y - this.rowHeight) / this.rowHeight)
                                + this.scrollIndex;
                        this.setMessage(this.items[this.selectedIndex]);
                        if (this.select !== null) {
                            this.select(this.selected, this.selectedIndex, null);
                        }
                    }
                    this.toggle();
                }
                this.pressed = false;
            }
        }
        else {
            if (this.expanded && this.scrollHover && !this.scrolling) {
                this.scrollHover = false;
            }
            if (this.expanded && this.checkScroll()) {
                this.scrollHover = true;
                if (mp & 2) {
                    this.scrolling = true;
                    this.yOffset = my - this.y - this.scrollOffset;
                }
            }
            else if (this.checkMouse()) {
                if (!this.hovered) {
                    this.hovered = true;
                    this.hover?.(null, true);
                    audio_1.playAudio('ui_hover');
                }
                this.hoverList.add(null);
                if (mp & 2) {
                    this.pressed = true;
                    this.xOffset = this.x - mx;
                    this.yOffset = this.y - my;
                    audio_1.playAudio('ui_press');
                }
                if (mp & 8) {
                }
                if (mp & 32) {
                }
            }
            else {
                if (mp & 2) {
                    this.retract();
                }
                if (this.hoverList.has(null)) {
                    this.hovered = false;
                    this.hoverList.delete(null);
                    this.hoverList.size === 0 && this.hover?.(null, false);
                }
            }
        }
    }
    act() {
        super.act();
        if (this.disable || this.hide) {
            return;
        }
        if (this.cursor === null) {
            return;
        }
        const controller = this.cursor.controller;
        if (this.cursorPressed && !controller.attack) {
            this.cursorPressed = false;
        }
        if (controller.specialPress) {
            this.retract();
            if (this.back !== null) {
                this.back(this.cursor);
            }
        }
        else if (!this.cursorPressed
            && controller.attackPress
            && this.lastScrolled !== ticks) {
            this.setMessage(this.items[this.selectedIndex]);
            if (this.select !== null) {
                this.select(this.selected, this.selectedIndex, this.cursor);
            }
            this.retract();
        }
        else {
            if (controller.vmove > 0) {
                if (ticks - this.lastScrolled
                    > (1.1 - controller.vmove * controller.vmove) * 30) {
                    this.selectedIndex = (this.selectedIndex + 1) % this.items.length;
                    if (this.selectedIndex >= this.scrollIndex + this.expandedRows) {
                        this.scrollIndex = this.selectedIndex - this.expandedRows + 1;
                    }
                    else if (this.selectedIndex < this.scrollIndex) {
                        this.scrollIndex = this.selectedIndex;
                    }
                    this.scrollOffset
                        = (this.scrollIndex / (this.items.length - this.expandedRows))
                            * (this.eh - this.rowHeight * 2);
                    if (this.changedLast) {
                        this.lastScrolled = ticks;
                    }
                    else {
                        this.lastScrolled = ticks + 12;
                        this.changedLast = true;
                    }
                }
            }
            else if (controller.vmove < 0) {
                if (ticks - this.lastScrolled
                    > (1.1 - controller.vmove * controller.vmove) * 30) {
                    this.selectedIndex
                        = this.selectedIndex > 0
                            ? this.selectedIndex - 1
                            : this.items.length - 1;
                    if (this.selectedIndex >= this.scrollIndex + this.expandedRows) {
                        this.scrollIndex = this.selectedIndex - this.expandedRows + 1;
                    }
                    else if (this.selectedIndex < this.scrollIndex) {
                        this.scrollIndex = this.selectedIndex;
                    }
                    this.scrollOffset
                        = (this.scrollIndex / (this.items.length - this.expandedRows))
                            * (this.eh - this.rowHeight * 2);
                    if (this.changedLast) {
                        this.lastScrolled = ticks;
                    }
                    else {
                        this.lastScrolled = ticks + 12;
                        this.changedLast = true;
                    }
                }
            }
            else {
                this.changedLast = false;
                this.lastScrolled = 0;
            }
        }
    }
    press(cursor) {
        if (this.disable || this.hide) {
            return;
        }
        if (this.cursor === null) {
            if (this.selectedIndex === -1) {
                this.selectedIndex = 0;
            }
            if (!this.expanded) {
                this.expand(cursor);
            }
        }
    }
    reset() {
        this.selectedIndex = -1;
        this.setMessage(this.defaultText);
        this.scrollIndex = 0;
    }
    paint() {
        const yOffset = this.rowHeight * 0.8 + this.rowHeight;
        ctx.lineWidth(2);
        if (this.hide) {
            return;
        }
        setFontSize(this.rowHeight);
        if (this !== activeUI && !this.hovered) {
            ctx.fillRGBA(0, 0, 0, 0.88);
            ctx.strokeRGBA(0.95, 0.95, 0.95, 1);
        }
        else {
            ctx.fillRGBA(0.4, 0.4, 0.3, 0.88);
            ctx.strokeRGBA(1, 1, 0.6, 1);
        }
        if (this.expanded) {
            fillRect(this.x, this.y, this.w, this.eh);
            drawLine(this.x, this.y + this.rowHeight, this.x + this.w, this.y + this.rowHeight);
            strokeRect(this.x, this.y, this.w, this.eh);
            if (this.expandedRows < this.items.length) {
                fillRect(this.x2 - (this.scrollHover ? 12 : 4), this.y + this.rowHeight + this.scrollOffset, this.scrollHover ? 16 : 8, this.rowHeight);
                strokeRect(this.x2 - (this.scrollHover ? 12 : 4), this.y + this.rowHeight + this.scrollOffset, this.scrollHover ? 16 : 8, this.rowHeight);
            }
            ctx.strokeRGBA(0, 0, 0, 1);
            ctx.fillStyle(uiTextColor);
            for (let i = 0; i < this.expandedRows && i + this.scrollIndex < this.items.length; i++) {
                if (i + this.scrollIndex === this.selectedIndex) {
                    if (this !== activeUI) {
                        ctx.strokeRGBA(0.9, 0.9, 0.9, 1);
                        ctx.fillRGBA(0.25, 0.25, 0.25, 1);
                    }
                    else {
                        ctx.strokeRGBA(1, 1, 1, 1);
                        ctx.fillRGBA(0.5, 0.5, 0.3, 1);
                    }
                    strokeRect(this.x - 4, i * this.rowHeight + this.y + this.rowHeight, this.w + 8, this.rowHeight);
                    fillRect(this.x - 4, i * this.rowHeight + this.y + this.rowHeight, this.w + 8, this.rowHeight);
                    ctx.strokeRGBA(0, 0, 0, 1);
                    ctx.fillStyle(uiTextColor);
                }
                paintText(this.translators[i + this.scrollIndex].value, this.x + 2, i * this.rowHeight + this.y + yOffset, this.w - 4);
            }
        }
        else {
            fillRect(this.x, this.y, this.w, this.rowHeight);
            strokeRect(this.x, this.y, this.w, this.rowHeight);
            ctx.fillStyle(uiTextColor);
        }
        ctx.strokeRGBA(0, 0, 0, 1);
        ctx.fillStyle(uiTextColor);
        paintText(this.translator.value, this.x + 2, this.y + this.rowHeight * 0.8, this.w - 4);
    }
}
let minRate = 0;
let maxRate = 0;
let avgRate = 0;
const fpsText = ['', '', '', '', '', '', '', '', '', '', ''];
let networkText = [''];
const sum = (n1, n2) => n1 + n2;
const max = (a, b) => Math.min(a, b);
const min = (a, b) => Math.max(a, b);
let startTime = 0;
const training = [];
const updateTrainingGraphs = () => {
    if (engine_1.Game.bubble === null) {
        return;
    }
    const entity = engine_1.Game.entity;
    const bb = engine_1.Game.bubble.bubble;
    const graphs = [
        {
            kb: engine_1.Game.kb,
            angle: engine_1.Game.angle,
            reverse: engine_1.Game.reverse,
            frameSkip: 2,
            color: new color_1.Color(gl_matrix_1.vec4.fromValues(0.5, 0.5, 0.5, 0.5)),
            lineWidth: 0.75,
        },
        {
            kb: engine_1.Game.kb,
            angle: engine_1.Game.angle - 18,
            reverse: engine_1.Game.reverse,
            frameSkip: 2,
            color: new color_1.Color(gl_matrix_1.vec4.fromValues(0.2, 0.5, 0.5, 0.5)),
            lineWidth: 0.75,
        },
        {
            kb: engine_1.Game.kb,
            angle: engine_1.Game.angle + 18,
            reverse: engine_1.Game.reverse,
            frameSkip: 2,
            color: new color_1.Color(gl_matrix_1.vec4.fromValues(0.8, 0.5, 0.5, 0.5)),
            lineWidth: 0.75,
        },
        {
            kb: engine_1.calcKnockback(bb.knockback, bb.growth, bb.damage, engine_1.Game.damage, 0, entity.weight, 1),
            angle: engine_1.Game.di,
            reverse: false,
            frameSkip: 2,
            color: new color_1.Color(gl_matrix_1.vec4.fromValues(0.9, 1, 0.5, 0.9)),
            lineWidth: 1.5,
        },
        {
            kb: engine_1.calcKnockback(bb.knockback, bb.growth, bb.damage, engine_1.Game.damage, 50, entity.weight, 1),
            angle: engine_1.Game.di,
            reverse: false,
            frameSkip: 2,
            color: new color_1.Color(gl_matrix_1.vec4.fromValues(0.75, 1, 0.5, 0.9)),
            lineWidth: 1.5,
        },
        {
            kb: engine_1.calcKnockback(bb.knockback, bb.growth, bb.damage, engine_1.Game.damage, 100, entity.weight, 1),
            angle: engine_1.Game.di,
            reverse: false,
            frameSkip: 2,
            color: new color_1.Color(gl_matrix_1.vec4.fromValues(0.5, 1, 0.5, 0.9)),
            lineWidth: 1.5,
        },
        {
            kb: engine_1.calcKnockback(bb.knockback, bb.growth, bb.damage, engine_1.Game.damage, 150, entity.weight, 1),
            angle: engine_1.Game.di,
            reverse: false,
            frameSkip: 2,
            color: new color_1.Color(gl_matrix_1.vec4.fromValues(0, 1, 0.5, 0.5)),
            lineWidth: 1.5,
        },
        {
            kb: engine_1.Game.kb,
            angle: engine_1.Game.di,
            reverse: false,
            frameSkip: 2,
            color: new color_1.Color(gl_matrix_1.vec4.fromValues(0.25, 1, 0.5, 0.9)),
            lineWidth: 3,
        },
    ];
    let bufIndex = 0;
    engine_1.Game.knockbackCurves.length = 0;
    if (engine_1.Game.curveBuffer.length === 0) {
        engine_1.Game.curveBuffer = new Float64Array(128);
    }
    for (let i = 0; i < graphs.length; i++) {
        const g = graphs[i];
        let kbx = (g.reverse ? -1 : 1) * math_1.angleX(g.angle) * g.kb;
        let kby = -math_1.angleY(g.angle) * g.kb;
        let dy = 0;
        let kb = g.kb;
        let x = engine_1.Game.x;
        let y = engine_1.Game.y;
        const stun = engine_1.calcStun(kb);
        let count = 2;
        let lx = x;
        let ly = y;
        engine_1.Game.curveBuffer[bufIndex] = x;
        engine_1.Game.curveBuffer[bufIndex + 1] = y;
        for (let j = 0; j < stun; j++) {
            let ratio = 0;
            const decay = entity.kbDecay;
            const up = kby - dy < 0;
            x = x + kbx;
            y = y + kby - dy;
            if (decay < kb) {
                ratio = 1 - Math.min(1, decay / kb);
            }
            kb = kb * ratio;
            kbx = kbx * ratio;
            kby = kby * ratio;
            dy = Math.max(dy - entity.arcSpeed, -entity.maxFallSpeed);
            if ((lx - x) * (lx - x) + (ly - y) * (ly - y) < 100
                && j !== stun - 1
                && up === kby - dy < 0) {
                continue;
            }
            lx = x;
            ly = y;
            while (bufIndex + count + 2 > engine_1.Game.curveBuffer.length) {
                const ob = engine_1.Game.curveBuffer;
                engine_1.Game.curveBuffer = new Float64Array(engine_1.Game.curveBuffer.length * 2);
                engine_1.Game.curveBuffer.set(ob);
            }
            engine_1.Game.curveBuffer[bufIndex + count] = x;
            engine_1.Game.curveBuffer[bufIndex + count + 1] = y;
            count = count + 2;
        }
        engine_1.Game.knockbackCurves.push({
            color: g.color,
            verts: new Float64Array(engine_1.Game.curveBuffer.buffer, bufIndex * Float64Array.BYTES_PER_ELEMENT, count),
            lineWidth: g.lineWidth,
        });
        bufIndex = bufIndex + count;
    }
};
exports.updateTrainingGraphs = updateTrainingGraphs;
const updateTraining = () => {
    training.length = 0;
    training.push('combo: ' + (engine_1.Game.comboCounter + 1).toString(), 'dmg:   ' + engine_1.Game.damage.toFixed(1), 'total: ' + engine_1.Game.totalDamage.toFixed(1), 'kb:    ' + engine_1.Game.kb.toFixed(1), 'angle: ' + engine_1.Game.angle.toFixed(1), '', 'stun:  ' + engine_1.Game.stun.toString(), 'early: ' + (engine_1.Game.prestun > 0 ? engine_1.Game.prestun - 1 : 0).toString(), 'late:  ' + engine_1.Game.poststun.toString(), 'stale: ' + (engine_1.Game.stale * 100).toFixed(0) + '%', '', 'di:    ' + engine_1.Game.di.toFixed(1), 'dimag: ' + (engine_1.Game.diMagnitude * 100).toFixed(0) + '%', 'sdi:   ' + engine_1.Game.sdi[0].toFixed(0) + ', ' + engine_1.Game.sdi[1].toFixed(0), 'asdi:  ' + engine_1.Game.asdi[0].toFixed(0) + ', ' + engine_1.Game.asdi[1].toFixed(0));
};
exports.updateTraining = updateTraining;
const tickTimes1 = new utils_1.RingBuffer(30);
const tickTimes2 = new utils_1.RingBuffer(30);
const drawTimes1 = new utils_1.RingBuffer(30);
const drawTimes2 = new utils_1.RingBuffer(30);
const particleTickTimes = new utils_1.RingBuffer(60);
const particleDrawTimes = new utils_1.RingBuffer(60);
const rafTimes = new utils_1.RingBuffer(30);
const frameTimes = new utils_1.Stopwatch(30);
const frameMs = 60 / 1000;
const scheduledCallbacks = [];
const todoScheduledCallbacks = [];
let runningSchedule = false;
const scheduleCallback = (fn) => !runningSchedule
    ? scheduledCallbacks.push(fn)
    : todoScheduledCallbacks.push(fn);
exports.scheduleCallback = scheduleCallback;
const drawStageElement = (element) => {
    const p1 = gl_matrix_1.vec3.fromValues(element.x, element.y, 10);
    const p2 = gl_matrix_1.vec3.fromValues(element.x2, element.y2, 10);
    const p3 = gl_matrix_1.vec3.fromValues(element.x2, element.y2, -10);
    const p4 = gl_matrix_1.vec3.fromValues(element.x, element.y, -10);
    gl_matrix_1.vec3.transformMat4(p1, p1, worldTransform);
    gl_matrix_1.vec3.multiply(p1, p1, screenScale);
    gl_matrix_1.vec3.transformMat4(p2, p2, worldTransform);
    gl_matrix_1.vec3.multiply(p2, p2, screenScale);
    gl_matrix_1.vec3.transformMat4(p3, p3, worldTransform);
    gl_matrix_1.vec3.multiply(p3, p3, screenScale);
    gl_matrix_1.vec3.transformMat4(p4, p4, worldTransform);
    gl_matrix_1.vec3.multiply(p4, p4, screenScale);
    ctx.strokeRGBA(1, 1, 1, 0.5);
    drawLine(element.x, element.y, element.x2, element.y2);
    ctx.moveTo(p1[0], p1[1]);
    ctx.lineTo(p2[0], p2[1]);
    ctx.lineTo(p3[0], p3[1]);
    ctx.lineTo(p4[0], p4[1]);
    ctx.closePath();
    if (element.top) {
        if (element.solid) {
            ctx.fillRGBA(0.125, 0.125, 0.125, 0.75);
        }
        else {
            ctx.fillRGBA(0.5, 0.5, 0.5, 0.5);
        }
    }
    else if (element.left) {
        ctx.fillRGBA(0.125, 0.125, 0.5, 0.75);
    }
    else if (element.right) {
        ctx.fillRGBA(0.125, 0.5, 0.125, 0.75);
    }
    else if (element.bottom) {
        ctx.fillRGBA(0.5, 0.125, 0.125, 0.75);
    }
    ctx.stroke();
    ctx.fill();
    if (element.leftOccupied) {
        fillCircle(element.x, element.y, 8);
    }
    if (element.rightOccupied) {
        fillCircle(element.x2, element.y2, 8);
    }
    return;
};
const msTime = (ms) => {
    if (ms > 0.01) {
        const time = (ms * 1000) | 0;
        return '     '.substr(time.toString().length) + time + 'ns';
    }
    else {
        const time = (ms * 1000000) | 0;
        return '     '.substr(time.toString().length) + time + 'µs';
    }
};
const msnsFramerate = (ms) => {
    const time = (ms * 1000) | 0;
    return ('     '.substr(time.toString().length)
        + time
        + 'ns '
        + ('   ' + (1000 / ms).toFixed(2)).substr(((1000 / ms).toFixed(2) + '').length - 4)
        + 'fps');
};
const backgroundStyle = new color_1.Color(gl_matrix_1.vec4.fromValues(0, 1, 0.2, 1));
const create = (controller) => {
    if (activeMode.connect) {
        activeMode.connect(controller);
    }
};
const destroy = (controller) => {
    if (activeMode.disconnect) {
        activeMode.disconnect(controller);
    }
};
const renderWaiting = [];
const renderTick1 = () => {
    exports.Effects.tick();
    for (const fn of renderWaiting) {
        fn();
    }
    renderWaiting.length = 0;
    ctx.strokeStyle(uiTextColor);
};
const renderWait = () => new Promise(resolve => renderWaiting.push(resolve));
const renderTick2 = () => {
    const l = animation_1.getHitbubbleCount();
    for (let i = 0; i < l; i++) {
        const hb = animation_1.hitbubbles[i];
        const b = hb.bubble;
        if (b.type === animation_1.HitbubbleType.shield) {
            continue;
        }
        if (hb.frame === 0) {
            exports.Effects.physicsImpulse(hb.x + (hb.x - hb.x2) * 0.5, hb.y + (hb.y - hb.y2) * 0.5, 0, hb.radius * 2, Math.sqrt(b.damage));
        }
    }
    for (let i = 0; i < animation_1.hurtbubbles.length; i++) {
        const hb = animation_1.hurtbubbles[i];
        const owner = hb.owner;
        const dx = owner.x - owner.lx;
        const dy = owner.y - owner.ly;
        const hbCount = (Math.sqrt(dx ** 2 + dy ** 2) * 0.5 * math_1.lqrandom()) | 0;
        if (owner.removed || hb.state === animation_1.HurtbubbleState.phased) {
            continue;
        }
        for (let j = hbCount; j > 0; j--) {
            if (math_1.lqrandom()
                >= (1 / owner.hurtbubbles.length)
                    * (!owner.hitlag && owner.stun <= 20 ? 0.15 : 1.25)) {
                continue;
            }
            const index = hb.colorIndex1;
            let color1 = owner.palette.lighter[index];
            const color2 = owner.palette.lighter[index];
            const x = hb.x + owner.x;
            const y = hb.y + owner.y;
            const x2 = hb.x2 + owner.x;
            const y2 = hb.y2 + owner.y;
            const ra = math_1.lqrandom() * 360;
            const r = math_1.lqrandom();
            const dist = (1 - r * r) * (hb.radius - Math.sqrt(hb.radius));
            let size = Math.sqrt(hb.radius);
            const rp = math_1.lqrandom();
            const cx = rp * (x2 - x) + x;
            const cy = rp * (y2 - y) + y;
            let kbx = 0;
            let kby = 0;
            let kbz = math_1.lqrandom() * 0.5;
            const r2 = math_1.lqrandom();
            let duration = 1;
            const isGlow = owner.stun > 0 && math_1.lqrandom() < 0.25;
            if (owner.kb > 1 && math_1.lqrandom() < 0.25) {
                const kba = owner.kba + math_1.lqrandom() * 18 - 9;
                kbx = kbx + math_1.angleX(kba) * owner.kb;
                kby = kby + math_1.angleY(kba) * owner.kb;
                kbz = kbz + Math.sqrt(kbx * kbx + kby * kby) * math_1.lqrandom() * 0.25;
            }
            if (isGlow) {
                color1 = owner.palette.darker[index];
            }
            else if (owner.stun > 0 || owner.lag > 0) {
                color1 = owner.palette.lighter[index];
                size = size * 2;
                duration = 0.25;
            }
            if (math_1.lqrandom() > 0.5) {
                kbz = -kbz;
            }
            const p = exports.Effects.hurtbubble(cx + math_1.angleX(ra) * dist + r2 * dx, cy + math_1.angleY(ra) * dist + r2 * dy, (hb.flip && owner.face === -1 ? -hb.z : hb.z)
                - (math_1.lqrandom() * 3 - 1.5) * hb.radius, 0, 0, 0, size, duration, color1, color2);
            if (isGlow) {
                p.draw = 0b11;
                p.glow = Math.pow(owner.stun * 4, 0.75) * 0.01;
                p.gravity = 0;
                p.dx = 0;
                p.dy = 0;
                p.duration = owner.stun;
            }
        }
    }
    renderTickEntities();
};
const tickFrame = () => {
    utils_1.objHas(cameraType, 'run') && cameraType.run();
    if (ticks - engine_1.activeModeTime > 5) {
        menuDoc && tickDoc(menuDoc);
        utils_1.objHas(activeMode, 'doc') && tickDoc(activeMode.doc);
    }
    renderTick1();
    renderTick2();
};
const renderStage = () => {
    let maxTop = 0;
    let maxRight = 0;
    let maxBottom = 0;
    let maxLeft = 0;
    const blastDrawDistance = 200;
    ctx.fillRGBA(0.125, 0.125, 0.125, 0.5);
    if (stage) {
        ctx.strokeStyle(uiTextColor);
        if ((engine_1.dbg.drawStage & 1) !== 0 && stage.prefab) {
            renderPrefab(stage.prefab, gfx.solid);
        }
        if ((engine_1.dbg.drawStage & 2) !== 0 || !stage.prefab) {
            stage.elements.forEach(drawStageElement);
        }
    }
    for (const p of engine_1.players) {
        if (p.removed) {
            continue;
        }
        if (p.y < stage.blastTop + blastDrawDistance) {
            maxTop = Math.max(maxTop, -(p.y - stage.blastTop - blastDrawDistance));
        }
        if (p.x > stage.blastRight - blastDrawDistance) {
            maxRight = Math.max(maxRight, p.x - stage.blastRight + blastDrawDistance);
        }
        if (p.y > stage.blastBottom - blastDrawDistance) {
            maxBottom = Math.max(maxBottom, p.y - stage.blastBottom + blastDrawDistance);
        }
        if (p.x < stage.blastLeft + blastDrawDistance) {
            maxLeft = Math.max(maxLeft, -(p.x - stage.blastLeft - blastDrawDistance));
        }
    }
    ctx.lineWidth(4);
    if (maxTop !== 0) {
        ctx.strokeRGBA(1, 0.85, 0.7, Math.min(maxTop / blastDrawDistance, 1));
        drawLine(stage.blastLeft, stage.blastTop, stage.blastRight, stage.blastTop);
    }
    if (maxRight !== 0) {
        ctx.strokeRGBA(1, 0.85, 0.7, Math.min(maxRight / blastDrawDistance, 1));
        drawLine(stage.blastRight, stage.blastTop, stage.blastRight, stage.blastBottom);
    }
    if (maxBottom !== 0) {
        ctx.strokeRGBA(1, 0.85, 0.7, Math.min(maxBottom / blastDrawDistance, 1));
        drawLine(stage.blastLeft, stage.blastBottom, stage.blastRight, stage.blastBottom);
    }
    if (maxLeft !== 0) {
        ctx.strokeRGBA(1, 0.85, 0.7, Math.min(maxLeft / blastDrawDistance, 1));
        drawLine(stage.blastLeft, stage.blastTop, stage.blastLeft, stage.blastBottom);
    }
    ctx.lineWidth(2);
};
const renderEntities = () => {
    for (const entity of engine_1.entities) {
        const c = entity.palette.base;
        const keyframe = entity.activeAnimation.keyframeData;
        if (entity.stun > 0) {
            const x = entity.x + entity.corebubble.x;
            const y = entity.y + entity.corebubble.y;
            const ease = entity.stun > 8 ? 1 : easing_1.Ease.quadIn(entity.stun / 8);
            drawPointLight(gl_matrix_1.vec3.set(parPos, x, y, 0), gl_matrix_1.vec4.set(parCol, c.rgba[0], c.rgba[1], c.rgba[2], ease * 5), 0.98, 1.0, 1.5, 2.0);
        }
        if (utils_1.objHas(keyframe, 'directional') && entity.controller) {
            const controller = entity.controller;
            const cx = entity.corebubble.x + entity.x;
            const cy = entity.corebubble.y + entity.y;
            const hm = keyframe.directional === 'vertical' ? 0 : controller.hmove;
            const vm = keyframe.directional === 'horizontal' ? 0 : controller.vmove;
            const x = cx + hm * 20;
            const y = cy + vm * 20;
            const x2 = cx + hm * 70;
            const y2 = cy + vm * 70;
            const l = ctx._lineWidth;
            ctx.lineWidth(15 * controller.distance() + 10);
            ctx.strokeAlpha(entity.palette.darker[0], 0.5);
            drawLine(x, y, x2, y2);
            ctx.lineWidth(11 * controller.distance() + 7);
            ctx.strokeAlpha(entity.palette.base, 0.5);
            drawLine(x, y, x2, y2);
            ctx.lineWidth(3 * controller.distance() + 5);
            ctx.strokeAlpha(entity.palette.lighter[0], 0.5);
            drawLine(x, y, x2, y2);
            ctx.lineWidth(l);
        }
    }
};
let effectLights = new Map();
let effectLightsSwap = new Map();
const renderTickEntities = () => {
    const swap = effectLights;
    for (const entity of engine_1.entities) {
        const keyframe = entity.activeAnimation.keyframeData;
        const animation = entity.activeAnimation;
        if (entity.removed) {
            continue;
        }
        if (animation.events !== null) {
            const events = animation.events;
            const f = animation.frame;
            for (const event of events) {
                const start = event.start ?? 0;
                const end = event.end ?? animation.duration ?? 0;
                const has = effectLightsSwap.has(event);
                let lingered = false;
                if (event.when === 'hitbubbles') {
                    if (!keyframe.hitbubbles
                        || animation.midframe < keyframe.hitbubbles[0].start
                        || animation.midframe >= keyframe.hitbubbles[0].end + 1) {
                        if (!has) {
                            continue;
                        }
                        lingered = true;
                    }
                }
                else if (f < start || f >= end) {
                    continue;
                }
                if (event.effect === 'glow') {
                    let cx = 0;
                    let cy = 0;
                    if (!event.color) {
                        gl_matrix_1.vec4.copy(parCol, entity.palette.base.rgba);
                        parCol[3] = parCol[3] * 50;
                    }
                    else {
                        gl_matrix_1.vec4.set(parCol, event.color[0], event.color[1], event.color[2], event.color[3]);
                    }
                    if (event.follow) {
                        const bindex = entity.namedbubbles.get(event.follow);
                        const bubble = entity.hurtbubbles[Math.abs(bindex) - 1];
                        cx = entity.x + (bindex > 0 ? bubble.x : bubble.x2);
                        cy = entity.y + (bindex > 0 ? bubble.y : bubble.y2);
                        if (!event.color) {
                            parCol[3] = 6 * bubble.radius;
                        }
                    }
                    else {
                        cx = entity.x;
                        cy = entity.y;
                    }
                    if (event.offset) {
                        cx += event.offset[0] * entity.face;
                        cy -= event.offset[1];
                    }
                    if (has) {
                        exports.Effects.remove(effectLightsSwap.get(event));
                    }
                    exports.Effects.light(15, cx, cy, 0, parCol[0], parCol[1], parCol[2], parCol[3], 0.4, 1.7, 1.25, 1.4);
                    if (!lingered) {
                        effectLights.set(event, exports.Effects.peek());
                    }
                }
            }
        }
    }
    effectLights = effectLightsSwap;
    effectLightsSwap = swap;
    effectLights.clear();
};
const hurtbubbleLegend = {
    normal: color_1.Color.fromHSLA(0.15, 1.0, 0.5, 0.5),
    ungrabbable: color_1.Color.fromHSLA(0.2, 1.0, 0.5, 0.5),
    invincible: color_1.Color.fromHSLA(0.5, 1.0, 0.5, 0.5),
    intangible: color_1.Color.fromHSLA(0.66, 1.0, 0.5, 0.5),
};
const renderRawHurtbubbles = () => {
    const l = hurtbubbleLegend;
    for (let i = 0; i < engine_1.entities.length; i++) {
        const entity = engine_1.entities[i];
        const x = entity.x;
        const y = entity.y;
        for (let j = 0; j < entity.hurtbubbleCount; j++) {
            const hb = entity.hurtbubbles[j];
            const color = hb.state === animation_1.HurtbubbleState.invincible
                ? l.invincible
                : hb.state === animation_1.HurtbubbleState.intangible
                    ? l.intangible
                    : hb.ungrabbable
                        ? l.ungrabbable
                        : l.normal;
            pathCapsule(hb.x + x, hb.y + y, hb.x2 + x, hb.y2 + y, hb.radius);
            ctx.fillAlpha(color, 0.33);
            ctx.fill();
            ctx.strokeAlpha(color, 1.0);
            ctx.stroke();
        }
    }
};
const renderHurtbubbles = () => {
    ctx.lineWidth(1);
    for (let i = 0; i < animation_1.hurtbubbles.length; i++) {
        const hb = animation_1.hurtbubbles[i];
        const owner = hb.owner;
        const palette = owner.palette;
        const index1 = hb.colorIndex1;
        const index2 = hb.colorIndex2;
        let fill1 = palette.colors[index1];
        let fill2 = palette.colors[index2];
        if (owner.removed || hb.state === animation_1.HurtbubbleState.phased) {
            continue;
        }
        if (utils_1.objHas(owner.activeAnimation, 'helpless')) {
        }
        if (owner.flash) {
            fill1 = palette.lighter[index1];
            fill2 = palette.lighter[index2];
        }
        else if (hb.state === animation_1.HurtbubbleState.invincible) {
            if (ticks % 12 < 6) {
                fill1 = palette.darker[index1];
                fill2 = palette.darker[index2];
            }
        }
        else if (hb.state === animation_1.HurtbubbleState.heavyArmor) {
            fill1 = palette.darker[index1];
            fill2 = palette.darker[index2];
        }
        else if (hb.state === animation_1.HurtbubbleState.intangible) {
            fill1 = palette.lighter[index1];
            fill2 = palette.lighter[index2];
        }
        else if (owner.stun > 0) {
        }
        {
            let x = hb.x + owner.x;
            let y = hb.y + owner.y;
            let x2 = hb.x2 + owner.x;
            let y2 = hb.y2 + owner.y;
            const r = hb.radius;
            const nudge = owner.playerNumber === -1 ? 0.005 : owner.playerNumber * 0.01;
            if (owner.lag > 0 && owner.hitlag) {
                x
                    = x
                        + math_1.angleX((((ticks / 4) | 0) % 16) * 33)
                            * (owner.lastInjury.knockback / 4);
                y
                    = y
                        + math_1.angleY((((ticks / 4) | 0) % 16) * 33)
                            * (owner.lastInjury.knockback / 4);
                x2
                    = x2
                        + math_1.angleX((((ticks / 4) | 0) % 16) * 33)
                            * (owner.lastInjury.knockback / 4);
                y2
                    = y2
                        + math_1.angleY((((ticks / 4) | 0) % 16) * 33)
                            * (owner.lastInjury.knockback / 4);
                if (owner.lag % 4 < 2) {
                    fill1 = palette.lighter[index1];
                    fill2 = palette.lighter[index2];
                }
                else {
                    fill1 = palette.lighter[index1];
                    fill2 = palette.lighter[index2];
                }
            }
            drawGem(x, y, x2, y2, r, owner.bubbleScale * hb.z + nudge, hb.flip ? owner.face : 1, hb.invert ? -owner.face : owner.face, fill1.rgba, fill2.rgba, hb.prefab, hb.rotateModel);
            ctx.lineWidth(1);
        }
    }
    ctx.lineWidth(2);
};
const renderHitbubbles = () => {
    const l = animation_1.getHitbubbleCount();
    for (let i = 0; i < l; i++) {
        const hb = animation_1.hitbubbles[i];
        const b = hb.bubble;
        const c = b.color;
        const cx = hb.x + (hb.x2 - hb.x) * 0.5;
        const cy = hb.y + (hb.y2 - hb.y) * 0.5;
        let r = hb.radius;
        if (b.type !== animation_1.HitbubbleType.shield) {
            if (engine_1.dbg.drawHitbubblesRaw || engine_1.dbg.drawHitbubbleInfo) {
                ctx.strokeAlpha(c, 0.9);
                strokeCapsule(hb.x, hb.y, hb.x2, hb.y2, r);
            }
            gl_matrix_1.vec4.copy(parCol, c.rgba);
            parCol[3] = 0.1;
            drawGem(hb.x, hb.y, hb.x2, hb.y2, r, 0, 1, 1, parCol, parCol);
        }
        else {
            const density = hb.entity.shield.density;
            drawGem(cx, cy, cx, cy, hb.entity.shield.energy * 12, -Math.abs(r) * 0.45 + hb.entity.shield.energy * 6, 1, 1, hb.entity.palette.lighter[2].rgba, hb.entity.palette.lighter[2].rgba);
            r = r * 0.75;
            gl_matrix_1.vec4.copy(parCol, hb.entity.palette.darker[1].rgba);
            gl_matrix_1.vec4.scale(parCol, parCol, 0.25);
            parCol[3] = 1 * r;
            if (hb.entity.lag > 0) {
                parCol[3] = 4 * r;
            }
            else if (hb.entity.stun > 0) {
                parCol[3] = 3 * r;
            }
            drawPointLight(gl_matrix_1.vec3.set(parPos, cx, cy, 0), parCol, 0.2 + density * 0.3, 1.5 - density * 0.5, 0.4 + density * 0.7, 0.9);
        }
        if (hb.bubble.type === animation_1.HitbubbleType.shield) {
            gl_matrix_1.vec4.copy(parCol, c.rgba);
            parCol[3] = easing_1.Ease.quadOut(parCol[3]) * 0.7;
            drawGem(hb.x, hb.y, hb.x2, hb.y2, r, 0, 1, 1, parCol, parCol);
        }
        if (!engine_1.dbg.drawHitbubbleInfo) {
            continue;
        }
        ctx.strokeStyle(c);
        if (b.sakurai) {
            ctx.strokeRGBA(1, 1, 1, 0.8);
            strokeCircle(hb.x, hb.y, r / 3);
        }
        else if (b.knockback > 0 || b.growth > 0) {
            const pa = hb.angle(0, true, 0);
            const flip = (hb.x - hb.entity.x) * hb.entity.face >= 0 ? 1 : -1;
            ctx.strokeRGBA(1, 1, 1, 0.8);
            drawLine(hb.x, hb.y, hb.x + r * math_1.angleX(pa) * flip, hb.y - r * math_1.angleY(pa));
        }
    }
};
const renderLedgeGrab = () => {
    for (let i = 0; i < engine_1.entities.length; i++) {
        const entity = engine_1.entities[i];
        if (!utils_1.objHas(entity.activeAnimation.keyframeData, 'noLedgeGrab')
            && entity.airborne
            && (utils_1.objHas(entity.activeAnimation.keyframeData, 'grabDirections')
                || entity.activeAnimation.grabDirections)) {
            const directions = utils_1.objHas(entity.activeAnimation.keyframeData, 'grabDirections')
                ? entity.activeAnimation.keyframeData.grabDirections
                : entity.activeAnimation.grabDirections;
            if (entity.face === 1) {
                if ((directions & 64) !== 0) {
                    ctx.strokeRGBA(0.85, 0.85, 0.25, 0.7);
                    strokeRect(entity.x, entity.y
                        - entity.grabStart * entity.skeletonScale
                        - entity.grabHeight, entity.forwardGrabRange, entity.grabHeight);
                }
                if ((directions & 128) !== 0) {
                    ctx.strokeRGBA(0.8, 0, 0.8, 0.7);
                    strokeRect(entity.x - entity.reverseGrabRange, entity.y
                        - entity.grabStart * entity.skeletonScale
                        - entity.grabHeight, entity.reverseGrabRange, entity.grabHeight);
                }
            }
            else {
                if ((directions & 64) !== 0) {
                    ctx.strokeRGBA(0.85, 0.85, 0.25, 0.7);
                    strokeRect(entity.x - entity.forwardGrabRange, entity.y
                        - entity.grabStart * entity.skeletonScale
                        - entity.grabHeight, entity.forwardGrabRange, entity.grabHeight);
                }
                if ((directions & 128) !== 0) {
                    ctx.strokeRGBA(0.8, 0, 0.8, 0.7);
                    strokeRect(entity.x, entity.y
                        - entity.grabStart * entity.skeletonScale
                        - entity.grabHeight, entity.reverseGrabRange, entity.grabHeight);
                }
            }
        }
    }
};
const translated = new Float64Array(8);
const renderPosition = () => {
    for (let i = 0; i < engine_1.entities.length; i++) {
        const entity = engine_1.entities[i];
        const x = entity.x;
        const y = entity.y;
        if (entity.ecb === null) {
            continue;
        }
        ctx.strokeStyle(entity.palette.base);
        for (let j = 0; j < entity.hurtbubbleCount; j++) {
            const hb = entity.hurtbubbles[j];
            strokeCapsule(hb.x + x, hb.y + y, hb.x2 + x, hb.y2 + y, hb.radius);
        }
    }
};
const ecbVelocity = gl_matrix_1.vec2.create();
const renderECB = () => {
    const step = 1.25;
    for (let i = 0; i < engine_1.entities.length; i++) {
        const entity = engine_1.entities[i];
        const metrics = entity.lastCollisions.metrics || [];
        const dx = entity.lastCollisions.velocity[0];
        const dy = entity.lastCollisions.velocity[1];
        let x = 0;
        let y = 0;
        if (entity.ecb === null) {
            continue;
        }
        ecbVelocity[0] = entity.dx + entity.slide + entity.kbx;
        ecbVelocity[1] = -(entity.dy + entity.kby);
        x = entity.x + ecbVelocity[0];
        y = entity.y + ecbVelocity[1];
        ctx.strokeRGBA(0, 0, 1, 1);
        ctx.fillRGBA(0, 0, 1, 0.6);
        collision_1.Polygon.translate(translated, entity.ecb.vertices, ecbVelocity);
        pathPolygonZ(translated, step * 1);
        ctx.fill();
        drawLineZ(entity.x, entity.y, step * 1, x, y, step * 1);
        drawPlus(x, y, step * 1, 3);
        x = entity.x;
        y = entity.y;
        if (entity.ecb.highlight === 0) {
            ctx.fillRGBA(1, 1, 0, 0.6);
            ctx.strokeRGBA(1, 1, 0, 1);
        }
        else {
            ctx.fillRGBA(1, 0.5, 0, 0.6);
            ctx.strokeRGBA(1, 0.5, 0, 1);
        }
        pathPolygonZ(entity.ecb.vertices, 0);
        ctx.fill();
        drawPlus(x, y, 0, 3);
        ctx.strokeRGBA(1, 1, 1, 1);
        drawLine(x, y, x - dx, y - dy);
        drawPlus(x - dx, y - dy, 0, 3);
        for (let j = 0; j < metrics.length; j++) {
            const metric = metrics[j];
            if (metric.skipped === 1) {
                ctx.strokeRGBA(1, 0, 0, 1);
            }
            else if (metric.skipped === 2) {
                ctx.strokeRGBA(1, 1, 0, 1);
            }
            else if (metric.skipped === 3) {
                ctx.strokeRGBA(1, 0.5, 0.5, 1);
            }
            else if (metric.skipped === 4) {
                ctx.strokeRGBA(0.5, 0.5, 0.5, 1);
            }
            else if (metric.element === entity.lastCollisions.element) {
                ctx.strokeRGBA(0, 1, 0, 1);
            }
            else {
                ctx.strokeRGBA(0.77, 0, 0.77, 1);
            }
            if (metric.time !== -1) {
                drawPlus(x - dx * (1 - metric.time), y - dy * (1 - metric.time), 0, 3);
            }
            drawLineZ(metric.element.x, metric.element.y, 0, metric.element.x2, metric.element.y2, 0);
        }
    }
};
const colorBuffer = new Float32Array(128);
const renderStatus = (player) => {
    const damage = player.damage;
    const c = player.palette.base;
    const darker = player.palette.darker[0];
    const lighter = player.palette.lighter[0];
    const damageText = Math.ceil(damage).toString(10);
    const width = ctx.measureText(damageText);
    let x = player.x + player.headbubble.x;
    let y = player.y + player.headbubble.y - player.headbubble.radius - 8;
    let stunBar = player.stun;
    let stunMax = player.stunInitial;
    if (player.lastInjury.frame + 30 > ticks) {
        x
            = x
                + math_1.angleX((ticks % 16) * 73)
                    * (player.lastInjury.knockback * 0.15)
                    * ((30 - (ticks - player.lastInjury.frame)) * 0.1);
        y
            = y
                + math_1.angleY((ticks % 16) * 73)
                    * (player.lastInjury.knockback * 0.15)
                    * ((30 - (ticks - player.lastInjury.frame)) * 0.1);
    }
    ctx.strokeRGBA(0.06, 0.06, 0.06, 0.7);
    ctx.fillAlpha(c, 0.5);
    point(x, y);
    ctx.moveTo(working[0] + (width * 0.5 + 0) * player.face, working[1] - 15);
    ctx.lineTo(working[0] + (width * 0.5 + 10) * player.face, working[1] - 5);
    ctx.lineTo(working[0] + (width * 0.5 + 0) * player.face, working[1] + 5);
    ctx.lineTo(working[0] - (width * 0.5 + 9) * player.face, working[1] + 5);
    ctx.lineTo(working[0] - (width * 0.5 + 5) * player.face, working[1] - 5);
    ctx.lineTo(working[0] - (width * 0.5 + 9) * player.face, working[1] - 15);
    ctx.closePath();
    colorBuffer.set(lighter.rgba, 0 * 4);
    colorBuffer.set(lighter.rgba, 1 * 4);
    colorBuffer.set(lighter.rgba, 2 * 4);
    colorBuffer.set(c.rgba, 3 * 4);
    colorBuffer.set(c.rgba, 4 * 4);
    colorBuffer.set(darker.rgba, 5 * 4);
    colorBuffer.set(darker.rgba, 6 * 4);
    colorBuffer.set(darker.rgba, 7 * 4);
    colorBuffer.set(darker.rgba, 8 * 4);
    colorBuffer.set(c.rgba, 9 * 4);
    colorBuffer.set(c.rgba, 10 * 4);
    colorBuffer.set(lighter.rgba, 11 * 4);
    ctx.strokeColors(colorBuffer);
    ctx.fill();
    ctx.lineWidth(4);
    ctx.lineWidth(2);
    ctx.fillRGBA(1, 1, 1, 0.7);
    fillText(damageText, x, y);
    strokeText(damageText, x, y);
    if (player.animation === 'shield' || player.animation === 'shieldhit') {
        stunBar = player.shield.stun;
        stunMax = player.shield.initialStun;
    }
    if (stunBar > 0) {
        const pct = stunBar / stunMax;
        const h = Math.max(stunMax ** 0.35 * 0.1, 1);
        const w = Math.max(stunMax ** 0.5 * 5, 20);
        const hhalf = h * 0.5;
        const whalf = w * 0.5;
        y = y + 7;
        ctx.fillRGBA(1, 1, 1, pct);
        fillRect(x - whalf, y - hhalf, w, h);
        ctx.fillRGBA(1, 1, 1, 0.9);
        fillRect(x - whalf * pct, y - hhalf, w * pct, h);
    }
};
const renderStockDisplays = () => {
    ctx.lineWidth(4);
    for (let i = 0; i < engine_1.players.length; i++) {
        const player = engine_1.players[i];
        const c = player.palette.base;
        const l = player.palette.lighter[0];
        const d = player.palette.darker[0];
        const w = 90;
        const x = (screenWidth / engine_1.players.length) * (i + 0.5) - w * 0.5;
        const y = screenHeight * 0.95;
        const lastReducedSince = ticks - player.shield.lastReduced;
        const meterY = 13 * 2;
        const meterH = 17.5 * 2 - 5;
        let shield = Math.max(Math.min(player.shield.energy, 1), 0);
        point(x, y);
        ctx.fillHSLA(l.hsla[0], Math.max(c.hsla[1] * 0.9 - 0.1, 0), l.hsla[2], 1);
        ctx.fillRect(working[0] - 5, working[1] - meterY, w + 10, meterH);
        ctx.fillHSLA(c.hsla[0], Math.max(c.hsla[1] * 0.9 - 0.1, 0), Math.max(c.hsla[2], 0.4), 1);
        ctx.fillRect(working[0] - 5, working[1] - meterY, (w + 10) * shield, meterH);
        if (lastReducedSince < constants.ENERGY_FAST_REGEN_TIME) {
            let lastShield = Math.max(Math.min(player.shield.lastEnergy, 1), 0);
            if (lastReducedSince > constants.ENERGY_FAST_REGEN_PAUSE) {
                lastShield
                    = lastShield
                        - ((lastShield - shield)
                            * (lastReducedSince - constants.ENERGY_FAST_REGEN_PAUSE))
                            / (constants.ENERGY_FAST_REGEN_TIME
                                - constants.ENERGY_FAST_REGEN_PAUSE);
            }
            shield = lastShield;
        }
        ctx.fillHSLA(d.hsla[0], d.hsla[1] * 0.2, d.hsla[2] * 0.8, 1);
        ctx.fillRect(working[0] - 5 + (w + 10) * shield, working[1] - meterY, (w + 10) * (1.0 - shield), meterH);
        ctx.strokeHSLA(c.hsla[0], c.hsla[1] * 0.2, c.hsla[2] * 0.2, 1);
        if (!player.removed) {
            const stockX = working[0] - 5;
            const stockY = working[1] - 25 + 25 * 0.5;
            ctx.moveTo(stockX, stockY);
            for (let j = 0; j < player.heartbeat.width; j++) {
                const wave = player.heartbeat.buffer[(player.heartbeat.index + j) % player.heartbeat.width];
                const stockScale = (25 * 0.5) / 128;
                ctx.lineTo(stockX + j, stockY + stockScale * wave);
            }
            ctx.lineWidth(3);
            ctx.stroke();
            ctx.strokeRGBA(0.8, 0.8, 0.8, 1);
            ctx.lineWidth(1);
            ctx.stroke();
            ctx.lineWidth(4);
        }
        ctx.strokeHSLA(c.hsla[0], c.hsla[1] * 0.2, c.hsla[2] * 0.2, 1);
        ctx.strokeRect(working[0] - 5, working[1] - 26.5, w + 10, 30);
        ctx.textAlign(GFX.TextAlign.center);
        setFontSize(55);
        ctx.strokeHSLA(c.hsla[0], 0.1, c.hsla[2] > 0.15 ? 0.01 : 0.9, 1);
        ctx.fillHSLA(c.hsla[0], 0.1, c.hsla[2] > 0.15 ? 0.01 : 0.9, 1);
        fillText(player.symbol, x - 10 + 3, y - 19);
        ctx.fillHSLA(c.hsla[0], c.hsla[1], c.hsla[2], 1);
        paintText(player.symbol, x - 10, y - 22);
        if (!player.removed && player.lastFall.frame + 60 < ticks) {
            const shakeDuration = 30;
            let tx = x + 40;
            let ty = y - 25;
            let damageSize = Math.min(player.damage * 0.1, 5);
            const damageColor = (Math.max(0, player.damage - 40) * 3) / 255;
            const damageBrightness = 0.9;
            let damageGlow = 0;
            let bga = 1;
            let fga = 1;
            if (player.lastInjury.frame + shakeDuration > ticks) {
                const ipct = 1 - (ticks - player.lastInjury.frame) / shakeDuration;
                tx
                    = tx
                        + math_1.angleX((ticks % 16) * 73.961)
                            * (player.lastInjury.knockback * 0.4 + 3)
                            * ipct;
                ty
                    = ty
                        + math_1.angleY((ticks % 16) * 73.961)
                            * (player.lastInjury.knockback * 0.4 + 3)
                            * ipct;
                damageGlow = ipct * 0.8;
                damageSize = damageSize + player.lastInjury.knockback * ipct;
            }
            tx = tx + 62;
            ty = ty - damageSize * 0.5 + 5;
            if (player.lastFall.frame + 75 > ticks) {
                fga = (ticks - player.lastFall.frame - 60) / 15;
                bga = fga * fga;
            }
            setFontSize(Math.floor(40 + damageSize));
            ctx.textAlign(GFX.TextAlign.right);
            ctx.strokeRGBA(0, 0, 0, bga);
            ctx.fillRGBA(0.06, 0.06, 0.01, bga);
            fillText(Math.ceil(player.damage).toString(10), tx + 3, ty + 3);
            ctx.fillRGBA(damageBrightness - damageColor * 0.5 + damageGlow, damageBrightness - damageColor + damageGlow, damageBrightness - damageColor + damageGlow, fga);
            paintText(Math.ceil(player.damage).toString(10), tx, ty);
            if (player.symbol === 'Xe') {
                const l = player.data.get('chargeSuper') | 0;
                ctx.strokeRGBA(0.3, 0.3, 0.6, 1.0);
                ctx.fillRGBA(0.8, 0.8, 1.0, 1.0);
                for (let i = 0; i < l; i++) {
                    drawCircle(x + i * 16 + 5, y + 14, 5);
                }
            }
            if (player.symbol === 'C') {
                const l = player.data.get('bashCount') | 0;
                ctx.strokeRGBA(0.3, 0.6, 0.5, 1.0);
                ctx.fillRGBA(0.5, 0.8, 0.6, 1.0);
                for (let i = 0; i < l; i++) {
                    drawCircle(x + i * 16 + 5, y + 14, 5);
                }
            }
            if (player.symbol === 'Rh') {
                const burned = player.data.get('bonusBurned') + player.data.get('burned').size | 0;
                ctx.strokeRGBA(0.3, 0.3, 0.6, 1.0);
                ctx.fillRGBA(0.8, 0.8, 1.0, 1.0);
                ctx.textAlign(GFX.TextAlign.center);
                setFontSize(20);
                paintText(burned.toString(10), x + w * 0.5, y + 25);
            }
        }
    }
    ctx.textAlign(GFX.TextAlign.left);
    ctx.lineWidth(2);
};
const drawStickbox = (x, y, w, h, hmove, vmove, hmoveLast = hmove, vmoveLast = vmove) => {
    const hw = w * 0.5;
    const hh = h * 0.5;
    ctx.fillRGBA(0, 0, 0, 0.75);
    ctx.strokeRGBA(0, 0, 0, 1);
    pathCircle(x, y, hw, 8);
    ctx.fill();
    ctx.stroke();
    ctx.strokeRGBA(0.5, 0.5, 0.5, 0.75);
    drawLine(x - hw, y, x + hw, y);
    drawLine(x, y - hh, x, y + hh);
    ctx.fillRGBA(1, 1, 1, 0.75);
    pathCapsule(x + hmoveLast * hw, y + vmoveLast * hh, x + hmove * hw, y + vmove * hh, 2, 8);
    ctx.stroke();
    pathCircle(x + hmove * hw, y + vmove * hh, 3, 8);
    ctx.fill();
};
const heatmap = [];
let startHeatmap = 0;
let lastHeatmap = 0;
const renderDebug = () => {
    if (engine_1.dbg.training && engine_1.Game.frame > ticks - 600) {
        const a = ticks - engine_1.Game.frame < 500 ? 1 : (600 - ticks + engine_1.Game.frame) / 100;
        for (let i = 0; i < engine_1.Game.knockbackCurves.length; i++) {
            const verts = engine_1.Game.knockbackCurves[i].verts;
            pathLines(verts);
            ctx.strokeAlpha(engine_1.Game.knockbackCurves[i].color, a);
            ctx.lineWidth(engine_1.Game.knockbackCurves[i].lineWidth);
            ctx.stroke();
        }
    }
    if (engine_1.dbg.drawHeatmap) {
        const l = animation_1.getHitbubbleCount();
        if (l > 0) {
            if (ticks - lastHeatmap > 60) {
                heatmap.length = 0;
                startHeatmap = ticks;
                lastHeatmap = ticks;
            }
            lastHeatmap = ticks;
            for (let i = 0; i < l; i++) {
                const hb = animation_1.hitbubbles[i];
                if (hb.bubble.type === animation_1.HitbubbleType.shield) {
                    continue;
                }
                heatmap.push({
                    frame: ticks - startHeatmap,
                    x: hb.x,
                    y: hb.y,
                    x2: hb.x2,
                    y2: hb.y2,
                    radius: hb.radius,
                });
            }
        }
        for (let i = heatmap.length - 1; i >= 0; i--) {
            const h = heatmap[i];
            const timestep = 9;
            pathCapsule(h.x, h.y, h.x2, h.y2, h.radius, timestep);
            ctx.fillRGBA(math_1.addClamp(2, -h.frame / timestep), math_1.addClamp(0, h.frame / timestep), math_1.addClamp(-1, h.frame / timestep / 2), 1);
            ctx.fill();
        }
    }
};
let lastCalc = 0;
const buttonNames = ['A', 'B', 'Y', 'Z', 'L', 'R'];
const renderDebugUI = () => {
    let rightHeight = 0;
    let row = 0;
    const now = perf_hooks_1.performance.now();
    const recalc = now - lastCalc > 333;
    if (recalc) {
        lastCalc = Math.max(lastCalc + 333, now);
    }
    ctx.strokeRGBA(0.06, 0.06, 0.06, 1);
    ctx.fillStyle(uiTextColor);
    setMonoSize(14);
    if (engine_1.dbg.performance !== 0) {
        if (recalc) {
            const t1 = tickTimes1.reduce(sum, 0) / tickTimes1.length;
            const d1 = drawTimes1.reduce(sum, 0) / drawTimes1.length;
            const t2 = tickTimes2.reduce(sum, 0) / tickTimes2.length;
            const d2 = drawTimes2.reduce(sum, 0) / drawTimes2.length;
            const pt = particleTickTimes.reduce(sum, 0) / particleTickTimes.length;
            const pd = particleDrawTimes.reduce(sum, 0) / particleDrawTimes.length;
            avgRate = 1000 / (frameTimes.reduce(sum, 0) / frameTimes.length);
            minRate = 1000 / frameTimes.reduce(min, frameTimes.peek());
            maxRate = 1000 / frameTimes.reduce(max, frameTimes.peek());
            fpsText[0] = `${avgRate.toFixed(2)} (${minRate.toFixed(0)}-${maxRate.toFixed(0)})`;
            fpsText[1]
                = 'f  ' + msnsFramerate(frameTimes.reduce(sum, 0) / frameTimes.length);
            fpsText[2] = 'w  ' + msnsFramerate(t1 + d1 + t2 + d2);
            fpsText[3] = 't1 ' + msTime(t1);
            fpsText[4] = 'd1 ' + msTime(d1);
            fpsText[5] = 't2 ' + msTime(t2);
            fpsText[6] = 'd2 ' + msTime(d2);
            fpsText[7] = 'pt ' + msTime(pt);
            fpsText[8] = 'pd ' + msTime(pd);
            fpsText[9] = 'tick/s ' + framesLastSecond.toFixed(0);
        }
        for (let i = 0; i < fpsText.length; i++) {
            if ((engine_1.dbg.performance & (1 << i)) !== 0) {
                fillText(fpsText[i], 2, fontSize * row + fontSize);
                row++;
            }
        }
        row++;
    }
    if (engine_1.dbg.network !== 0) {
        networkText = [
            `frame=${ticks}`,
            `max buffer=${maxBuffer}`,
            ...controllers_1.connected.map(c => {
                const latency = !c.peer
                    ? 0
                    : (c.peer.channel.requestLatency.slice(-10).reduce((a, b) => a + b)
                        / c.peer.channel.requestLatency.length
                        + 0.5)
                        | 0;
                const peerFrame = c.client.sizeOffset + c.client.size;
                return `P${c.portNumber}: ${c.peer ? `ping=${latency.toFixed(1)}; ` : ''}ahead=${peerFrame - ticks}; start=${c.client.sizeOffset}`;
            }),
        ];
        ctx.fillRGBA(0, 0, 0, 0.5);
        fillRect(0, fontSize * row, 200, fontSize * (row + networkText.length + 1));
        ctx.fillStyle(uiTextColor);
        for (let i = 0; i < networkText.length; i++) {
            fillText(networkText[i], 2, fontSize * row + fontSize);
            row++;
        }
    }
    if (engine_1.dbg.training) {
        const top = fpsText.length * fontSize + fontSize * 2;
        for (let i = 0; i < training.length; i++) {
            fillText(training[i], 2, fontSize * i + top);
        }
    }
    if (engine_1.dbg.animations) {
        let drawn = 0;
        rightHeight = rightHeight + 12;
        ctx.strokeRGBA(1, 1, 1, 1);
        for (let i = 0; i < engine_1.entities.length; i++) {
            const e = engine_1.entities[i];
            const y = rightHeight + 13 * drawn;
            const tableWidth = 555;
            let tableX = screenWidth - tableWidth - 10;
            if (e.removed) {
                continue;
            }
            ctx.fillRGBA(0, 0, 0, 0.75);
            fillRect(tableX, y - 11, tableWidth + 5, 12);
            ctx.fillRGBA(1, 1, 1, 1);
            tableX = tableX + 5;
            fillText(e.name + (e.playerNumber === -1 ? '' : '-' + e.playerNumber), tableX, y);
            tableX = tableX + 100;
            ctx.textAlign(GFX.TextAlign.right);
            drawLine(tableX + 5, y - 12, tableX + 5, y + 1);
            tableX = tableX + 65;
            fillText(e.x.toFixed(1), tableX, y);
            tableX = tableX + 60;
            fillText(e.y.toFixed(1), tableX, y);
            drawLine(tableX + 5, y - 12, tableX + 5, y + 1);
            tableX = tableX + 45;
            fillText((e.dx + (e.airborne ? 0 : e.slide)).toFixed(1), tableX, y);
            tableX = tableX + 45;
            fillText(e.dy.toFixed(1), tableX, y);
            drawLine(tableX + 5, y - 12, tableX + 5, y + 1);
            tableX = tableX + 45;
            fillText(e.kbx.toFixed(1), tableX, y);
            tableX = tableX + 45;
            fillText(e.kby.toFixed(1), tableX, y);
            drawLine(tableX + 5, y - 12, tableX + 5, y + 1);
            tableX = tableX + 10;
            ctx.textAlign(GFX.TextAlign.left);
            fillText(e.animation, tableX, y);
            tableX = screenWidth - 25;
            ctx.textAlign(GFX.TextAlign.right);
            fillText(e.activeAnimation ? e.activeAnimation.frame + '' : '(null)', tableX, y);
            tableX = screenWidth - 10;
            fillText(e.airborne ? 'A' : 'G', tableX, y);
            ctx.textAlign(GFX.TextAlign.left);
            drawn++;
        }
        rightHeight = rightHeight + 13 * drawn;
    }
    if (engine_1.dbg.listHitbubbles) {
        const hbCount = animation_1.getHitbubbleCount();
        const tableWidth = 500;
        const tableX = screenWidth - tableWidth - 10;
        rightHeight = rightHeight + 12;
        for (let i = 0; i < hbCount; i++) {
            const y = rightHeight + 13 * i;
            const hb = animation_1.hitbubbles[i];
            const e = hb.entity;
            ctx.fillRGBA(0, 0, 0, 0.75);
            fillRect(tableX, y - 11, tableWidth, 12);
            ctx.fillRGBA(1, 1, 1, 1);
            fillText(`${e.name + (e.playerNumber === -1 ? '' : '-' + (e.playerNumber + 1))}; ${animation_1.HitbubbleType[hb.bubble.type]}; kb ${hb.bubble.knockback}/${hb.bubble.growth}; a ${hb.bubble.angle}; dmg ${hb.bubble.damage}; ${hb.bubble.x},${hb.bubble.y},${hb.radius}`, tableX + 5, y);
        }
        rightHeight = rightHeight + 13 * hbCount;
    }
    if (engine_1.dbg.controllers) {
        let drawn = 0;
        for (let i = 0; i < engine_1.entities.length; i++) {
            const e = engine_1.entities[i];
            const c = engine_1.entities[i].controller;
            if (e.removed) {
                continue;
            }
            if (!engine_1.dbg.drawAllControllers && e.playerNumber < 0) {
                continue;
            }
            if (c !== null) {
                const buttons = (c.attack ? 1 << 0 : 0)
                    | (c.special ? 1 << 1 : 0)
                    | (c.jump ? 1 << 2 : 0)
                    | (c.grab ? 1 << 3 : 0)
                    | (c.shield1Hard ? 1 << 4 : 0)
                    | (c.shield2Hard ? 1 << 5 : 0);
                const buttonPresses = (c.attackPress ? 1 << 0 : 0)
                    | (c.specialPress ? 1 << 1 : 0)
                    | (c.jumpPress ? 1 << 2 : 0)
                    | (c.grabPress ? 1 << 3 : 0)
                    | (c.shield1HardPress ? 1 << 4 : 0)
                    | (c.shield2HardPress ? 1 << 5 : 0);
                const y = drawn * 60 + 30;
                ctx.fillRGBA(1, 1, 1, 0.25);
                fillRect(172, y - 27, 152, 55);
                drawStickbox(200, y, 45, 45, c.hmove, c.vmove, c.hmoveLast, c.vmoveLast);
                drawStickbox(250, y, 45, 45, c.hright, c.vright);
                ctx.strokeRGBA(1, 1, 0.4, 0.5);
                if (c.upTap < 4) {
                    drawLine(200, y, 200, y - 22);
                }
                if (c.rightTap < 4) {
                    drawLine(200, y, 200 + 22, y);
                }
                if (c.downTap < 4) {
                    drawLine(200, y, 200, y + 22);
                }
                if (c.leftTap < 4) {
                    drawLine(200, y, 200 - 22, y);
                }
                ctx.fillRGBA(0, 0, 0, 0.5);
                fillRect(280, y - 22, 29, 10);
                fillRect(280, y + 12, 29, 10);
                ctx.fillRGBA(0.8, 0.8, 0.2, 0.9);
                fillRect(280, y - 22, 29 * c.shield1, 10);
                fillRect(280, y + 12, 29 * c.shield2, 10);
                for (let j = 0; j < 6; j++) {
                    const b = buttonNames[j];
                    const bx = b !== 'L' && b !== 'R'
                        ? 280 + 10 * j
                        : 280 + 30;
                    const by = b === 'L'
                        ? y - 17
                        : b === 'R'
                            ? y + 17
                            : y;
                    if ((buttons & (1 << j)) === 0) {
                        ctx.fillRGBA(0, 0, 0, 0.5);
                    }
                    else if ((buttonPresses & (1 << j)) === 0) {
                        switch (b) {
                            case 'A':
                                ctx.fillRGBA(0.1, 0.7, 0.1, 0.9);
                                break;
                            case 'B':
                                ctx.fillRGBA(0.8, 0.1, 0.1, 0.9);
                                break;
                            case 'Z':
                                ctx.fillRGBA(0.7, 0.1, 0.7, 0.9);
                                break;
                            default:
                                ctx.fillRGBA(0.7, 0.7, 0.2, 0.9);
                                break;
                        }
                    }
                    else {
                        switch (b) {
                            case 'A':
                                ctx.fillRGBA(0.5, 1.0, 0.5, 0.9);
                                break;
                            case 'B':
                                ctx.fillRGBA(1.0, 0.7, 0.7, 0.9);
                                break;
                            case 'Z':
                                ctx.fillRGBA(1.0, 0.5, 1.0, 0.9);
                                break;
                            default:
                                ctx.fillRGBA(1.1, 1.1, 0.8, 0.9);
                                break;
                        }
                    }
                    fillRect(bx, by - 5, 8, 10);
                    ctx.fillRGBA(0, 0, 0, 0.95);
                    fillText(b, bx, by + 5);
                }
                drawn++;
            }
        }
    }
};
const renderDoc = (doc) => {
    for (const n of doc) {
        const t = n;
        const style = t.box.style;
        if (utils_1.objHas(style, 'background')) {
            const color = style.background;
            ctx.fillRGBA(color[0], color[1], color[2], color[3]);
            pathRect(t.box.inner.x, t.box.inner.y, t.box.inner.w, t.box.inner.h);
            ctx.fill();
        }
        if (utils_1.objHas(style, 'border_size')
            && utils_1.objHas(style, 'border_color')) {
            const color = style.border_color;
            ctx.strokeRGBA(color[0], color[1], color[2], color[3]);
            ctx.lineWidth(style.border_size);
            strokeRect(t.box.inner.x, t.box.inner.y, t.box.inner.w, t.box.inner.h);
        }
        if (t.components.has('range') && utils_1.objHas(t.box.style, 'bar_color')) {
            const range = t.components.get('range');
            const pct = (range.value - range.min) / (range.max - range.min);
            const color = t.box.style.bar_color;
            ctx.fillRGBA(color[0], color[1], color[2], color[3]);
            pathRect(t.box.inner.x + 5, t.box.inner.y + 5, (t.box.inner.w - 10) * pct, t.box.inner.h - 10);
            ctx.fill();
        }
        if (t.components.has('text')) {
            const comp = t.components.get('text');
            ctx.textAlign(utils_1.objHas(style, 'align')
                ? GFX.TextAlign[style.align]
                : GFX.TextAlign.left);
            setFontSize(t.box.content.h);
            if (utils_1.objHas(style, 'color')) {
                const color = style.color;
                ctx.fillRGBA(color[0], color[1], color[2], color[3]);
            }
            else {
                ctx.fillRGBA(1, 1, 1, 1);
            }
            fillText(comp.translator.value, t.box.content.x
                + (ctx._textAlign === GFX.TextAlign.left
                    ? 0
                    : ctx._textAlign === GFX.TextAlign.center
                        ? t.box.content.w * 0.5
                        : t.box.content.w), t.box.content.y + t.box.content.h * 0.865, comp.parent.box.inner.w);
        }
        if (t.components.has('toggle')) {
            const toggle = t.components.get('toggle');
            if (toggle.value !== toggle.invert) {
                ctx.strokeRGBA(1, 0.4, 0.2, 0.9);
                drawLine(t.box.inner.x, t.box.inner.y + t.box.inner.h / 2, t.box.inner.x + t.box.inner.w, t.box.inner.y + t.box.inner.h / 2);
            }
        }
    }
};
const docEvents = [];
const mouseMoveDoc = (doc) => {
    for (const n of doc.byComponent.get('pointer')) {
        const box = n.box.inner;
        const c = n.components.get('pointer');
        if (c.synchronized && networking_1.isOnline()) {
            continue;
        }
        if (box.x > mx || box.y > my || box.x + box.w < mx || box.y + box.h < my) {
            if (n.box.class.has('is:hover')) {
                n.box.class.delete('is:hover');
                n.box.resolveStyle();
            }
            continue;
        }
        if (!n.box.class.has('is:hover')) {
            n.box.class.add('is:hover');
            n.events.emit('hover');
            n.box.resolveStyle();
        }
    }
};
mousePointer.event.listen('move', () => {
    if (menuDoc) {
        mouseMoveDoc(menuDoc);
        return;
    }
    if (utils_1.objHas(activeMode, 'doc')) {
        mouseMoveDoc(activeMode.doc);
    }
});
const mouseStateDoc = (doc, pointer, status, state) => {
    if (status === 'press') {
        if (state) {
            if (activeNode !== null && !networking_1.isOnline()) {
                activeNode.box.class.delete('is:active');
                activeNode.box.resolveStyle();
                activeNode = null;
            }
            for (const n of doc.byComponent.get('pointer')) {
                const box = n.box.inner;
                const c = n.components.get('pointer');
                if (c.synchronized && networking_1.isOnline()) {
                    n.events.emit('error');
                    continue;
                }
                if (box.x > mx
                    || box.y > my
                    || box.x + box.w < mx
                    || box.y + box.h < my) {
                    continue;
                }
                n.box.class.add('is:press');
                n.box.class.add('is:active');
                n.events.emit('press', null);
                n.box.resolveStyle();
                activeNode = n;
            }
        }
        else {
            if (activeNode === null) {
                return;
            }
            const box = activeNode.box.inner;
            const c = activeNode.components.get('pointer');
            if (c.synchronized && networking_1.isOnline()) {
                return;
            }
            activeNode.box.class.delete('is:press');
            activeNode.box.resolveStyle();
            if (box.x > mx
                || box.y > my
                || box.x + box.w < mx
                || box.y + box.h < my) {
                return;
            }
            activeNode.events.emit('trigger');
            c.click !== null && c.click(activeNode, pointer);
        }
    }
};
mousePointer.state.listen((pointer, status, state) => {
    if (menuDoc) {
        mouseStateDoc(menuDoc, pointer, status, state);
        return;
    }
    if (utils_1.objHas(activeMode, 'doc')) {
        mouseStateDoc(activeMode.doc, pointer, status, state);
    }
});
let startedMoving = -1;
let lastMove = -1;
const docKey = [0, 0];
const tickDoc = (doc) => {
    let velocity = 0;
    let adjust = 0;
    const axis = [0, 1];
    const adjustAxis = [1, 0];
    if (activeNode === null) {
        if (docKey[0] !== 0 || docKey[1] !== 0) {
            setActiveNode(doc.byComponent.get('pointer').entries().next().value[0]);
            startedMoving = ticks;
            lastMove = ticks;
        }
        for (const c of controllers_1.connected) {
            if (Math.abs(c.hmove) > 0.1
                || Math.abs(c.vmove) > 0.1
                || c.attackPress
                || c.startPress
                || c.specialPress
                || c.dupPress
                || c.ddownPress
                || c.dleftPress
                || c.drightPress) {
                setActiveNode(doc.byComponent.get('pointer').entries().next().value[0]);
                startedMoving = ticks;
                lastMove = ticks;
                break;
            }
        }
        if (activeNode === null) {
            return;
        }
    }
    if (activeNode.components.has('nav')) {
        if (activeNode.components.get('nav').direction === 'horizontal') {
            axis[0] = 1;
            axis[1] = 0;
            adjustAxis[0] = 0;
            adjustAxis[1] = 1;
        }
    }
    velocity = axis[0] * docKey[0] + axis[1] * docKey[1];
    adjust = adjustAxis[0] * docKey[0] + adjustAxis[1] * docKey[1];
    for (const c of controllers_1.connected) {
        velocity = velocity + axis[0] * c.hmove + axis[1] * c.vmove;
        adjust = adjust + adjustAxis[0] * c.hmove + adjustAxis[1] * c.vmove;
        velocity
            = velocity
                + (c.dup > 0 ? -axis[1] : 0)
                + (c.ddown > 0 ? axis[1] : 0)
                + (c.dleft > 0 ? -axis[0] : 0)
                + (c.dright > 0 ? axis[0] : 0);
        adjust
            = adjust
                + (c.dup > 0 ? -adjustAxis[1] : 0)
                + (c.ddown > 0 ? adjustAxis[1] : 0)
                + (c.dleft > 0 ? -adjustAxis[0] : 0)
                + (c.dright > 0 ? adjustAxis[0] : 0);
        if (c.attackPress || c.startPress) {
            const component = activeNode.components.get('pointer');
            if (!component.synchronized && networking_1.isOnline() && c.name === 'network') {
                continue;
            }
            activeNode.events.emit('press', c);
            activeNode.events.emit('trigger', c);
            if (c.startPress) {
                activeNode.events.emit('start', c);
                doc.events.emit('start', c);
            }
            component.click && component.click(activeNode, c);
            return;
        }
        if (c.specialPress) {
            activeNode.events.emit('back', c);
            doc.events.emit('back', c);
        }
    }
    if (Math.abs(adjust) < 0.25 || Math.abs(velocity) >= Math.abs(adjust)) {
        adjust = 0;
    }
    else {
        velocity = 0;
    }
    if (Math.abs(velocity) <= 0.1 && Math.abs(adjust) <= 0.1) {
        startedMoving = -1;
        return;
    }
    else if (startedMoving === -1) {
        startedMoving = ticks;
        lastMove = ticks;
        if (Math.abs(adjust) > 0.1) {
            activeNode.events.emit(adjust > 0 ? 'next' : 'previous');
        }
        if (Math.abs(velocity) > 0.1) {
            let next = velocity > 0 ? activeNode.nextSibling() : activeNode.previousSibling();
            if (next === null) {
                next
                    = activeNode.parent.children[velocity > 0 ? 0 : activeNode.parent.children.length - 1];
            }
            if (next !== null) {
                setActiveNode(next);
            }
        }
    }
    else {
        const rampTime = 10;
        const ramp = 12;
        const base = 5;
        const ease = Math.min(easing_1.Ease.quadIn((lastMove - startedMoving) / rampTime), 1);
        const delay = ((1.0 - ease) * ramp + base) / Math.abs(velocity);
        const adjustDelay = ((1.0 - ease) * ramp + base) / Math.abs(adjust);
        if (Math.abs(velocity) > 0.1 && ticks - lastMove >= delay) {
            let next = velocity > 0 ? activeNode.nextSibling() : activeNode.previousSibling();
            lastMove = ticks;
            if (next === null) {
                next
                    = activeNode.parent.children[velocity > 0 ? 0 : activeNode.parent.children.length - 1];
            }
            if (next !== null) {
                setActiveNode(next);
            }
        }
        if (Math.abs(adjust) > 0.1 && ticks - lastMove >= adjustDelay) {
            activeNode.events.emit(adjust > 0 ? 'next' : 'previous');
            lastMove = ticks;
        }
    }
};
const keydownDoc = (doc, e) => {
    const key = e.key;
    if (networking_1.isOnline()) {
        return false;
    }
    if (key === 'TAB') {
        const boxes = Array.from(doc.byComponent.get('pointer'));
        const direction = (e.mod & 3) === 0 ? 1 : -1;
        let index = -1;
        if (boxes.length === 0) {
            return true;
        }
        if (activeNode === null) {
            index = direction === 1 ? 0 : boxes.length - 1;
        }
        else {
            index = (boxes.indexOf(activeNode) + direction) % boxes.length;
            if (index < 0) {
                index = boxes.length - 1;
            }
        }
        setActiveNode(boxes[index]);
        return true;
    }
    else if (key === 'SPACE' || key === 'RETURN') {
        if (activeNode !== null) {
            activeNode.events.emit('trigger');
            activeNode.components.get('pointer').click
                && activeNode.components.get('pointer').click(activeNode, null);
        }
        return true;
    }
    switch (key) {
        case 'UP':
            docKey[1] = -1;
            return true;
        case 'DOWN':
            docKey[1] = 1;
            return true;
        case 'LEFT':
            docKey[0] = -1;
            return true;
        case 'RIGHT':
            docKey[0] = 1;
            return true;
    }
    return false;
};
const keyupDoc = (_doc, e) => {
    const key = e.key;
    if (networking_1.isOnline()) {
        return false;
    }
    switch (key) {
        case 'UP':
            docKey[1] = 0;
            return true;
        case 'DOWN':
            docKey[1] = 0;
            return true;
        case 'LEFT':
            docKey[0] = 0;
            return true;
        case 'RIGHT':
            docKey[0] = 0;
            return true;
    }
    return false;
};
const renderUI = () => {
    ctx.textAlign(GFX.TextAlign.center);
    if (engine_1.dbg.drawTimer && (began !== -1 || countTo !== -1)) {
        setFontSize(40);
        began !== -1
            && paintText(timeString(ticks - began), screenWidth / 2, fontSize * 2);
        countTo !== -1
            && paintText(timeString(countTo - ticks), screenWidth / 2, fontSize * 2);
    }
    if (engine_1.Game.comboCounter > 0) {
        setFontSize(20);
        paintText((engine_1.Game.comboCounter + 1).toFixed(0), screenWidth / 2, fontSize * 1);
    }
    ctx.textAlign(GFX.TextAlign.left);
};
const tickUI = () => {
    if (mp & 2) {
        mp = mp ^ 2;
    }
    if (mp & 8) {
        mp = mp ^ 8;
    }
    if (mp & 32) {
        mp = mp ^ 32;
    }
    mwdx = 0;
    mwdy = 0;
};
let framesLastSecond = 0;
let rendersThisSecond = 0;
let nextSecondOn = 0;
const keyboardBindings = {
    BACKQUOTE: 'console',
    t: 'say',
    z: 'frame step',
    x: 'freeze',
    v: 'save state',
    b: 'restore state',
    F11: 'fullscreen',
};
const keyDown = (e) => {
    const key = e.key;
    const action = utils_1.objHas(keyboardBindings, key)
        ? keyboardBindings[key]
        : '';
    if (action === 'fullscreen'
        || (key === 'RETURN' && (e.mod & 768) !== 0)) {
        Native.toggleFullscreen();
        return;
    }
    if (terminal_1.debugActive()) {
        return;
    }
    if (controllers_1.pollEvent(e)) {
        return;
    }
    if (action === 'console') {
        terminal_1.debugActivate(true);
        return;
    }
    if (action === 'say') {
        terminal_1.debugActivate(true, 'say ');
        return;
    }
    if (networking_1.isOnline()) {
        if (e.key === 'ESCAPE') {
            networking_1.disconnectNetplay();
        }
        return;
    }
    if (e.key === 'ESCAPE') {
        if (menuDoc) {
            menuDoc.free();
            menuDoc = null;
        }
        else {
        }
    }
    if (activeUI !== null) {
        let wasok = activeUI.keyDown(e);
        if (e.key === 'ESCAPE') {
            wasok = true;
            if (activeUI !== null) {
                activeUI = null;
            }
        }
        if (wasok) {
            return;
        }
    }
    if (menuDoc) {
        keydownDoc(menuDoc, e);
        return;
    }
    if (utils_1.objHas(activeMode, 'doc') && keydownDoc(activeMode.doc, e)) {
        return;
    }
    if (action === 'frame step') {
        if (tasOn) {
            exports.scheduleCallback(() => {
                gamePaused = false;
                exports.scheduleCallback(() => {
                    gamePaused = true;
                });
            });
        }
        return;
    }
    if (action === 'freeze') {
        if (!tasOn) {
            tasOn = true;
            engine_1.dbg.log('Froze\n');
            gamePaused = true;
            return;
        }
        tasOn = false;
        engine_1.dbg.log('Thawed\n');
        gamePaused = false;
        return;
    }
    if (action === 'save state') {
        engine_1.dbg.log('Saved state\n');
        savestate();
        return;
    }
    if (action === 'restore state') {
        if (savedState.entities.length > 0) {
            engine_1.dbg.log('Restored state\n');
        }
        loadstate();
        return;
    }
};
const pollEvent = (e) => {
    if (e === null) {
        return false;
    }
    switch (e.type) {
        case 6:
            keyDown(e);
            return true;
        case 5:
            if (menuDoc) {
                keyupDoc(menuDoc, e);
                return true;
            }
            if (utils_1.objHas(activeMode, 'doc') && keyupDoc(activeMode.doc, e)) {
                return true;
            }
            if (activeUI !== null) {
                if (activeUI.keyUp(e)) {
                    return true;
                }
            }
            return controllers_1.pollEvent(e);
        case 8:
        case 9:
        case 11:
        case 13:
        case 12:
            return controllers_1.pollEvent(e);
        case 7:
            if (activeUI !== null) {
                activeUI.text(e.text);
            }
            return true;
        case 1:
            setMousePos(e.x, e.y);
            return true;
        case 3:
            mp = mp | (3 << (e.button * 2));
            setMousePos(e.x, e.y);
            if (e.button === 0) {
                mousePointer.state.add('press');
            }
            else if (e.button === 1) {
                mousePointer.state.add('back');
            }
            else if (e.button === 2) {
                mousePointer.state.add('context');
            }
            return true;
        case 2:
            mp = mp ^ (1 << (e.button * 2));
            setMousePos(e.x, e.y);
            if (e.button === 0) {
                mousePointer.state.delete('press');
            }
            else if (e.button === 1) {
                mousePointer.state.delete('back');
            }
            else if (e.button === 2) {
                mousePointer.state.delete('context');
            }
            return true;
        case 4:
            mwdx = mwdx + e.x;
            mwdy = mwdy - e.y;
            return true;
        case 0:
            refreshCanvasSize(e.x, e.y);
            return true;
        case 14:
            exitGame = true;
            return true;
        case 15:
            controllers_1.connectAdapter(Native.buffers.adapter);
            return true;
    }
    return false;
};
let skippedLastFrame = false;
const frameTick = () => {
    const dbgActive = terminal_1.debugActive();
    const currentTime = (perf_hooks_1.performance.now() - startTime) * frameMs;
    const toFrame = (renderTime - lostFrames) | 0;
    let frameDiff = (toFrame - frames) | 0;
    let renderedThisFrame = false;
    let polledThisFrame = true;
    let e = null;
    renderTime = currentTime;
    rafTimes.push(utils_1.stopwatch.stop());
    !skippedLastFrame && frameTimes.lap();
    Native.poll();
    if (frameDiff > 30) {
        if (!networking_1.networkEstablished()) {
            lostFrames = lostFrames + frameDiff - 1;
            frameDiff = 1;
        }
        else {
            frameDiff = Math.ceil(Math.sqrt(frameDiff)) | 0;
        }
    }
    if (nextSecondOn < perf_hooks_1.performance.now()) {
        framesLastSecond = rendersThisSecond;
        rendersThisSecond = 0;
        nextSecondOn = perf_hooks_1.performance.now() + 1000;
    }
    for (;;) {
        e = Native.pollEvent();
        if (e === null) {
            break;
        }
        if (dbgActive && terminal_1.pollEvent(e)) {
            continue;
        }
        if (pollEvent(e)) {
            continue;
        }
        if (e.type !== 5) {
            console.log('Got unknown event:', e);
        }
    }
    if (frameDiff === 0) {
        Native.tick();
        utils_1.stopwatch.start();
        setImmediate(frameTick);
        skippedLastFrame = true;
        return;
    }
    if (networking_1.networkWaiting()) {
        exports.loseFrames(frameDiff);
        frameDiff = 0;
    }
    skippedLastFrame = false;
    Native.pollUSB();
    controllers_1.pollUSB();
    networking_1.pollNetwork(pollTicks);
    controllers_1.checkConnections();
    controllers_1.processConnection(pollTicks, destroy);
    for (let i = 0; i < frameDiff; i++) {
        controllers_1.tickConnections(pollTicks, create);
        for (const c of controllers_1.connected) {
            if (!c.polling) {
                continue;
            }
            const outerSize = c.client.size + c.client.sizeOffset;
            if (outerSize >= pollTicks && c.client.sizeOffset <= pollTicks) {
                if (outerSize - frames > maxBuffer) {
                    polledThisFrame = false;
                    continue;
                }
                const oldFrame = c.client.read(pollTicks);
                const frame = c.poll();
                if (!c.client.commit(pollTicks, frame)) {
                    console.log('Error in commit!', pollTicks);
                    console.log('old', controllers_1.prettyFrame(oldFrame, pollTicks));
                    console.log('new', controllers_1.prettyFrame(c.client.read(pollTicks), pollTicks));
                }
            }
            else {
                console.error('Controller in weird state!', c.name, c.client.size, c.client.sizeOffset, outerSize, pollTicks);
            }
        }
        let startPressed = false;
        const ok = controllers_1.inputCapacitor.read(frames);
        if (polledThisFrame) {
            pollTicks++;
        }
        else {
            exports.loseFrames(1);
        }
        if (!ok) {
            break;
        }
        if (!renderedThisFrame) {
            renderedThisFrame = true;
            rendersThisSecond++;
        }
        for (let j = 0; j < controllers_1.connected.length; j++) {
            const c = controllers_1.connected[j];
            c.step(c.client.cache);
            if ((ticks - engine_1.activeModeTime > 5) && c.startPress && !startPressed) {
                startPressed = true;
                if (gamePaused) {
                    activeMode.unpause && activeMode.unpause(c);
                }
                else {
                    activeMode.pause && activeMode.pause(c);
                }
                activeMode.start && activeMode.start(c);
            }
        }
        runningSchedule = true;
        while (scheduledCallbacks.length > 0) {
            scheduledCallbacks.shift()();
        }
        runningSchedule = false;
        scheduledCallbacks.push(...todoScheduledCallbacks);
        todoScheduledCallbacks.length = 0;
        if (!gamePaused) {
            utils_1.stopwatch.start();
            engine_1.engineTick1(ticks);
            if (ticks - engine_1.activeModeTime > 5) {
                engine_1.engineTick2(ticks);
            }
            tickTimes1.push(utils_1.stopwatch.stop());
            utils_1.stopwatch.start();
            tickFrame();
            tickTimes2.push(utils_1.stopwatch.stop());
            ticks++;
        }
        else {
            activeMode.paused && activeMode.paused();
        }
        frames++;
    }
    networking_1.sendNetplay();
    utils_1.stopwatch.start();
    ctx.fillStyle(backgroundStyle);
    worldTransform = combined;
    ctx.transform = combined;
    for (let i = 0; i < exports.mouseListeners.length; i++) {
        !exports.mouseListeners[i].disable
            && !exports.mouseListeners[i].removed
            && exports.mouseListeners[i].mouseListener();
    }
    renderStage();
    utils_1.objHas(activeMode, 'prePaint') && activeMode.prePaint();
    exports.Effects.render();
    if (engine_1.dbg.drawLedgeGrab) {
        renderLedgeGrab();
    }
    if (engine_1.dbg.drawECB) {
        renderECB();
    }
    if (engine_1.dbg.drawRawPosition) {
        renderPosition();
    }
    ctx.strokeStyle(uiTextColor);
    ctx.fillStyle(uiTextColor);
    exports.Effects.renderTop();
    ctx.strokeStyle(uiTextColor);
    ctx.fillStyle(uiTextColor);
    if (!gamePaused) {
        utils_1.objHas(activeMode, 'paint') && activeMode.paint();
        worldTransform = uiView;
        ctx.transform = uiView;
        if (engine_1.dbg.drawUI) {
            utils_1.objHas(activeMode, 'paintUIEarly') && activeMode.paintUIEarly();
        }
        worldTransform = combined;
        ctx.transform = combined;
    }
    else {
        if (utils_1.objHas(activeMode, 'paintPaused')) {
            activeMode.paintPaused();
        }
        else if (utils_1.objHas(activeMode, 'paint')) {
            activeMode.paint();
        }
    }
    if (engine_1.dbg.drawUI) {
        renderDebug();
        worldTransform = uiView;
        ctx.transform = uiView;
        menuDoc && renderDoc(menuDoc);
        utils_1.objHas(activeMode, 'doc') && renderDoc(activeMode.doc);
        for (let i = 0; i < engine_1.uiEntities.length; i++) {
            !engine_1.uiEntities[i].removed
                && !engine_1.uiEntities[i].hide
                && engine_1.uiEntities[i].paint
                && engine_1.uiEntities[i].paint();
        }
        utils_1.objHas(activeMode, 'paintUI') && activeMode.paintUI();
        gamePaused
            && utils_1.objHas(activeMode, 'paintUIPaused')
            && activeMode.paintUIPaused();
        renderStockDisplays();
        exports.Effects.renderUI();
        ctx.strokeStyle(uiTextColor);
        ctx.fillStyle(uiTextColor);
        setMonoSize(15);
        renderDebugUI();
        setSansSize(15);
        ctx.strokeStyle(uiTextColor);
        ctx.fillRGBA(0.85, 0.85, 0.85, 1);
        ctx.strokeRGBA(0.15, 0.15, 0.15, 1);
        renderUI();
        tickUI();
        ctx.textAlign(GFX.TextAlign.right);
        setMonoSize(14);
        ctx.fillRGBA(1, 1, 1, 0.8);
        ctx.fillText(`antistatic v${engine_1.version} alpha`, screenWidth / 2 - 5, screenHeight / 2 - 7);
        ctx.textAlign(GFX.TextAlign.left);
    }
    if (engine_1.dbg.drawTerminal) {
        ctx.strokeStyle(uiTextColor);
        ctx.fillStyle(uiTextColor);
        setMonoSize(15);
        terminal_1.renderTick(ctx, -screenWidth / 2, -screenHeight / 2, screenWidth, screenHeight - 50, 15);
    }
    drawTimes1.push(utils_1.stopwatch.stop());
    utils_1.stopwatch.start();
    worldTransform = combined;
    ctx.transform = combined;
    if (engine_1.dbg.drawHitbubbles) {
        renderHitbubbles();
    }
    if (engine_1.dbg.drawHitbubbles) {
        renderHitbubbles();
    }
    if (engine_1.dbg.drawCharacters) {
        renderHurtbubbles();
        renderEntities();
    }
    if (engine_1.dbg.drawHurtbubbles) {
        renderRawHurtbubbles();
    }
    setSansSize(14);
    if (engine_1.dbg.drawUI && engine_1.dbg.drawStatus) {
        ctx.strokeRGBA(0, 0, 0, 1);
        ctx.textAlign(GFX.TextAlign.center);
        for (let i = 0; i < engine_1.entities.length; i++) {
            const player = engine_1.entities[i];
            if (player.dummy || player.important) {
                renderStatus(player);
            }
        }
        ctx.textAlign(GFX.TextAlign.left);
    }
    drawTimes2.push(utils_1.stopwatch.stop());
    gfx.camera(view, perspective, combined, cameraPosition, cameraVector, uiView);
    gfx.renderFrame();
    Native.tick();
    Native.render();
    if (exitGame) {
        Native.exitGame();
        setTimeout(() => Native.quitting.then(() => {
            process.exit(0);
        }), 0);
        return;
    }
    utils_1.stopwatch.start();
    setImmediate(frameTick);
};
const translators = {
    pressStart: i18n_1.makeTranslator('press-start'),
    Characters: i18n_1.makeTranslator('Characters'),
    Stages: i18n_1.makeTranslator('Stages'),
    AI: i18n_1.makeTranslator('AI'),
    quitInfo: i18n_1.makeTranslator('quit-info'),
    paused: i18n_1.makeTranslator('paused'),
    tagInfo: i18n_1.makeTranslator('tag-info'),
};
const logos = null;
const spawnSandbag = (name = '', props = {}) => {
    const defaultCharacter = utils_1.objHas(fsutils_1.config, 'defaultCharacter')
        && characters.includes(fsutils_1.config.defaultCharacter)
        ? fsutils_1.config.defaultCharacter
        : 'Silicon';
    const a = new engine_1.Animatable({
        type: 0,
        name: name !== '' ? name : defaultCharacter,
        controller: new controllers_1.Controller(null),
        dummy: true,
        ...props
    });
    engine_1.entities.push(a);
    a.setAI(engine_1.ai);
    a.setPalette(color_1.ColorPalette.random());
    return a;
};
const renderDebugMode = () => {
    let activeCursors = 0;
    const menuUI = [];
    const start = (controller) => {
        const hook = controller.hook;
        if (hook instanceof Cursor) {
            hook.removed = true;
            controller.hook = hook.entity;
            activeCursors--;
            if (activeCursors <= 0) {
                closeMenu();
            }
        }
        else {
            if (activeCursors <= 0) {
                openMenu();
            }
            activeCursors++;
            if (hook.cursor === null) {
                hook.cursor = new CharacterCursor(controller, 110 + controller.portNumber * 15, 75, {
                    spring: [0, 0, 900, 400],
                    entity: hook,
                    color: hook.palette.base,
                    seed: hook.palette.seed,
                    colorSelectable: true,
                    tick: function () {
                        if (!this.controller.grabPress) {
                            this.entity.lag = 1;
                        }
                        if (this.controller.jumpPress) {
                            this.entity.style = (math_1.lqrandomSync() * this.entity.styles.length) | 0;
                            this.setColor(color_1.Color.random());
                        }
                    },
                });
                menuUI.push(hook.cursor);
                engine_1.uiEntities.push(hook.cursor);
            }
            else {
                hook.cursor.removed = false;
                controller.hook = hook.cursor;
            }
        }
    };
    const connect = (controller) => {
        const defaultCharacter = utils_1.objHas(fsutils_1.config, 'defaultCharacter')
            && characters.includes(fsutils_1.config.defaultCharacter)
            ? fsutils_1.config.defaultCharacter
            : 'Silicon';
        const chardude = new engine_1.Animatable({
            type: 0,
            name: defaultCharacter,
            important: true,
            controller
        });
        const entrance = stage.entrances[engine_1.players.length % stage.entrances.length];
        chardude.x = entrance.x;
        chardude.y = entrance.y;
        chardude.face = entrance.face ? 1 : -1;
        chardude.stocks = 3;
        chardude.style = controller.style % chardude.styles.length;
        chardude.setPalette(controller.color
            ? color_1.ColorPalette.fromColorSeed(controller.color, controller.seed, chardude.styles[chardude.style])
            : color_1.ColorPalette.random());
        controller.color = chardude.palette.base;
        controller.seed = chardude.palette.seed;
        controller.style = chardude.style;
        controller.team = chardude.team;
        controller.hook = chardude;
        controller.character = defaultCharacter;
        chardude.playerNumber = controller.portNumber;
        engine_1.entities.push(chardude);
        engine_1.players.push(chardude);
    };
    const disconnect = (controller) => {
        const hook = controller.hook;
        hook.removed = true;
        if (hook instanceof Cursor) {
            hook.removed = true;
            hook.entity.removed = true;
            controller.hook = hook.entity;
            activeCursors--;
            if (activeCursors <= 0) {
                engine_1.uiEntities.length = 0;
                for (let i = 0; i < menuUI.length; i++) {
                    menuUI[0].disable = true;
                }
            }
            engine_1.players.splice(engine_1.players.indexOf(hook.entity), 1);
        }
        else {
            engine_1.players.splice(engine_1.players.indexOf(hook), 1);
        }
    };
    const paint = () => {
        ctx.drawImage(logos, screenWidth / 2 - 212, -screenHeight / 2, 212, 61);
        ctx.fillRGBA(1, 1, 1, 0.95);
        setFontSize(20);
        ctx.textAlign(GFX.TextAlign.right);
        ctx.fillText(translators.pressStart.value, screenWidth / 2 - 5, -screenHeight / 2 + 50);
        ctx.textAlign(GFX.TextAlign.left);
    };
    const openMenu = () => {
        engine_1.uiEntities.push(...menuUI);
        for (let i = 0; i < menuUI.length; i++) {
            menuUI[i].disable = false;
            menuUI[i].hide = false;
        }
    };
    const closeMenu = () => {
        activeCursors = 0;
        engine_1.uiEntities.length = 0;
        activeUI = null;
        for (let i = 0; i < menuUI.length; i++) {
            menuUI[i].disable = true;
            menuUI[i].hide = true;
            if (menuUI[i] instanceof Cursor) {
                const cursor = menuUI[i];
                cursor.removed = true;
                cursor.controller.hook = cursor.entity;
            }
        }
    };
    const control = (_entity, controller) => {
        if (!controller.shield) {
            if (controller.drightPress) {
                savestate();
                return true;
            }
            if (controller.dleftPress) {
                loadstate();
                return true;
            }
            return false;
        }
        if (controller.dupPress) {
            engine_1.dbg.training = true;
            return true;
        }
        if (controller.drightPress) {
            engine_1.dbg.controllers = true;
            return true;
        }
        if (controller.ddownPress) {
            engine_1.dbg.training = false;
            return true;
        }
        if (controller.dleftPress) {
            engine_1.dbg.controllers = false;
            return true;
        }
        if (networking_1.isOnline()) {
            return false;
        }
        if (controller.drightPress) {
            if (!tasOn) {
                tasOn = true;
                engine_1.dbg.log('Froze\n');
                gamePaused = true;
            }
            else {
                tasOn = false;
                engine_1.dbg.log('Thawed\n');
                gamePaused = false;
            }
            return true;
        }
        if (controller.drightPress) {
            if (tasOn) {
                exports.scheduleCallback(() => {
                    gamePaused = false;
                    exports.scheduleCallback(() => {
                        gamePaused = true;
                    });
                });
            }
            return true;
        }
        return false;
    };
    engine_1.setStage(new stage_1.Stage(stage_1.stageByName(fsutils_1.config.defaultStage ? fsutils_1.config.defaultStage : 'Ruins')));
    menuUI.push(new Button('Sandbag', null, 600, 50, 200, 50, {
        click: () => spawnSandbag(),
        press: () => spawnSandbag(),
    }));
    menuUI.push(new Selection('mode-' + constants.NAME, null, 650, 20, 100, 20, 2, Object.getOwnPropertyNames(engine_1.modes).map(s => 'mode-' + s), {
        select: (selected, _index) => {
            engine_1.setConstants(engine_1.modes[selected.split('-')[1]]);
        },
    }));
    menuUI.push(new Button('Close-Symbol', null, 800, 20, 20, 20, {
        click: () => {
            closeMenu();
        },
        press: () => {
            closeMenu();
        },
    }));
    menuUI.push(new Selection(stage_1.stages[0].name, null, 10, 110, 350, 50, 9, stage_1.stages.map(val => val.name), {
        select: (_selected, index) => {
            exports.scheduleCallback(() => {
                exports.Effects.reset();
                engine_1.setStage(new stage_1.Stage(stage_1.stages[index]));
                for (let i = 0; i < engine_1.entities.length; i++) {
                    if (!engine_1.entities[i].airborne) {
                        engine_1.entities[i].setAnimation('airborne', true, true);
                    }
                    engine_1.entities[i].airborne = true;
                    engine_1.entities[i].platform = null;
                    engine_1.entities[i].hover = null;
                }
            });
        },
    }));
    menuUI.push(new Selection('Swap-Character', null, 400, 110, 400, 50, 9, characters, {
        select: function (selected, _index, cursor) {
            if (cursor instanceof CharacterCursor) {
                cursor.swapCharacter && cursor.swapCharacter(selected);
            }
            else {
                const a = new engine_1.Animatable({
                    type: 0,
                    name: selected,
                    controller: new controllers_1.Controller(null),
                    dummy: true,
                });
                a.setAI(engine_1.ai);
                a.setPalette(color_1.ColorPalette.random());
                engine_1.entities.push(a);
            }
            this.reset();
        },
        back: function () {
            this.reset();
        },
    }));
    menuUI.push(new Selection('Overlays', null, 350, 70, 150, 20, 8, [
        'training',
        'controllers',
        'animations',
        'drawHitbubbleInfo',
        'drawECB',
        'drawHeatmap',
        'drawRawPosition',
        'drawStage',
    ], {
        select: function (selected, _index) {
            if (selected === 'drawStage') {
                engine_1.dbg.drawStage = (engine_1.dbg.drawStage + 1) % 3;
            }
            else if (selected === 'drawRawPosition') {
                ;
                engine_1.dbg[selected] = !engine_1.dbg[selected];
                engine_1.dbg.drawHitbubblesRaw = engine_1.dbg[selected];
            }
            else {
                ;
                engine_1.dbg[selected] = !engine_1.dbg[selected];
            }
            this.reset();
        },
        back: function () {
            this.reset();
        },
    }));
    menuUI.push(new Button('Exit', null, 10, 20, 75, 20, {
        click: () => {
            exitGame = true;
        },
        press: () => {
            exitGame = true;
        },
    }));
    menuUI.push(new Button('main', null, 120, 20, 150, 20, {
        click: () => {
            setActiveMode(renderMain);
        },
        press: () => {
            setActiveMode(renderMain);
        },
    }));
    menuUI.push(new Button('versus', null, 100, 50, 200, 50, {
        click: () => {
            setActiveMode(renderVersusSelect);
        },
        press: () => {
            setActiveMode(renderVersusSelect);
        },
    }));
    cameraType = fitOnScreen;
    began = ticks;
    countTo = -1;
    fsutils_1.config.lastMode = 'debug';
    fsutils_1.config.save();
    terminal_1.getShell().execute('sh -q /home/debug_start');
    return {
        start,
        connect,
        control,
        disconnect,
        run: () => { },
        started: true,
        paintUI: paint,
    };
};
class Dropper {
    constructor(owner) {
        this.x = 0;
        this.y = 0;
        this.holding = false;
        this.owner = null;
        this.returned = 0;
        this.owner = owner;
        this.x = owner.x;
        this.y = owner.y;
        this.holding = true;
        this.color = owner instanceof Cursor ? owner.color : owner.palette.base;
        owner.dropper = this;
    }
}
exports.Dropper = Dropper;
const renderVersusSelect = (data = {}) => {
    const droppers = [];
    const cursors = [];
    let stageSelected = -1;
    const selectStage = (val) => {
        if (stageSelected === val) {
            return;
        }
        exports.Effects.reset();
        stageNeutralCamera();
        stageSelected = val;
        engine_1.setStage(new stage_1.Stage(stage_1.stages[val]));
    };
    const buttonHandlers = (val) => {
        return {
            hover: (_cursor, hovered) => {
                if (!hovered) {
                    return;
                }
                selectStage(val);
            },
            click: (_cursor) => {
                stageSelected = val;
                start();
            },
            press: (_cursor) => {
                stageSelected = val;
                start();
            },
        };
    };
    let sandbags = {
        Silicon: 0,
        Carbon: 0,
        Iron: 0,
        Helium: 0,
        Xenon: 0,
        Rhodium: 0,
        'Random-Q': 0,
    };
    if (utils_1.objHas(data, 'sandbags')) {
        sandbags = data.sandbags;
    }
    const dropperSpawnBoxes = {};
    const choosableCharacters = characters.concat(['Random-Q']);
    const characterWidth = 50;
    const bTranslators = new Map();
    for (let i = 0; i < choosableCharacters.length; i++) {
        const character = choosableCharacters[i];
        const s = { x: 200, y: 170 + 50 * i, w: characterWidth, h: 50 };
        const button = new Button(character, null, s.x, s.y, 200, s.h - 5, {
            press: (cursor) => {
                if (!cursor.dropper.holding
                    && (cursor.dropper.x - cursor.x) ** 2
                        + (cursor.dropper.y - cursor.y) ** 2
                        < 20 ** 2) {
                    cursor.back();
                }
                else {
                    cursor.character(character);
                }
            },
        });
        bTranslators.set(character, button.translator);
        dropperSpawnBoxes[character] = s;
        engine_1.uiEntities.push(button);
        engine_1.uiEntities.push(new Button('Sub-Symbol', null, s.x + 220, s.y + s.h * 0.25 - 2.5, s.h * 0.5, s.h * 0.5, () => {
            sandbags[character] = Math.max(sandbags[character] - 1, 0);
            updateStartUI();
        }));
        engine_1.uiEntities.push(new Button('Add-Symbol', null, s.x + 270, s.y + s.h * 0.25 - 2.5, s.h * 0.5, s.h * 0.5, () => {
            sandbags[character]++;
            updateStartUI();
        }));
    }
    const cursorMap = new WeakMap();
    const uiMap = new WeakMap();
    const updateModels = () => {
    };
    const updateUI = () => {
        for (const cursor of cursors) {
            for (const e of engine_1.uiEntities) {
                if (e instanceof ObjectButton
                    && e.owner === cursor
                    && e.translator.id === 'Tapjump-Symbol') {
                    e.y = 140 + controllers_1.connected.indexOf(cursor.controller) * 90;
                    e.y2 = e.y + e.h;
                }
            }
        }
    };
    const disconnect = (controller) => {
        if (uiMap.has(controller)) {
            for (const a of uiMap.get(controller)) {
                engine_1.uiEntities.splice(engine_1.uiEntities.indexOf(a), 1);
            }
        }
        if (!cursorMap.has(controller)) {
            console.warn('controller not in cursor map');
            return;
        }
        const cursor = cursorMap.get(controller);
        const index = cursors.indexOf(cursor);
        if (index !== -1) {
            droppers.splice(droppers.indexOf(cursor.dropper), 1);
            cursors.splice(index, 1);
            engine_1.uiEntities.splice(engine_1.uiEntities.indexOf(cursor), 1);
            if (teams) {
                redoTeamColors();
            }
        }
        updateUI();
    };
    const connect = (controller) => {
        const cursor = new CharacterCursor(controller, 150, 90 + 20 + controllers_1.connected.indexOf(controller) * 90 + 30 * 0.33, {
            back: function () {
                this.selected = null;
                this.controller.character = null;
                if (!this.dropper.holding) {
                    this.dropper.holding = true;
                    this.dropper.returned = ticks;
                }
                updateStartUI();
            },
            select: function (name) {
                this.selected = name;
                this.controller.character = name;
                if (this.dropper) {
                    this.dropper.holding = false;
                    this.dropper.x = this.x;
                    this.dropper.y = this.y;
                }
                updateStartUI();
            },
            selected: null,
            character: function (name) {
                this.select(name);
                if (data.teams) {
                    redoTeamColors();
                }
                updateStartUI();
            },
            color: controller.color,
            colorSelectable: true,
            restrict: [25, 15, 1200, 750],
            tick: function () {
                if (this.controller.jumpPress) {
                    if (this.entity) {
                        this.entity.style = (math_1.lqrandomSync() * this.entity.styles.length) | 0;
                    }
                    if (teams) {
                        this.controller.team
                            = (this.controller.team + 1) % teamNames.length;
                        redoTeamColors();
                        updateStartUI();
                    }
                    else {
                        this.setColor(color_1.Color.random());
                        updateModels();
                    }
                }
            },
        });
        cursorMap.set(controller, cursor);
        const uiElements = [];
        uiMap.set(controller, uiElements);
        if (data.players) {
            for (let i = 0; i < data.players.length; i++) {
                if (data.players[i].controller === controller) {
                    cursor.color = controller.color;
                    cursor.select(controller.character);
                }
            }
        }
        if (!cursor.color) {
            if (controller.color) {
                cursor.color = controller.color;
            }
            else {
                controller.color = cursor.color = color_1.Color.random();
            }
        }
        if (controller.character) {
            cursor.character(controller.character || 'Silicon');
        }
        const index = controllers_1.connected.indexOf(controller);
        droppers.push(new Dropper(cursor));
        console.log('connected', controller.portNumber);
        cursors.push(cursor);
        controller.color = cursor.color;
        if (controller.character) {
            cursor.select(controller.character);
            cursor.dropper.holding = false;
            const s = dropperSpawnBoxes[controller.character];
            cursor.dropper.x = s.x + 15 + 5 * index;
            cursor.dropper.y = s.y + s.h * 0.5;
        }
        updateModels();
        uiElements.push(new ObjectButton(controller, 'noTapJump', '!boolean', 'Tapjump-Symbol', null, 150, 140 + index * 90, 26, 26, null, false, cursor));
        for (const e of uiElements) {
            engine_1.uiEntities.push(e);
        }
        engine_1.uiEntities.push(cursor);
        began = -1;
        countTo = -1;
        if (teams) {
            redoTeamColors();
        }
        cursors.sort((a, b) => a.controller.portNumber - b.controller.portNumber);
        updateStartUI();
    };
    const redoTeamColors = () => {
        const teamCounts = [0, 0, 0, 0, 0, 0, 0, 0, 0];
        for (let i = 0; i < cursors.length; i++) {
            const t = cursors[i].controller.team;
            if (!cursors[i].dropper.holding) {
                cursors[i].setColor(teamColors[t][teamCounts[t] % teamColors[t].length]);
                teamCounts[t]++;
            }
        }
        updateModels();
    };
    const portHeight = 90;
    const menuPrefab = model_1.Prefab.build(menuPrefabData);
    const paintUIEarly = () => {
        ctx.fillRGBA(0.18, 0.18, 0.14, 0.8);
        fillRect(0, 0, screenWidth, screenHeight);
        ctx.drawImage(logos, screenWidth / 2 - 212, -screenHeight / 2, 212, 61);
        if (stageSelected === -1) {
            renderPrefab(menuPrefab, gfx.solid);
        }
        setFontSize(30);
        ctx.strokeRGBA(0, 0, 0, 1);
        for (let i = 0; i < cursors.length; i++) {
            const y = 90 + i * portHeight;
            ctx.fillStyle(cursors[i].color);
            fillRect(0, y, 190, portHeight - 5);
            if (cursors[i].selected) {
                ctx.fillRGBA(0, 0, 0, 0.85);
            }
            else {
                ctx.fillRGBA(0, 0, 0, 0.95);
            }
            fillRect(5, y + 5, 180, fontSize + 10);
            ctx.fillStyle(cursors[i].color);
            if (cursors[i].selected) {
                paintText(bTranslators.get(cursors[i].selected).value, 50, y + fontSize + 5, 130);
            }
            ctx.textAlign(GFX.TextAlign.right);
            paintText('P' + (cursors[i].controller.portNumber + 1), 43, y + fontSize + 5);
            ctx.textAlign(GFX.TextAlign.left);
        }
        ctx.fillRGBA(1, 1, 1, 1);
        ctx.textAlign(GFX.TextAlign.center);
        setFontSize(30);
        paintText(translators.Characters.value, 355, 125);
        if (!stageUIHidden) {
            paintText(translators.Stages.value, 800, 125);
            ctx.strokeRGBA(255, 255, 255, 1);
            drawLine(520, 100, 520, 700);
            ctx.strokeRGBA(0, 0, 0, 1);
        }
        paintText(translators.AI.value, 455, 165);
        for (let i = 0; i < choosableCharacters.length; i++) {
            paintText(sandbags[choosableCharacters[i]] + '', 455, 193 + 50 * i + 50 * 0.25 - 2.5);
        }
        ctx.textAlign(GFX.TextAlign.left);
    };
    const paintUI = () => {
        for (let i = 0; i < droppers.length; i++) {
            const dropper = droppers[i];
            if (dropper.holding) {
                if (ticks - dropper.returned > 30) {
                    dropper.x = dropper.owner.x;
                    dropper.y = dropper.owner.y;
                }
                else {
                    dropper.x
                        = dropper.x
                            - (dropper.x - dropper.owner.x) * ((ticks - dropper.returned) / 30);
                    dropper.y
                        = dropper.y
                            - (dropper.y - dropper.owner.y) * ((ticks - dropper.returned) / 30);
                }
            }
            ctx.strokeRGBA(0, 0, 0, 1);
            strokeCircle(dropper.x, dropper.y, dropper.holding ? 13 : 12);
            ctx.fillStyle(dropper.owner instanceof Cursor
                ? dropper.owner.color
                : dropper.owner.palette.base);
            ctx.strokeRGBA(1, 1, 1, 1);
            drawCircle(dropper.x, dropper.y, dropper.holding ? 12 : 10);
            if (!dropper.holding) {
                ctx.textAlign(GFX.TextAlign.center);
                setFontSize(18);
                ctx.fillRGBA(1, 1, 1, 1);
                paintText(dropper.owner.controller.portNumber + 1 + '', dropper.x, dropper.y + 6);
            }
        }
    };
    cameraType = fixedScreen(0, 0, 600, 450);
    stageNeutralCamera();
    engine_1.uiEntities.push(new Button('Back', null, 1100, 5, 100, 50, {
        click: () => {
            setActiveMode(renderMain);
        },
        press: () => {
            setActiveMode(renderMain);
        },
    }));
    engine_1.uiEntities.push(new Button('Exit', null, 1100, 75, 100, 50, {
        click: () => {
            exitGame = true;
        },
        press: () => {
            exitGame = true;
        },
    }, true));
    let teams = !!data.teams;
    let timeLimit = data.timeLimit ? data.timeLimit : 28800;
    let stocks = data.stocks ? data.stocks : 3;
    const startError = () => {
        const playerCt = controllers_1.connected.filter(c => !!c.character).length;
        let sandbagTotal = 0;
        for (const n of Object.getOwnPropertyNames(sandbags)) {
            sandbagTotal += sandbags[n];
        }
        if (playerCt === 0 || playerCt + sandbagTotal < 2) {
            return `Not enough players to start a game, found ${playerCt + sandbagTotal}`;
        }
        if (teams) {
            const teamSet = new Set();
            for (let i = 0; i < controllers_1.connected.length; i++) {
                if (controllers_1.connected[i].character) {
                    teamSet.add(controllers_1.connected[i].team);
                }
            }
            if (teamSet.size < 2) {
                return `Not enough teams to start a game, found ${teamSet.size}`;
            }
        }
        return '';
    };
    const checkStart = () => {
        const err = startError();
        if (err === '') {
            return true;
        }
        audio_1.playAudio('error');
        terminal_1.dbg(err);
        return false;
    };
    let stageUIHidden = false;
    const updateStartUI = () => {
        updateModels();
        stageUIHidden = startError() !== '';
        for (const b of stageUI) {
            b.hide = stageUIHidden;
            b.disable = stageUIHidden;
        }
    };
    const startKind = (group) => {
        const filtered = group === 'legal'
            ? stage_1.stages.filter(s => s.kind === 'neutral' || s.kind === 'counterpick')
            : stage_1.stages.filter(s => s.kind === 'neutral');
        stageSelected = stage_1.stages.indexOf(filtered[(math_1.lqrandomSync() * filtered.length) | 0]);
        start();
    };
    const start = () => {
        if (!checkStart()) {
            stageSelected = -1;
            return;
        }
        if (stageSelected === -1) {
            startKind('neutral');
            return;
        }
        setActiveMode(renderBattle, {
            stageNum: stageSelected,
            stocks,
            teams,
            timeLimit,
            sandbags,
        });
        stageSelected = -1;
    };
    const updateFFA = () => {
        if (teams) {
            ffaBtn.setText('Teams');
            redoTeamColors();
        }
        else {
            ffaBtn.setText('FFA');
        }
    };
    const toggleFFA = () => {
        teams = !teams;
        updateFFA();
        updateStartUI();
    };
    const ffaBtn = new Button('FFA', null, 10, 5, 100, 50, {
        click: toggleFFA,
    });
    if (teams) {
        ffaBtn.setText('Teams');
        redoTeamColors();
    }
    engine_1.uiEntities.push(ffaBtn);
    const stageUI = [];
    const startButton = new Button('Start-Battle', null, 750, 5, 300, 50, {
        click: start,
    });
    stageUI.push(startButton);
    let neutrals = 0;
    let counterpicks = 0;
    const stageY = 170;
    stageUI.push(new Button('Starter', null, 550, stageY - 20, 700, 30, () => startKind('neutral')));
    stageUI.push(new Button('Legal', null, 550, stageY + 110, 700, 30, () => startKind('legal')));
    const mb = new Button('Miscellaneous', null, 550, stageY + 255, 700, 30);
    mb.colors.bg = color_1.Color.fromRGBA(0, 0, 0, 0);
    mb.colors.outline = color_1.Color.fromRGBA(0, 0, 0, 0);
    stageUI.push(mb);
    for (let i = 0; i < stage_1.stages.length; i++) {
        if (stage_1.stages[i].kind === 'neutral') {
            stageUI.push(new Button(stage_1.stages[i].name, null, 600 + ((neutrals / 2) | 0) * 200, stageY + 20 + (neutrals % 2) * 40, 180, 30, buttonHandlers(i)));
            neutrals++;
        }
        else if (stage_1.stages[i].kind === 'counterpick') {
            stageUI.push(new Button(stage_1.stages[i].name, null, 600 + ((counterpicks / 2) | 0) * 200, stageY + 150 + (counterpicks % 2) * 40, 180, 30, buttonHandlers(i)));
            counterpicks++;
        }
        else if (stage_1.stages[i].kind === 'misc') {
            stageUI.push(new Button(stage_1.stages[i].name, null, 600 + (((i - neutrals - counterpicks) / 3) | 0) * 200, stageY + 295 + ((i - neutrals - counterpicks) % 3) * 40, 180, 30, buttonHandlers(i)));
        }
        else {
            const b = new Button(stage_1.stages[i].name, null, 600 + (((i - neutrals - counterpicks) / 3) | 0) * 200, stageY + 295 + ((i - neutrals - counterpicks) % 3) * 40, 180, 30, buttonHandlers(i));
            b.colors.bg.a(0.25);
            b.colors.outline.a(0.25);
            b.colors.color.a(0.25);
            b.colors.stroke.a(0.25);
            stageUI.push(b);
        }
    }
    for (let i = 0; i < stageUI.length; i++) {
        engine_1.uiEntities.push(stageUI[i]);
    }
    const hostButton = new Button('Host-Netplay', null, 400, 5, 300, 20, {
        click: async () => {
            console.log('click was not assigned?');
        },
        press: _c => {
            hostButton.click(0, 0);
        },
    }, true);
    const joinButton = new Button('Join-Netplay-Clipboard', null, 400, 35, 300, 20, {
        click: () => {
            if (networking_1.netStatus.status === 'connecting'
                || networking_1.netStatus.status === 'establishing'
                || networking_1.netStatus.status === 'waiting'
                || networking_1.netStatus.status === 'hosting') {
                networking_1.cancelConnect();
                return;
            }
            joinButton.disable = true;
            const lobby = Native.paste();
            if (lobby === '') {
                engine_1.dbg.log('Need lobby code in clipboard');
                return;
            }
            engine_1.dbg.log('Joining using lobby code in clipboard');
            checkControllers();
            networking_1.join(lobby);
        },
    }, true);
    const checkControllers = () => {
        if (controllers_1.connected.length === 0) {
            let found = false;
            for (let i = 0; i < controllers_1.unconnected.length; i++) {
                if (controllers_1.unconnected[i].name !== 'keyboard') {
                    controllers_1.connecting.push(controllers_1.unconnected[i]);
                    controllers_1.unconnected.splice(i, 1);
                    found = true;
                    return;
                }
            }
            if (!found) {
                for (let i = 0; i < controllers_1.unconnected.length; i++) {
                    if (controllers_1.unconnected[i].name === 'keyboard') {
                        controllers_1.connecting.push(controllers_1.unconnected[i]);
                        controllers_1.unconnected.splice(i, 1);
                        return;
                    }
                }
            }
        }
    };
    engine_1.uiEntities.push(hostButton);
    engine_1.uiEntities.push(joinButton);
    networking_1.netStatus.listen(status => {
        switch (status) {
            case 'uninitialized':
            case 'initialized':
            case 'error':
                hostButton.setText('Host-Netplay');
                hostButton.disable = false;
                hostButton.hide = false;
                joinButton.setText('Join-Netplay-Clipboard');
                joinButton.disable = false;
                joinButton.hide = false;
                hostButton.click = () => {
                    hostButton.disable = true;
                    checkControllers();
                    networking_1.host();
                    Native.copy(networking_1.getLobby().lobbyKey);
                    engine_1.dbg.log('Copied lobby code to clipboard');
                    console.log(`Copied lobby code to clipboard: ${networking_1.getLobby().lobbyKey}`);
                    hostButton.disable = false;
                };
                if (status === 'error') {
                    terminal_1.dbg('Error connecting: ' + networking_1.netStatus.data);
                }
                return;
            case 'canceling':
            case 'disconnecting':
                hostButton.disable = true;
                hostButton.hide = true;
                joinButton.disable = true;
                joinButton.hide = true;
                return;
            case 'connecting':
                hostButton.setText('Copy-Lobby-Code');
                hostButton.disable = false;
                hostButton.hide = false;
                joinButton.setText('Cancel');
                joinButton.disable = false;
                joinButton.hide = false;
                hostButton.click = () => {
                    Native.copy(networking_1.getLobby().lobbyKey);
                    engine_1.dbg.log('Copied lobby code to clipboard');
                    hostButton.disable = false;
                };
                return;
            case 'hosting':
            case 'waiting':
                hostButton.setText('Copy-Lobby-Code');
                hostButton.disable = false;
                hostButton.hide = false;
                joinButton.setText('Cancel');
                joinButton.disable = false;
                joinButton.hide = false;
                hostButton.click = () => {
                    Native.copy(networking_1.getLobby().lobbyKey);
                    engine_1.dbg.log('Copied lobby code to clipboard');
                    hostButton.disable = false;
                };
                return;
            case 'establishing':
            case 'connected':
                hostButton.setText('Disconnect');
                hostButton.disable = false;
                hostButton.hide = false;
                joinButton.disable = true;
                joinButton.hide = true;
                hostButton.click = async () => {
                    hostButton.disable = true;
                    await networking_1.disconnectNetplay();
                    hostButton.disable = false;
                };
                for (const c of controllers_1.connected) {
                    if (!cursorMap.has(c)) {
                        continue;
                    }
                    const dropper = cursorMap.get(c).dropper;
                    if (!dropper || !c.character) {
                        continue;
                    }
                    const s = dropperSpawnBoxes[c.character];
                    dropper.x = s.x + 15 + 5 * controllers_1.connected.indexOf(c);
                    dropper.y = s.y + s.h * 0.5;
                }
                return;
            default:
                ((s) => { console.log('invalid status:', s); })(status);
        }
    });
    const modeSelector = new Selection('mode-' + constants.NAME, null, 200, 15, 150, 30, 2, Object.getOwnPropertyNames(engine_1.modes).map(s => `mode-${s}`), {
        select: (selected, _index) => {
            engine_1.setConstants(engine_1.modes[selected.split('-')[1]]);
        },
    });
    engine_1.uiEntities.push(modeSelector);
    let synced = false;
    const syncVersus = (netdata) => {
        if (!synced) {
            synced = true;
        }
        cursors.sort((a, b) => a.controller.portNumber - b.controller.portNumber);
        for (let i = 0; i < cursors.length; i++) {
            const cursor = cursors[i];
            cursor.x = 150;
            cursor.y = 90 + 20 + i * 90 + 30 * 0.33;
        }
        updateUI();
        if (netdata === null) {
            return;
        }
        teams = netdata.teams;
        stocks = netdata.stocks;
        timeLimit = netdata.timeLimit;
        sandbags = netdata.sandbags;
        modeSelector.selected = engine_1.getConstants().NAME;
        updateFFA();
    };
    const run = () => {
        if (synced) {
            syncVersus(null);
            synced = false;
        }
    };
    const getState = () => {
        return {
            teams,
            stocks,
            timeLimit,
            sandbags,
        };
    };
    fsutils_1.config.lastMode = 'versus';
    fsutils_1.config.save();
    terminal_1.getShell().execute('sh -q /home/versus_start');
    updateStartUI();
    return {
        start,
        connect,
        disconnect,
        sync: syncVersus,
        getState,
        paintUIEarly,
        paintUI,
        run,
        started: true,
    };
};
const teamNames = ['Red', 'Green', 'Blue', 'Grey'];
const teamColors = [
    [
        new color_1.Color(gl_matrix_1.vec4.fromValues(0, 1, 0.5, 1.0)),
        new color_1.Color(gl_matrix_1.vec4.fromValues(0, 0.5, 0.7, 1.0)),
        new color_1.Color(gl_matrix_1.vec4.fromValues(0, 0.75, 0.3, 1.0)),
    ],
    [
        new color_1.Color(gl_matrix_1.vec4.fromValues(0.33, 1, 0.5, 1.0)),
        new color_1.Color(gl_matrix_1.vec4.fromValues(0.33, 0.5, 0.7, 1.0)),
        new color_1.Color(gl_matrix_1.vec4.fromValues(0.33, 0.75, 0.3, 1.0)),
    ],
    [
        new color_1.Color(gl_matrix_1.vec4.fromValues(0.66, 1, 0.5, 1.0)),
        new color_1.Color(gl_matrix_1.vec4.fromValues(0.66, 0.5, 0.7, 1.0)),
        new color_1.Color(gl_matrix_1.vec4.fromValues(0.66, 0.75, 0.3, 1.0)),
    ],
    [
        new color_1.Color(gl_matrix_1.vec4.fromValues(0.88, 1, 0.5, 1.0)),
        new color_1.Color(gl_matrix_1.vec4.fromValues(0.88, 0.5, 0.7, 1.0)),
        new color_1.Color(gl_matrix_1.vec4.fromValues(0.88, 0.75, 0.3, 1.0)),
    ],
];
const renderBattle = (data) => {
    let timer = 180;
    let timerKind = 0;
    const fakeLead = { stocks: -1337, damage: 1337, playerNumber: -1 };
    const run = () => {
        let dead = 0;
        let undead = null;
        let lead = fakeLead;
        if (timer !== -1) {
            timer--;
            if (timer === 1) {
                if (timerKind === 0) {
                    o.started = true;
                    if (data.timeLimit) {
                        countTo = ticks + data.timeLimit;
                    }
                }
                else if (timerKind === 1) {
                    data.players = [...engine_1.players];
                    setActiveMode(renderPostBattle, data);
                    gamePaused = false;
                }
            }
            return;
        }
        for (let i = 0; i < engine_1.players.length; i++) {
            if (engine_1.players[i].removed
                && (engine_1.players[i].controller.attackPress
                    || engine_1.players[i].controller.specialPress)
                && engine_1.players[i].controller.attack
                && engine_1.players[i].controller.special
                && data.teams) {
                for (let j = 0; j < engine_1.players.length; j++) {
                    if (engine_1.players[j].team === engine_1.players[i].team && engine_1.players[j].stocks > 0) {
                        engine_1.players[j].stocks--;
                        engine_1.players[i].stocks++;
                        alive++;
                        teams[engine_1.players[i].team]++;
                        engine_1.players[i].removed = false;
                        engine_1.players[i].setAnimation('respawn', true);
                        engine_1.players[i].animations['respawn'].step();
                    }
                }
            }
        }
        if (countTo !== -1 && ticks === countTo - 660) {
            exports.Effects.countdown(40, 20, 10, 'TIME OUT');
        }
        if (alive <= 1 || (data.teams && teamsAlive <= 1)) {
            if (alive === 0) {
                const winMessage = 'TIE';
                terminal_1.dbg(winMessage);
                timerKind = 1;
                timer = 240;
                exports.Effects.message(40, 0, 230, winMessage);
            }
            for (let i = 0; i < engine_1.players.length; i++) {
                const player = engine_1.players[i];
                let winMessage = '';
                if (player.removed) {
                    continue;
                }
                winMessage = 'Winner: Player ' + (player.playerNumber + 1);
                if (data.teams) {
                    winMessage = 'Winner: Team ' + teamNames[player.team];
                }
                terminal_1.dbg(winMessage);
                timerKind = 1;
                timer = 240;
                exports.Effects.message(40, 0, 230, winMessage);
                return;
            }
        }
        for (let i = 0; i < engine_1.players.length; i++) {
            if (engine_1.players[i].stocks < 0) {
                dead++;
            }
            else {
                if (engine_1.players[i].stocks >= lead.stocks
                    && engine_1.players[i].damage < lead.damage) {
                    lead = engine_1.players[i];
                }
                undead = engine_1.players[i];
            }
        }
        if (dead >= engine_1.players.length - 1) {
            terminal_1.dbg('Winner: ' + (undead.playerNumber + 1));
            timerKind = 1;
            timer = 240;
            exports.Effects.message(40, 0, 230, 'Winner: Player ' + (undead.playerNumber + 1));
        }
        if (ticks === countTo) {
            terminal_1.dbg('Winner: ' + (lead.playerNumber + 1));
            timerKind = 1;
            timer = 240;
            exports.Effects.message(40, 0, 230, 'Winner: Player ' + (lead.playerNumber + 1));
        }
    };
    engine_1.setStage(new stage_1.Stage(stage_1.stages[data.stageNum]));
    exports.Effects.countdown(40, 20, 3, 'GO');
    let alive = 0;
    const teams = {};
    let teamsAlive = 0;
    const insertPlayer = (entity) => {
        const entrance = stage.entrances[engine_1.players.length % stage.entrances.length];
        entity.x = entrance.x;
        entity.y = entrance.y;
        entity.face = entrance.face ? 1 : -1;
        entity.stocks = data.stocks;
        entity.playerNumber = engine_1.players.length;
        engine_1.players.push(entity);
        alive++;
        if (teams[entity.team]) {
            teams[entity.team]++;
        }
        else {
            teamsAlive++;
            teams[entity.team] = 1;
        }
    };
    const spawnPlayer = (character, controller = null, team = 0) => {
        let entity = null;
        if (character === 'Random-Q') {
            character = characters[(math_1.lqrandomSync() * characters.length) | 0];
        }
        entity = new engine_1.Animatable({ type: 0, name: character, important: true, controller });
        entity.team = team;
        engine_1.entities.push(entity);
        if (controller !== null && controller.color !== null) {
            entity.style = controller.style % entity.styles.length;
            entity.setPalette(color_1.ColorPalette.fromColorSeed(controller.color, controller.seed, entity.styles[entity.style]));
        }
        else {
            entity.setPalette(color_1.ColorPalette.random());
        }
        if (controller !== null) {
            controller.hook = entity;
        }
        insertPlayer(entity);
        return entity;
    };
    const connect = (controller) => {
        if (!controller.color) {
            controller.color = color_1.Color.random();
        }
        if (!controller.character) {
            return;
        }
        spawnPlayer(controller.character, controller, controller.team);
    };
    const initialize = () => {
        if (utils_1.objHas(data, 'sandbags')) {
            const sandbags = data.sandbags;
            for (const n of Object.keys(sandbags)) {
                for (let i = 0; i < sandbags[n]; i++) {
                    const a = spawnSandbag(n, { dummy: false, important: true });
                    insertPlayer(a);
                }
            }
        }
    };
    const paint = () => { };
    const pause = (c = null) => {
        if (c !== null && c.shield && c.attack && c.start) {
            gamePaused = false;
            data.players = [...engine_1.players];
            setActiveMode(renderPostBattle, data);
        }
        else {
            gamePaused = true;
        }
    };
    const paused = () => {
        for (let i = 0; i < controllers_1.connected.length; i++) {
            const controller = controllers_1.connected[i];
            if (controller.hook) {
                const hook = controller.hook;
                if (controller.dleftPress || controller.drightPress) {
                    const direction = controller.drightPress ? 1 : -1;
                    const playerIndex = engine_1.players.indexOf(hook);
                    let j = playerIndex;
                    let k = engine_1.players.length;
                    j
                        = j + direction < 0
                            ? engine_1.players.length - 1
                            : (j + direction) % engine_1.players.length;
                    while (j !== playerIndex && k--) {
                        if (!engine_1.players[j].sourceController?.gamepad) {
                            hook.sourceController = new controllers_1.Controller(null);
                            engine_1.players[j].sourceController = controller;
                            controller.hook = engine_1.players[j];
                            break;
                        }
                        j = j + direction < 0 ? engine_1.players.length - 1 : j % engine_1.players.length;
                    }
                }
                else if (controller.ddownPress) {
                    hook.sourceController = new controllers_1.Controller(null);
                    controller.hook = null;
                }
            }
            else if (controller.dupPress) {
                for (let j = 0; j < engine_1.players.length; j++) {
                    if (!engine_1.players[j].sourceController?.gamepad) {
                        engine_1.players[j].sourceController = controller;
                        controller.hook = engine_1.players[j];
                        break;
                    }
                }
            }
        }
    };
    const paintUIPaused = () => {
        setFontSize(30);
        ctx.fillRGBA(1, 1, 1, 0.8);
        ctx.textAlign(GFX.TextAlign.right);
        ctx.fillText(translators.paused.value, screenWidth / 2 - 5, -screenHeight / 2 + 30);
        setFontSize(20);
        ctx.fillText(translators.quitInfo.value, screenWidth / 2 - 5, -screenHeight / 2 + 50);
        ctx.textAlign(GFX.TextAlign.center);
        for (let i = 0; i < engine_1.players.length; i++) {
            const player = engine_1.players[i];
            const x = 15 + i * 35;
            ctx.fillStyle(player.palette.base);
            fillRect(x, 100, 30, 45);
            ctx.fillRGBA(1, 1, 1, 0.8);
            fillText(player.symbol, x + 15, 120);
            if (player.sourceController?.gamepad) {
                fillText('P' + (player.sourceController.portNumber + 1), x + 15, 140);
            }
            else {
                fillText('-', x + 15, 140);
            }
        }
        let drawn = 0;
        for (let i = 0; i < controllers_1.connected.length; i++) {
            const controller = controllers_1.connected[i];
            if (!controller.hook) {
                const x = 15 + drawn * 35;
                drawn++;
                ctx.fillStyle(controller.color);
                fillRect(x, 155, 30, 25);
                ctx.fillRGBA(1, 1, 1, 0.8);
                fillText('P' + (controller.portNumber + 1), x + 15, 175);
            }
        }
        ctx.textAlign(GFX.TextAlign.left);
        fillText(translators.tagInfo.value, 15, 200);
    };
    const unpause = (c) => {
        gamePaused = false;
        if (c.shield && c.attack && c.start) {
            data.players = [...engine_1.players];
            setActiveMode(renderPostBattle, data);
        }
    };
    const ko = (entity) => {
        if (entity.stocks < 0 && data.stocks >= 0) {
            alive--;
            teams[entity.team]--;
            if (teams[entity.team] <= 0) {
                teamsAlive--;
            }
            entity.removed = true;
        }
    };
    const disconnect = (controller) => {
        if (!controller.hook) {
            return;
        }
        terminal_1.dbg('disconnected in-game');
        const hook = controller.hook;
        hook.sourceController = new controllers_1.Controller(null);
        controller.hook = null;
        pause();
    };
    audio_1.playMusic('allMusic');
    cameraType = fitOnScreen;
    countTo = -1;
    began = -1;
    terminal_1.getShell().execute('sh -q /home/battle_start');
    const o = {
        pause,
        initialize,
        disconnect,
        unpause,
        paint,
        paused,
        paintUIPaused,
        run,
        connect,
        ko,
        started: false,
    };
    return o;
};
const renderPostBattle = (data = {}) => {
    cameraType = fixedScreen(0, 0, 600, 450);
    const disconnect = (_controller) => { };
    const connect = (_controller) => { };
    const prePaint = () => { };
    const paintUIEarly = () => { };
    stageNeutralCamera();
    const menuPrefab = model_1.Prefab.build(menuPrefabData);
    const paint = () => {
        renderPrefab(menuPrefab, gfx.solid);
    };
    const paintUI = () => {
        if (!data.players) {
            return;
        }
        ctx.fillRGBA(0.95, 0.95, 0.95, 1);
        setFontSize(20);
        fillText(translators.pressStart.value, 480, 30);
        for (let i = 0; i < data.players.length; i++) {
            const player = data.players[i];
            const w = 250;
            const h = 400;
            const innerMargin = 3;
            const margin = 15;
            const x = 30 + (w + margin) * i;
            const y = 50;
            const rowHeight = 22;
            let row = 1;
            setFontSize(20);
            ctx.fillStyle(player.palette.lighter[0]);
            fillRect(x, y, w, h);
            ctx.fillRGBA(0, 0, 0, 1);
            fillText((player.playerNumber !== -1 ? `P${player.playerNumber}: ` : '')
                + player.name, x + innerMargin, y + innerMargin + row++ * rowHeight);
            fillText('KOs: ' + player.stats.kos, x + innerMargin, y + innerMargin + row++ * rowHeight);
            fillText('Falls: ' + player.stats.falls, x + innerMargin, y + innerMargin + row++ * rowHeight);
            fillText('SDs: ' + player.stats.sds, x + innerMargin, y + innerMargin + row++ * rowHeight);
            row++;
            fillText('Damage given: ' + player.stats.damage.toFixed(1), x + innerMargin, y + innerMargin + row++ * rowHeight);
            fillText('Damage taken: ' + player.stats.damageTaken.toFixed(1), x + innerMargin, y + innerMargin + row++ * rowHeight);
            fillText('Damage blocked: ' + player.stats.blocked.toFixed(1), x + innerMargin, y + innerMargin + row++ * rowHeight);
            fillText('Parries: ' + player.stats.parries, x + innerMargin, y + innerMargin + row++ * rowHeight);
            fillText('Power shields: ' + player.stats.powerHurts, x + innerMargin, y + innerMargin + row++ * rowHeight);
            row++;
            fillText('Ledge grabs: ' + player.stats.ledgeGrabs, x + innerMargin, y + innerMargin + row++ * rowHeight);
            fillText('Ledge time: ' + player.stats.hangtime, x + innerMargin, y + innerMargin + row++ * rowHeight);
            row++;
            fillText('Lag cancels: ' + player.stats.lagCancels, x + innerMargin, y + innerMargin + row++ * rowHeight);
            fillText('Velocity cancels: ' + player.stats.vCancels, x + innerMargin, y + innerMargin + row++ * rowHeight);
        }
    };
    countTo = -1;
    began = -1;
    const checkStart = () => {
        return true;
    };
    const start = () => {
        if (!checkStart()) {
            return;
        }
        setActiveMode(renderVersusSelect, data);
    };
    audio_1.playMusic('allMusic');
    terminal_1.getShell().execute('sh -q /home/results_start');
    return {
        start,
        connect,
        disconnect,
        paint,
        prePaint,
        paintUIEarly,
        paintUI,
        run: () => { },
        started: true,
    };
};
PCB.Thing.registerStyleCallback('sfx.trigger', (thing, _, value) => {
    if (!value) {
        return;
    }
    thing.events.listen('trigger', () => {
        audio_1.playAudio(value);
    });
}, _thing => { });
PCB.Thing.registerStyleCallback('sfx.hover', (thing, _, value) => {
    if (!value) {
        return;
    }
    thing.events.listen('hover', () => {
        audio_1.playAudio(value);
    });
}, _thing => { });
PCB.Thing.registerStyleCallback('sfx.active', (thing, _, value) => {
    if (!value) {
        return;
    }
    thing.events.listen('active', () => {
        audio_1.playAudio(value);
    });
}, _thing => { });
PCB.Thing.registerStyleCallback('sfx.press', (thing, _, value) => {
    if (!value) {
        return;
    }
    thing.events.listen('press', () => {
        audio_1.playAudio(value);
    });
}, _thing => { });
PCB.Thing.registerStyleCallback('sfx.error', (thing, _, value) => {
    if (!value) {
        return;
    }
    thing.events.listen('error', () => {
        audio_1.playAudio(value);
    });
}, _thing => { });
PCB.Thing.registerStyleCallback('sfx.next', (thing, _, value) => {
    if (!value) {
        return;
    }
    thing.events.listen('next', () => {
        audio_1.playAudio(value);
    });
}, _thing => { });
PCB.Thing.registerStyleCallback('sfx.previous', (thing, _, value) => {
    if (!value) {
        return;
    }
    thing.events.listen('previous', () => {
        audio_1.playAudio(value);
    });
}, _thing => { });
PCB.Thing.registerStyleCallback('sfx.back', (thing, _, value) => {
    if (!value) {
        return;
    }
    thing.events.listen('back', () => {
        audio_1.playAudio(value);
    });
}, _thing => { });
PCB.Thing.registerStyleCallback('sfx.start', (thing, _, value) => {
    if (!value) {
        return;
    }
    thing.events.listen('start', () => {
        audio_1.playAudio(value);
    });
}, _thing => { });
const lastWindowSize = gl_matrix_1.vec2.create();
const resizeDoc = (init = true) => {
    if (!init && gl_matrix_1.vec2.exactEquals(windowSize, lastWindowSize)) {
        return;
    }
    gl_matrix_1.vec2.copy(lastWindowSize, windowSize);
    if (activeMode?.doc) {
        const style = activeMode.doc.box.style;
        const pct = Math.min(screenHeight / 900, screenWidth / 1100);
        const w = 1100 * pct;
        const h = 900 * pct;
        style.top = Math.max(0, (screenHeight - h) * 0.5);
        style.left = Math.max(0, (screenWidth - w) * 0.5);
        style.width = w;
        style.height = h;
        activeMode.doc.box.reflow();
        console.log('resizing doc to', style.left, style.width, style.height);
    }
    if (menuDoc) {
        const style = menuDoc.box.style;
        const pct = Math.min(screenHeight / 900, screenWidth / 1100);
        const w = 1100 * pct;
        const h = 900 * pct;
        style.top = Math.max(0, (screenHeight - h) * 0.5);
        style.left = Math.max(0, (screenWidth - w) * 0.5);
        style.width = w;
        style.height = h;
        activeMode.doc.box.reflow();
        console.log('resizing menu doc to', style.left, style.width, style.height);
    }
};
exports.resizeDoc = resizeDoc;
const baseStyles = [
    {
        class: 'title',
        align: 'center',
        vAlign: 'center',
        top: '3%',
        width: '100%',
        height: '15%',
        color: [0.16, 0.13, 0.13, 1],
        background: [1, 0.66, 0, 1],
        border_color: [0.16, 0.13, 0.13, 1],
        border_size: 15,
        padding: 5,
    },
    {
        class: 'options',
        left: '10%',
        top: '3%',
        height: '81%',
        width: '80%',
    },
    {
        class: 'options .',
        align: 'center',
        vAlign: 'center',
        width: '100%',
        height: '12%',
        padding: 12,
        margin: '0.5%',
        color: [1, 0.43, 0, 1],
        background: [0.25, 0.22, 0.22, 1],
        border_color: [0.7, 0.7, 0.7, 1],
        border_size: 3,
        'sfx.hover': 'ui_hover',
        'sfx.active': 'ui_hover',
        'sfx.press': 'ui_press',
        'sfx.trigger': 'ui_trigger',
        'sfx.error': 'ui_error',
    },
    {
        class: 'options back',
        'sfx.back': 'ui_press',
        'sfx.trigger': '',
    },
    {
        class: 'options.controls .',
        height: '7%',
        width: '50%',
        margin: 1,
        border_size: 1,
        padding: 3,
    },
    {
        class: 'options.controls',
        width: '80%',
        left: '20%',
    },
    {
        class: 'options.leftcol',
        width: '20%',
        top: '-60%',
        left: '-5%',
        margin: 3,
    },
    {
        class: 'options.leftcol .',
    },
    {
        class: 'options has:cycle|has:toggle|has:range',
        'sfx.next': 'ui_trigger',
        'sfx.previous': 'ui_press',
    },
    {
        class: 'options is:hover',
        color: [1, 0.5, 0.05, 1],
        background: [0.32, 0.29, 0.26, 1],
        border_color: [0.77, 0.77, 0.77, 1],
    },
    {
        class: 'options is:active',
        color: [0.05, 0.05, 0.05, 1],
        background: [0.8, 0.53333, 0, 1],
        border_color: [0.85, 0.85, 0.85, 1],
    },
    {
        class: 'options is:press',
        color: [0.16, 0.13, 0.13, 1],
        background: [1, 0.66, 0, 1],
        border_color: [0.9, 0.9, 0.9, 1],
    },
    {
        class: 'options is:checked',
        color: [0.16, 0.13, 0.13, 1],
        background: [1, 0.66, 0, 1],
        border_color: [0.9, 0.9, 0.9, 1],
    },
    {
        class: 'options has:range',
        bar_color: [0.16, 0.1, 0, 1],
    },
    {
        class: 'options has:range.is:hover',
        bar_color: [0.35, 0.23, 0, 1],
    },
    {
        class: 'options has:range.is:active',
        background: [0.35, 0.23, 0, 1],
        bar_color: [0.7, 0.3, 0, 1],
    },
    {
        class: 'options has:range.is:press',
        background: [0.35, 0.23, 0, 1],
        bar_color: [1, 0.43, 0, 1],
    },
];
const renderMain = (_data = {}) => {
    cameraType = fixedScreen(0, 0, 600, 450);
    const disconnect = (_controller) => { };
    const connect = (_controller) => { };
    const prePaint = () => { };
    const paintUIEarly = () => { };
    const paintUI = () => { };
    stageNeutralCamera();
    const menuPrefab = model_1.Prefab.build(menuPrefabData);
    const paint = () => {
        renderPrefab(menuPrefab, gfx.solid);
    };
    countTo = -1;
    began = -1;
    const doc = PCB.loadTree({
        style: baseStyles,
        nodes: [
            {
                text: 'ANTISTATIC',
                class: 'title',
            },
            {
                class: 'options',
                nodes: [
                    {
                        text: 'Versus',
                        pointer: {},
                    },
                    {
                        text: 'Training',
                        pointer: {},
                    },
                    {
                        text: 'Settings',
                        pointer: {},
                    },
                    {
                        text: 'Controls',
                        pointer: {},
                    },
                    {
                        text: 'Discord',
                        pointer: {
                            synchronized: false,
                        },
                    },
                    {
                        text: 'Guide',
                        pointer: {
                            synchronized: false,
                        },
                    },
                    {
                        text: 'Exit',
                        pointer: {
                            synchronized: false,
                        },
                    },
                ],
            },
        ],
    });
    doc
        .getLabel('Versus')
        .events.listen('trigger', () => setActiveMode(renderVersusSelect));
    doc
        .getLabel('Training')
        .events.listen('trigger', () => setActiveMode(renderDebugMode));
    doc
        .getLabel('Discord')
        .events.listen('trigger', () => open('https://discord.gg/ZGJvA8P'));
    doc
        .getLabel('Guide')
        .events.listen('trigger', () => open('https://bluehexagons.com/antistatic/guide'));
    doc
        .getLabel('Settings')
        .events.listen('trigger', () => setActiveMode(renderSettings));
    doc
        .getLabel('Controls')
        .events.listen('trigger', () => setActiveMode(renderControls));
    doc.getLabel('Exit').events.listen('trigger', () => (exitGame = true));
    doc.reflow();
    global.mainMenu = doc;
    terminal_1.getShell().execute('sh -q /home/menu_start');
    return {
        connect,
        disconnect,
        paint,
        prePaint,
        paintUIEarly,
        paintUI,
        run: () => { },
        started: true,
        doc,
    };
};
const renderSettings = (_data = {}) => {
    cameraType = fixedScreen(0, 0, 600, 450);
    countTo = -1;
    began = -1;
    stageNeutralCamera();
    const menuPrefab = model_1.Prefab.build(menuPrefabData);
    const paint = () => {
        renderPrefab(menuPrefab, gfx.solid);
    };
    const doc = PCB.loadTree({
        style: baseStyles,
        nodes: [
            {
                text: 'SETTINGS',
                class: 'title',
            },
            {
                class: 'options',
                nodes: [
                    {
                        text: 'Audio',
                        pointer: {},
                    },
                    {
                        text: 'Video',
                        pointer: {},
                    },
                    {
                        id: 'language',
                        value: false,
                        pointer: {
                            synchronized: false,
                        },
                        cycle: {
                            obj: fsutils_1.config,
                            prop: 'locale',
                            default: i18n_1.activeLocale,
                            values: [
                                'en',
                                'es',
                                'fr',
                                'it',
                                'de',
                                'ru',
                                'ja',
                                'zh',
                                'zh-hk',
                                'ko',
                            ],
                        },
                    },
                    {
                        text: {
                            id: 'start-mode',
                            data: { value: '' },
                        },
                        value: false,
                        pointer: {
                            synchronized: false,
                        },
                        cycle: {
                            obj: fsutils_1.config,
                            prop: 'startMode',
                            default: 'main',
                            values: ['main', 'versus', 'training'],
                        },
                    },
                    {
                        text: 'Back',
                        class: 'back',
                        pointer: {},
                    },
                ],
            },
        ],
    });
    doc
        .getLabel('Audio')
        .events.listen('trigger', () => setActiveMode(renderAudioSettings));
    doc
        .getLabel('Video')
        .events.listen('trigger', () => setActiveMode(renderVideoSettings));
    const startLang = fsutils_1.config.locale;
    doc.getId('language').events.listen('change', () => {
        fsutils_1.config.save();
    });
    doc.getLabel('start-mode').events.listen('change', () => {
        fsutils_1.config.save();
    });
    doc
        .getLabel('Back')
        .events.listen('trigger', () => doc.events.emit('back', null));
    doc.events.listen('back', () => {
        if (fsutils_1.config.locale !== startLang) {
            i18n_1.updateLocale();
        }
        setActiveMode(renderMain);
        audio_1.playAudio('ui_trigger');
    });
    doc.reflow();
    global.mainMenu = doc;
    terminal_1.getShell().execute('sh -q /home/settings_start');
    return { paint, run: () => { }, started: true, doc };
};
const renderAudioSettings = (_data = {}) => {
    cameraType = fixedScreen(0, 0, 600, 450);
    countTo = -1;
    began = -1;
    stageNeutralCamera();
    const menuPrefab = model_1.Prefab.build(menuPrefabData);
    const paint = () => {
        renderPrefab(menuPrefab, gfx.solid);
    };
    const doc = PCB.loadTree({
        style: baseStyles,
        nodes: [
            {
                text: 'AUDIO',
                class: 'title',
            },
            {
                class: 'options',
                nodes: [
                    {
                        text: {
                            id: 'Audio-volume',
                            data: {
                                value: 0.0,
                            },
                        },
                        pointer: {
                            synchronized: false,
                        },
                        range: {
                            obj: fsutils_1.config,
                            prop: 'masterVolume',
                            min: 0,
                            max: 1,
                            round: 1,
                            default: 1,
                            kind: 'percent',
                        },
                    },
                    {
                        text: 'Play-Audio',
                        value: false,
                        pointer: {
                            synchronized: false,
                        },
                        toggle: {
                            obj: fsutils_1.config,
                            prop: 'muteAudio',
                            default: false,
                            invert: false,
                        },
                    },
                    {
                        text: {
                            id: 'SFX-volume',
                            data: {
                                value: 0.0,
                            },
                        },
                        pointer: {
                            synchronized: false,
                        },
                        range: {
                            obj: fsutils_1.config,
                            prop: 'sfxVolume',
                            min: 0,
                            max: 1,
                            round: 1,
                            default: 1,
                            kind: 'percent',
                        },
                    },
                    {
                        text: 'Play-Music',
                        value: false,
                        pointer: {
                            synchronized: false,
                        },
                        toggle: {
                            obj: fsutils_1.config,
                            prop: 'muteMusic',
                            default: false,
                            invert: false,
                        },
                    },
                    {
                        text: {
                            id: 'Music-volume',
                            data: {
                                value: 0.0,
                            },
                        },
                        pointer: {
                            synchronized: false,
                        },
                        range: {
                            obj: fsutils_1.config,
                            prop: 'musicVolume',
                            min: 0,
                            max: 1,
                            round: 1,
                            default: 1,
                            kind: 'percent',
                        },
                    },
                    {
                        text: 'Back',
                        class: 'back',
                        pointer: {},
                    },
                ],
            },
        ],
    });
    doc.getLabel('Play-Audio').events.listen('change', () => {
        audio_1.updateAudio();
    });
    doc.getLabel('Play-Music').events.listen('change', () => {
        audio_1.updateAudio();
    });
    doc.getLabel('Audio-volume').events.listen('change', () => {
        audio_1.updateAudio();
    });
    doc.getLabel('SFX-volume').events.listen('change', () => {
        audio_1.updateAudio();
    });
    doc.getLabel('Music-volume').events.listen('change', () => {
        audio_1.updateAudio();
    });
    doc
        .getLabel('Back')
        .events.listen('trigger', () => doc.events.emit('back', null));
    doc.events.listen('back', () => {
        setActiveMode(renderSettings);
        audio_1.playAudio('ui_trigger');
    });
    doc.reflow();
    global.mainMenu = doc;
    terminal_1.getShell().execute('sh -q /home/settings_start');
    return { paint, run: () => { }, started: true, doc };
};
const renderVideoSettings = (_data = {}) => {
    cameraType = fixedScreen(0, 0, 600, 450);
    countTo = -1;
    began = -1;
    stageNeutralCamera();
    const menuPrefab = model_1.Prefab.build(menuPrefabData);
    const paint = () => {
        renderPrefab(menuPrefab, gfx.solid);
    };
    const doc = PCB.loadTree({
        style: baseStyles,
        nodes: [
            {
                text: 'VIDEO',
                class: 'title',
            },
            {
                class: 'options',
                nodes: [
                    {
                        id: 'fullscreen',
                        pointer: {
                            synchronized: false,
                        },
                        cycle: {
                            obj: fsutils_1.config,
                            prop: 'fullscreen',
                            default: 'Windowed',
                            values: ['Windowed', 'Fullscreen'],
                        },
                    },
                    {
                        id: 'shadowQuality',
                        useIndex: true,
                        pointer: {
                            synchronized: false,
                        },
                        cycle: {
                            obj: fsutils_1.config,
                            prop: 'shadowQuality',
                            default: 'shadow-highest',
                            values: ['shadow-off', 'shadow-low', 'shadow-highest'],
                        },
                    },
                    {
                        id: 'ssao',
                        useIndex: true,
                        pointer: {
                            synchronized: false,
                        },
                        cycle: {
                            obj: fsutils_1.config,
                            prop: 'ssaoQuality',
                            default: 'ssao-full',
                            values: ['ssao-off', 'ssao-on', 'ssao-full'],
                        },
                    },
                    {
                        id: 'antialias',
                        useIndex: true,
                        pointer: {
                            synchronized: false,
                        },
                        cycle: {
                            obj: fsutils_1.config,
                            prop: 'antialias',
                            default: 'aa-fxaa',
                            values: ['aa-off', 'aa-fxaa'],
                        },
                    },
                    {
                        text: 'Back',
                        class: 'back',
                        pointer: {},
                    },
                ],
            },
        ],
    });
    doc.getId('fullscreen').events.listen('change', () => {
        Native.toggleFullscreen();
        fsutils_1.config.save();
    });
    for (const s of ['antialias', 'ssao', 'shadowQuality']) {
        doc.getId(s).events.listen('change', () => {
            GFX.updateGraphics(fsutils_1.config);
        });
    }
    doc.getId('fullscreen').events.listen('change', () => {
        Native.toggleFullscreen();
        fsutils_1.config.save();
    });
    doc
        .getLabel('Back')
        .events.listen('trigger', () => doc.events.emit('back', null));
    doc.events.listen('back', () => {
        setActiveMode(renderSettings);
        audio_1.playAudio('ui_trigger');
    });
    doc.reflow();
    global.mainMenu = doc;
    terminal_1.getShell().execute('sh -q /home/settings_start');
    return { paint, run: () => { }, started: true, doc };
};
const renderControls = (_data = {}) => {
    cameraType = fixedScreen(0, 0, 600, 450);
    countTo = -1;
    began = -1;
    stageNeutralCamera();
    const menuPrefab = model_1.Prefab.build(menuPrefabData);
    const paint = () => {
        renderPrefab(menuPrefab, gfx.solid);
    };
    const doc = PCB.loadTree({
        style: baseStyles,
        nodes: [
            {
                text: 'CONTROLS',
                class: 'title',
            },
            {
                class: 'options',
                id: 'options',
            },
        ],
    });
    const options = doc.getId('options');
    const layouts = controllers_1.getAllMappings();
    for (const k of layouts) {
        const node = doc.build({
            text: k.layout.name + (k.name === 'default' ? '' : '.' + k.name),
            pointer: {},
        });
        options.append(node);
        node.events.listen('trigger', () => {
            setActiveMode(renderRemap, k);
        });
    }
    options.append(doc.build({
        text: 'Back',
        class: 'back',
        pointer: {},
    }));
    doc
        .getLabel('Back')
        .events.listen('trigger', () => doc.events.emit('back', null));
    doc.events.listen('back', () => {
        setActiveMode(renderMain);
        audio_1.playAudio('ui_trigger');
    });
    doc.reflow();
    global.mainMenu = doc;
    terminal_1.getShell().execute('sh -q /home/controls_start');
    return { paint, run: () => { }, started: true, doc };
};
const renderRemap = (mapping) => {
    cameraType = fixedScreen(0, 0, 600, 450);
    countTo = -1;
    began = -1;
    stageNeutralCamera();
    const menuPrefab = model_1.Prefab.build(menuPrefabData);
    const paint = () => {
        renderPrefab(menuPrefab, gfx.solid);
    };
    const doc = PCB.loadTree({
        style: baseStyles,
        nodes: [
            {
                text: `${mapping.layout.name}${mapping.name === 'default' ? '' : '.' + mapping.name}`,
                class: 'title',
            },
            {
                class: 'options controls',
                id: 'options',
            },
            {
                class: 'options leftcol',
                id: 'leftcol',
                nodes: [
                    {
                        text: 'Modify',
                        class: 'back',
                        pointer: {},
                    },
                    {
                        text: 'Save',
                        class: 'back',
                        pointer: {},
                    },
                    {
                        text: 'Reset',
                        class: 'back',
                        pointer: {},
                    },
                    {
                        text: 'Cancel',
                        class: 'back',
                        pointer: {},
                    },
                ],
            },
        ],
    });
    const options = doc.getId('options');
    const leftcol = doc.getId('leftcol');
    const changes = new Map();
    const values = new Map();
    for (const k of Object.getOwnPropertyNames(mapping.mapping)) {
        const original = mapping.mapping[k];
        let action = mapping.mapping[k];
        const node = doc.build({
            text: `${k}: ${action}`,
            pointer: {},
        });
        const text = node.components.get('text');
        const actions = controllers_1.axisList.includes(action) ? controllers_1.axisList : controllers_1.buttonList;
        values.set(k, action);
        node.events.listen('next', () => {
            const index = (actions.indexOf(action) + 1) % (actions.length + 1);
            action = actions[index] || '';
        });
        node.events.listen('previous', () => {
            let index = (actions.indexOf(action) - 1) % (actions.length + 1);
            if (index < 0) {
                index = actions.length - 1;
            }
            action = actions[index] || '';
        });
        node.events.listen('press', (_controller, _e) => {
            node.events.emit('next');
            text.setText(`${k}: ${action}`);
            values.set(k, action);
            if (action !== original) {
                changes.set(k, action);
            }
            else if (changes.has(k)) {
                changes.delete(k);
            }
        });
        options.append(node);
    }
    doc.getLabel('Modify').events.listen('trigger', () => {
        setActiveNode(options.children[0]);
    });
    doc.getLabel('Save').events.listen('trigger', async () => {
        const data = utils_1.mapToObject(values);
        await controllers_1.saveMapping(mapping, data);
        doc.events.emit('back', null);
    });
    doc.getLabel('Reset').events.listen('trigger', async () => {
        await controllers_1.resetMapping(mapping);
        doc.events.emit('back', null);
    });
    doc.getLabel('Cancel').events.listen('trigger', () => {
        setActiveMode(renderControls);
        audio_1.playAudio('ui_trigger');
    });
    doc.events.listen('back', _e => {
        if (leftcol.contains(activeNode)) {
            setActiveMode(renderControls);
        }
        else {
            setActiveNode(doc.getLabel('Save'));
        }
        audio_1.playAudio('ui_trigger');
    });
    doc.reflow();
    global.mainMenu = doc;
    terminal_1.getShell().execute('sh -q /home/remap_start');
    return { paint, run: () => { }, started: true, doc };
};
const renderBlank = (data = {}) => {
    const disconnect = (_controller) => { };
    const connect = (_controller) => { };
    const prePaint = () => { };
    const paintUIEarly = () => { };
    const paintUI = () => { };
    cameraType = fixedScreen(0, 0, 600, 450);
    countTo = -1;
    began = -1;
    const checkStart = () => {
        return true;
    };
    const start = () => {
        if (!checkStart()) {
            return;
        }
        setActiveMode(renderVersusSelect, data);
    };
    terminal_1.getShell().execute('sh -q /home/blank_start');
    return {
        start,
        connect,
        disconnect,
        prePaint,
        paintUIEarly,
        paintUI,
        run: () => { },
        started: true,
    };
};
const mpos = gl_matrix_1.vec3.create();
const setMousePos = (x, y) => {
    mpos[0] = x - screenWidth * 0.5;
    mpos[1] = y - screenHeight * 0.5;
    mpos[2] = 0;
    gl_matrix_1.vec3.transformMat4(mpos, mpos, screenToUI);
    mx = mpos[0];
    my = mpos[1];
    mousePointer.lx = mousePointer.x;
    mousePointer.ly = mousePointer.y;
    mousePointer.x = mx;
    mousePointer.y = my;
    mousePointer.event.emit('move', mousePointer);
};
const refreshCanvasSize = (unscaledWidth, unscaledHeight) => {
    screenWidth = unscaledWidth;
    screenHeight = unscaledHeight;
    gl_matrix_1.vec2.set(windowSize, screenWidth, screenHeight);
    screenScale[0] = screenScale4[0] = screenWidth * 0.5;
    screenScale[1] = screenScale4[1] = -screenHeight * 0.5;
    screenScale[2] = screenScale4[2] = -1;
    screenScale4[3] = 1;
    ctx.resize(screenWidth, screenHeight);
    gfx.updateViewport();
    resizeCamera();
};
const initGame = () => {
    gfx = GFX.init(fsutils_1.config);
    ctx = new GFX.Canvas(gfx);
    refreshCanvasSize(gfx.width, gfx.height);
    audio_1.playMusic('allMusic');
    characters.forEach(name => engine_1.Animatable.loadCharacter(name));
    let gameMode = renderMain;
    switch (fsutils_1.config.startMode) {
        case 'versus':
            gameMode = renderVersusSelect;
            break;
        case 'training':
            gameMode = renderDebugMode;
            break;
        case 'main':
            gameMode = renderMain;
            break;
        case 'blank':
            gameMode = renderBlank;
            break;
    }
    setActiveMode(gameMode);
    scheduledCallbacks.shift()();
    initCamera();
    startTime = perf_hooks_1.performance.now();
    frameTimes.startLaps();
    controllers_1.readyKeyboardControllers();
    setImmediate(() => Native.startTicking(frameTick));
};
let replayOn = false;
let tasOn = false;
const savedState = {
    entities: [],
};
const savestate = () => {
    savedState.entities.length = 0;
    for (let i = 0; i < engine_1.entities.length; i++) {
        const entity = engine_1.entities[i];
        savedState.entities.push({
            e: entity,
            x: entity.x,
            y: entity.y,
            dx: entity.dx,
            dy: entity.dy,
            slide: entity.slide,
            kb: entity.kb,
            kbx: entity.kbx,
            kby: entity.kby,
            kbDecay: entity.kbDecay,
            lag: entity.lag,
            stun: entity.stun,
            damage: entity.damage,
            stocks: entity.stocks,
            face: entity.face,
            animation: entity.animation,
            airborne: entity.airborne,
            platform: entity.platform,
            hover: entity.hover,
        });
    }
};
const loadstate = () => {
    for (let i = 0; i < savedState.entities.length; i++) {
        const state = savedState.entities[i];
        const entity = state.e;
        if (entity.removed) {
            continue;
        }
        entity.x = state.x;
        entity.y = state.y;
        entity.dx = state.dx;
        entity.dy = state.dy;
        entity.slide = state.slide;
        entity.kb = state.kb;
        entity.kbx = state.kbx;
        entity.kby = state.kby;
        entity.kbDecay = state.kbDecay;
        entity.lag = state.lag;
        entity.stun = state.stun;
        entity.airborne = state.airborne;
        entity.damage = state.damage;
        entity.stocks = state.stocks;
        entity.face = state.face;
        entity.platform = state.platform;
        entity.hover = state.hover;
        entity.lx = state.x;
        entity.ly = state.y;
        entity.shield.energy = 1;
        entity.shield.lastEnergy = 1;
        entity.shield.wait = 0;
        entity.shield.stun = 0;
        entity.setAnimation(state.animation, true, true);
    }
};
let oldCamera = null;
const binObj = {
    replay: {
        mode: 0b001,
        exec: (_args, _stdin, stdout, _stderr, _wd, _env) => {
            if (!replayOn) {
                replayOn = true;
                stdout.write('Recording replay\n');
                for (const c of controllers_1.connected) {
                    c.replay = new controllers_1.Replay(c);
                }
                return 0;
            }
            replayOn = false;
            stdout.write('Finished recording\n');
            for (const c of controllers_1.connected) {
                c.replay = null;
            }
            return 0;
        },
    },
    sandbag: {
        mode: 0b001,
        exec: (args, _stdin, stdout, _stderr, _wd, _env) => {
            if (args.length === 1) {
                stdout.write('spawning sandbag\n');
                spawnSandbag();
                return;
            }
            for (let i = 1; i < args.length; i++) {
                stdout.write(`spawning sandbag ${args[i]}\n`);
                spawnSandbag(args[i]);
            }
        },
    },
    tas: {
        mode: 0b001,
        exec: (_args, _stdin, stdout, _stderr, _wd, _env) => {
            if (!tasOn) {
                tasOn = true;
                stdout.write('Recording TAS\n');
                for (const c of controllers_1.connected) {
                    c.replay = new controllers_1.Replay(c);
                }
                return 0;
            }
            tasOn = false;
            stdout.write('Finished TAS\n');
            for (const c of controllers_1.connected) {
                c.replay = null;
            }
            return 0;
        },
    },
    play: {
        mode: 0b001,
        exec: (_args, _stdin, stdout, _stderr, _wd, _env) => {
            stdout.write('Playing replays\n');
            for (const c of controllers_1.connected) {
                c.replay?.play(ticks);
            }
            return 0;
        },
    },
    pause: {
        mode: 0b001,
        exec: (_args, _stdin, stdout, _stderr, _wd, _env) => {
            stdout.write('Pausing replays\n');
            for (const c of controllers_1.connected) {
                c.replay?.pause();
            }
            return 0;
        },
    },
    'camera-impulse': {
        mode: 0b001,
        exec: (args, _stdin, _stdout, stderr, _wd, _env) => {
            if (args.length < 4) {
                stderr.write('Error: need three arguments, x y z');
                return 1;
            }
            exports.addCameraImpulse(+args[1] || 0, +args[2] || 0, +args[3] || 0);
            return 0;
        },
    },
    cinema: {
        mode: 0b001,
        exec: async (args, _stdin, stdout, stderr, _wd, _env) => {
            if (args.length === 1 || args[1] === 'help') {
                stdout.write(`Usage: cinema [frame duration] from [x] [y] [z] to [x] [x-ease] [y] [y-ease] [z] [z-ease]
`);
                return 0;
            }
            if (args.length < 3) {
                return 0;
            }
            if (args.length < 13) {
                stdout.write('Better cinema command coming later... probably\n');
                return 0;
            }
            const duration = parseInt(args[1], 10);
            if (args[2] !== 'from') {
                stderr.write('Invalid syntax');
                return 1;
            }
            const x1 = -args[3];
            const y1 = +args[4];
            const z1 = +args[5];
            if (args[6] !== 'to') {
                stderr.write('Invalid syntax');
            }
            const x2 = x1 + +args[7];
            const xe = easing_1.Ease[args[8]];
            const y2 = y1 - +args[9];
            const ye = easing_1.Ease[args[10]];
            const z2 = z1 - +args[11];
            const ze = easing_1.Ease[args[12]];
            oldCamera = cameraType;
            cameraType = { run: () => { } };
            for (let i = 0; i < duration; i++) {
                const pct = i / duration;
                await renderWait();
                cameraPan[0] = xe(pct) * x2 - x1;
                cameraPan[1] = ye(pct) * y2 - y1;
                cameraPan[2] = ze(pct) * z2 - z1;
                updateCamera();
            }
            cameraType = oldCamera;
            return 0;
        },
    },
    lookat: {
        mode: 0b001,
        exec: (args, _stdin, _stdout, _stderr, _wd, _env) => {
            if (args.length > 4) {
                cameraPan[0] = +args[4] || 0;
                cameraPan[1] = +args[5] || 0;
                cameraPan[2] = +args[6] || 0;
            }
            exports.freeCamera(null);
            gl_matrix_1.mat4.targetTo(view, gl_matrix_1.vec3.fromValues(cameraPan[0], cameraPan[1], cameraPan[2]), gl_matrix_1.vec3.fromValues(+args[1] || 0, +args[2] || 0, +args[3] || 0), upVector);
            gl_matrix_1.mat4.invert(iview, view);
            gl_matrix_1.vec3.set(cameraVector, view[8], view[9], view[10]);
            gl_matrix_1.vec3.set(cameraPosition, iview[12], iview[13], iview[14]);
            console.log(cameraPan, cameraPosition);
            audio_1.setPosition(cameraPan[0], cameraPan[1], cameraPan[2]);
            gl_matrix_1.mat4.multiply(combined, perspective, view);
        }
    },
    camera: {
        mode: 0b001,
        exec: (args, _stdin, stdout, stderr, _wd, _env) => {
            if (args.length === 1) {
                stdout.write(`camera position: ${cameraPan[0].toFixed(2)}, ${cameraPan[1].toFixed(2)}, ${cameraPan[2].toFixed(2)}\n`);
                return 0;
            }
            if (args.length >= 2 && args[1] === 'help') {
                stdout.write(`camera help - this message
camera freeze - freezes the camera in place
camera unfreeze - thaws the camera
camera [x] [y] [z] - moves the camera to the given coordinates
`);
                return 0;
            }
            if (args.length >= 2 && args[1] === 'freeze') {
                oldCamera = cameraType;
                exports.freeCamera(null);
                return 0;
            }
            if (args.length >= 2 && args[1] === 'unfreeze') {
                cameraType = oldCamera;
                initCamera();
                engine_1.dbg.log('disabling free camera');
                return 0;
            }
            if (args.length < 4) {
                stderr.write('Error: need three arguments, x y z');
                return 1;
            }
            cameraPan[0] = +args[1] || 0;
            cameraPan[1] = +args[2] || 0;
            cameraPan[2] = +args[3] || 0;
            exports.freeCamera(null);
            return 0;
        },
    },
    'switch-scene': {
        mode: 0b001,
        exec: (args, _stdin, stdout, _stderr, _wd, _env) => {
            const gameModes = {
                versus: renderVersusSelect,
                training: renderDebugMode,
                main: renderMain,
                blank: renderBlank,
            };
            if (args.length === 1 || !utils_1.objHas(gameModes, args[1])) {
                stdout.write(`Usage: switch-scene [mode]\nMode may be one of: ${Object.getOwnPropertyNames(gameModes).join(', ')}\n`);
                return 1;
            }
            setActiveMode(gameModes[args[1]]);
            return 0;
        },
    },
    fullscreen: {
        mode: 0b001,
        exec: (_args, _stdin, _stdout, _stderr, _wd, _env) => {
            Native.toggleFullscreen();
            return 0;
        },
    },
};
terminal_1.addDirToIndex('/', {
    contents: utils_1.map({
        bin: {
            contents: utils_1.map(binObj),
        },
    }),
});
//# sourceMappingURL=rendering.js.map