"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ai = exports.setStage = exports.engineTick2 = exports.engineTick1 = exports.toggleConstants = exports.setConstants = exports.getConstants = exports.getActiveMode = exports.setActiveMode = exports.calculateDI = exports.Animatable = exports.registerEntity = exports.entityLoaders = exports.dbg = exports.uiEntities = exports.entities = exports.players = exports.modes = exports.calcColor = exports.getTicks = exports.Game = exports.activeModeTime = exports.calcStun = exports.calcKnockback = exports.version = exports.EntityType = void 0;
const gl_matrix_1 = require("gl-matrix");
const AI = require("./ai");
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 fsutils_1 = require("./fsutils");
const math_1 = require("./math");
const mathvm_1 = require("./mathvm");
const networking_1 = require("./networking");
const rendering_1 = require("./rendering");
const stage_1 = require("./stage");
const terminal_1 = require("./terminal");
const utils_1 = require("./utils");
var EntityType;
(function (EntityType) {
    EntityType[EntityType["character"] = 0] = "character";
    EntityType[EntityType["projectile"] = 1] = "projectile";
})(EntityType = exports.EntityType || (exports.EntityType = {}));
exports.version = '0.7.5';
const calcKnockback = (kbb, kbgrowth, baseDmg, dmg, entityDmg, weight, scale) => (kbgrowth * (0.014 * (((baseDmg + 2) * (dmg * 2 + entityDmg)) / 20) * weight + 0.18)
    + kbb)
    * constants.KNOCKBACK_MOD
    * scale;
exports.calcKnockback = calcKnockback;
const calcStun = (knockback) => Math.ceil(Math.max(constants.STUN_MIN, (knockback - constants.KB_STUN_THRESHOLD)
    * constants.STUN_MOD
    / constants.KB_TO_STUN_FACTOR
    + constants.STUN_PLUS));
exports.calcStun = calcStun;
exports.activeModeTime = 0;
let activeMode = null;
exports.Game = {
    comboCounter: 0,
    totalDamage: 0,
    damage: 0,
    kb: 0,
    basekb: 0,
    edmg: 0,
    bubble: null,
    angle: 0,
    reverse: false,
    stale: 0,
    stun: 0,
    prestun: 0,
    poststun: 0,
    di: 0,
    diMagnitude: 0,
    asdi: gl_matrix_1.vec2.create(),
    sdi: gl_matrix_1.vec2.create(),
    x: 0,
    y: 0,
    curveBuffer: new Float64Array(0),
    knockbackCurves: [],
    frame: 0,
    entity: null,
};
let ticks = 0;
const getTicks = () => ticks;
exports.getTicks = getTicks;
const calcColor = (r, g, b, a) => {
    return (((((r / 255) * 3 + 0.5) | 0) << 6)
        + ((((g / 255) * 3 + 0.5) | 0) << 4)
        + ((((b / 255) * 3 + 0.5) | 0) << 2)
        + ((a * 3 + 0.5) | 0));
};
exports.calcColor = calcColor;
exports.modes = {
    Antistatic: {
        NAME: 'Antistatic',
        MODE_FLAG: 'as',
        KB_DECAY: 0.343,
        KB_TO_STUN_FACTOR: 0.343,
        KB_STUN_THRESHOLD: 0,
        KB_TRANSITION_THRESHOLD: 1,
        GROUNDED_KB_MOD: 0.9,
        GROUNDED_STUN_MOD: 0.9,
        AIR_DODGE_OK: true,
        SPOT_DODGE_OK: true,
        METEOR_CANCEL_OK: true,
        WALL_TECH_OK: true,
        WALL_JUMP_OK: true,
        WALL_JUMP_TIMING: 2,
        WALL_JUMP_ITAN: 6,
        SHAKEOUT_OK: true,
        POWERSHIELD_OK: true,
        LIGHT_SHIELD_OK: true,
        POWERSHIELD_FRAMES: 2,
        SHIELD_DAMAGE_BONUS_MOD: 0.25,
        SHIELD_DAMAGE_MOD: 0.75,
        SHIELD_STUN_MIN: 2,
        SHIELD_STUN_BONUS_DMG: 2.45,
        SHIELD_STUN_MOD: 0.5,
        SHIELD_STUN_PLUS: 1,
        SHIELD_LIGHTSTUN_BONUS_DMG: 5,
        SHIELD_LIGHTSTUN_MOD: 1.2,
        SHIELD_LIGHTSTUN_PLUS: 1,
        ENERGY_CHARGE_DELAY: 42,
        ENERGY_BASE_REGEN: 0.00175,
        ENERGY_CRUMPLE_REGEN: 0.004,
        ENERGY_FAST_REGEN_MOD: 1.5,
        ENERGY_FAST_REGEN_DELAY: 12,
        ENERGY_FAST_REGEN_TIME: 180,
        ENERGY_FAST_REGEN_PAUSE: 60,
        SHIELD_BASE_DECAY: 0.0007,
        SPECIAL_ENERGY_WAIT: 42,
        SPECIAL_ENERGY_SCALE: 1,
        HOLDING_ENERGY_WAIT: 120,
        KNOCKBACK_MOD: 0.88,
        DAMAGE_MOD: 1,
        STUN_MOD: 1,
        STUN_MIN: 6,
        STUN_PLUS: 0,
        STUN_CARRY: 0.25,
        SOFTLAND: 10,
        JAB_RESET: 10,
        LAG_CANCEL_MUL: 1.5,
        LAG_CANCEL_TIMER: 10,
        LAG_CANCEL_WINDOW: 10,
        LAG_CANCEL_OVERLAP: 3,
        LAG_CANCEL_ENERGY_BASE: 0.005,
        LAG_CANCEL_ENERGY_MUL: 0.001,
        LAG_CANCEL_ENERGY_WAIT_BASE: 1,
        LAG_CANCEL_ENERGY_WAIT_MUL: 1.25,
        V_CANCEL_WINDOW: 20,
        V_CANCEL_MOD: 0.95,
        HORIZONTAL_KNOCKDOWN_THRESHOLD: 10,
        CROUCH_CANCEL_MOD: 0.9,
        CROUCH_CANCEL_DMG_MOD: 0.9,
        CROUCH_CANCEL_LAG_MOD: 0.666,
        LAG_MOD: 3 / 5,
        LAG_PLUS: 1,
        DI_MOD: 18,
        AUTO_LAG_CANCEL: false,
        INSTANT_LAG_CANCEL: false,
        TECH_TIMER: 40,
        TECH_WINDOW: 20,
        TECH_OVERLAP: 0,
        FASTFALL_ALWAYS: true,
        SAKURAI_ANGLE: 44,
        SAKURAI_AIR: 45,
        SAKURAI_MIN: 32,
        SAKURAI_MAX: 44,
        DASH_GRAB_OK: true,
        TAP_SCALE: 1.3671,
        CHARGE_TAP_OK: true,
        STALE_DAMAGE_MOD: 1,
        STALE_KNOCKBACK_MOD: 0,
        STALE_STACK_SIZE: 9,
        STALE_VALUE: 0.01,
        STALE_DI_MULTIPLIER: 1.333,
        STAR_KO_KB_THRESHOLD: 1,
        JUMP_BUFFER_WINDOW: 20,
        LEDGE_REGRAB_RESTRICT: 14,
        CLANK_DAMAGE: 8,
        LEDGE_INTANGIBILITY: 37,
        LEDGE_HANG_TIME: 300,
        LEDGE_OCCUPANCY_PERCENT: 0.75,
        DASHACCEL_MOD: 1,
        DASHDANCE_MOD: 1,
        DASHBOOST: 0,
        DASHGRIP: 1,
        DASHINITIAL_MOD: 1,
        FASTFALL_THRESHOLD: 2,
        AERIAL_FASTFALL_THRESHOLD: 5,
        FASTFALL_MOD: 1,
        LEDGE_GRAB_HIT_DELAY: 50,
        GRAB_FALLEN_OK: false,
        GRAB_BREAKOUT_KB: 9,
        THROW_BY_WEIGHT: 2,
    },
    '19XX': {
        NAME: '19XX',
        MODE_FLAG: '19XX',
        KB_DECAY: 0.29,
        KB_TO_STUN_FACTOR: 0.29,
        KB_STUN_THRESHOLD: 0,
        KB_TRANSITION_THRESHOLD: 1,
        GROUNDED_KB_MOD: 0.8,
        GROUNDED_STUN_MOD: 0.9,
        AIR_DODGE_OK: false,
        SPOT_DODGE_OK: false,
        METEOR_CANCEL_OK: false,
        WALL_TECH_OK: false,
        WALL_JUMP_OK: false,
        WALL_JUMP_TIMING: 0,
        WALL_JUMP_ITAN: 0,
        SHAKEOUT_OK: false,
        POWERSHIELD_OK: false,
        LIGHT_SHIELD_OK: false,
        POWERSHIELD_FRAMES: 0,
        SHIELD_DAMAGE_BONUS_MOD: 0,
        SHIELD_DAMAGE_MOD: 1,
        SHIELD_STUN_MIN: 2,
        SHIELD_STUN_BONUS_DMG: 0,
        SHIELD_STUN_MOD: 1.62,
        SHIELD_STUN_PLUS: 4,
        SHIELD_LIGHTSTUN_BONUS_DMG: 0,
        SHIELD_LIGHTSTUN_MOD: 1.62,
        SHIELD_LIGHTSTUN_PLUS: 4,
        ENERGY_CHARGE_DELAY: 42,
        ENERGY_BASE_REGEN: 0.001,
        ENERGY_CRUMPLE_REGEN: 0.004,
        ENERGY_FAST_REGEN_MOD: 2,
        ENERGY_FAST_REGEN_DELAY: 12,
        ENERGY_FAST_REGEN_TIME: 180,
        ENERGY_FAST_REGEN_PAUSE: 60,
        SHIELD_BASE_DECAY: 0.0007,
        SPECIAL_ENERGY_WAIT: 42,
        SPECIAL_ENERGY_SCALE: 0.25,
        HOLDING_ENERGY_WAIT: 120,
        KNOCKBACK_MOD: 0.8,
        DAMAGE_MOD: 1,
        STUN_MOD: 1.1,
        STUN_MIN: 10,
        STUN_PLUS: 6,
        STUN_CARRY: 0.25,
        SOFTLAND: 10,
        JAB_RESET: 10,
        LAG_CANCEL_MUL: 1.5,
        LAG_CANCEL_TIMER: 11,
        LAG_CANCEL_WINDOW: 11,
        LAG_CANCEL_OVERLAP: 11,
        LAG_CANCEL_ENERGY_BASE: 0.01,
        LAG_CANCEL_ENERGY_MUL: 0.0005,
        LAG_CANCEL_ENERGY_WAIT_BASE: 0,
        LAG_CANCEL_ENERGY_WAIT_MUL: 1,
        V_CANCEL_WINDOW: 0,
        V_CANCEL_MOD: 1,
        HORIZONTAL_KNOCKDOWN_THRESHOLD: 11,
        CROUCH_CANCEL_MOD: 0.7,
        CROUCH_CANCEL_DMG_MOD: 1,
        CROUCH_CANCEL_LAG_MOD: 0.7,
        LAG_MOD: 3 / 5,
        LAG_PLUS: 2,
        DI_MOD: 0,
        AUTO_LAG_CANCEL: false,
        INSTANT_LAG_CANCEL: true,
        TECH_TIMER: 20,
        TECH_WINDOW: 20,
        TECH_OVERLAP: 20,
        FASTFALL_ALWAYS: false,
        SAKURAI_ANGLE: 42.5,
        SAKURAI_AIR: 43,
        SAKURAI_MIN: 32,
        SAKURAI_MAX: 32,
        DASH_GRAB_OK: false,
        TAP_SCALE: 0,
        CHARGE_TAP_OK: false,
        STALE_DAMAGE_MOD: 1,
        STALE_KNOCKBACK_MOD: 1,
        STALE_STACK_SIZE: 3,
        STALE_VALUE: 0.03,
        STALE_DI_MULTIPLIER: 1.1,
        STAR_KO_KB_THRESHOLD: 1,
        JUMP_BUFFER_WINDOW: 20,
        LEDGE_REGRAB_RESTRICT: 14,
        CLANK_DAMAGE: 8,
        LEDGE_INTANGIBILITY: 37,
        LEDGE_HANG_TIME: 300,
        LEDGE_OCCUPANCY_PERCENT: 0.25,
        DASHACCEL_MOD: 0.5,
        DASHDANCE_MOD: 0,
        DASHBOOST: 0.33,
        DASHGRIP: 0.25,
        DASHINITIAL_MOD: 0.75,
        FASTFALL_THRESHOLD: 1,
        AERIAL_FASTFALL_THRESHOLD: 0,
        FASTFALL_MOD: 0.5,
        LEDGE_GRAB_HIT_DELAY: 50,
        GRAB_FALLEN_OK: true,
        GRAB_BREAKOUT_KB: 9,
        THROW_BY_WEIGHT: 1,
    },
};
let constants = exports.modes.Antistatic;
exports.players = [];
exports.entities = [];
exports.uiEntities = [];
const removed = [];
utils_1.watchCharacters((name) => {
    console.log('replacing', name);
    const l = exports.entities.length;
    for (let i = 0; i < l; i++) {
        if (exports.entities[i].name === name) {
            console.log('replaced', exports.entities[i].name, i);
            exports.entities[i].replace(new Animatable({ type: 0, name: name }));
        }
    }
});
exports.dbg = (() => {
    const dbgObject = {
        animations: false,
        controllers: false,
        drawBuffers: false,
        drawCharacters: true,
        drawECB: false,
        drawHeatmap: false,
        drawHitbubbles: true,
        drawHitbubbleInfo: false,
        drawHitbubblesRaw: false,
        drawHurtbubbles: false,
        drawLedgeGrab: false,
        drawRawPosition: false,
        drawStage: 1,
        drawStatus: true,
        drawTags: true,
        drawTerminal: true,
        drawUI: true,
        drawTimer: true,
        drawAllControllers: false,
        enabled: true,
        freezePhysicsParticles: false,
        listHitbubbles: false,
        network: 0,
        performance: 0,
        training: false,
        log: (...args) => {
            terminal_1.dbg(...args);
        },
        dump: (object, ...names) => {
            if (names.length > 0) {
                for (let i = 0; i < names.length; i++) {
                    dbgObject.log(names[i], object[names[i]]);
                }
            }
            else {
                for (const key of Object.getOwnPropertyNames(object)) {
                    dbgObject.log(key, object[key]);
                }
            }
        },
    };
    return dbgObject;
})();
let stage = null;
exports.entityLoaders = new Map();
const registerEntity = (name, loader, handlers) => {
    if (exports.entityLoaders.has(name)) {
        console.log('Entity loader replaced:', name);
    }
    exports.entityLoaders.set(name, {
        build: loader,
        handlers: handlers || {},
    });
};
exports.registerEntity = registerEntity;
const animationInList = (anim, list) => list !== null && (list.includes('all')
    || list.includes(anim)
    || (anim.startsWith('airjump')
        && list.includes('airjump'))
    || (anim.startsWith('hop')
        && list.includes('hop')));
