"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CustomTrapdoors = void 0;
const isaac_typescript_definitions_1 = require("isaac-typescript-definitions");
const cachedClasses_1 = require("../../../core/cachedClasses");
const constants_1 = require("../../../core/constants");
const decorators_1 = require("../../../decorators");
const ISCFeature_1 = require("../../../enums/ISCFeature");
const ModCallbackCustom_1 = require("../../../enums/ModCallbackCustom");
const GridEntityTypeCustom_1 = require("../../../enums/private/GridEntityTypeCustom");
const StageTravelState_1 = require("../../../enums/private/StageTravelState");
const TrapdoorAnimation_1 = require("../../../enums/private/TrapdoorAnimation");
const easing_1 = require("../../../functions/easing");
const frames_1 = require("../../../functions/frames");
const log_1 = require("../../../functions/log");
const playerCenter_1 = require("../../../functions/playerCenter");
const playerIndex_1 = require("../../../functions/playerIndex");
const players_1 = require("../../../functions/players");
const positionVelocity_1 = require("../../../functions/positionVelocity");
const roomData_1 = require("../../../functions/roomData");
const roomTransition_1 = require("../../../functions/roomTransition");
const stage_1 = require("../../../functions/stage");
const tstlClass_1 = require("../../../functions/tstlClass");
const utils_1 = require("../../../functions/utils");
const vector_1 = require("../../../functions/vector");
const ReadonlySet_1 = require("../../../types/ReadonlySet");
const DefaultMap_1 = require("../../DefaultMap");
const Feature_1 = require("../../private/Feature");
const constants_2 = require("./customStages/constants");
const DEBUG = false;
/** This also applies to crawl spaces. The value was determined through trial and error. */
const TRAPDOOR_OPEN_DISTANCE = 60;
const TRAPDOOR_OPEN_DISTANCE_AFTER_BOSS = TRAPDOOR_OPEN_DISTANCE * 2.5;
const TRAPDOOR_BOSS_REACTION_FRAMES = 30;
const TRAPDOOR_TOUCH_DISTANCE = 16.5;
const ANIMATIONS_THAT_PREVENT_STAGE_TRAVEL = new ReadonlySet_1.ReadonlySet([
    "Death",
    "Happy",
    "Sad",
    "Jump",
]);
const PIXELATION_TO_BLACK_FRAMES = 60;
const OTHER_PLAYER_TRAPDOOR_JUMP_DELAY_GAME_FRAMES = 6;
const OTHER_PLAYER_TRAPDOOR_JUMP_DURATION_GAME_FRAMES = 5;
const v = {
    run: {
        state: StageTravelState_1.StageTravelState.NONE,
        /** The render frame that this state was reached. */
        stateRenderFrame: null,
        customTrapdoorActivated: null,
    },
    level: {
        /** Indexed by room list index and grid index. */
        trapdoors: new DefaultMap_1.DefaultMap(() => new Map()),
    },
};
class CustomTrapdoors extends Feature_1.Feature {
    /** Indexed by custom trapdoor ID. */
    destinationFuncMap = new Map();
    /** @internal */
    v = v;
    /**
     * In order to represent a black sprite, we just use the first frame of the boss versus screen
     * animation. However, we must lazy load the sprite in order to prevent issues with mods that
     * replace the vanilla files. (For some reason, loading the sprites will cause the overwrite to no
     * longer apply on the second and subsequent runs.)
     */
    blackSprite = Sprite();
    customGridEntities;
    disableInputs;
    ponyDetection;
    roomClearFrame;
    runInNFrames;
    runNextRoom;
    stageHistory;
    /** @internal */
    constructor(customGridEntities, disableInputs, ponyDetection, roomClearFrame, runInNFrames, runNextRoom, stageHistory) {
        super();
        this.featuresUsed = [
            ISCFeature_1.ISCFeature.CUSTOM_GRID_ENTITIES,
            ISCFeature_1.ISCFeature.DISABLE_INPUTS,
            ISCFeature_1.ISCFeature.PONY_DETECTION,
            ISCFeature_1.ISCFeature.ROOM_CLEAR_FRAME,
            ISCFeature_1.ISCFeature.RUN_IN_N_FRAMES,
            ISCFeature_1.ISCFeature.RUN_NEXT_ROOM,
            ISCFeature_1.ISCFeature.STAGE_HISTORY,
        ];
        this.callbacksUsed = [
            // 2
            [isaac_typescript_definitions_1.ModCallback.POST_RENDER, this.postRender],
        ];
        this.customCallbacksUsed = [
            [
                ModCallbackCustom_1.ModCallbackCustom.POST_GRID_ENTITY_CUSTOM_UPDATE,
                this.postGridEntityCustomUpdateTrapdoor,
                [GridEntityTypeCustom_1.GridEntityTypeCustom.TRAPDOOR_CUSTOM],
            ],
            [
                ModCallbackCustom_1.ModCallbackCustom.POST_PEFFECT_UPDATE_REORDERED,
                this.postPEffectUpdateReordered,
            ],
        ];
        this.customGridEntities = customGridEntities;
        this.disableInputs = disableInputs;
        this.ponyDetection = ponyDetection;
        this.roomClearFrame = roomClearFrame;
        this.runInNFrames = runInNFrames;
        this.runNextRoom = runNextRoom;
        this.stageHistory = stageHistory;
    }
    // ModCallback.POST_RENDER (2)
    postRender = () => {
        this.checkAllPlayersJumpComplete();
        this.checkPixelationToBlackComplete();
        this.checkSecondPixelationHalfWay();
        this.checkAllPlayersLayingDownComplete();
        this.drawBlackSprite();
    };
    checkAllPlayersJumpComplete() {
        if (v.run.state !== StageTravelState_1.StageTravelState.PLAYERS_JUMPING_DOWN) {
            return;
        }
        if (anyPlayerPlayingExtraAnimation()) {
            return;
        }
        const renderFrameCount = Isaac.GetFrameCount();
        const roomGridIndex = (0, roomData_1.getRoomGridIndex)();
        v.run.state = StageTravelState_1.StageTravelState.PIXELATION_TO_BLACK;
        v.run.stateRenderFrame = renderFrameCount;
        this.logStateChanged();
        // In order to display the pixelation effect that should happen when we go to a new floor, we
        // need to start a room transition. We arbitrarily pick the current room for this purpose. (We
        // do not have to worry about Curse of the Maze here, because even if we are taken to a
        // different room, it will not matter, since we will be traveling to a new floor after the
        // screen fades to black.)
        (0, roomTransition_1.teleport)(roomGridIndex, isaac_typescript_definitions_1.Direction.NO_DIRECTION, isaac_typescript_definitions_1.RoomTransitionAnim.PIXELATION);
        // Next, we wait a certain amount of render frames for the pixelation to fade the screen to
        // black.
    }
    checkPixelationToBlackComplete() {
        if (v.run.state !== StageTravelState_1.StageTravelState.PIXELATION_TO_BLACK ||
            v.run.stateRenderFrame === null) {
            return;
        }
        const renderFrameScreenBlack = v.run.stateRenderFrame + PIXELATION_TO_BLACK_FRAMES;
        if ((0, frames_1.isBeforeRenderFrame)(renderFrameScreenBlack)) {
            return;
        }
        v.run.state = StageTravelState_1.StageTravelState.WAITING_FOR_FIRST_PIXELATION_TO_END;
        this.logStateChanged();
        // Now, we display a black sprite on top of the pixelation effect, to prevent showing the rest
        // of the animation.
        const hud = cachedClasses_1.game.GetHUD();
        hud.SetVisible(false);
        // If the pixelation effect is not fully allowed to complete, the game's internal buffer will
        // not be flushed. The consequence of this is that after 11 custom stage transitions, the
        // "log.txt" starts to become become spammed with: [ASSERT] - PushRenderTarget: stack overflow!
        // In order to work around this, we fully let the animation complete by only continuing the
        // stage transition on the next game frame.
        this.runInNFrames.runNextGameFrame(() => {
            const level = cachedClasses_1.game.GetLevel();
            const startingRoomIndex = level.GetStartingRoomIndex();
            const futureRenderFrameCount = Isaac.GetFrameCount();
            v.run.state =
                StageTravelState_1.StageTravelState.WAITING_FOR_SECOND_PIXELATION_TO_GET_HALF_WAY;
            v.run.stateRenderFrame = futureRenderFrameCount;
            this.goToCustomTrapdoorDestination();
            // Start another pixelation effect. This time, we will keep the screen black with the sprite,
            // and then remove the black sprite once the pixelation effect is halfway complete.
            (0, roomTransition_1.teleport)(startingRoomIndex, isaac_typescript_definitions_1.Direction.NO_DIRECTION, isaac_typescript_definitions_1.RoomTransitionAnim.PIXELATION);
        });
    }
    goToCustomTrapdoorDestination() {
        if (v.run.customTrapdoorActivated === null) {
            // This should never happen; provide some sane default values.
            v.run.customTrapdoorActivated = {
                destinationName: undefined,
                destinationStage: isaac_typescript_definitions_1.LevelStage.BASEMENT_1,
                destinationStageType: isaac_typescript_definitions_1.StageType.ORIGINAL,
                open: true,
                firstSpawn: true,
            };
        }
        const destinationFunc = this.getDestinationFunc(v.run.customTrapdoorActivated);
        destinationFunc(v.run.customTrapdoorActivated.destinationName, v.run.customTrapdoorActivated.destinationStage, v.run.customTrapdoorActivated.destinationStageType);
    }
    getDestinationFunc(customTrapdoorDescription) {
        if (customTrapdoorDescription.destinationName === undefined) {
            return goToVanillaStage;
        }
        const destinationFunc = this.destinationFuncMap.get(customTrapdoorDescription.destinationName);
        if (destinationFunc === undefined) {
            return goToVanillaStage;
        }
        return destinationFunc;
    }
    checkSecondPixelationHalfWay() {
        if (v.run.state !==
            StageTravelState_1.StageTravelState.WAITING_FOR_SECOND_PIXELATION_TO_GET_HALF_WAY ||
            v.run.stateRenderFrame === null) {
            return;
        }
        const renderFrameScreenBlack = v.run.stateRenderFrame + PIXELATION_TO_BLACK_FRAMES;
        if ((0, frames_1.isBeforeRenderFrame)(renderFrameScreenBlack)) {
            return;
        }
        v.run.state = StageTravelState_1.StageTravelState.PIXELATION_TO_ROOM;
        this.logStateChanged();
        const hud = cachedClasses_1.game.GetHUD();
        hud.SetVisible(true);
        this.runNextRoom.runNextRoom(() => {
            v.run.state = StageTravelState_1.StageTravelState.PLAYERS_LAYING_DOWN;
            this.logStateChanged();
            // After the room transition, the players will be placed next to a door, but they should be in
            // the center of the room to emulate what happens on a vanilla stage.
            (0, playerCenter_1.movePlayersToCenter)();
            for (const player of (0, playerIndex_1.getAllPlayers)()) {
                player.AnimateAppear();
                // We need to restore the original collision classes.
                player.EntityCollisionClass = isaac_typescript_definitions_1.EntityCollisionClass.ALL;
                player.GridCollisionClass = isaac_typescript_definitions_1.EntityGridCollisionClass.GROUND;
            }
            const level = cachedClasses_1.game.GetLevel();
            const stage = level.GetStage();
            if (stage !== constants_2.CUSTOM_FLOOR_STAGE) {
                // The vanilla streak text shows just before the player stands up, which is a few frames
                // from now. We arbitrarily play it now instead of waiting to avoid the extra complexity.
                level.ShowName(false);
            }
        });
    }
    checkAllPlayersLayingDownComplete() {
        if (v.run.state !== StageTravelState_1.StageTravelState.PLAYERS_LAYING_DOWN) {
            return;
        }
        if (anyPlayerPlayingExtraAnimation()) {
            return;
        }
        v.run.state = StageTravelState_1.StageTravelState.NONE;
        this.logStateChanged();
        const tstlClassName = (0, tstlClass_1.getTSTLClassName)(this);
        (0, utils_1.assertDefined)(tstlClassName, "Failed to find get the class name for the custom trapdoor feature.");
        this.disableInputs.enableAllInputs(tstlClassName);
    }
    drawBlackSprite() {
        if (v.run.state !== StageTravelState_1.StageTravelState.WAITING_FOR_FIRST_PIXELATION_TO_END &&
            v.run.state !==
                StageTravelState_1.StageTravelState.WAITING_FOR_SECOND_PIXELATION_TO_GET_HALF_WAY) {
            return;
        }
        if (!this.blackSprite.IsLoaded()) {
            this.blackSprite.Load("gfx/ui/boss/versusscreen.anm2", true);
            this.blackSprite.SetFrame("Scene", 0);
            this.blackSprite.Scale = Vector(100, 100);
        }
        this.blackSprite.RenderLayer(0, constants_1.VectorZero);
    }
    // ModCallbackCustom.POST_GRID_ENTITY_CUSTOM_UPDATE
    // GridEntityTypeCustom.TRAPDOOR_CUSTOM
    postGridEntityCustomUpdateTrapdoor = (gridEntity) => {
        const roomListIndex = (0, roomData_1.getRoomListIndex)();
        const gridIndex = gridEntity.GetGridIndex();
        const roomTrapdoorMap = v.level.trapdoors.getAndSetDefault(roomListIndex);
        const trapdoorDescription = roomTrapdoorMap.get(gridIndex);
        if (trapdoorDescription === undefined) {
            return;
        }
        this.checkCustomTrapdoorOpenClose(gridEntity, trapdoorDescription);
        this.checkCustomTrapdoorPlayerTouched(gridEntity, trapdoorDescription);
    };
    checkCustomTrapdoorOpenClose(gridEntity, trapdoorDescription) {
        /** By default, trapdoors will never close if they are already open. */
        if (trapdoorDescription.open) {
            return;
        }
        if (this.shouldTrapdoorOpen(gridEntity, trapdoorDescription.firstSpawn)) {
            openCustomTrapdoor(gridEntity, trapdoorDescription);
        }
    }
    shouldTrapdoorOpen(gridEntity, firstSpawn) {
        const room = cachedClasses_1.game.GetRoom();
        const roomClear = room.IsClear();
        return (!(0, positionVelocity_1.anyPlayerCloserThan)(gridEntity.Position, TRAPDOOR_OPEN_DISTANCE) &&
            !this.isPlayerCloseAfterBoss(gridEntity.Position) &&
            !shouldBeClosedFromStartingInRoomWithEnemies(firstSpawn, roomClear));
    }
    isPlayerCloseAfterBoss(position) {
        const room = cachedClasses_1.game.GetRoom();
        const roomType = room.GetType();
        const roomClearGameFrame = this.roomClearFrame.getRoomClearGameFrame();
        // In order to prevent a player from accidentally entering a freshly-spawned trapdoor after
        // killing the boss of the floor, we use a wider open distance for a short amount of frames.
        if (roomType !== isaac_typescript_definitions_1.RoomType.BOSS ||
            roomClearGameFrame === undefined ||
            (0, frames_1.onOrAfterRenderFrame)(roomClearGameFrame + TRAPDOOR_BOSS_REACTION_FRAMES)) {
            return false;
        }
        return (0, positionVelocity_1.anyPlayerCloserThan)(position, TRAPDOOR_OPEN_DISTANCE_AFTER_BOSS);
    }
    checkCustomTrapdoorPlayerTouched(gridEntity, trapdoorDescription) {
        if (v.run.state !== StageTravelState_1.StageTravelState.NONE) {
            return;
        }
        if (!trapdoorDescription.open) {
            return;
        }
        const playersTouching = Isaac.FindInRadius(gridEntity.Position, TRAPDOOR_TOUCH_DISTANCE, isaac_typescript_definitions_1.EntityPartition.PLAYER);
        for (const playerEntity of playersTouching) {
            const player = playerEntity.ToPlayer();
            if (player === undefined) {
                continue;
            }
            if (
            // We don't want a Pony dash to transition to a new floor or a crawl space.
            !this.ponyDetection.isPlayerUsingPony(player) &&
                !(0, playerIndex_1.isChildPlayer)(player) &&
                canPlayerInteractWithTrapdoor(player)) {
                this.playerTouchedCustomTrapdoor(gridEntity, trapdoorDescription, player);
                return; // Prevent two players from touching the same entity.
            }
        }
    }
    playerTouchedCustomTrapdoor(gridEntity, trapdoorDescription, player) {
        v.run.state = StageTravelState_1.StageTravelState.PLAYERS_JUMPING_DOWN;
        v.run.customTrapdoorActivated = trapdoorDescription;
        this.logStateChanged();
        const tstlClassName = (0, tstlClass_1.getTSTLClassName)(this);
        (0, utils_1.assertDefined)(tstlClassName, "Failed to find get the class name for the custom trapdoor feature.");
        // We don't want to allow pausing, since that will allow render frames to pass without advancing
        // the stage traveling logic. (We track how many render frames have passed to know when to move
        // to the next step.)
        const whitelist = new ReadonlySet_1.ReadonlySet([isaac_typescript_definitions_1.ButtonAction.CONSOLE]);
        this.disableInputs.disableAllInputsExceptFor(tstlClassName, whitelist);
        setPlayerAttributes(player, gridEntity.Position);
        dropTaintedForgotten(player);
        player.PlayExtraAnimation("Trapdoor");
        const otherPlayers = (0, playerIndex_1.getOtherPlayers)(player);
        for (const [i, otherPlayer] of otherPlayers.entries()) {
            const gameFramesToWaitBeforeJumping = OTHER_PLAYER_TRAPDOOR_JUMP_DELAY_GAME_FRAMES * (i + 1);
            const otherPlayerPtr = EntityPtr(otherPlayer);
            this.runInNFrames.runInNGameFrames(() => {
                this.startDelayedJump(otherPlayerPtr, gridEntity.Position);
            }, gameFramesToWaitBeforeJumping);
        }
    }
    startDelayedJump(entityPtr, trapdoorPosition) {
        const entity = entityPtr.Ref;
        if (entity === undefined) {
            return;
        }
        const player = entity.ToPlayer();
        if (player === undefined) {
            return;
        }
        player.PlayExtraAnimation("Trapdoor");
        this.adjustPlayerPositionToTrapdoor(entityPtr, player.Position, trapdoorPosition);
    }
    adjustPlayerPositionToTrapdoor(entityPtr, startPos, endPos) {
        if (v.run.state !== StageTravelState_1.StageTravelState.PLAYERS_JUMPING_DOWN) {
            return;
        }
        const entity = entityPtr.Ref;
        if (entity === undefined) {
            return;
        }
        const player = entity.ToPlayer();
        if (player === undefined) {
            return;
        }
        this.runInNFrames.runNextRenderFrame(() => {
            this.adjustPlayerPositionToTrapdoor(entityPtr, startPos, endPos);
        });
        const sprite = player.GetSprite();
        if (sprite.IsFinished("Trapdoor")) {
            player.Position = endPos;
            player.Velocity = constants_1.VectorZero;
            return;
        }
        const frame = sprite.GetFrame();
        if (frame >= OTHER_PLAYER_TRAPDOOR_JUMP_DURATION_GAME_FRAMES) {
            // We have already arrived at the trapdoor.
            player.Position = endPos;
            player.Velocity = constants_1.VectorZero;
            return;
        }
        // Make the player jump towards the trapdoor. We use an easing function so that the distance
        // traveled is not linear, emulating what the game does.
        const totalDifference = endPos.sub(startPos);
        const progress = frame / OTHER_PLAYER_TRAPDOOR_JUMP_DURATION_GAME_FRAMES;
        const easeProgress = (0, easing_1.easeOutSine)(progress);
        const differenceForThisFrame = totalDifference.mul(easeProgress);
        const targetPosition = startPos.add(differenceForThisFrame);
        player.Position = targetPosition;
        player.Velocity = constants_1.VectorZero;
    }
    // ModCallbackCustom.POST_PEFFECT_UPDATE_REORDERED
    postPEffectUpdateReordered = (player) => {
        this.checkJumpComplete(player);
    };
    checkJumpComplete(player) {
        if (v.run.state !== StageTravelState_1.StageTravelState.PLAYERS_JUMPING_DOWN) {
            return;
        }
        // In this state, the players are jumping down the hole (i.e. playing the "Trapdoor" animation).
        // When it completes, they will return to normal (i.e. just standing on top of the trapdoor).
        // Thus, make them invisible at the end of the animation. (They will automatically be set to
        // visible again once we travel to the next floor.)
        const sprite = player.GetSprite();
        if (sprite.IsFinished("Trapdoor")) {
            player.Visible = false;
        }
    }
    shouldTrapdoorSpawnOpen(gridEntity, firstSpawn) {
        const room = cachedClasses_1.game.GetRoom();
        const roomClear = room.IsClear();
        // Trapdoors created after a room has already initialized should spawn closed by default:
        // - Trapdoors created after bosses should spawn closed so that players do not accidentally jump
        //   into them.
        // - Trapdoors created by We Need to Go Deeper should spawn closed because the player will be
        //   standing on top of them.
        if ((0, frames_1.isAfterRoomFrame)(0)) {
            return false;
        }
        // If we just entered a new room with enemies in it, spawn the trapdoor closed so that the
        // player has to defeat the enemies first before using the trapdoor.
        if (!roomClear) {
            return false;
        }
        // If we just entered a new room that is already cleared, spawn the trapdoor closed if we are
        // standing close to it, and open otherwise.
        return this.shouldTrapdoorOpen(gridEntity, firstSpawn);
    }
    logStateChanged() {
        if (DEBUG) {
            (0, log_1.log)(`Custom trapdoors state changed: ${StageTravelState_1.StageTravelState[v.run.state]} (${v.run.state})`);
        }
    }
    /**
     * Helper function to specify where your custom trapdoor should take the player. Call this once at
     * the beginning of your mod for each kind of custom trapdoor that you want to have. The provided
     * `destinationFunc` will be executed when the player jumps into the trapdoor and the pixelation
     * effect fades to black.
     *
     * Registration is needed so that custom trapdoors can be serializable when the player saves and
     * quits.
     *
     * In order to use this function, you must upgrade your mod with `ISCFeature.CUSTOM_TRAPDOORS`.
     *
     * @param destinationName The integer that identifies the type of custom trapdoor. It should
     *                        correspond to a local `CustomTrapdoorType` enum in your mod. The integer
     *                        can be any unique value and can safely overlap with values chosen by
     *                        other mods.
     * @param destinationFunc A function that takes the player to the destination that you want.
     *                        Inside this function, use the `setStage` or `setCustomStage` helper
     *                        functions, or do something completely custom.
     */
    registerCustomTrapdoorDestination(destinationName, destinationFunc) {
        if (this.destinationFuncMap.has(destinationName)) {
            error(`Failed to register a custom trapdoor type of ${destinationName} since this custom trapdoor type has already been registered.`);
        }
        this.destinationFuncMap.set(destinationName, destinationFunc);
    }
    /**
     * Helper function to spawn a trapdoor grid entity that will take a player to a vanilla stage or
     * custom location.
     *
     * - If you want to create a custom trapdoor that goes to a vanilla stage, pass `undefined` for
     *   the `destinationName` parameter.
     * - If you want to create a custom trapdoor that takes the player to a custom location, you must
     *   have registered the corresponding `destinationName` at the beginning of your mod with the
     *   `registerCustomTrapdoorDestination` function. (This is necessary so that custom trapdoors can
     *   be serializable when the player saves and quits.)
     *
     * Under the hood, the custom trapdoor is represented by a decoration grid entity and is manually
     * respawned every time the player re-enters the room.
     *
     * In order to use this function, you must upgrade your mod with `ISCFeature.CUSTOM_TRAPDOORS`.
     *
     * @param gridIndexOrPosition The location in the room to spawn the trapdoor.
     * @param destinationName Optional. A string representing the name of the of destination that the
     *                        custom trapdoor will take the player to. Default is undefined, which
     *                        will take the player to a vanilla stage.
     * @param destinationStage Optional. The first argument that will be passed to the
     *                         `destinationFunc` corresponding to this custom trapdoor. This is
     *                         essentially metadata for the custom trapdoor. Leave this undefined if
     *                         your corresponding custom trapdoor function does not care what the
     *                         destination stage should be. Default is the "normal" next vanilla
     *                         stage.
     * @param destinationStageType Optional. The second argument that will be passed to the
     *                             `destinationFunc` corresponding to this custom trapdoor. This is
     *                             essentially metadata for the custom trapdoor. Leave this undefined
     *                             if your corresponding custom trapdoor function does not care what
     *                             the destination stage type should be. Default is the "normal" next
     *                             vanilla stage type.
     * @param anm2Path Optional. The path to the anm2 file to use. By default, the vanilla trapdoor
     *                 anm2 of "gfx/grid/door_11_trapdoor.anm2" will be used. The specified anm2 file
     *                 must have animations called "Opened", "Closed", and "Open Animation".
     * @param spawnOpen Optional. Whether to spawn the trapdoor in an open state. By default, behavior
     *                  will be used that emulates a vanilla trapdoor.
     */
    spawnCustomTrapdoor(gridIndexOrPosition, destinationName, destinationStage, destinationStageType, anm2Path = "gfx/grid/door_11_trapdoor.anm2", spawnOpen) {
        if (destinationName !== undefined &&
            !this.destinationFuncMap.has(destinationName)) {
            error(`Failed to spawn a custom trapdoor with a destination of "${destinationName}" since a destination with that name has not been registered with the "registerCustomTrapdoorDestination" function. (If you are trying to go to a custom stage, the custom stage library should automatically do this for you when your mod first boots.)`);
        }
        if (destinationStage === undefined) {
            destinationStage = this.stageHistory.getNextStageWithHistory();
        }
        if (destinationStageType === undefined) {
            destinationStageType = this.stageHistory.getNextStageTypeWithHistory();
        }
        const room = cachedClasses_1.game.GetRoom();
        const roomListIndex = (0, roomData_1.getRoomListIndex)();
        const gridIndex = (0, vector_1.isVector)(gridIndexOrPosition)
            ? room.GetGridIndex(gridIndexOrPosition)
            : gridIndexOrPosition;
        const gridEntity = this.customGridEntities.spawnCustomGridEntity(GridEntityTypeCustom_1.GridEntityTypeCustom.TRAPDOOR_CUSTOM, gridIndexOrPosition, isaac_typescript_definitions_1.GridCollisionClass.NONE, anm2Path, TrapdoorAnimation_1.TrapdoorAnimation.OPENED);
        const firstSpawn = (0, frames_1.isAfterRoomFrame)(0);
        const open = spawnOpen ?? this.shouldTrapdoorSpawnOpen(gridEntity, firstSpawn);
        const roomTrapdoorMap = v.level.trapdoors.getAndSetDefault(roomListIndex);
        const customTrapdoorDescription = {
            destinationName,
            destinationStage,
            destinationStageType,
            open,
            firstSpawn,
        };
        roomTrapdoorMap.set(gridIndex, customTrapdoorDescription);
        const sprite = gridEntity.GetSprite();
        const animation = open
            ? TrapdoorAnimation_1.TrapdoorAnimation.OPENED
            : TrapdoorAnimation_1.TrapdoorAnimation.CLOSED;
        sprite.Play(animation, true);
        return gridEntity;
    }
}
exports.CustomTrapdoors = CustomTrapdoors;
__decorate([
    decorators_1.Exported
], CustomTrapdoors.prototype, "registerCustomTrapdoorDestination", null);
__decorate([
    decorators_1.Exported
], CustomTrapdoors.prototype, "spawnCustomTrapdoor", null);
function anyPlayerPlayingExtraAnimation() {
    const players = (0, playerIndex_1.getAllPlayers)();
    return players.some((player) => !player.IsExtraAnimationFinished());
}
function shouldBeClosedFromStartingInRoomWithEnemies(firstSpawn, roomClear) {
    return firstSpawn && !roomClear;
}
function openCustomTrapdoor(gridEntity, trapdoorDescription) {
    trapdoorDescription.open = true;
    const sprite = gridEntity.GetSprite();
    sprite.Play(TrapdoorAnimation_1.TrapdoorAnimation.OPEN_ANIMATION, true);
}
function canPlayerInteractWithTrapdoor(player) {
    // Players cannot interact with stage travel entities when items are queued or while playing
    // certain animations.
    const sprite = player.GetSprite();
    const animation = sprite.GetAnimation();
    return (!player.IsHoldingItem() &&
        !ANIMATIONS_THAT_PREVENT_STAGE_TRAVEL.has(animation));
}
function setPlayerAttributes(trapdoorPlayer, position) {
    // Snap the player to the exact position of the trapdoor so that they cleanly jump down the hole.
    trapdoorPlayer.Position = position;
    for (const player of (0, playerIndex_1.getAllPlayers)()) {
        // Disable the controls to prevent the player from moving, shooting, and so on. (We also disable
        // the inputs in the `INPUT_ACTION` callback, but that does not prevent mouse inputs.)
        player.ControlsEnabled = false;
        // Freeze all players.
        player.Velocity = constants_1.VectorZero;
        // We don't want enemy attacks to move the players.
        player.EntityCollisionClass = isaac_typescript_definitions_1.EntityCollisionClass.NONE;
        player.GridCollisionClass = isaac_typescript_definitions_1.EntityGridCollisionClass.NONE;
        player.StopExtraAnimation();
    }
}
function dropTaintedForgotten(player) {
    if ((0, players_1.isCharacter)(player, isaac_typescript_definitions_1.PlayerType.FORGOTTEN_B)) {
        const taintedSoul = player.GetOtherTwin();
        if (taintedSoul !== undefined) {
            taintedSoul.ThrowHeldEntity(constants_1.VectorZero);
        }
    }
}
/** The default `destinationFunc` for a custom trapdoor. */
function goToVanillaStage(_destinationName, destinationStage, destinationStageType) {
    (0, stage_1.setStage)(destinationStage, destinationStageType);
}