require("../characters/carbon");
require("../characters/helium");
require("../characters/iron");
require("../characters/rhodium");
require("../characters/silicon");
require("../characters/xenon");
class Animatable extends rendering_1.Entity {
    constructor(instanceData) {
        super();
        this.namedbubbles = new Map();
        this.hurtbubbles = [];
        this.animation = '';
        this.lastAnimation = '';
        this.animationSpeed = 1;
        this.airborne = true;
        this.sourceController = null;
        this.controller = null;
        this.scheduledAnimation = ['', false, false];
        this.animations = {};
        this.platform = null;
        this.hover = null;
        this.pummels = 0;
        this.grabTime = 0;
        this.lastInjury = {
            lastFrame: false,
            type: 0,
            entity: null,
            bubble: null,
            damage: 0,
            knockback: 0,
            angle: 0,
            flags: 0,
            frame: 0,
            stun: 0,
            lag: 0,
            landedSince: false,
            stale: 0,
        };
        this.lastShield = {
            lastFrame: false,
            type: 0,
            entity: null,
            bubble: null,
            damage: 0,
            knockback: 0,
            angle: 0,
            flags: 0,
            frame: 0,
            stun: 0,
        };
        this.lastCollision = {
            lastFrame: false,
            type: 0,
            entity: null,
            bubble: null,
            damage: 0,
            knockback: 0,
            angle: 0,
            flags: 0,
            frame: 0,
            stale: 0,
            stun: 0,
        };
        this.lastClash = {
            lastFrame: false,
            type: 0,
            entity: null,
            ownBubble: null,
            bubble: animation_1.BubbleReference,
            damage: 0,
            knockback: 0,
            angle: 0,
            flags: 0,
            frame: 0,
        };
        this.data = new Map();
        this.defaultData = null;
        this.chooseAnimation = null;
        this.init = null;
        this.playerNumber = -1;
        this.launched = false;
        this.stageCollided = false;
        this.status = new Set();
        this.events = new animation_1.EventSystem(this);
        this.armored = 0;
        this.invincible = 0;
        this.intangible = 0;
        this.vulnerable = 0;
        this.storedSpeed = 0;
        this.flash = 0;
        this.buffertime = 0;
        this.lagCancel = 0;
        this.teched = 0;
        this.palette = null;
        this.styles = [];
        this.style = 0;
        this.styleLength = 0;
        this.team = 0;
        this.lastFall = { x: 0, y: 0, frame: -200 };
        this.px = 0;
        this.dropper = null;
        this.handlers = null;
        this.damage = 0;
        this.stocks = 0;
        this.points = 0;
        this.stale = { moves: [], cursor: 0 };
        this.staleAs = null;
        this.collided = [];
        this.fastfalling = false;
        this.fastfall = false;
        this.airjumps = 0;
        this.pseudojumps = 0;
        this.regrabs = 0;
        this.face = 1;
        this.follow = true;
        this.buffer = '';
        this.activeAnimation = null;
        this.ledgeHang = false;
        this.ledgeReleased = 10;
        this.friendly = this;
        this.control = Animatable.defaultControl;
        this.lastFrame = null;
        this.currentFrame = null;
        this.removed = false;
        this.x = 0;
        this.y = 0;
        this.nx = 0;
        this.ny = 0;
        this.lx = 0;
        this.ly = 0;
        this.dx = 0;
        this.dy = 0;
        this.kbx = 0;
        this.kby = 0;
        this.kba = 0;
        this.kb = 0;
        this.okb = 0;
        this.kbf = 0;
        this.slide = 0;
        this.lag = 0;
        this.hitlag = false;
        this.stun = 0;
        this.outStun = 0;
        this.stunInitial = 0;
        this.held = null;
        this.dummy = false;
        this.important = true;
        this.velocity = gl_matrix_1.vec2.create();
        this.type = 0;
        this.maxFallSpeed = 0;
        this.hurtbubbleCount = 0;
        this.headbubble = null;
        this.corebubble = null;
        this.arcSpeed = 0;
        this.fallSpeed = 0;
        this.fastfallSpeed = 0;
        this.initialFallSpeed = 0;
        this.walkSpeed = 0;
        this.walkAcceleration = 0;
        this.aerodynamics = 1;
        this.airResistance = 1;
        this.riseFriction = 1;
        this.fallFriction = 1;
        this.weight = 1;
        this.width = 0;
        this.height = 0;
        this.ecb = null;
        this.airAcceleration = 0;
        this.airSpeed = 0;
        this.friction = 1;
        this.kbFriction = 0;
        this.kbStandFriction = 0;
        this.kbDecayFriction = 0;
        this.kbDecayScale = 0;
        this.name = '';
        this.symbol = '';
        this.phasing = false;
        this.phase = false;
        this.skeletonScale = 1;
        this.bubbleScale = 1;
        this.permadeath = false;
        this.floating = false;
        this.defaultAnimation = '';
        this.landingAudio = '';
        this.heavyLandingAudio = '';
        this.lagCancelAudio = '';
        this.launchResistance = 0;
        this.flinchThreshold = 0;
        this.softland = 1;
        this.sdi = 0;
        this.asdi = 0;
        this.kbDecay = 0;
        this.slideDecay = 0;
        this.stunBreak = 0;
        this.reverseGrabRange = 0;
        this.forwardGrabRange = 0;
        this.grabStart = 0;
        this.grabHeight = 0;
        this.shield = {
            x: 0,
            y: 0,
            x2: 0,
            y2: 0,
            bubble: null,
            wait: 0,
            density: 0,
            lastEnergy: 1,
            lastReduced: -1000,
            energy: 1,
            regen: 0,
            decay: 0,
            multiplier: 0,
            baseSize: 0,
            growth: 0,
            mobility: 0,
            lightshield: 0,
            powershield: 0,
            stun: 0,
            initialStun: 0,
        };
        this.wallJump = 0;
        this.lastHb = [0, 0];
        this.hbTime = 0;
        this.colorSelectable = true;
        this.wallJumpElement = null;
        this.wallJumpSide = 'left';
        this.grabbedOn = 0;
        this.platformDrop = false;
        this.lastMissTech = 0;
        this.cursor = null;
        this.heartbeat = new animation_1.Heartbeat(100);
        this.lastCollisions = {
            element: null,
            velocity: gl_matrix_1.vec2.create(),
            metrics: [],
        };
        this.ai = null;
        this.aidata = null;
        this.stats = {
            kos: 0,
            falls: 0,
            sds: 0,
            groundFrames: 0,
            airFrames: 0,
            damage: 0,
            damageTaken: 0,
            damageAbsorbed: 0,
            knockbackAbsorbed: 0,
            hits: 0,
            shieldHits: 0,
            powerHits: 0,
            parryHits: 0,
            hurts: 0,
            shieldHurts: 0,
            powerHurts: 0,
            parries: 0,
            blocked: 0,
            powerBlocked: 0,
            parryBlocked: 0,
            shieldDamage: 0,
            powerDamage: 0,
            parryDamage: 0,
            fallpct: 0,
            kopct: 0,
            sdpct: 0,
            lagCancels: 0,
            vCancels: 0,
            heavyLands: 0,
            ledgeGrabs: 0,
            ledgeReGrabs: 0,
            ledgeMaxReGrabs: 0,
            hangtime: 0,
            walltech: 0,
            walltechmiss: 0,
            techmiss: 0,
            techin: 0,
            techout: 0,
            tech: 0,
            staleMoves: 0,
            veryStaleMoves: 0,
        };
        if (instanceData !== null) {
            this.instantiate(instanceData);
        }
    }
    instantiate(instanceData) {
        this.animation = !instanceData.airborne ? 'idle' : 'airborne';
        this.lastAnimation = this.animation;
        this.airborne = !!instanceData.airborne;
        this.sourceController = instanceData.controller || null;
        this.controller = new controllers_1.Controller(null);
        this.shield.bubble = animation_1.Hitbubble.from(this, {
            type: 'shield',
        });
        this.dummy = !!instanceData.dummy;
        this.important = !!instanceData.important;
        if (instanceData.type === 0) {
            Animatable.loadCharacter(instanceData.name, this, instanceData);
        }
    }
    static loadCharacter(name, entity, _instanceData) {
        if (!entity) {
            return;
        }
        if (!exports.entityLoaders.has(name)) {
            console.warn('entity loader not found for:', name);
            return;
        }
        const data = exports.entityLoaders.get(name).build(true);
        entity.handlers = exports.entityLoaders.get(name).handlers;
        animation_1.Animation.prepareAnimationData(data.animations, entity.handlers);
        entity.load(data);
        entity.reset();
    }
    load(character) {
        const data = character.hurtbubbles;
        for (let i = 0; i < data.length; i++) {
            const hb = new animation_1.Hurtbubble(this, data[i], i);
            if (utils_1.objHas(data[i], 'name')) {
                this.namedbubbles.set(data[i].name, i + 1);
                this.namedbubbles.set(data[i].name + '2', -i - 1);
            }
            this.hurtbubbles.push(hb);
        }
        const anims = character.animations;
        const animNames = Object.getOwnPropertyNames(anims);
        for (let i = 0; i < animNames.length; i++) {
            const animation = new animation_1.Animation(this, anims[animNames[i]]);
            this.animations[animation.name] = animation;
        }
        console.log('Animations loaded:', character.name, Object.getOwnPropertyNames(this.animations).length);
        this.style = 0;
        if (utils_1.objHas(character, 'styles')) {
            this.styles = character.styles;
            this.style = (math_1.lqrandomSync() * this.styles.length) | 0;
            this.styleLength = this.styles[this.style].rules.length;
        }
        else {
            this.styles = [];
            this.styleLength = 0;
        }
        this.setPalette(utils_1.objHas(character, 'color')
            ? color_1.ColorPalette.random(new color_1.Color(new Float32Array(character.color)))
            : color_1.ColorPalette.random());
        this.type = character.type | 0;
        this.fallSpeed = +character.fallSpeed || 0;
        this.maxFallSpeed = +character.maxFallSpeed || 0;
        this.arcSpeed = +character.arcSpeed || 0;
        this.hurtbubbleCount = this.hurtbubbles.length;
        this.headbubble = this.hurtbubbles[character.headbubble] || null;
        this.corebubble = this.hurtbubbles[character.corebubble] || null;
        this.fastfallSpeed = +character.fastfallSpeed || 0;
        this.initialFallSpeed = +character.initialFallSpeed || 0;
        this.walkSpeed = +character.walkSpeed || 0;
        this.walkAcceleration = +character.walkAcceleration || 0;
        this.aerodynamics = +character.aerodynamics || 0;
        this.airResistance = +character.airResistance || 0;
        this.riseFriction = +character.riseFriction || 0;
        this.fallFriction = +character.fallFriction || 0;
        this.weight = +character.weight || 0;
        this.width = +character.width || 0;
        this.height = -character.height || 0;
        this.airAcceleration = +character.airAcceleration || 0;
        this.airSpeed = +character.airSpeed || 0;
        this.friction = +character.friction || 0;
        this.kbFriction = +character.kbFriction || 0;
        this.kbStandFriction = +character.kbStandFriction || 0;
        this.kbDecayFriction = +character.kbDecayFriction || 0;
        this.kbDecayScale = +character.kbDecayScale || 0;
        this.name = character.name || 'n/a';
        this.symbol = character.symbol || 'n/a';
        this.phasing = !!character.phasing;
        this.shield.multiplier = +character.shieldMultiplier || 0;
        this.shield.baseSize = +character.shieldMinSize || 0;
        this.shield.growth = +character.shieldGrowth || 0;
        this.shield.lightshield = +character.lightShieldGrowth || 0;
        this.shield.mobility = +character.shieldMobility || 0;
        this.shield.powershield = +character.powershieldSize || 0;
        this.shield.regen = +character.shieldRegen || 0;
        this.shield.decay = +character.shieldDecay || 0;
        this.shield.x = +character.shieldX || 0;
        this.shield.y = +character.shieldY || 0;
        this.shield.x2 = +character.shieldX2 || 0;
        this.shield.y2 = +character.shieldY2 || 0;
        this.permadeath = !!character.permadeath;
        this.floating = !!character.floating;
        this.defaultAnimation = character.defaultAnimation || 'idle';
        this.landingAudio = character.landingAudio || '';
        this.heavyLandingAudio = character.heavyLandingAudio || '';
        this.lagCancelAudio = character.lagCancelAudio || '';
        this.launchResistance = +character.launchResistance || 0;
        this.flinchThreshold = +character.flinchThreshold || 0;
        this.softland = +character.softland || 0;
        this.sdi = +character.sdi || 0;
        this.asdi = +character.asdi || 0;
        this.slideDecay = +character.slideDecay || 0;
        this.stunBreak = +character.stunBreak || 0;
        this.reverseGrabRange = +character.reverseGrabRange || 0;
        this.forwardGrabRange = +character.forwardGrabRange || 0;
        this.grabStart = +character.grabStart || 0;
        this.grabHeight = +character.grabHeight || 0;
        this.chooseAnimation = character.chooseAnimation || null;
        this.init = character.init || null;
        this.animation = this.defaultAnimation;
        this.lastAnimation = this.animation;
        this.activeAnimation = this.animations[this.animation];
        this.lastFrame = this.activeAnimation.keyframes[0].hurtbubbles || [];
        this.currentFrame = this.lastFrame;
        this.defaultData = character.data || null;
        this.skeletonScale = +character.skeletonScale || 1;
        this.bubbleScale = +character.bubbleScale || 1;
        this.ecb = new collision_1.Polygon([
            0,
            0,
            -this.width,
            this.height * 0.5,
            0,
            this.height,
            this.width,
            this.height * 0.5,
        ]);
        this.ecb.update(this.x, this.y);
        if (character.onCreate) {
            character.onCreate(this);
        }
        this.setAnimation(this.animation, true);
        this.refresh();
    }
    spawn(spawn) {
        const entity = spawnAnimatable({
            type: spawn.type || 0,
            name: spawn.name,
            recycle: true,
            airborne: true,
        });
        for (const key of Object.getOwnPropertyNames(spawn)) {
            if (!rendering_1.Entity.exclusions.includes(key)) {
                ;
                entity[key] = spawn[key];
            }
        }
        entity.x = this.x + this.face * spawn.x;
        entity.y = this.y - spawn.y;
        if (spawn.dx) {
            entity.dx = this.face * spawn.dx;
        }
        if (!spawn.neutral) {
            entity.friendly = this;
        }
        if (!spawn.singleFacing) {
            entity.face = entity.dx ? Math.sign(entity.dx) : this.face;
        }
        if (spawn.stale) {
            entity.staleAs = this;
        }
    }
    setAI(ai) {
        if (this.ai !== null && utils_1.objHas(this.ai, 'listeners')) {
            this.events.remove(this.ai.listeners);
        }
        this.ai = ai;
        if (ai === null) {
            return;
        }
        ai.init(this, ai, this.sourceController.frame);
        if (utils_1.objHas(ai, 'listeners')) {
            this.events.listen(ai.listeners);
        }
    }
    schedule(name, force = false, interruptInterrupt = false) {
        if (this.scheduledAnimation[0] === '' || force) {
            this.scheduledAnimation[0] = name;
            this.scheduledAnimation[1] = force;
            this.scheduledAnimation[2] = interruptInterrupt;
        }
    }
    addHitbubble(bubble) {
        animation_1.addHitbubble(this, bubble, this.x + this.face * bubble.x, this.y - bubble.y, this.x + this.face * bubble.x2, this.y - bubble.y2, bubble.radius * this.skeletonScale, 1);
    }
    playAudio(sfx, volume = 1, pitch = 1) {
        audio_1.playAudio(sfx, volume, pitch, [this.x, this.y, 0]);
    }
    addStatus(kind, source, duration = -1) {
        const s = animation_1.statusFactory(kind);
        s.apply(this, source, duration === -1 ? s.defaultDuration : duration);
        this.status.add(s);
        return s;
    }
    setAnimation(animationName, force = false, interruptInterrupt = false) {
        const activeAnimation = this.activeAnimation;
        const kfd = activeAnimation.keyframeData;
        const lastAnimation = this.animation;
        let targetAnimation = null;
        let canInterrupt = false;
        let cannotCancel = false;
        let intoAnimation = activeAnimation;
        let canCancel = false;
        let cancelAs = animationName;
        if (animationName === 'dashgrab' && !constants.DASH_GRAB_OK) {
            animationName = 'grab';
            cancelAs = animationName;
        }
        if (utils_1.objHas(this.animations, constants.MODE_FLAG + ':' + animationName)) {
            animationName = constants.MODE_FLAG + ':' + animationName;
        }
        targetAnimation = this.animations[animationName];
        if (!targetAnimation) {
            console.warn('Animation not found (early):', animationName);
            return false;
        }
        cannotCancel = ((utils_1.objHas(kfd, 'noCancel')
            && animationInList(cancelAs, activeAnimation.data.get('noCancel')))
            || (activeAnimation.data.has('noCancel')
                && animationInList(cancelAs, activeAnimation.data.get('noCancel'))));
        canCancel = (utils_1.objHas(kfd, 'cancellable') && animationInList(cancelAs, kfd.cancellable)
            || animationInList(cancelAs, activeAnimation.cancellable)
            || (utils_1.objHas(kfd, 'redirect') && utils_1.objHas(kfd.redirect, animationName))
            || (activeAnimation.data.has('redirect')
                && utils_1.objHas(activeAnimation.data.get('redirect'), animationName)));
        if (animationName === 'dashgrab' && !constants.DASH_GRAB_OK) {
            animationName = 'grab';
        }
        canInterrupt
            = activeAnimation.iasa !== 0
                && activeAnimation.duration - activeAnimation.iasa
                    <= activeAnimation.frame
                && (animationName !== 'crouch'
                    || (this.animation !== 'dtilt' && this.animation !== 'dtap'));
        if (!force && !canInterrupt && (cannotCancel || !canCancel)) {
            const bufferable = utils_1.objHas(kfd, 'bufferable')
                ? kfd.bufferable
                : activeAnimation.data.has('bufferable')
                    ? activeAnimation.data.get('bufferable')
                    : null;
            if (bufferable
                && !activeAnimation.data.has('unbufferable')
                && animationInList(cancelAs, bufferable)) {
                this.buffer = animationName;
                this.buffertime = activeAnimation.data.get('buffertime') || 6;
            }
            return false;
        }
        if (this.chooseAnimation) {
            let toAnimation = animationName;
            if (activeAnimation.data.has('redirect')) {
                toAnimation
                    = activeAnimation.data.get('redirect')[animationName] || animationName;
            }
            if (utils_1.objHas(kfd, 'redirect')) {
                toAnimation = kfd.redirect[animationName] || animationName;
            }
            animationName = this.chooseAnimation(toAnimation);
        }
        if (!utils_1.objHas(this.animations, animationName)) {
            console.warn('Animation not found:', this.name, `(${this.playerNumber + 1})`, animationName);
            return false;
        }
        if (activeAnimation.transition !== animationName
            && this.animations[animationName].data.has('cost')) {
            const cost = +this.animations[animationName].data.get('cost')
                * constants.SPECIAL_ENERGY_SCALE;
            if (cost <= this.shield.energy) {
                this.shield.lastEnergy = this.shield.energy;
                this.shield.energy = this.shield.energy - cost;
                this.shield.lastReduced = ticks;
                this.shield.wait = Math.max(this.shield.wait, constants.SPECIAL_ENERGY_WAIT);
            }
            else {
                this.playAudio('error');
                return false;
            }
        }
        this.lastFrame = this.currentFrame;
        if (!interruptInterrupt
            && activeAnimation.data.has('interrupted')
            && activeAnimation.transition !== animationName
            && (force || !activeAnimation.data.has('noCancelInterrupt'))) {
            const name = activeAnimation.data.get('interrupted')(this, this.controller, activeAnimation, kfd);
            if (name) {
                animationName = name;
            }
        }
        if (activeAnimation.cancellable?.includes(animationName)) {
            activeAnimation.data.has('canceled')
                && activeAnimation.data.get('canceled')(this, this.controller, activeAnimation, kfd);
        }
        if (this.buffertime > 0) {
            this.buffertime = 0;
            this.buffer = '';
        }
        activeAnimation.reset();
        if (utils_1.objHas(kfd, 'redirect') && kfd.redirect[animationName]) {
            animationName = kfd.redirect[animationName];
        }
        else if (activeAnimation.data.has('redirect')
            && activeAnimation.data.get('redirect')[animationName]) {
            animationName = activeAnimation.data.get('redirect')[animationName];
        }
        if (animationName === 'dashgrab' && !constants.DASH_GRAB_OK) {
            animationName = 'grab';
        }
        this.scheduledAnimation[0] = '';
        this.animation = animationName;
        intoAnimation = this.animations[animationName];
        this.activeAnimation = intoAnimation;
        intoAnimation.continued
            = activeAnimation.transition === activeAnimation.name
                && activeAnimation.name === animationName;
        if (intoAnimation.start) {
            ;
            intoAnimation.start(this, this.controller, intoAnimation, intoAnimation.keyframeData);
            if (this.activeAnimation !== intoAnimation) {
                return true;
            }
        }
        if (intoAnimation.data.has('variations')) {
            const v = intoAnimation.data.get('variations');
            const roll = math_1.lqrandomSync();
            let i = 0;
            for (; i < v.length; i++) {
                if (!v[i].chance || roll < v[i].chance) {
                    break;
                }
            }
            if (i < v.length && v[i].name !== this.animation) {
                this.animation = v[i].name;
                this.activeAnimation = this.animations[v[i].name];
                intoAnimation = this.activeAnimation;
            }
        }
        if (intoAnimation.data.has('audio')) {
            this.playAudio(intoAnimation.data.get('audio'));
        }
        this.lastAnimation = lastAnimation;
        if (intoAnimation.type === animation_1.AnimationType.throw && this.held !== null) {
            intoAnimation.ticks
                = (this.held.weight - 1) * constants.THROW_BY_WEIGHT + 1;
        }
        return true;
    }
    reset() {
        this.team = 0;
        this.animationSpeed = 1;
        this.refresh();
        if (this.init) {
            this.init();
        }
    }
    refresh() {
        this.wallJumpElement = null;
        this.wallJump = 0;
        this.wallJumpSide = 'left';
        this.lastHb[0] = 0;
        this.lastHb[1] = 0;
        this.hbTime = 0;
        this.pummels = 0;
        this.grabTime = 0;
        this.launched = false;
        this.collided.length = 0;
        this.phase = false;
        this.x = 0;
        this.y = 0;
        this.dx = 0;
        this.dy = 0;
        this.kbx = 0;
        this.kby = 0;
        this.kba = 0;
        this.kb = 0;
        this.okb = 0;
        this.kbf = 0;
        this.slide = 0;
        this.lag = 0;
        this.hitlag = false;
        this.stun = 0;
        this.outStun = 0;
        this.stunInitial = 0;
        this.shield.wait = 0;
        this.shield.energy = 1;
        this.shield.stun = 0;
        this.shield.initialStun = 0;
        this.hover = null;
        this.platform = null;
        this.lastMissTech = 0;
        this.status.clear();
        if (this.stale.moves) {
            this.stale.moves.length = 0;
            this.stale.cursor = 0;
        }
        if (this.defaultData !== null) {
            for (const k of Object.getOwnPropertyNames(this.defaultData)) {
                this.data.set(k, this.defaultData[k]);
            }
        }
        this.invincible = 0;
        this.intangible = 0;
        this.vulnerable = 0;
        this.staleAs = null;
        this.buffer = '';
        this.buffertime = 0;
        this.fastfall = false;
        this.fastfalling = false;
        this.airjumps = 0;
        this.pseudojumps = 0;
        this.regrabs = 0;
        this.lagCancel = 0;
        this.teched = 0;
        this.face = 1;
        this.lastCollision.lastFrame = false;
        this.lastShield.lastFrame = false;
        this.lastInjury.lastFrame = false;
        this.lastClash.lastFrame = false;
        this.activeAnimation.reset();
        if (this.init) {
            this.init();
        }
    }
    setPalette(palette) {
        this.palette = palette;
        if (this.style >= this.styles.length) {
            return;
        }
        this.palette.randomize(this.styles[this.style]);
        this.shield.bubble.color.set(this.palette.lighter[0]);
        const bubbles = this.styles[this.style].bubbles;
        if (Array.isArray(bubbles)) {
            for (let i = 0; i < this.hurtbubbles.length; i++) {
                this.hurtbubbles[i].colorIndex1 = bubbles[i] || 0;
            }
        }
        else {
            for (let i = 0; i < this.hurtbubbles.length; i++) {
                const b = bubbles[this.hurtbubbles[i].name];
                if (Array.isArray(b)) {
                    this.hurtbubbles[i].colorIndex1 = b[0] | 0;
                    this.hurtbubbles[i].colorIndex2 = b[1] | 0;
                }
                else {
                    this.hurtbubbles[i].colorIndex1 = b | 0;
                    this.hurtbubbles[i].colorIndex2 = b | 0;
                }
            }
        }
    }
    replace(entity) {
        const index = exports.players.indexOf(this);
        entity.style = this.style;
        entity.setPalette(this.palette);
        entity.damage = this.damage;
        entity.stocks = this.stocks;
        entity.points = this.points;
        entity.x = this.x;
        entity.y = this.y;
        entity.airborne = this.airborne;
        entity.platform = this.platform;
        entity.hover = this.hover;
        entity.schedule(this.animation, true, true);
        entity.sourceController = this.sourceController;
        entity.controller = this.controller;
        entity.sourceController.character = entity.name;
        entity.dummy = this.dummy;
        entity.cursor = this.cursor;
        entity.stats = this.stats;
        entity.important = this.important;
        entity.playerNumber = this.playerNumber;
        entity.ai = this.ai;
        entity.aidata = this.aidata;
        if (this.cursor) {
            entity.cursor = this.cursor;
            entity.cursor.entity = entity;
        }
        this.removed = true;
        if (exports.entities.includes(this)) {
            exports.entities.splice(exports.entities.indexOf(this), 1, entity);
        }
        else {
            exports.entities.push(entity);
        }
        if (index !== -1) {
            exports.players.splice(index, 1, entity);
        }
        if (this.held !== null) {
            this.held.setAnimation('airborne', true, true);
            this.held.airborne = true;
        }
    }
}
exports.Animatable = Animatable;
Animatable.defaultControl = (entity, controller) => {
    if (controller.ddown) {
        if (entity.colorSelectable) {
            if (controller.specialPress) {
                return;
            }
            if (controller.attackPress) {
                return;
            }
        }
        if (controller.jumpPress) {
            controller.noTapJump = !controller.noTapJump;
            exports.dbg.log(`Tap jump for p${entity.playerNumber + 1}`, controller.noTapJump ? 'off' : 'on');
        }
    }
    if (controller.debugging) {
        if ((controller.dup
            || controller.dleft
            || controller.dright
            || controller.ddown)
            && controller.startPress) {
            controller.debugging = false;
            return;
        }
        if (controller.dup) {
            if (controller.attackPress) {
                exports.dbg.drawStage = (exports.dbg.drawStage + 1) & 3;
                exports.dbg.log('Drawing stage:', (exports.dbg.drawStage & 1) !== 0 ? 'on' : 'off', '; colliders:', (exports.dbg.drawStage & 2) !== -1 ? 'on' : 'off');
            }
            if (controller.jumpPress) {
                rendering_1.freeCamera(controller);
            }
            return;
        }
        if (controller.dright) {
            if (controller.attackPress && entity instanceof Animatable) {
                entity.refresh();
                entity.stocks = 3;
                exports.dbg.log('Reset player ' + (entity.playerNumber + 1));
            }
            return;
        }
        if (controller.dleft) {
            if (controller.grabPress) {
                exports.dbg.drawLedgeGrab = !exports.dbg.drawLedgeGrab;
                exports.dbg.log('Draw ledge grab data', exports.dbg.drawLedgeGrab ? 'on' : 'off');
            }
            if (controller.specialPress) {
                exports.dbg.drawCharacters = !exports.dbg.drawCharacters;
                exports.dbg.log('Draw characters', exports.dbg.drawCharacters ? 'on' : 'off');
            }
            if (controller.attackPress) {
                exports.dbg.drawHitbubbles = !exports.dbg.drawHitbubbles;
                exports.dbg.log('Draw hitbubbles', exports.dbg.drawHitbubbles ? 'on' : 'off');
            }
            return;
        }
    }
    else {
        if (controller.ddown && controller.attack && controller.special) {
            controller.debugging = true;
            exports.dbg.log('Debug mode enabled for player ', entity.playerNumber + 1);
            exports.dbg.log('^A - stage drawing; ^Y - free camera; >A - reset character; vY - toggle tap jump; vL - debug text; vZ - ledge grab; vB - hurtbubbles; vA - hitbubbles');
        }
    }
    if (!(entity instanceof Animatable)) {
        return;
    }
    if (entity.activeAnimation
        && entity.activeAnimation.frame < 8
        && controller.special
        && ((entity.face === 1 && controller.left)
            || (entity.face === -1 && controller.right))
        && entity.activeAnimation.data.has('reversible')) {
        entity.face = -entity.face;
        if (entity.activeAnimation.data.has('reverse')) {
            entity.setAnimation(entity.activeAnimation.data.get('reverse'), true);
        }
    }
    if (controller.shieldHardPress && entity.teched <= constants.TECH_OVERLAP) {
        entity.teched = constants.TECH_TIMER;
    }
    if ((entity.face === 1 && controller.hmove < -0.25)
        || (entity.face === -1 && controller.hmove > 0.25)) {
        controller.reversible = 8;
    }
    else if (controller.reversible > 0) {
        if (Math.abs(controller.hmove) > 0.4
            && ((entity.face === -1 && controller.hmove < -0.25)
                || (entity.face === 1 && controller.hmove > 0.25))) {
            controller.reversible = 0;
        }
        else {
            controller.reversible--;
        }
    }
    if (entity.airborne) {
        if ((controller.shieldPress || controller.grabPress)
            && entity.lagCancel <= constants.LAG_CANCEL_OVERLAP) {
            entity.lagCancel = constants.LAG_CANCEL_TIMER;
        }
        if (entity.wallJump
            && entity.animation !== 'walljump'
            && entity.animations.walljump
            && ((entity.wallJumpSide === 'right' && controller.leftTap === 0)
                || (entity.wallJumpSide === 'left' && controller.rightTap === 0))
            && entity.setAnimation('walljump')) {
            entity.wallJump = 0;
            rendering_1.Effects.thump(entity.x, entity.y, 0, entity.dy, entity.width, gl_matrix_1.vec2.negate(gl_matrix_1.vec2.create(), entity.wallJumpElement.normal()));
            entity.fastfall = false;
            entity.face = entity.wallJumpSide === 'left' ? 1 : -1;
            entity.pseudojumps++;
            entity.intangible = Math.max(entity.intangible, constants.WALL_JUMP_ITAN);
        }
        else if (controller.jumpPress
            || (!controller.noTapJump && controller.upTap === 0)) {
            let jumped = false;
            let animationName = '';
            const h = controller.hmove;
            if (entity.animations.airjumpN
                && entity.animations.airjumpN.data.get('jumps') > entity.airjumps) {
                jumped = true;
                animationName = 'airjumpN';
            }
            else if (entity.animations['airjump' + entity.airjumps]) {
                jumped = true;
                animationName = 'airjump' + entity.airjumps;
            }
            if (jumped) {
                if (Math.abs(h) > 0.33) {
                    if (Math.sign(h) === entity.face
                        && entity.animations[animationName + 'forth']) {
                        animationName = animationName + 'forth';
                    }
                    else if (Math.sign(h) !== entity.face
                        && entity.animations[animationName + 'back']) {
                        animationName = animationName + 'back';
                    }
                }
                if (entity.setAnimation(animationName)) {
                    rendering_1.Effects.airjump(entity.x, entity.y);
                    entity.fastfall = false;
                }
            }
        }
        if (constants.AIR_DODGE_OK && controller.shieldHardPress) {
            entity.setAnimation('airdodge');
        }
        if (controller.grabPress
            && entity.animations.zair
            && entity.setAnimation('zair')) {
        }
        else if (controller.specialPress) {
            if (controller.vmove > 0.5) {
                if (entity.setAnimation('airdownspecial')) {
                    if ((entity.face === 1 && controller.hmove < -0.2)
                        || (entity.face === -1 && controller.hmove > 0.2)) {
                        entity.face = -entity.face;
                    }
                }
            }
            else if (controller.vmove < -0.5) {
                if (entity.setAnimation('airupspecial')) {
                    if (controller.hmove !== 0) {
                        if ((entity.face === 1 && controller.hmove < -0.2)
                            || (entity.face === -1 && controller.hmove > 0.2)) {
                            entity.face = -entity.face;
                        }
                    }
                }
            }
            else if (controller.hmove < -0.5) {
                if (entity.setAnimation('airsidespecial')) {
                    entity.face = -1;
                }
            }
            else if (controller.hmove > 0.5) {
                if (entity.setAnimation('airsidespecial')) {
                    entity.face = 1;
                }
            }
            else if (entity.setAnimation('airspecial') && controller.reversible) {
                entity.face = -entity.face;
            }
        }
        else if (controller.attackPress || controller.grabPress) {
            if (controller.shield
                && entity.animations.zair
                && entity.setAnimation('zair')) {
            }
            else if (controller.up) {
                entity.setAnimation('uair');
            }
            else if (controller.down) {
                entity.setAnimation('dair');
            }
            else if (entity.face === 1) {
                if (controller.hmove < -0.3) {
                    entity.setAnimation('bair');
                }
                else if (controller.hmove > 0.3) {
                    entity.setAnimation('fair');
                }
                else {
                    entity.setAnimation('nair');
                }
            }
            else {
                if (controller.left) {
                    entity.setAnimation('fair');
                }
                else if (controller.right) {
                    entity.setAnimation('bair');
                }
                else {
                    entity.setAnimation('nair');
                }
            }
        }
        if (controller.rup === 2) {
            entity.setAnimation('uair');
        }
        else if (controller.rdown === 2) {
            entity.setAnimation('dair');
        }
        else if (entity.face === 1) {
            if (controller.rleft === 2) {
                entity.setAnimation('bair');
            }
            else if (controller.rright === 2) {
                entity.setAnimation('fair');
            }
        }
        else {
            if (controller.rleft === 2) {
                entity.setAnimation('fair');
            }
            else if (controller.rright === 2) {
                entity.setAnimation('bair');
            }
        }
        if (controller.downTap < 6
            && ((entity.dy < constants.AERIAL_FASTFALL_THRESHOLD
                && entity.activeAnimation.type === animation_1.AnimationType.aerial)
                || entity.dy <= constants.FASTFALL_THRESHOLD)) {
            if (constants.FASTFALL_ALWAYS
                || entity.activeAnimation.type === animation_1.AnimationType.movement
                || entity.activeAnimation.type === animation_1.AnimationType.passive) {
                entity.fastfall = true;
            }
        }
        if (entity.activeAnimation.noFastfall
            || utils_1.objHas(entity.activeAnimation.keyframeData, 'noFastfall')) {
            entity.fastfall = false;
        }
    }
    else {
        if (controller.ddownPress) {
            entity.setAnimation('dtaunt');
        }
        if (controller.dleftPress || controller.drightPress) {
            entity.setAnimation('staunt');
        }
        if (controller.dupPress) {
            entity.setAnimation('utaunt');
        }
        if (controller.down
            && controller.vmove > 0.66
            && entity.animation !== 'crouch'
            && entity.animation !== 'crouched') {
            entity.setAnimation('crouch');
        }
        else if (Math.abs(controller.hmove) > 0.3) {
            if (controller.leftTap <= 1 || controller.rightTap <= 1) {
                if (entity.animation !== 'dash') {
                    if (entity.face === Math.sign(controller.hmove)
                        && entity.setAnimation('dash')) {
                        rendering_1.Effects.skid(entity.x, entity.y, entity.animations.dash.data.get('initialSpeed') * -entity.face);
                        entity.face = controller.hmove >= 0 ? 1 : -1;
                    }
                    else if (entity.face !== Math.sign(controller.hmove)
                        && entity.setAnimation('pivot')) {
                        entity.face = controller.hmove >= 0 ? 1 : -1;
                    }
                }
                if (entity.animation === 'skid') {
                    if ((entity.face === 1 && controller.left)
                        || (entity.face === -1 && controller.right)) {
                        entity.setAnimation('turnaround', true);
                    }
                }
            }
        }
        else {
            if (entity.animation === 'walk' || entity.animation === 'stride') {
                entity.setAnimation('idle');
            }
        }
        if ((controller.jumpPress
            || (!controller.noTapJump && controller.upTap < 2))
            && entity.setAnimation('hop')) {
            entity.px = entity.dx;
            entity.animations['hop'].data.set('tapJumped', !controller.jumpPress);
        }
        if (controller.specialPress) {
            if (controller.hmove < -0.6
                || (controller.hmove < -0.5 && Math.abs(controller.vmove) < 0.5)) {
                if (entity.setAnimation('sidespecial')) {
                    entity.face = -1;
                }
            }
            else if (controller.hmove > 0.6
                || (controller.hmove > 0.5 && Math.abs(controller.vmove) < 0.5)) {
                if (entity.setAnimation('sidespecial')) {
                    entity.face = 1;
                }
            }
            else if (controller.vmove < -0.5) {
                if (entity.setAnimation('upspecial')) {
                    if ((entity.face === 1 && controller.hmove < -0.2)
                        || (entity.face === -1 && controller.hmove > 0.2)) {
                        entity.face = -entity.face;
                    }
                }
            }
            else if (controller.vmove > 0.5) {
                if (entity.setAnimation('downspecial')) {
                    if ((entity.face === 1 && controller.hmove < -0.2)
                        || (entity.face === -1 && controller.hmove > 0.2)) {
                        entity.face = -entity.face;
                    }
                }
            }
            else {
                if (entity.setAnimation('groundspecial') && controller.reversible) {
                    entity.face = -entity.face;
                }
            }
        }
        else if (controller.rup === 2) {
            entity.setAnimation('utap');
        }
        else if (controller.rdown === 2) {
            entity.setAnimation('dtap');
        }
        else if (controller.rleft === 2 || controller.rright === 2) {
            const face = controller.rright > 0 ? 1 : -1;
            if (entity.face !== face) {
                entity.setAnimation('ftappivot');
            }
            else {
                entity.setAnimation('ftap');
            }
        }
        else if (controller.grabPress && entity.setAnimation('grab')) {
        }
        else if (controller.attackPress) {
            let turn = false;
            if (entity.activeAnimation.type === animation_1.AnimationType.shield) {
                entity.setAnimation('grab');
            }
            else if (controller.shield > 0.01 && entity.setAnimation('grab')) {
                turn = true;
            }
            else if (controller.upTap < 6) {
                turn = entity.setAnimation('utap');
            }
            else if (controller.downTap < 6) {
                turn = entity.setAnimation('dtap');
            }
            else if (controller.leftTap < 6 || controller.rightTap < 6) {
                const face = controller.hmove > 0 ? 1 : -1;
                if (entity.face !== face) {
                    entity.setAnimation('ftappivot');
                }
                else {
                    entity.setAnimation('ftap');
                }
            }
            else if ((controller.hmove > 0.35 || controller.hmove < -0.33)
                && Math.abs(controller.hmove) > Math.abs(controller.vmove)) {
                if (entity.setAnimation('ftilt')) {
                    entity.face = Math.sign(controller.hmove);
                }
            }
            else if (controller.vmove < -0.3) {
                turn = entity.setAnimation('utilt');
            }
            else if (controller.vmove > 0.3) {
                turn = entity.setAnimation('dtilt');
            }
            else {
                turn = entity.setAnimation('jab');
            }
            if (turn) {
                if (controller.hmove > 0.1) {
                    entity.face = 1;
                }
                else if (controller.hmove < -0.1) {
                    entity.face = -1;
                }
            }
        }
        else if (controller.shield > 0.01
            || (controller.grab && !controller.grabPress)) {
            entity.setAnimation('shieldup');
        }
    }
};
const spawnAnimatable = (data) => {
    if (data.recycle) {
        for (let i = removed.length - 1; i >= 0; i--) {
            if (removed[i].removed && removed[i].name === data.name) {
                removed[i].removed = false;
                removed[i].refresh();
                return removed[i];
            }
        }
    }
    const entity = new Animatable(data);
    exports.entities.push(entity);
    return entity;
};
const tempHitMap = new Map();
const testHitBubbleCollisions = () => {
    const l = animation_1.getHitbubbleCount();
    for (let i = 0; i < l; i++) {
        let hb = animation_1.hitbubbles[i];
        let b = hb.bubble;
        let owner = hb.entity;
        let type = b.type;
        const x = hb.x;
        const y = hb.y;
        const x2 = hb.x2;
        const y2 = hb.y2;
        const radius = hb.radius;
        if (!owner) {
            continue;
        }
        if (type === animation_1.HitbubbleType.phasing || type === animation_1.HitbubbleType.wind) {
            continue;
        }
        for (let hi = i + 1; hi < l; hi++) {
            let thb = animation_1.hitbubbles[hi];
            const tb = thb.bubble;
            const htype = tb.type === animation_1.HitbubbleType.special && thb.entity.airborne
                ? animation_1.HitbubbleType.aerial
                : tb.type;
            let collideAs = owner.collided;
            let hitbubbleOwner = thb.entity;
            if (!hitbubbleOwner) {
                continue;
            }
            if (htype === animation_1.HitbubbleType.shield && type === animation_1.HitbubbleType.shield) {
                continue;
            }
            if ((htype === animation_1.HitbubbleType.shield && type === animation_1.HitbubbleType.grab)
                || (htype === animation_1.HitbubbleType.shield && type === animation_1.HitbubbleType.grab)) {
                continue;
            }
            if (htype === animation_1.HitbubbleType.phasing || htype === animation_1.HitbubbleType.wind) {
                continue;
            }
            if (!((htype !== animation_1.HitbubbleType.aerial && type !== animation_1.HitbubbleType.aerial)
                || (htype === animation_1.HitbubbleType.aerial && type === animation_1.HitbubbleType.shield)
                || (type === animation_1.HitbubbleType.aerial && htype === animation_1.HitbubbleType.shield))) {
                if (htype !== 4 && type !== 4) {
                    continue;
                }
            }
            if (!((type === animation_1.HitbubbleType.grab && htype === animation_1.HitbubbleType.grab)
                || (type !== animation_1.HitbubbleType.grab && htype !== animation_1.HitbubbleType.grab))) {
                continue;
            }
            if (owner === hitbubbleOwner) {
                continue;
            }
            if (owner.friendly === hitbubbleOwner
                || owner.friendly === hitbubbleOwner.friendly) {
                continue;
            }
            if (b.flags & animation_1.HitbubbleFlag.skip) {
                if (!tempHitMap.has(hb)) {
                    tempHitMap.set(hb, []);
                }
                collideAs = tempHitMap.get(hb);
            }
            if (collideAs.includes(hitbubbleOwner)) {
                continue;
            }
            {
                const hx = thb.x;
                const hy = thb.y;
                const hx2 = thb.x2;
                const hy2 = thb.y2;
                let d1 = b.damage;
                const d2 = tb.damage;
                let k1 = b.knockback;
                const k2 = tb.knockback;
                let c1 = false;
                let c2 = false;
                if (!collision_1.capsuleCollision(x, y, x2, y2, radius, hx, hy, hx2, hy2, thb.radius)) {
                    continue;
                }
                if (htype === animation_1.HitbubbleType.shield || type === animation_1.HitbubbleType.shield) {
                    let flags = b.flags;
                    let bonusDamage = d1;
                    let lag = 0;
                    let angle = 0;
                    let kx = 0;
                    let lightStun = 0;
                    let heavyStun = 0;
                    let powershielded = false;
                    if (type === animation_1.HitbubbleType.shield) {
                        const h = owner;
                        const tempb = hb;
                        owner = hitbubbleOwner;
                        hitbubbleOwner = h;
                        d1 = d2;
                        bonusDamage = d1;
                        k1 = k2;
                        hb = thb;
                        thb = tempb;
                        b = tb;
                        type = htype;
                        flags = tb.flags;
                        collideAs = owner.collided;
                        if (b.flags & 2) {
                            if (!tempHitMap.has(hb)) {
                                tempHitMap.set(hb, []);
                            }
                            collideAs = tempHitMap.get(hb);
                        }
                        if (collideAs.includes(hitbubbleOwner)) {
                            continue;
                        }
                    }
                    if (b.data.has('shieldDamage')) {
                        d1 = d1 + b.data.get('shieldDamage');
                    }
                    d1 = d1 * constants.SHIELD_DAMAGE_MOD;
                    collideAs.push(hitbubbleOwner);
                    lag = d1 * constants.LAG_MOD + constants.LAG_PLUS;
                    if (b.data.has('lag')) {
                        lag = lag + b.data.get('lag');
                    }
                    owner.lag = Math.ceil(lag);
                    angle = hb.angle(hitbubbleOwner.x < owner.x ? 180 : 0, hitbubbleOwner.airborne, 0);
                    if (owner.activeAnimation.charged) {
                        const scale = owner.activeAnimation.charged
                            * (owner.activeAnimation.data.has('scale')
                                ? owner.activeAnimation.data.get('scale')
                                : constants.TAP_SCALE);
                        d1 = d1 + d1 * scale;
                    }
                    heavyStun
                        = ((d1 + constants.SHIELD_STUN_BONUS_DMG)
                            * constants.SHIELD_STUN_MOD
                            + constants.SHIELD_STUN_PLUS)
                            | 0;
                    lightStun = Math.max(((d1 + constants.SHIELD_LIGHTSTUN_BONUS_DMG)
                        * constants.SHIELD_LIGHTSTUN_MOD
                        + constants.SHIELD_LIGHTSTUN_PLUS) | 0, heavyStun);
                    hitbubbleOwner.stun = Math.max(hitbubbleOwner.stun * constants.STUN_CARRY, heavyStun, constants.SHIELD_STUN_MIN);
                    hitbubbleOwner.stunInitial = hitbubbleOwner.stun;
                    hitbubbleOwner.shield.stun = Math.max(hitbubbleOwner.shield.stun * constants.STUN_CARRY, lightStun, constants.SHIELD_STUN_MIN);
                    hitbubbleOwner.shield.initialStun = hitbubbleOwner.shield.stun;
                    if (~flags & animation_1.HitbubbleFlag.no_self_lag) {
                        hitbubbleOwner.lag = owner.lag;
                    }
                    hitbubbleOwner.shield.wait = hitbubbleOwner.stun + 1;
                    d1 = d1 + bonusDamage * constants.SHIELD_DAMAGE_BONUS_MOD;
                    hitbubbleOwner.lastShield.frame = ticks;
                    hitbubbleOwner.lastShield.lastFrame = true;
                    hitbubbleOwner.lastShield.entity = owner;
                    hitbubbleOwner.lastShield.bubble = hb;
                    hitbubbleOwner.lastShield.damage = d1;
                    hitbubbleOwner.lastShield.knockback = k1;
                    hitbubbleOwner.lastShield.angle = angle;
                    hitbubbleOwner.lastShield.type = type;
                    hitbubbleOwner.lastShield.stun = hitbubbleOwner.stun;
                    hitbubbleOwner.lastShield.flags = flags;
                    hitbubbleOwner.stats.blocked = hitbubbleOwner.stats.blocked + d1;
                    hitbubbleOwner.stats.shieldHurts++;
                    owner.stats.shieldDamage = owner.stats.shieldDamage + d1;
                    owner.stats.shieldHits++;
                    powershielded
                        = constants.POWERSHIELD_OK
                            && hitbubbleOwner.activeAnimation.frame
                                < constants.POWERSHIELD_FRAMES
                            && hitbubbleOwner.activeAnimation.data.has('powershield')
                            && hitbubbleOwner.activeAnimation.data.get('powershield');
                    if (powershielded) {
                        hitbubbleOwner.stats.powerBlocked
                            = hitbubbleOwner.stats.powerBlocked + d1;
                        hitbubbleOwner.stats.powerHurts++;
                        owner.stats.powerDamage = owner.stats.powerDamage + d1;
                        owner.stats.powerHits++;
                    }
                    else {
                        hitbubbleOwner.shield.lastEnergy = hitbubbleOwner.shield.energy;
                        hitbubbleOwner.shield.energy
                            = hitbubbleOwner.shield.energy - d1 / 100;
                        hitbubbleOwner.shield.lastReduced = ticks;
                    }
                    kx = math_1.angleX(angle) * k1
                        + exports.calcKnockback(k1, b.growth, b.damage, b.damage, 40, hitbubbleOwner.weight, 0.373) * (owner.x <= hitbubbleOwner.x ? 1 : -1);
                    if (Math.abs(kx) > 0.01) {
                        if (!owner.airborne) {
                            const kbx = kx * -0.4;
                            owner.kbDecay = constants.KB_DECAY;
                            owner.okb = Math.abs(kbx);
                            owner.kb = Math.abs(kbx);
                            owner.kbx = kbx;
                            owner.kby = 0;
                            owner.kba = kbx >= 0 ? 0 : 180;
                        }
                        kx = kx * (1 - hitbubbleOwner.shield.density * 0.6)
                            + hitbubbleOwner.controller.hmove * 3;
                        hitbubbleOwner.kbDecay
                            = constants.KB_DECAY;
                        hitbubbleOwner.okb = Math.abs(kx);
                        hitbubbleOwner.kb = Math.abs(kx);
                        hitbubbleOwner.kbx = kx;
                        hitbubbleOwner.kby = 0;
                        hitbubbleOwner.kba = kx >= 0 ? 0 : 180;
                    }
                    if (b.data.has('onBlocked')) {
                        b.data.get('onBlocked')(hb, thb);
                    }
                    if (owner.activeAnimation.data.has('blocked')) {
                        owner.activeAnimation.data.get('blocked')(owner);
                    }
                    if (utils_1.objHas(owner.activeAnimation.keyframeData, 'blocked')) {
                        owner.activeAnimation.keyframeData.blocked(owner);
                    }
                    if (b.data.has('blockAudio')) {
                        audio_1.playAudio(b.data.get('blockAudio'), 1, 1, [b.x2, b.y2, 0]);
                    }
                    else {
                        audio_1.playAudio('tick', 1, 1, [b.x2, b.y2, 0]);
                    }
                    continue;
                }
                else if (Math.abs(d1 - d2) < constants.CLANK_DAMAGE) {
                    c1 = true;
                    c2 = true;
                    audio_1.playAudio('clash', 1, 1, [b.x2, b.y2, 0]);
                }
                else if (d1 + k1 > d2 + k2) {
                    c1 = false;
                    c2 = true;
                }
                else {
                    c1 = true;
                    c2 = false;
                }
                if (c1) {
                    const lag = (d1 + d2) * constants.LAG_MOD * 0.5 + constants.LAG_PLUS;
                    let lagAdd = 0;
                    collideAs = owner.collided;
                    if (b.flags & animation_1.HitbubbleFlag.skip) {
                        if (tempHitMap.has(hb)) {
                            tempHitMap.set(hb, []);
                        }
                        collideAs = tempHitMap.get(hb);
                    }
                    collideAs.push(hitbubbleOwner);
                    owner.lastClash.lastFrame = true;
                    owner.lastClash.entity = hitbubbleOwner;
                    if (owner.activeAnimation.data.has('clashed')) {
                        owner.activeAnimation.data.get('clashed')(owner, owner.controller);
                    }
                    if ((owner.activeAnimation.type === animation_1.AnimationType.attack
                        || owner.activeAnimation.data.has('recoil'))
                        && owner.activeAnimation.data.get('recoil') !== 'none') {
                        owner.schedule(owner.activeAnimation.data.get('recoil') || 'recoil', true);
                    }
                    if (b.data.has('lag')) {
                        lagAdd = b.data.get('lag') * 0.5;
                    }
                    if (tb.data.has('lag')) {
                        lagAdd = lagAdd + tb.data.get('lag') * 0.5;
                    }
                    owner.lag = Math.ceil(lag + lagAdd);
                    hitbubbleOwner.lag = Math.max(hitbubbleOwner.lag, owner.lag);
                }
                if (c2) {
                    const lag = (d1 + d2) * constants.LAG_MOD * 0.5 + constants.LAG_PLUS;
                    let lagAdd = 0;
                    collideAs = hitbubbleOwner.collided;
                    if (tb.flags & animation_1.HitbubbleFlag.skip) {
                        if (tempHitMap.has(thb)) {
                            tempHitMap.set(thb, []);
                        }
                        collideAs = tempHitMap.get(thb);
                    }
                    collideAs && collideAs.push(owner);
                    hitbubbleOwner.lastClash.lastFrame = true;
                    hitbubbleOwner.lastClash.entity = owner;
                    if (hitbubbleOwner.activeAnimation.data.has('clashed')) {
                        hitbubbleOwner.activeAnimation.data.get('clashed')(hitbubbleOwner, c1 === c2);
                    }
                    if ((hitbubbleOwner.activeAnimation.type === animation_1.AnimationType.attack
                        || hitbubbleOwner.activeAnimation.data.has('recoil'))
                        && hitbubbleOwner.activeAnimation.data.get('recoil') !== 'none') {
                        hitbubbleOwner.schedule(hitbubbleOwner.activeAnimation.data.get('recoil') || 'recoil', true);
                    }
                    if (b.data.has('lag')) {
                        lagAdd = b.data.get('lag') * 0.5;
                    }
                    if (tb.data.has('lag')) {
                        lagAdd = lagAdd + tb.data.get('lag') * 0.5;
                    }
                    owner.lag = Math.ceil(lag + lagAdd);
                    hitbubbleOwner.lag = Math.max(hitbubbleOwner.lag, owner.lag);
                }
            }
        }
    }
};
const testCollisions = () => {
    const l = animation_1.getHitbubbleCount();
    for (let i = 0; i < l; i++) {
        const hb = animation_1.hitbubbles[i];
        const b = hb.bubble;
        const hx = hb.x;
        const hy = hb.y;
        const hx2 = hb.x2;
        const hy2 = hb.y2;
        const type = b.type;
        const flags = b.flags;
        if (type === animation_1.HitbubbleType.shield) {
            continue;
        }
        for (let j = 0; j < animation_1.hurtbubbles.length; j++) {
            const hurtbubble = animation_1.hurtbubbles[j];
            const hurtbubbleOwner = hurtbubble.owner;
            const x = hurtbubble.x + hurtbubbleOwner.x;
            const y = hurtbubble.y + hurtbubbleOwner.y;
            const x2 = hurtbubble.x2 + hurtbubbleOwner.x;
            const y2 = hurtbubble.y2 + hurtbubbleOwner.y;
            const radius = hurtbubble.radius;
            const hitbubbleOwner = hb.entity;
            let collideAs = hitbubbleOwner.collided;
            if (hurtbubbleOwner.removed
                || hurtbubble.state === animation_1.HurtbubbleState.phased
                || hurtbubble.state === animation_1.HurtbubbleState.intangible
                || hurtbubble.state === animation_1.HurtbubbleState.decoration) {
                continue;
            }
            if (hurtbubble.ungrabbable && type === animation_1.HitbubbleType.grab) {
                continue;
            }
            if (hurtbubble.state === animation_1.HurtbubbleState.protected
                && type !== animation_1.HitbubbleType.grab) {
                continue;
            }
            if ((flags & animation_1.HitbubbleFlag.ground) === 1 && hurtbubbleOwner.airborne) {
                continue;
            }
            if ((flags & animation_1.HitbubbleFlag.air) === 1 && !hurtbubbleOwner.airborne) {
                continue;
            }
            if (!hitbubbleOwner
                || hurtbubbleOwner === hitbubbleOwner
                || hitbubbleOwner.friendly === hurtbubbleOwner) {
                continue;
            }
            if (b.flags & animation_1.HitbubbleFlag.skip) {
                if (!tempHitMap.has(hb)) {
                    tempHitMap.set(hb, []);
                }
                collideAs = tempHitMap.get(hb);
            }
            if (collideAs.includes(hurtbubbleOwner)) {
                continue;
            }
            if (type === animation_1.HitbubbleType.grab
                && (hurtbubbleOwner.activeAnimation.data.has('ungrabbable')
                    || (hurtbubbleOwner.activeAnimation.name.startsWith('hop')
                        && hurtbubbleOwner.activeAnimation.frame < 4))) {
                if (!(constants.GRAB_FALLEN_OK
                    && hurtbubbleOwner.animation !== 'fall'
                    && hurtbubbleOwner.animation !== 'fallen'
                    && hurtbubbleOwner.animation !== 'reset')) {
                    continue;
                }
            }
            if ((b.data.has('hitTest') && !b.data.get('hitTest')(b, hurtbubble))
                || !collision_1.capsuleCollision(x, y, x2, y2, radius, hx, hy, hx2, hy2, hb.radius)) {
                continue;
            }
            if (b.data.has('onHit')) {
                b.data.get('onHit')(hb, hurtbubble);
            }
            {
                const parried = hurtbubbleOwner.activeAnimation.data.has('onInjured')
                    && hurtbubbleOwner.activeAnimation.data.get('onInjured')(hb, hurtbubble);
                const ignored = parried && hb.bubble.type === animation_1.HitbubbleType.object;
                const kbbase = b.knockback;
                let baseDamage = b.damage * constants.DAMAGE_MOD;
                let damage = baseDamage;
                let charge = 1;
                let stale = ~flags & animation_1.HitbubbleFlag.no_stale
                    ? hitbubbleOwner.activeAnimation.staled
                    : 0;
                let kbstale = 1;
                let kbcancel = 1;
                let originalDamage = 0;
                if (hitbubbleOwner.activeAnimation.charged) {
                    charge
                        = hitbubbleOwner.activeAnimation.charged
                            * ((hitbubbleOwner.activeAnimation.data.has('scale')
                                ? hitbubbleOwner.activeAnimation.data.get('scale')
                                : constants.TAP_SCALE)
                                - 1);
                    baseDamage = damage = damage + damage * charge;
                }
                collideAs.push(hurtbubbleOwner);
                if (!hitbubbleOwner.activeAnimation.hit
                    && ~flags & animation_1.HitbubbleFlag.no_stale) {
                    const staled = !hitbubbleOwner.staleAs
                        ? hitbubbleOwner.stale
                        : hitbubbleOwner.staleAs.stale;
                    const staleName = !b.data.has('staleAs')
                        ? hitbubbleOwner.animation
                        : b.data.get('staleAs');
                    stale = 0;
                    for (let ii = 0; ii < staled.moves.length; ii++) {
                        const index = (staled.cursor - 1 - ii + constants.STALE_STACK_SIZE)
                            % constants.STALE_STACK_SIZE;
                        if (staled.moves[index] === staleName) {
                            stale
                                = stale
                                    + constants.STALE_VALUE * constants.STALE_STACK_SIZE
                                    - ii * constants.STALE_VALUE;
                        }
                    }
                    hitbubbleOwner.activeAnimation.staled = stale;
                    if (~flags & animation_1.HitbubbleFlag.no_stale_add) {
                        staled.moves[staled.cursor] = staleName;
                        staled.cursor = (staled.cursor + 1) % constants.STALE_STACK_SIZE;
                    }
                    hitbubbleOwner.activeAnimation.hit = true;
                }
                damage = damage * (1 - stale * constants.STALE_DAMAGE_MOD);
                kbstale = 1 - stale * constants.STALE_KNOCKBACK_MOD;
                if (hurtbubble.state === animation_1.HurtbubbleState.invincible) {
                    if (~flags & animation_1.HitbubbleFlag.wind) {
                        if (~flags & animation_1.HitbubbleFlag.no_self_lag) {
                            let lag = damage * constants.LAG_MOD + constants.LAG_PLUS;
                            if (b.data.has('lag')) {
                                lag = lag + b.data.get('lag');
                            }
                            if (b.data.has('setLag')) {
                                lag = b.data.get('setLag');
                            }
                            hitbubbleOwner.lag = Math.max(hitbubbleOwner.lag, Math.ceil(lag));
                        }
                    }
                    continue;
                }
                if (hurtbubbleOwner.animation === 'crouched'
                    || hurtbubbleOwner.animation === 'crouch') {
                    kbcancel = kbcancel * constants.CROUCH_CANCEL_MOD;
                }
                if (hurtbubbleOwner.teched > constants.V_CANCEL_WINDOW
                    && hurtbubbleOwner.airborne
                    && (hurtbubbleOwner.activeAnimation.type === animation_1.AnimationType.movement
                        || hurtbubbleOwner.activeAnimation.type === animation_1.AnimationType.passive)) {
                    console.log('v-cancelled');
                    hurtbubbleOwner.stats.vCancels++;
                    kbcancel = kbcancel * constants.V_CANCEL_MOD;
                }
                if (ignored) {
                    kbcancel = 0;
                    originalDamage = damage;
                    baseDamage = 0;
                    damage = 0;
                }
                {
                    const kbgrowth = b.growth;
                    const weight = type === animation_1.HitbubbleType.throw ? 1 : hurtbubbleOwner.weight;
                    const knockback = exports.calcKnockback(kbbase, kbgrowth, baseDamage, damage, hurtbubbleOwner.damage, weight, kbcancel * kbstale);
                    const angle = hb.angle(hitbubbleOwner.x - hurtbubbleOwner.x, hurtbubbleOwner.airborne, hurtbubbleOwner.damage);
                    const kx = math_1.angleX(angle) * knockback;
                    let ky = math_1.angleY(angle) * knockback;
                    let stun = 0;
                    const windbox = (flags & animation_1.HitbubbleFlag.wind) !== 0;
                    const isCombo = hurtbubbleOwner.stun > 0
                        || type === animation_1.HitbubbleType.throw
                        || hurtbubbleOwner.activeAnimation.transition === 'held';
                    let pitch = 1;
                    let volume = 1;
                    const prestun = hurtbubbleOwner.stun;
                    if (ky < 0.001 && ky > -0.001) {
                        ky = 0;
                    }
                    hurtbubbleOwner.kbf = flags;
                    if (hurtbubble.state !== animation_1.HurtbubbleState.heavyArmor) {
                        if (!windbox) {
                            if ((flags & animation_1.HitbubbleFlag.no_turnaround) === 0) {
                                if (type === animation_1.HitbubbleType.object && kx !== 0) {
                                    hurtbubbleOwner.face = Math.sign(-kx);
                                }
                                else {
                                    hurtbubbleOwner.face
                                        = Math.sign(hitbubbleOwner.x - hurtbubbleOwner.x)
                                            || hurtbubbleOwner.face;
                                }
                            }
                            hurtbubbleOwner.dx = 0;
                            hurtbubbleOwner.dy = 0;
                            hurtbubbleOwner.slide = 0;
                        }
                        if (knockback > 0 || !windbox) {
                            hurtbubbleOwner.kb = knockback;
                            hurtbubbleOwner.okb = knockback;
                            hurtbubbleOwner.kbx = kx;
                            hurtbubbleOwner.kby = ky;
                            hurtbubbleOwner.kba = angle;
                            if (b.data.has('addVelocity')) {
                                hurtbubbleOwner.kbx
                                    = hurtbubbleOwner.kbx
                                        + hitbubbleOwner.dx * b.data.get('addVelocity');
                                hurtbubbleOwner.kby
                                    = hurtbubbleOwner.kby
                                        + hitbubbleOwner.dy * b.data.get('addVelocity');
                            }
                            hurtbubbleOwner.kbDecay
                                = constants.KB_DECAY;
                        }
                        if (!windbox) {
                            stun = exports.calcStun(knockback);
                            hurtbubbleOwner.stun = Math.max(hurtbubbleOwner.stun * constants.STUN_CARRY, stun) | 0;
                            hurtbubbleOwner.stunInitial = hurtbubbleOwner.stun;
                            hurtbubbleOwner.launched = true;
                        }
                        else if ((ky > 0.01
                            || Math.abs(kx) > constants.HORIZONTAL_KNOCKDOWN_THRESHOLD)
                            && !hurtbubbleOwner.airborne) {
                            hurtbubbleOwner.ledgeReleased = 0;
                        }
                    }
                    else {
                        hurtbubbleOwner.stats.damageAbsorbed
                            = hurtbubbleOwner.stats.damageAbsorbed + damage;
                        hurtbubbleOwner.stats.knockbackAbsorbed
                            = hurtbubbleOwner.stats.knockbackAbsorbed + knockback;
                    }
                    if (!windbox && type !== animation_1.HitbubbleType.throw) {
                        let lag = damage * constants.LAG_MOD;
                        if (b.data.has('lag')) {
                            lag = lag + b.data.get('lag');
                        }
                        if (b.data.get('setLag')) {
                            lag = b.data.get('setLag');
                        }
                        if (~flags & animation_1.HitbubbleFlag.no_self_lag) {
                            hitbubbleOwner.lag = Math.max(hitbubbleOwner.lag, Math.ceil(lag + constants.LAG_PLUS));
                        }
                        if (ignored) {
                        }
                        else if (hurtbubbleOwner.animation === 'crouched'
                            || hurtbubbleOwner.animation === 'crouch') {
                            damage = damage * constants.CROUCH_CANCEL_DMG_MOD;
                            hurtbubbleOwner.lag = Math.max(hurtbubbleOwner.lag, Math.ceil(lag * constants.CROUCH_CANCEL_LAG_MOD + constants.LAG_PLUS));
                        }
                        else {
                            hurtbubbleOwner.lag = Math.max(hurtbubbleOwner.lag, Math.ceil(lag + constants.LAG_PLUS));
                        }
                    }
                    if (b.data.has('effect')) {
                        switch (b.data.get('effect')) {
                            case 'electric':
                                hurtbubbleOwner.lag = Math.floor(hurtbubbleOwner.lag * 1.5);
                                break;
                        }
                    }
                    if (hurtbubbleOwner.lag > 0) {
                        hurtbubbleOwner.hitlag = true;
                    }
                    if (hurtbubbleOwner.animation === 'ledgehang'
                        || hurtbubbleOwner.animation === 'ledgegrab') {
                        const entity = hurtbubbleOwner;
                        const animation = entity.activeAnimation;
                        entity.airborne = true;
                        entity.x
                            = entity.x
                                + animation.data.get('xOffset') * (entity.ledgeHang ? 1 : -1);
                        entity.y = entity.y + animation.data.get('yOffset');
                        entity.ecb.update(entity.x, entity.y);
                    }
                    if (hurtbubbleOwner.launched) {
                        const entity = hurtbubbleOwner;
                        entity.nx
                            = entity.nx
                                + Math.max(Math.min((hx - (entity.x + entity.nx)) / 2, 10), -10);
                        if (entity.airborne) {
                            entity.ny
                                = entity.ny
                                    - Math.max(Math.min((hy - (entity.y + entity.ny)) / 2, 10), -10);
                        }
                    }
                    rendering_1.addCameraImpulse(-kx * 0.3, ky * 0.3, 0);
                    hurtbubbleOwner.damage = hurtbubbleOwner.damage + damage;
                    exports.dbg.drawUI
                        && damage > 0
                        && rendering_1.Effects.fadingText(Math.ceil(damage).toString(10), Math.ceil(damage * damage * 0.05) + 14, 1, 1, 0.75, {
                            x: hurtbubbleOwner.x + hurtbubbleOwner.headbubble.x,
                            y: hurtbubbleOwner.y + hurtbubbleOwner.headbubble.y,
                        }, 0);
                    if (!ignored) {
                        hurtbubbleOwner.lastInjury.frame = ticks;
                        hurtbubbleOwner.lastInjury.lastFrame = true;
                        hurtbubbleOwner.lastInjury.entity = hitbubbleOwner.friendly;
                        hurtbubbleOwner.lastInjury.bubble = hb;
                        hurtbubbleOwner.lastInjury.damage = damage;
                        hurtbubbleOwner.lastInjury.knockback = knockback;
                        hurtbubbleOwner.lastInjury.angle = angle;
                        hurtbubbleOwner.lastInjury.type = type;
                        hurtbubbleOwner.lastInjury.stun = stun;
                        hurtbubbleOwner.lastInjury.lag = hurtbubbleOwner.lag;
                        hurtbubbleOwner.lastInjury.flags = flags;
                        hurtbubbleOwner.lastInjury.landedSince = false;
                        hurtbubbleOwner.lastInjury.stale = stale;
                        hurtbubbleOwner.stats.damageTaken
                            = hurtbubbleOwner.stats.damageTaken + damage;
                        hurtbubbleOwner.stats.hurts++;
                    }
                    hitbubbleOwner.lastCollision.frame = ticks;
                    hitbubbleOwner.lastCollision.lastFrame = true;
                    hitbubbleOwner.lastCollision.entity = hurtbubbleOwner;
                    hitbubbleOwner.lastCollision.bubble = hb;
                    hitbubbleOwner.lastCollision.damage = damage;
                    hitbubbleOwner.lastCollision.knockback = knockback;
                    hitbubbleOwner.lastCollision.angle = angle;
                    hitbubbleOwner.lastCollision.type = type;
                    hitbubbleOwner.lastCollision.stun = stun;
                    hitbubbleOwner.lastCollision.flags = flags;
                    hitbubbleOwner.lastCollision.stale = stale;
                    hitbubbleOwner.stats.damage = hitbubbleOwner.stats.damage + damage;
                    hitbubbleOwner.stats.hits++;
                    if (stale > 0) {
                        hitbubbleOwner.stats.staleMoves++;
                        if (stale > 0.25) {
                            hitbubbleOwner.stats.veryStaleMoves++;
                        }
                    }
                    if (parried) {
                        hurtbubbleOwner.stats.parries++;
                        hurtbubbleOwner.stats.parryBlocked
                            = hurtbubbleOwner.stats.parryBlocked + originalDamage;
                        hitbubbleOwner.stats.parryHits++;
                        hitbubbleOwner.stats.parryDamage
                            = hurtbubbleOwner.stats.parryDamage + originalDamage;
                        hitbubbleOwner.flash = 12;
                        audio_1.playAudio('highhit', 0.5, 0.5, [hb.x2, hb.y2, 0]);
                        audio_1.playAudio('hit', 0.3, 3.5, [hb.x2, hb.y2, 0]);
                        audio_1.playAudio('meteorcancel', 1, 2.0, [hb.x2, hb.y2, 0]);
                        rendering_1.Effects.burst(hx, hy, 3, knockback * 0.1, hurtbubbleOwner.palette.base);
                        if (ignored) {
                            continue;
                        }
                    }
                    if (type === animation_1.HitbubbleType.grab) {
                        if (!isCombo) {
                            exports.Game.comboCounter = 0;
                        }
                    }
                    pitch = b.audio.pitch;
                    pitch = pitch
                        + pitch * (!isCombo
                            ? 0
                            : exports.Game.totalDamage * 0.001 + exports.Game.comboCounter * 0.033)
                        + pitch * (math_1.lqrandom() * 0.02 - 0.01 - Math.pow(knockback, 0.75) * 0.001);
                    volume = b.audio.volume;
                    volume = volume * 0.6
                        + volume * (Math.pow(knockback, 0.75) * 0.2 + exports.Game.comboCounter * 0.01);
                    audio_1.playAudio(b.audio.name, volume, pitch, [hb.x2, hb.y2, 0]);
                    if (type === animation_1.HitbubbleType.ground
                        || type === animation_1.HitbubbleType.aerial
                        || type === animation_1.HitbubbleType.special
                        || type === animation_1.HitbubbleType.throw) {
                        if (isCombo) {
                            exports.Game.comboCounter = exports.Game.comboCounter + 1;
                            exports.Game.totalDamage = exports.Game.totalDamage + damage;
                            if (type !== animation_1.HitbubbleType.throw) {
                                rendering_1.Effects.combo(hx, hy, kx
                                    * ((flags & animation_1.HitbubbleFlag.no_reverse) === 0
                                        || hurtbubbleOwner.x >= hitbubbleOwner.x
                                        ? 1
                                        : -1), ky, radius, angle, hurtbubbleOwner.lag, hb.bubble.color);
                            }
                            else {
                            }
                        }
                        else {
                            exports.Game.comboCounter = 0;
                            exports.Game.totalDamage = damage;
                            rendering_1.Effects.hit(hx, hy, kx
                                * ((flags & animation_1.HitbubbleFlag.no_reverse) === 0
                                    || hurtbubbleOwner.x >= hitbubbleOwner.x
                                    ? 1
                                    : -1), ky, radius, angle, hurtbubbleOwner.lag, hb.bubble.color);
                        }
                        exports.Game.damage = damage;
                        exports.Game.kb = knockback;
                        exports.Game.edmg = hurtbubbleOwner.damage - damage;
                        exports.Game.basekb = kbbase;
                        exports.Game.bubble = hb;
                        exports.Game.angle = angle;
                        exports.Game.stale = stale;
                        exports.Game.prestun = prestun;
                        exports.Game.poststun
                            = prestun <= 0
                                ? ticks - hurtbubbleOwner.outStun < 120
                                    ? 1 + ticks - hurtbubbleOwner.outStun
                                    : -1
                                : 0;
                        exports.Game.stun = hurtbubbleOwner.stun;
                        exports.Game.di = angle;
                        exports.Game.diMagnitude = 0;
                        exports.Game.frame = ticks;
                        exports.Game.entity = hurtbubbleOwner;
                        exports.Game.x = hurtbubbleOwner.x;
                        exports.Game.y = hurtbubbleOwner.y;
                        gl_matrix_1.vec2.set(exports.Game.sdi, 0, 0);
                        gl_matrix_1.vec2.set(exports.Game.asdi, 0, 0);
                        if (exports.dbg.training) {
                            rendering_1.updateTraining();
                            rendering_1.updateTrainingGraphs();
                        }
                    }
                }
            }
        }
    }
};
const halfCircle = Math.PI / 180;
const calculateDI = (hitAngle, di, intensity = 1, _flags = 0, _stale = 0) => {
    const diModifier = constants.DI_MOD * (intensity > 0.7 ? 1 : intensity * 1.4);
    const angle = hitAngle - di;
    const influence = (Math.sin(angle * halfCircle) * intensity) ** 2;
    if (angle < 0 && angle > -180) {
        return hitAngle + influence * diModifier;
    }
    return hitAngle - influence * diModifier;
};
exports.calculateDI = calculateDI;
const hitlagEntity = (entity, controller) => {
    let leftGround = false;
    let asdiX = 0;
    let asdiY = 0;
    if (controller.leftTap === 0
        || controller.rightTap === 0
        || controller.upTap === 0
        || controller.downTap === 0) {
        const a = controller.angle();
        entity.nx = entity.nx - entity.sdi * math_1.angleX(a);
        entity.ny = entity.ny - entity.sdi * math_1.angleY(a);
    }
    else if (controller.rleft === 2
        || controller.rright === 2
        || controller.rup === 2
        || controller.rdown === 2) {
        const a = controller.rangle();
        entity.nx = entity.nx - entity.sdi * math_1.angleX(a);
        entity.ny = entity.ny - entity.sdi * math_1.angleY(a);
    }
    if (!entity.airborne) {
        entity.ny = 0;
    }
    gl_matrix_1.vec2.set(exports.Game.sdi, exports.Game.sdi[0] + entity.nx, exports.Game.sdi[1] + entity.ny);
    if (entity.lag > 0) {
        exports.dbg.training && rendering_1.updateTraining();
        return;
    }
    if (Math.abs(controller.hright) > 0.2 || Math.abs(controller.vright) > 0.2) {
        asdiX = entity.asdi * controller.hright * (entity.lastInjury.knockback / 4);
        asdiY = entity.asdi * controller.vright * (entity.lastInjury.knockback / 4);
    }
    else {
        asdiX = entity.asdi * controller.hmove * (entity.lastInjury.knockback / 4);
        asdiY = entity.asdi * controller.vmove * (entity.lastInjury.knockback / 4);
    }
    gl_matrix_1.vec2.set(exports.Game.asdi, asdiX, asdiY);
    entity.nx = entity.nx + asdiX;
    if (entity.airborne) {
        entity.ny = entity.ny + asdiY;
    }
    exports.Game.frame = ticks;
    exports.dbg.training && rendering_1.updateTraining();
    if (entity.kb <= 0) {
        return;
    }
    if (!entity.airborne && entity.kby > 0.01) {
        entity.airborne = true;
        leftGround = true;
    }
    if (!entity.airborne) {
        entity.kby = 0;
    }
    if (leftGround) {
        entity.dx = entity.dx + entity.platform.dx;
        entity.dy = entity.dy + entity.platform.dy;
    }
};
const launchEntity = (entity) => {
    const controller = entity.controller;
    const strong = entity.okb > entity.softland * constants.SOFTLAND
        || entity.lastInjury.bubble.bubble.data.has('strong');
    const knockdown = entity.kbx > constants.HORIZONTAL_KNOCKDOWN_THRESHOLD || strong;
    if ((entity.kbf & animation_1.HitbubbleFlag.fixed) === 0) {
        let cangle = entity.events.dispatch('di', entity.kba);
        let cdist = 0;
        let angle = 0;
        if (cangle !== null) {
            cdist = 1;
        }
        if (cangle !== null || controller) {
            if (cangle === null
                && (Math.abs(controller.hmove) >= 0.1 || Math.abs(controller.vmove) >= 0.1)) {
                cangle = math_1.computeAngle(-controller.hmove, controller.vmove);
                cdist = controller.distance();
            }
            angle = exports.calculateDI(entity.kba, cangle, cdist, entity.kbf, entity.lastInjury.stale);
            entity.kbx = math_1.angleX(angle) * entity.kb;
            entity.kby = math_1.angleY(angle) * entity.kb;
            exports.Game.di = angle;
            exports.Game.diMagnitude = cdist;
            exports.Game.frame = ticks;
            exports.Game.x = entity.x;
            exports.Game.y = entity.y;
            if (exports.dbg.training) {
                rendering_1.updateTraining();
                rendering_1.updateTrainingGraphs();
            }
        }
    }
    if (entity.lastInjury.type === animation_1.HitbubbleType.throw && entity.kbx !== 0) {
        entity.face = entity.kbx > 0 ? -1 : 1;
    }
    entity.ledgeReleased = 0;
    entity.launched = false;
    if (entity.airborne
        || entity.animation === 'ledgegrab'
        || entity.animation === 'ledgehang') {
        if (!constants.METEOR_CANCEL_OK || ~entity.lastInjury.flags & animation_1.HitbubbleFlag.meteor) {
            if (entity.lastInjury.bubble.bubble.data.has('airhitAnimation')) {
                entity.setAnimation(entity.lastInjury.bubble.bubble.data.get('airhitAnimation'));
                return;
            }
            entity.setAnimation(!strong
                ? 'weakairhit'
                : Math.abs(entity.kba - 90) < 16
                    ? 'jugglehit'
                    : 'airhit', true);
            return;
        }
        entity.setAnimation('meteorhit', true);
        return;
    }
    if (entity.okb + Math.abs(entity.kby) * 0.5 < entity.launchResistance) {
        entity.kbx = entity.okb * Math.sign(entity.kbx);
        entity.kba = math_1.computeAngle(-entity.kbx, -entity.kby);
        entity.kby = 0;
    }
    if (entity.kby < 0 && -entity.kby > Math.abs(entity.kbx)) {
        entity.setAnimation('bounced', true);
        entity.kbx = entity.kbx * 0.75;
        entity.kby = -entity.kby * 1.25;
        entity.kb = Math.sqrt(entity.kbx ** 2 + entity.kby ** 2);
        entity.kba = math_1.computeAngle(-entity.kbx, -entity.kby);
        entity.lag = entity.lag + 2;
        entity.stun *= 1.5;
        entity.airborne = true;
        return;
    }
    if (entity.okb <= constants.JAB_RESET
        && (entity.animation === 'fallen' || entity.animation === 'fall')) {
        entity.stun = 0;
        entity.setAnimation('reset', true);
        return;
    }
    if (entity.lastInjury.bubble.bubble.data.has('hitAnimation')) {
        entity.setAnimation(entity.lastInjury.bubble.bubble.data.get('hitAnimation'));
        return;
    }
    if (entity.kby > entity.launchResistance || knockdown) {
        entity.setAnimation(!strong && !knockdown
            ? 'weakairhit'
            : Math.abs(entity.kba - 90) < 16
                ? 'jugglehit'
                : 'airhit', true);
        entity.airborne = true;
    }
    else {
        entity.stun = Math.ceil(entity.stun * constants.GROUNDED_STUN_MOD);
        entity.kbx = entity.kbx * constants.GROUNDED_KB_MOD;
        entity.kby = entity.kby * constants.GROUNDED_KB_MOD;
        entity.kb = entity.kb * constants.GROUNDED_KB_MOD;
        entity.kby = 0;
        entity.okb = Math.abs(entity.kbx);
        entity.setAnimation(!strong ? 'weakhit' : 'hit', true);
    }
};
const processLedges = (entity, controller) => {
    let grab = 0;
    let ledgegrabbed = null;
    let directions = 0;
    if (ticks - entity.lastInjury.frame < constants.LEDGE_GRAB_HIT_DELAY) {
        return;
    }
    if (!utils_1.objHas(entity.activeAnimation.keyframeData, 'grabDirections')
        && !utils_1.objHas(entity.activeAnimation, 'grabDirections')) {
        return;
    }
    directions = utils_1.objHas(entity.activeAnimation.keyframeData, 'grabDirections')
        ? entity.activeAnimation.keyframeData.grabDirections
        : entity.activeAnimation.grabDirections;
    if (entity.ledgeReleased <= constants.LEDGE_REGRAB_RESTRICT
        || entity.lag > 0
        || utils_1.objHas(entity.activeAnimation.keyframeData, 'noLedgeGrab')
        || !entity.airborne
        || (controller
            && controller.vmove > 0.5
            && Math.abs(controller.hmove) < 0.2)
        || (!utils_1.objHas(entity.activeAnimation.keyframeData, 'grabDirections')
            && entity.activeAnimation.grabDirections === 0)) {
        return;
    }
    if ((directions & (animation_1.GrabDirection.forward | animation_1.GrabDirection.backward)) !== 0
        && (((directions & animation_1.GrabDirection.up) !== 0 && entity.dy >= 0)
            || ((directions & animation_1.GrabDirection.left) !== 0 && entity.dx <= 0)
            || ((directions & animation_1.GrabDirection.down) !== 0 && entity.dy < -0.01)
            || ((directions & animation_1.GrabDirection.right) !== 0 && entity.dx >= 0))) {
        for (let i = 0; i < stage.elements.length; i++) {
            grab = stage.elements[i].testLedgeGrab(entity, directions);
            if (grab > 0) {
                ledgegrabbed = stage.elements[i];
                break;
            }
        }
    }
    if (!ledgegrabbed) {
        return;
    }
    entity.airborne = false;
    entity.dx = 0;
    entity.dy = 0;
    entity.kb = 0;
    entity.kbx = 0;
    entity.kby = 0;
    entity.airjumps = 0;
    entity.slide = 0;
    entity.fastfall = false;
    entity.platform = ledgegrabbed;
    entity.wallJump = 0;
    entity.hover = entity.platform;
    if (grab === 1) {
        entity.x = ledgegrabbed.x;
        entity.y = ledgegrabbed.y;
        entity.face = 1;
    }
    else if (grab === 2) {
        entity.x = ledgegrabbed.x2;
        entity.y = ledgegrabbed.y2;
        entity.face = -1;
    }
    rendering_1.Effects.ledgegrab(entity.x - entity.face * 3, entity.y - 3, entity.palette.base);
    if (!entity.activeAnimation.data.has('ledgestall')) {
        entity.intangible = constants.LEDGE_INTANGIBILITY;
    }
    else {
        entity.intangible
            = (constants.LEDGE_INTANGIBILITY / Math.max(entity.pseudojumps, 1)) | 0;
    }
    entity.vulnerable = Math.min(Math.max(entity.vulnerable, ((entity.regrabs * 6) / 5) & ~1), entity.intangible);
    entity.regrabs++;
    entity.grabbedOn = ticks;
    entity.playAudio('ledgegrab');
    entity.ledgeHang = entity.face === 1;
    if (entity.ledgeHang) {
        entity.platform.leftOccupied = 2;
    }
    else {
        entity.platform.rightOccupied = 2;
    }
    entity.setAnimation('ledgegrab', true);
    gl_matrix_1.vec2.set(entity.velocity, 0, 0);
    entity.stats.ledgeGrabs++;
    entity.stats.ledgeMaxReGrabs = Math.max(entity.stats.ledgeMaxReGrabs, entity.regrabs);
    if (entity.regrabs > 1) {
        entity.stats.ledgeReGrabs++;
    }
};
const landEntity = (entity, controller) => {
    const dy = entity.dy - entity.kby;
    const fastfell = entity.fastfall;
    let landingAudio = entity.landingAudio;
    entity.airjumps = 0;
    entity.pseudojumps = 0;
    entity.regrabs = 0;
    entity.fastfall = false;
    if (entity.lag > 0
        || entity.activeAnimation.data.has('airborne')
        || utils_1.objHas(entity.activeAnimation.keyframeData, 'airborne')) {
        return;
    }
    if (entity.activeAnimation.data.has('techable')) {
        entity.kbx = entity.kbx * 0.75;
        entity.kby = entity.kby * 0.75;
        entity.kb = entity.kb * 0.75;
    }
    entity.airborne = false;
    entity.dy = 0;
    entity.velocity[1] = 0;
    if (entity.activeAnimation.type === animation_1.AnimationType.movement) {
        entity.slide = entity.dx;
        entity.dx = 0;
    }
    else {
        entity.slide = 0;
    }
    if (entity.slide >= 0) {
        entity.slide = entity.slide - entity.platform.dx;
        if (entity.slide < 0) {
            entity.dx = entity.dx + entity.slide;
            entity.slide = 0;
        }
    }
    else {
        entity.slide = entity.slide - entity.platform.dx;
        if (entity.slide > 0) {
            entity.dx = entity.dx + entity.slide;
            entity.slide = 0;
        }
    }
    if (entity.activeAnimation.type === animation_1.AnimationType.movement
        || entity.activeAnimation.type === animation_1.AnimationType.passive) {
        entity.velocity[0]
            = entity.dx + entity.kbx * entity.kbStandFriction + entity.slide;
    }
    else {
        entity.velocity[0]
            = entity.dx + entity.kbx * entity.kbFriction + entity.slide;
    }
    if ((entity.activeAnimation.data.has('early')
        && entity.activeAnimation.frame
            < entity.activeAnimation.data.get('early'))
        || (entity.activeAnimation.data.has('late')
            && entity.activeAnimation.frame
                > entity.activeAnimation.duration
                    - entity.activeAnimation.data.get('late'))) {
        if (Math.abs(dy) - entity.initialFallSpeed - entity.fallSpeed * 5 > 0) {
            entity.setAnimation('airborne-cancel', true);
        }
        else {
            entity.setAnimation('idle', true);
            return;
        }
    }
    else if (entity.activeAnimation.data.has('cancel')
        || utils_1.objHas(entity.activeAnimation.keyframeData, 'cancel')) {
        if (entity.activeAnimation.data.has('techable')) {
            let option = 'miss';
            const eventResult = entity.events.dispatch('tech');
            rendering_1.Effects.thump(entity.x, entity.y, entity.dx, 0, entity.width * 1.25, entity.platform.normal());
            if (entity.teched > constants.TECH_TIMER - constants.TECH_WINDOW) {
                option = 'tech';
                if (controller.left) {
                    if (entity.face === -1) {
                        option = 'techforth';
                    }
                    else {
                        option = 'techback';
                    }
                }
                else if (controller.right) {
                    if (entity.face === 1) {
                        option = 'techforth';
                    }
                    else {
                        option = 'techback';
                    }
                }
            }
            else if (eventResult !== null) {
                option = eventResult;
            }
            if (option !== 'miss') {
                entity.setAnimation(option, true);
                entity.stats.tech++;
                entity.stun = 0;
                entity.teched = 0;
                entity.activeAnimation.data.has('tech')
                    && entity.activeAnimation.data.get('tech')(entity, 3);
                return;
            }
        }
        const oldAnimation = entity.activeAnimation;
        const cancelAnimation = utils_1.objHas(entity.activeAnimation.keyframeData, 'cancel')
            ? entity.activeAnimation.keyframeData.cancel
            : entity.activeAnimation.data.get('cancel') || 'cancel';
        if (entity.activeAnimation.data.has('techable')) {
            entity.stats.techmiss++;
        }
        entity.activeAnimation.data.has('missedtech')
            && entity.activeAnimation.data.get('missedtech')(entity, 3);
        if (cancelAnimation !== 'continue') {
            if (cancelAnimation === 'airborne-cancel') {
                entity.stun = 0;
                if (fastfell && entity.heavyLandingAudio) {
                    landingAudio = entity.heavyLandingAudio;
                }
                else if (entity.landingAudio) {
                    landingAudio = entity.landingAudio;
                }
                if (Math.abs(dy) - entity.initialFallSpeed - entity.fallSpeed * 5 > 0) {
                    entity.setAnimation('airborne-cancel', true);
                }
                else {
                    entity.setAnimation('idle', true);
                    return;
                }
            }
            else {
                entity.setAnimation(cancelAnimation, true);
            }
        }
        if ((oldAnimation.type === animation_1.AnimationType.aerial
            || oldAnimation.data.has('lagCancellable'))
            && (constants.AUTO_LAG_CANCEL
                || entity.lagCancel
                    > constants.LAG_CANCEL_TIMER - constants.LAG_CANCEL_WINDOW)) {
            const canim = entity.animations[cancelAnimation];
            const landingLag = canim.duration - canim.iasa;
            const shieldDamage = constants.LAG_CANCEL_ENERGY_BASE
                + landingLag * constants.LAG_CANCEL_ENERGY_MUL;
            entity.shield.lastEnergy = entity.shield.energy;
            entity.shield.energy = Math.max(0, entity.shield.energy - Math.max(0, shieldDamage));
            entity.shield.lastReduced = ticks;
            entity.shield.wait = Math.max(entity.shield.wait, constants.LAG_CANCEL_ENERGY_WAIT_BASE
                + landingLag * constants.LAG_CANCEL_ENERGY_WAIT_MUL) | 0;
            entity.stats.lagCancels++;
            if (constants.INSTANT_LAG_CANCEL) {
                entity.setAnimation('airborne-cancel', true);
                entity.flash = 8;
            }
            else {
                if (entity.lagCancelAudio) {
                    landingAudio = entity.lagCancelAudio;
                    entity.flash = 8;
                }
                entity.activeAnimation.ticks = constants.LAG_CANCEL_MUL;
            }
        }
        else if (oldAnimation.type === animation_1.AnimationType.aerial
            && !oldAnimation.data.has('noLCancel')) {
            landingAudio = entity.heavyLandingAudio;
            entity.stats.heavyLands++;
        }
        entity.lagCancel = 0;
    }
    else {
        entity.stun = 0;
        if (fastfell && entity.heavyLandingAudio) {
            landingAudio = entity.heavyLandingAudio;
        }
        else if (entity.landingAudio) {
            landingAudio = entity.landingAudio;
        }
        if (Math.abs(dy) - entity.initialFallSpeed - entity.fallSpeed * 5 > 0) {
            entity.setAnimation('airborne-cancel', true);
        }
        else {
            entity.setAnimation('idle', true);
            return;
        }
    }
    landingAudio && entity.playAudio(landingAudio);
    rendering_1.Effects.thump(entity.x, entity.y, entity.dx, 0, entity.width * 1.25, entity.platform.normal());
};
const v4 = gl_matrix_1.vec4.create();
const rot = gl_matrix_1.quat.create();
const checkKOed = (entity) => {
    if (entity.x >= stage.blastLeft
        && entity.x <= stage.blastRight
        && (entity.y >= stage.blastTop
            || (entity.airborne
                && (!entity.activeAnimation.data.has('starKO')
                    || entity.kb < constants.STAR_KO_KB_THRESHOLD)))
        && entity.y <= stage.blastBottom) {
        return;
    }
    if (entity.important && !entity.dummy) {
        if (entity.playerNumber >= 0) {
            rendering_1.Effects.damageShatter(entity);
            entity.heartbeat.ping(entity.stocks, entity.damage);
            if (entity.lastInjury.entity
                && ticks
                    - (entity.lastInjury.frame
                        + entity.lastInjury.frame
                        + entity.lastInjury.lag)
                    < 300) {
                const e = entity.lastInjury.entity.friendly;
                entity.stats.falls++;
                entity.stats.fallpct = entity.stats.fallpct + entity.damage;
                e.stats.kos++;
                e.stats.kopct = e.stats.kopct + entity.damage;
                exports.dbg.log(`${entity.name} KOed by ${entity.lastInjury.entity.name} at ${entity.damage.toFixed(2)}`);
            }
            else {
                entity.stats.sds++;
                entity.stats.sdpct = entity.stats.fallpct + entity.damage;
                exports.dbg.log(`${entity.name} SDed at ${entity.damage.toFixed(2)}`);
            }
        }
    }
    if (entity.important || entity.dummy) {
        if (entity.x < stage.blastLeft) {
            gl_matrix_1.quat.identity(rot);
            gl_matrix_1.quat.rotateZ(rot, rot, Math.PI * 1.5);
        }
        if (entity.x > stage.blastRight) {
            gl_matrix_1.quat.identity(rot);
            gl_matrix_1.quat.rotateZ(rot, rot, Math.PI * 0.5);
        }
        if (entity.y < stage.blastTop) {
            gl_matrix_1.quat.identity(rot);
        }
        if (entity.y > stage.blastBottom) {
            gl_matrix_1.quat.identity(rot);
            gl_matrix_1.quat.rotateZ(rot, rot, Math.PI);
        }
        for (let i = 0; i < 300; i++) {
            const inward = math_1.lqrandom() * 200 - 100;
            const x = math_1.lqrandom() * inward * 2 - inward;
            const y = math_1.lqrandom() * inward * 2 - inward;
            const dx = inward / 20;
            const dy = 15 - math_1.lqrandom() * 20 + (100 - Math.abs(inward)) * 0.3;
            const dz = (math_1.lqrandom() - 0.5) * 10;
            const color = entity.palette.lighter[(entity.palette.lighter.length * math_1.lqrandom()) | 0]
                .rgba;
            const radius = 0.5 + math_1.lqrandom() * math_1.lqrandom() * 3;
            let p = null;
            gl_matrix_1.vec4.set(v4, dz, dy, dx, 0);
            gl_matrix_1.vec4.transformQuat(v4, v4, rot);
            v4[1] = v4[1] - 10;
            if (v4[1] > 0) {
                v4[1] = v4[1] * 0.01;
            }
            p = rendering_1.Effects.addPhysicsParticle(entity.x + x, entity.y + y, 0, v4[0], v4[1], v4[2], radius, 3, color, (80 + 60 * math_1.lqrandom()) | 0, 0.5);
            p.draw = math_1.lqrandom() > 0.75 ? 0b10 : 0b1;
            p.glow = 0.1 + math_1.lqrandom() * 2;
        }
    }
    entity.lastFall.x = entity.x;
    entity.lastFall.y = entity.y;
    entity.lastFall.frame = ticks;
    entity.damage = 0;
    entity.stocks--;
    entity.points--;
    if (entity.permadeath) {
        entity.removed = true;
    }
    else {
        entity.playAudio('blastzoned');
        activeMode.ko && activeMode.ko(entity);
        if (!entity.removed) {
            entity.setAnimation('respawn', true);
            entity.activeAnimation.step();
        }
    }
    entity.refresh();
    entity.x = stage.spawns[0].x;
    entity.y = stage.spawns[0].y;
};
const entityHandle = (entity, controller) => {
    if (controller
        && activeMode?.control?.(entity, controller) !== true
        && entity.control) {
        entity.control(entity, controller);
    }
    entity.lx = entity.x;
    entity.ly = entity.y;
    entity.nx = 0;
    entity.ny = 0;
    if (entity.scheduledAnimation[0] !== '') {
        entity.setAnimation(entity.scheduledAnimation[0], entity.scheduledAnimation[1], entity.scheduledAnimation[2]);
        entity.scheduledAnimation[0] = '';
    }
    if (entity.lastCollision.lastFrame) {
        if (entity.activeAnimation.data.has('grabbed')
            && (entity.lastCollision.type === animation_1.AnimationType.holding
                || entity.lastCollision.type === animation_1.AnimationType.throw)) {
            entity.activeAnimation.data.get('grabbed')(entity, controller, entity.activeAnimation);
        }
        if (entity.activeAnimation.data.has('collided')) {
            entity.activeAnimation.data.get('collided')(entity, controller, entity.activeAnimation);
        }
    }
    if (entity.lastInjury.lastFrame
        && entity.activeAnimation.data.has('injured')) {
        entity.activeAnimation.data.get('injured')(entity, controller);
    }
    if (entity.lastShield.lastFrame
        && entity.activeAnimation.data.has('shielded')) {
        entity.activeAnimation.data.get('shielded')(entity, controller, entity.activeAnimation, entity.lastShield);
    }
    if (entity.hitlag && entity.stun > 0 && controller !== null) {
        hitlagEntity(entity, controller);
    }
    if (entity.hitlag && entity.lag <= 0) {
        entity.hitlag = false;
    }
};
const entityPhysics = (entity, controller) => {
    if (entity.lag > 0) {
        gl_matrix_1.vec2.set(entity.velocity, entity.nx, -entity.ny);
        return;
    }
    if (entity.airborne) {
        const gravity = utils_1.objHas(entity.activeAnimation.keyframeData, 'gravity')
            ? entity.activeAnimation.keyframeData.gravity
            : entity.activeAnimation.gravity;
        const fallSpeed = (entity.stun <= 0 || entity.kb <= 0 ? entity.fallSpeed : entity.arcSpeed) * gravity;
        const maxFallSpeed = -entity.maxFallSpeed * gravity;
        const friction = entity.activeAnimation.aerodynamics * entity.aerodynamics;
        const resistance = entity.activeAnimation.airResistance + entity.airResistance;
        entity.velocity[1] = -entity.dy - entity.kby - entity.ny;
        if (entity.dy > maxFallSpeed) {
            entity.dy = Math.max(entity.dy - fallSpeed, maxFallSpeed);
            if (entity.stun <= 0) {
                if (entity.dy <= 0.001 && entity.dy > -entity.initialFallSpeed) {
                    entity.dy = entity.dy - entity.initialFallSpeed * gravity;
                }
            }
        }
        if (entity.fastfall && entity.dy < constants.FASTFALL_THRESHOLD) {
            entity.dy = -(entity.maxFallSpeed
                + (entity.fastfallSpeed - entity.maxFallSpeed) * constants.FASTFALL_MOD);
            if (!entity.fastfalling) {
                entity.flash = 3;
                entity.fastfalling = true;
            }
        }
        if (!entity.fastfall) {
            entity.fastfalling = false;
        }
        if (entity.stun >= 0
            && !entity.activeAnimation.nodi
            && !utils_1.objHas(entity.activeAnimation.keyframeData, 'nodi')) {
            if (controller && controller.hmove !== 0) {
                let speedDiff = controller.hmove * entity.airAcceleration;
                if (Math.sign(speedDiff) === Math.sign(entity.dx)) {
                    speedDiff = Math.sign(speedDiff) * Math.min(Math.abs(speedDiff), Math.max(entity.airSpeed + entity.airResistance - Math.abs(entity.dx), 0));
                }
                entity.dx += speedDiff;
            }
        }
        if (entity.dy < 0) {
            entity.dy
                = entity.dy * entity.activeAnimation.fallFriction * entity.fallFriction;
        }
        else {
            entity.dy
                = entity.dy * entity.activeAnimation.riseFriction * entity.riseFriction;
        }
        entity.dx = (entity.dx - Math.sign(entity.dx) * Math.min(Math.abs(entity.dx), resistance)) * friction;
        entity.velocity[0] = entity.dx + entity.kbx + entity.nx;
        return;
    }
    if (entity.stun <= 0
        && ((controller && controller.downTap < 6) || entity.platformDrop)
        && entity.platform !== null) {
        if (!entity.platform.solid
            && entity.animation !== 'platformdrop'
            && entity.setAnimation('platformdrop', entity.platformDrop)) {
            entity.fastfall = false;
            controller.downTap = 2;
        }
        entity.platformDrop = false;
    }
    if (entity.activeAnimation.type === animation_1.AnimationType.movement) {
        entity.velocity[0]
            = entity.dx + entity.kbx * entity.kbStandFriction + entity.nx;
    }
    else {
        entity.velocity[0] = entity.dx + entity.kbx * entity.kbFriction + entity.nx;
    }
    if (entity.slide !== 0) {
        entity.velocity[0] = entity.velocity[0] + entity.slide;
        if (entity.activeAnimation.data.has('slideFriction')) {
            entity.slide
                = entity.slide * entity.activeAnimation.data.get('slideFriction');
        }
        else {
            if (entity.slide > 0) {
                entity.slide = Math.max(0, entity.slide - entity.slideDecay);
            }
            else {
                entity.slide = Math.min(0, entity.slide + entity.slideDecay);
            }
        }
    }
    if (!entity.airborne && entity.platform) {
        entity.velocity[0] = entity.velocity[0] + entity.platform.dx;
    }
    if (utils_1.objHas(entity.activeAnimation.keyframeData, 'friction')) {
        entity.dx = entity.dx * entity.activeAnimation.keyframeData.friction;
    }
    else if (entity.activeAnimation.data.has('friction')) {
        entity.dx = entity.dx * entity.activeAnimation.data.get('friction');
    }
    else {
        entity.dx = entity.dx * entity.friction;
    }
    if (Math.abs(entity.dx) < 0.01) {
        entity.dx = 0;
    }
    if (entity.activeAnimation.data.has('kbFriction')) {
        const f = entity.activeAnimation.data.get('kbFriction');
        entity.kb = entity.kb * f;
        entity.kbx = entity.kbx * f;
        entity.kby = entity.kby * f;
    }
};
const entityEffects = (entity) => {
    if (entity.kb <= 0 && (entity.kbx !== 0 || entity.kby !== 0)) {
        console.error('kb was set to 0, kbx and kby were not', entity.kbx, entity.kby, entity.kbDecay);
        entity.kbx = 0;
        entity.kby = 0;
        entity.kb = 0;
    }
    if (entity.lag <= 0 && entity.kb > 0) {
        if (!entity.airborne && entity.kby !== 0) {
            entity.kb = Math.abs(entity.kbx);
            entity.kby = 0;
        }
        else {
            let ratio = 0;
            const decay = entity.airborne
                ? entity.kbDecay
                : entity.kbDecay * entity.kbDecayFriction
                    + entity.kb * entity.kbDecayScale;
            if (decay < entity.kb) {
                ratio = 1 - Math.min(1, decay / entity.kb);
            }
            if (entity.stun === 0 || entity.kb * ratio > constants.KB_TRANSITION_THRESHOLD) {
                entity.kb = entity.kb * ratio;
                entity.kbx = entity.kbx * ratio;
                entity.kby = entity.kby * ratio;
            }
            else {
                const kb = Math.min(entity.kb, constants.KB_TRANSITION_THRESHOLD) * ratio;
                ratio = Math.min(kb / entity.kb, 1);
                entity.dx = entity.dx + entity.kbx * ratio;
                entity.dy = entity.dy + entity.kby * ratio + entity.fallSpeed;
                entity.kb = 0;
                entity.kbx = 0;
                entity.kby = 0;
            }
        }
    }
    if (entity.intangible > 0) {
        entity.intangible = entity.intangible - 1;
        if (entity.invincible > 1) {
            entity.invincible = entity.invincible - 1;
        }
        if (entity.armored > 1) {
            entity.armored = entity.armored - 1;
        }
        if (entity.vulnerable <= 0) {
            for (let i = 0; i < entity.hurtbubbles.length; i++) {
                entity.hurtbubbles[i].state = animation_1.HurtbubbleState.intangible;
            }
        }
    }
    else if (entity.invincible > 0) {
        entity.invincible = entity.invincible - 1;
        if (entity.armored > 1) {
            entity.armored = entity.armored - 1;
        }
        for (let i = 0; i < entity.hurtbubbles.length; i++) {
            if (entity.hurtbubbles[i].state !== animation_1.HurtbubbleState.intangible) {
                entity.hurtbubbles[i].state = animation_1.HurtbubbleState.invincible;
            }
        }
    }
    else if (entity.armored > 0) {
        entity.armored = entity.armored - 1;
        for (let i = 0; i < entity.hurtbubbles.length; i++) {
            if (entity.hurtbubbles[i].state !== animation_1.HurtbubbleState.intangible
                && entity.hurtbubbles[i].state !== animation_1.HurtbubbleState.invincible) {
                entity.hurtbubbles[i].state = animation_1.HurtbubbleState.heavyArmor;
            }
        }
    }
    if (entity.vulnerable > 0) {
        entity.vulnerable = entity.vulnerable - 1;
        for (let i = 0; i < entity.hurtbubbles.length; i++) {
            entity.hurtbubbles[i].state = 1;
        }
    }
    if (entity.flash > 0) {
        entity.flash = entity.flash - 1;
    }
    if (entity.lag <= 0) {
        if (entity.stun > 0) {
            entity.stun = entity.stun - 1;
            if (entity.stun === 0) {
                entity.outStun = ticks;
                entity.flash = 3;
                if (entity.airborne) {
                    rendering_1.Effects.burst(entity.x, entity.y + entity.height * 0.5, 5, 5, entity.palette.lighter[0]);
                }
            }
        }
        if (entity.shield.stun > 0) {
            entity.shield.stun = entity.shield.stun - 1;
        }
        if (entity.activeAnimation.type !== animation_1.AnimationType.shield
            && entity.shield.energy < 1
            && entity.shield.wait === 0) {
            const lastReducedSince = ticks - entity.shield.lastReduced;
            let shieldTo = entity.shield.energy;
            entity.shield.energy
                = entity.shield.energy + entity.shield.regen * constants.ENERGY_BASE_REGEN;
            if (lastReducedSince < constants.ENERGY_FAST_REGEN_TIME) {
                let lastShield = Math.max(Math.min(entity.shield.lastEnergy, 1), 0);
                if (lastReducedSince > constants.ENERGY_FAST_REGEN_PAUSE) {
                    lastShield
                        = lastShield
                            - ((lastShield - shieldTo)
                                * (lastReducedSince - constants.ENERGY_FAST_REGEN_PAUSE))
                                / (constants.ENERGY_FAST_REGEN_TIME
                                    - constants.ENERGY_FAST_REGEN_PAUSE);
                }
                shieldTo = lastShield;
            }
            if (entity.shield.energy < shieldTo
                && ticks
                    > entity.shield.lastReduced
                        + constants.ENERGY_FAST_REGEN_DELAY
                        + entity.shield.wait) {
                entity.shield.energy = Math.min(shieldTo, entity.shield.energy
                    + entity.shield.regen
                        * entity.shield.regen
                        * constants.ENERGY_BASE_REGEN
                        * constants.ENERGY_FAST_REGEN_MOD);
            }
            if (entity.shield.energy > 1) {
                entity.shield.energy = 1;
            }
        }
        if (entity.shield.wait > 0) {
            entity.shield.wait = entity.shield.wait - 1;
        }
        entity.ledgeReleased = entity.ledgeReleased + 1;
        if (entity.wallJump > 0) {
            if (entity.airborne) {
                entity.wallJump = entity.wallJump - 1;
            }
            else {
                entity.wallJump = 0;
            }
        }
        for (const s of entity.status) {
            s.tick(entity);
        }
    }
    if (entity.airborne) {
        entity.stats.airFrames = entity.stats.airFrames + 1;
    }
    else {
        entity.stats.groundFrames = entity.stats.groundFrames + 1;
    }
    if (entity.lastCollision.lastFrame
        || entity.lastInjury.lastFrame
        || entity.lastClash.lastFrame
        || entity.lastShield.lastFrame) {
        entity.lastCollision.lastFrame = entity.lastInjury.lastFrame = entity.lastClash.lastFrame = entity.lastShield.lastFrame = false;
    }
};
const entityGroundCollide = (entity, controller) => {
    let clipLeft = null;
    let clipRight = null;
    let slid = 0;
    let slidSide = 0;
    if (entity.animation === 'airhit'
        || entity.animation === 'jugglehit'
        || entity.animation === 'meteorhit') {
        return;
    }
    if (entity.platform) {
        do {
            if (entity.x >= entity.platform.x && entity.x <= entity.platform.x2) {
                entity.y = entity.platform.yAt(entity.x);
                slid = 0;
            }
            else if (entity.x > entity.platform.x2) {
                if (stage.findPlatformLeft(entity.platform.x2, entity.platform.y2)) {
                    entity.platform = stage.findPlatformLeft(entity.platform.x2, entity.platform.y2);
                    entity.hover = entity.platform;
                    slid = 1;
                }
                else {
                    slid = 2;
                    slidSide = 1;
                }
            }
            else if (entity.x < entity.platform.x) {
                if (stage.findPlatformRight(entity.platform.x, entity.platform.y)) {
                    entity.platform = stage.findPlatformRight(entity.platform.x, entity.platform.y);
                    entity.hover = entity.platform;
                    slid = 1;
                }
                else {
                    slid = 3;
                    slidSide = -1;
                }
            }
        } while (slid === 1);
        if (slid === 1) {
            entity.y = entity.platform.yAt(entity.x);
            slid = 0;
        }
    }
    else {
        slid = 1;
    }
    if (slid === 0) {
        return;
    }
    const hasSlid = entity.activeAnimation.data.has('slid');
    const slideTo = hasSlid ? entity.activeAnimation.data.get('slid') : '';
    if (hasSlid
        && slideTo !== 'stop'
        && slideTo !== 'cancel'
        && slideTo !== 'noTeeter') {
        slid = 1;
    }
    else if (entity.wallJump <= 0
        && ((hasSlid && (slideTo === 'stop' || slideTo === 'cancel'))
            || (!hasSlid
                && entity.activeAnimation.type !== 0
                && entity.activeAnimation.type !== 5
                && entity.activeAnimation.type !== 4))) {
        if (slideTo === 'cancel') {
            slid = entity.face === 1 ? 4 : 5;
        }
    }
    else if (slid === 2) {
        if (entity.face === 1) {
            if (!controller || controller.hmove > 0.85) {
                slid = 1;
            }
            else if (slideTo !== 'noTeeter') {
                slid = 4;
            }
        }
        else {
            slid = 1;
        }
    }
    else if (slid === 3) {
        if (entity.face === -1) {
            if (!controller || controller.hmove < -0.85) {
                slid = 1;
            }
            else if (slideTo !== 'noTeeter') {
                slid = 5;
            }
        }
        else {
            slid = 1;
        }
    }
    if (entity.platform !== null && entity.stageCollided) {
        slid = 1;
    }
    if (slidSide === 1 && slid !== 1) {
        clipRight = entity.platform.x2;
        entity.x = entity.platform.x2;
        entity.y = entity.platform.yAt(entity.x);
        if (slid === 2 || slid === 4) {
            entity.dx = 0;
            entity.slide = 0;
        }
    }
    else if (slidSide === -1 && slid !== 1) {
        clipLeft = entity.platform.x;
        entity.x = entity.platform.x;
        entity.y = entity.platform.yAt(entity.x);
        if (slid === 3 || slid === 5) {
            entity.dx = 0;
            entity.slide = 0;
        }
    }
    if (slid === 1) {
        entity.airborne = true;
        entity.fastfall = false;
        entity.fastfalling = false;
        entity.dx = entity.dx + entity.slide * 0.5;
        entity.kbx = entity.kbx * 0.75;
        entity.kby = entity.kby * 0.75;
        entity.kb = entity.kb * 0.75;
        if (entity.stun <= 0) {
            entity.dx = entity.dx + entity.kbx * 0.33;
            entity.kbx = 0;
            entity.kby = 0;
            entity.kb = 0;
        }
        entity.slide = 0;
        entity.dy = -entity.initialFallSpeed;
        if (slideTo !== 'continue') {
            if (entity.platform) {
                entity.dx = entity.dx + entity.platform.dx;
                entity.dy = entity.dy + entity.platform.dy;
            }
            if (!entity.wallJump
                && hasSlid
                && slideTo !== 'stop'
                && slideTo !== 'noTeeter'
                && slideTo !== 'slide') {
                entity.setAnimation(slideTo, true);
            }
            else {
                if (entity.activeAnimation.type === animation_1.AnimationType.movement) {
                    entity.setAnimation('airborne', true);
                }
                else {
                    entity.setAnimation('airborne-slid', true);
                }
            }
        }
    }
    else if (slid === 4 || slid === 5) {
        if (entity.activeAnimation.data.has('cancel')) {
            entity.setAnimation(entity.activeAnimation.data.get('cancel'));
        }
        else {
            entity.setAnimation('teeter', true);
        }
    }
    if (clipRight !== null && entity.x > clipRight) {
        entity.x = clipRight;
        entity.activeAnimation.resetBubbles();
    }
    if (clipLeft !== null && entity.x < clipLeft) {
        entity.x = clipLeft;
        entity.activeAnimation.resetBubbles();
    }
};
const displacement = gl_matrix_1.vec2.create();
const v3 = gl_matrix_1.vec3.create();
const entityCollide = (entity, controller) => {
    let result = null;
    let leftCollided = false;
    let rightCollided = false;
    let topCollided = false;
    let bottomCollided = false;
    const passthrough = controller && controller.vmove > 0.75;
    entity.stageCollided = false;
    if (entity.phasing || entity.phase) {
        if (entity.phase) {
            entity.phase = false;
        }
        return;
    }
    if (entity.airborne) {
        const phasing = controller?.vmove >= 0.75
            && entity.activeAnimation.data.has('platformDroppable');
        if (stage.traceDown(entity, phasing, v3)) {
            let landOK = true;
            if (entity.activeAnimation.data.has('stageTouch')) {
                entity.activeAnimation.data.get('stageTouch')(entity, 3);
                if (entity.removed || entity.dy > 0) {
                    landOK = false;
                }
            }
            if (landOK
                && (entity.platform.solid
                    || (controller && controller.vmove < 0.75)
                    || !entity.activeAnimation.data.has('platformDroppable'))) {
                entity.y = v3[1];
                entity.ecb.update(entity.x, entity.y);
                entity.velocity[1] = 0;
                landEntity(entity, controller);
            }
        }
    }
    result = stage.collide(entity.ecb, entity.velocity, Math.max(entity.y, entity.y + entity.velocity[1]), passthrough);
    entity.lastCollisions.element = result.element;
    gl_matrix_1.vec2.copy(entity.lastCollisions.velocity, entity.velocity);
    entity.lastCollisions.metrics = result.metrics;
    if (result.element !== null) {
        if (result.element.left) {
            leftCollided = true;
        }
        if (result.element.right) {
            rightCollided = true;
        }
        if (result.element.top) {
            topCollided = true;
        }
        if (result.element.bottom) {
            bottomCollided = true;
        }
        gl_matrix_1.vec2.sub(entity.velocity, entity.velocity, result.result.responseVector);
    }
    entity.x = entity.x + entity.velocity[0];
    entity.y = entity.y + entity.velocity[1];
    entity.ecb.update(entity.x, entity.y);
    if (result.element !== null && !topCollided) {
        stage.resolve(displacement, entity.ecb, entity.y, passthrough);
        entity.x = entity.x - displacement[0];
        entity.y = entity.y - displacement[1];
        if (Math.abs(displacement[0]) < 0.001) {
            entity.stageCollided = true;
        }
    }
    if (leftCollided || rightCollided || bottomCollided) {
        entity.activeAnimation.data.has('stageTouch')
            && entity.activeAnimation.data.get('stageTouch')(entity, leftCollided ? 2 : 4);
    }
    if (leftCollided || rightCollided) {
        if (entity.airborne) {
            entity.wallJump = constants.WALL_JUMP_TIMING;
            entity.wallJumpSide = leftCollided ? 'right' : 'left';
            entity.wallJumpElement = result.element;
        }
    }
    if (entity.lag <= 0
        && (bottomCollided || rightCollided || leftCollided)
        && entity.activeAnimation.data.has('techable')) {
        const techKind = leftCollided ? 2 : rightCollided ? 4 : 1;
        const eventResult = entity.events.dispatch('walltech');
        rendering_1.Effects.thump(entity.x, topCollided ? entity.y + entity.height : entity.y, entity.dx, entity.dy, entity.width * 1.25, result.element.normal());
        if (constants.WALL_TECH_OK
            && (eventResult
                || entity.teched > constants.TECH_TIMER - constants.TECH_WINDOW)) {
            entity.stats.walltech++;
            entity.dx = 0;
            entity.dy = 0;
            entity.kbx = 0;
            entity.kby = 0;
            entity.kb = 0;
            entity.stun = 0;
            entity.face = leftCollided ? -1 : rightCollided ? 1 : entity.face;
            entity.setAnimation(leftCollided || rightCollided ? 'walltech' : 'rooftech', true);
            entity.activeAnimation.data.has('tech')
                && entity.activeAnimation.data.get('tech')(entity, techKind);
        }
        else if (entity.kb > 1 && entity.lastMissTech < ticks - 2) {
            entity.lastMissTech = ticks + 5;
            entity.stats.walltechmiss++;
            if (leftCollided || rightCollided) {
                entity.kbx
                    = (leftCollided ? -1 : 1)
                        * Math.max(entity.kbx * Math.sign(entity.kbx), 2);
                entity.dx = leftCollided ? -1 : 1;
            }
            if (topCollided || bottomCollided) {
                entity.kby
                    = (topCollided ? 1 : -1) * (entity.kby * Math.sign(entity.kby) + 3);
                entity.dy = topCollided ? -1 : 1;
            }
            entity.stun = Math.round(entity.stun * 0.75);
            entity.kbx = entity.kbx * 0.5;
            entity.kby = entity.kby * 0.5;
            entity.kba = math_1.computeAngle(entity.kbx, entity.kby);
            entity.lag = 5;
            entity.activeAnimation.data.has('missedtech')
                && entity.activeAnimation.data.get('missedtech')(entity, techKind);
        }
    }
    if (!entity.airborne) {
        entityGroundCollide(entity, controller);
    }
};
const entityPreAct = (entity) => {
    entity.activeAnimation.interact !== null
        && entity.activeAnimation.interact(entity, entity.controller, entity.activeAnimation, entity.activeAnimation.keyframeData);
};
const entityAct = (entity) => {
    let oldAnimation = '';
    const controller = entity.controller;
    if (entity.removed) {
        if (entity.held && entity.held.animation === 'held') {
            entity.held.setAnimation('airborne', true, true);
            entity.held.airborne = true;
        }
        return;
    }
    entity.ecb.highlight = 0;
    entityHandle(entity, controller);
    processLedges(entity, controller);
    if (entity.buffertime > 0 && entity.buffer) {
        if (!entity.animations[entity.buffer].data.has('cost')
            || +entity.animations[entity.buffer].data.get('cost') <= entity.shield.energy) {
            if (entity.setAnimation(entity.buffer)) {
                entity.buffertime = 0;
                entity.buffer = '';
            }
            else {
                entity.buffertime--;
            }
        }
    }
    while (entity.animation !== oldAnimation) {
        oldAnimation = entity.animation;
        entity.activeAnimation.step();
    }
    entityEffects(entity);
    entityPhysics(entity, controller);
};
const entityPostAct = (entity) => {
    let oldAnimation = entity.animation;
    entityCollide(entity, entity.controller);
    checkKOed(entity);
    for (let i = 0; i < exports.entities.length; i++) {
        const e = exports.entities[i];
        if (e.launched) {
            launchEntity(e);
        }
    }
    while (entity.animation !== oldAnimation) {
        oldAnimation = entity.animation;
        entity.activeAnimation.step();
    }
    entity.ecb.update(entity.x, entity.y);
    if (entity.lag > 0) {
        entity.lag = entity.lag - 1;
    }
    if (entity.lagCancel > 0) {
        entity.lagCancel = entity.lagCancel - 1;
    }
    if (entity.teched > 0) {
        entity.teched = entity.teched - 1;
    }
};
const setActiveMode = (mode, data = {}) => {
    rendering_1.scheduleCallback(() => {
        exports.entities.forEach(entity => {
            entity.remove();
        });
        networking_1.netStatus.clear();
        exports.entities.length = 0;
        exports.uiEntities.length = 0;
        rendering_1.resetMouseListeners();
        exports.activeModeTime = ticks;
        exports.players.length = 0;
        animation_1.hurtbubbles.length = 0;
        rendering_1.Effects.reset();
        exports.Game.comboCounter = 0;
        stage = new stage_1.Stage();
        if (activeMode && activeMode.doc) {
            activeMode.doc.free();
        }
        activeMode = mode(data);
        controllers_1.connected.forEach(controller => {
            controller.hook = undefined;
            controller.reset();
            activeMode.connect && activeMode.connect(controller);
        });
        rendering_1.sync(stage, activeMode);
        activeMode.initialize && activeMode.initialize();
        rendering_1.resizeDoc();
    });
};
exports.setActiveMode = setActiveMode;
const getActiveMode = () => activeMode;
exports.getActiveMode = getActiveMode;
const constantListeners = [];
const getConstants = (callback = null) => {
    if (!callback) {
        return constants;
    }
    constantListeners.push(callback);
    callback(constants);
    return constants;
};
exports.getConstants = getConstants;
const setConstants = (c) => {
    exports.dbg.log('Changed game mode to:', c.NAME);
    constants = c;
    for (const l of constantListeners) {
        l(constants);
    }
};
exports.setConstants = setConstants;
const toggleConstants = () => {
    exports.setConstants(constants === exports.modes['19XX'] ? exports.modes.Antistatic : exports.modes['19XX']);
    exports.dbg.log(constants === exports.modes['19XX'] ? '19XX Enabled' : '19XX Disabled');
};
exports.toggleConstants = toggleConstants;
const engineTick1 = (_ticks) => {
    ticks = _ticks;
    activeMode.run();
    stage.act();
    if (activeMode.started) {
        const f = ticks * 0.25;
        if (f === (f | 0)) {
            for (let i = 0; i < exports.players.length; i++) {
                const hb = exports.players[i].heartbeat;
                const time = hb.width / (exports.players[i].stocks + 1);
                if (exports.players[i].stocks >= 0 && f >= hb.lastPulse + time) {
                    hb.impulse = 128;
                    hb.lastPulse = hb.lastPulse + time;
                    if (hb.lastPulse + time < f - time) {
                        hb.lastPulse = f;
                    }
                }
                hb.tick();
            }
        }
        for (let i = 0; i < exports.entities.length; i++) {
            const entity = exports.entities[i];
            if (entity.ai !== null) {
                entity.ai.tick(entity, entity.ai, entity.sourceController.frame);
            }
            if (entity.sourceController !== null) {
                entity.controller.step(entity.sourceController.frame);
            }
        }
    }
    tempHitMap.clear();
    utils_1.swapRemoved(exports.entities, removed, true);
    utils_1.swapRemoved(removed, exports.entities, false);
};
exports.engineTick1 = engineTick1;
const entityInteract = (entity) => {
    if (entity.removed
        || entity.airborne
        || entity.type === EntityType.projectile
        || entity.lag > 0
        || entity.animation === 'ledgegrab'
        || entity.animation === 'ledgehang') {
        return;
    }
    for (let i = 0; i < exports.entities.length; i++) {
        const e = exports.entities[i];
        let result = null;
        if (e === entity
            || e.type === EntityType.projectile
            || e.airborne
            || e.lag > 0
            || e.animation === 'ledgegrab'
            || e.animation === 'ledgehang') {
            continue;
        }
        result = collision_1.polygonCollision(entity.ecb, e.ecb, gl_matrix_1.vec2.fromValues(0, 0));
        if (!result.intersect) {
            continue;
        }
        entity.ecb.highlight = 1;
        if (!entity.activeAnimation.data.has('rooted')) {
            entity.dx
                = entity.dx
                    + (e.x === entity.x
                        ? math_1.lqrandomSync() * 0.1 - 0.05
                        : e.x > entity.x
                            ? -0.05
                            : 0.05);
        }
        if (!e.activeAnimation.data.has('rooted')) {
            e.dx
                = e.dx
                    + (e.x === entity.x
                        ? math_1.lqrandomSync() * 0.1 - 0.05
                        : e.x < entity.x
                            ? -0.05
                            : 0.05);
        }
    }
};
const engineTick2 = (tick) => {
    ticks = tick;
    if (ticks - exports.activeModeTime > 5) {
        for (let i = 0; i < exports.uiEntities.length; i++) {
            if (!exports.uiEntities[i].disable && !exports.uiEntities[i].removed) {
                exports.uiEntities[i].act();
            }
        }
    }
    animation_1.clearHitbubbles();
    for (let i = 0; i < exports.entities.length; i++) {
        entityInteract(exports.entities[i]);
    }
    for (let i = 0; i < exports.entities.length; i++) {
        entityPreAct(exports.entities[i]);
    }
    for (let i = 0; i < exports.entities.length; i++) {
        entityAct(exports.entities[i]);
    }
    testHitBubbleCollisions();
    testCollisions();
    for (let i = 0; i < exports.entities.length; i++) {
        entityPostAct(exports.entities[i]);
    }
};
exports.engineTick2 = engineTick2;
const setStage = (_stage) => {
    stage = _stage;
    rendering_1.sync(stage, activeMode);
};
exports.setStage = setStage;
utils_1.Sync.loading.promise.then(() => {
    console.log('Loading Done!');
    rendering_1.sync(stage, activeMode);
});
exports.ai = AI.getDefault();
const binObj = {
    'sb-dmg': {
        mode: 0b001,
        exec: (args, _stdin, _stdout, _stderr, _wd, _env) => {
            const dmg = parseFloat(args[1]);
            for (let i = 0; i < exports.entities.length; i++) {
                if (exports.entities[i].dummy) {
                    exports.entities[i].damage = dmg;
                }
            }
            return 0;
        },
    },
    'set-dmg': {
        mode: 0b001,
        exec: (args, _stdin, _stdout, _stderr, _wd, _env) => {
            let p = null;
            let dmg = 0;
            if (args.length > 2) {
                p = parseInt(args[1], 10) + 1;
                dmg = parseFloat(args[2]);
            }
            else {
                dmg = parseFloat(args[1]);
            }
            for (let i = 0; i < exports.entities.length; i++) {
                if (p === null || exports.entities[i].playerNumber === p) {
                    exports.entities[i].damage = dmg;
                }
            }
            return 0;
        },
    },
    'set-prop': {
        mode: 0b001,
        exec: (args, _stdin, stdout, _stderr, _wd, _env) => {
            let p = null;
            let prop = '';
            let val = '';
            if (args.length < 3) {
                stdout.write('Must include at least two arguments: property, and value');
                return 0;
            }
            if (args.length > 3) {
                p = parseInt(args[1], 10) + 1;
                prop = args[2];
                val = args[3];
            }
            else {
                prop = args[1];
                val = args[2];
            }
            for (let i = 0; i < exports.entities.length; i++) {
                if (p === null || exports.entities[i].playerNumber === p) {
                    const e = exports.entities[i];
                    switch (typeof e[prop]) {
                        case 'string':
                            e[prop] = val;
                            break;
                        case 'number':
                            e[prop] = parseFloat(val);
                            break;
                        case 'boolean':
                            e[prop] = utils_1.ok.has(val);
                            break;
                    }
                    if (prop === 'x' || prop === 'y') {
                        e.ecb.update(e.x, e.y);
                    }
                }
            }
            return 0;
        },
    },
    'set-stocks': {
        mode: 0b001,
        exec: (args, _stdin, _stdout, _stderr, _wd, _env) => {
            let p = null;
            let stocks = 0;
            if (args.length > 2) {
                p = parseInt(args[1], 10) + 1;
                stocks = parseInt(args[2], 10);
            }
            else {
                stocks = parseInt(args[1], 10);
            }
            for (let i = 0; i < exports.entities.length; i++) {
                if (p === null || exports.entities[i].playerNumber === p) {
                    exports.entities[i].stocks = stocks;
                }
            }
            return 0;
        },
    },
    unstale: {
        mode: 0b001,
        exec: (_args, _stdin, _stdout, _stderr, _wd, _env) => {
            for (let i = 0; i < exports.entities.length; i++) {
                if (exports.entities[i].stale && exports.entities[i].stale.moves) {
                    exports.entities[i].stale.moves.length = 0;
                    exports.entities[i].stale.cursor = 0;
                }
            }
            return 0;
        },
    },
    ai: {
        mode: 0b001,
        exec: fsutils_1.manageConstObject(exports.ai, 'ai'),
    },
    constants: {
        mode: 0b001,
        exec: fsutils_1.manageConstObject(constants, 'constants'),
    },
    dbg: {
        mode: 0b001,
        exec: fsutils_1.manageConstObject(exports.dbg, 'dbg'),
    },
    math: {
        mode: 0b001,
        exec: (args, _stdin, stdout, _stderr, _wd, _env) => {
            const eq = args.slice(1).join(' ');
            stdout.write(`${eq} = ${mathvm_1.runMath(eq)}\n`);
        },
    },
    'set-animation': {
        mode: 0b001,
        exec: (args, _stdin, _stdout, _stderr, _wd, _env) => {
            const animation = args[1];
            for (let i = 0; i < exports.players.length; i++) {
                exports.players[i].setAnimation(animation, true, true);
            }
        },
    },
    'random-state': {
        mode: 0b001,
        exec: (_args, _stdin, stdout, _stderr, _wd, _env) => {
            stdout.write(utils_1.toBase64(math_1.lqrandomsync.buffer) + '\n');
        },
    },
};
terminal_1.addDirToIndex('/', {
    contents: utils_1.map({
        bin: { contents: utils_1.map(binObj) },
    }),
});
global.calcColor = exports.calcColor;
global.dumpHitbubbles = () => animation_1.hitbubbles;
global.dumpEntities = () => exports.entities;
//# sourceMappingURL=engine.js.map