UNPKG

isaacscript-common

Version:

Helper functions and features for IsaacScript mods.

603 lines (602 loc) • 31.8 kB
"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 to match * vanilla behavior. */ 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() { // This should never be null. Regardless, we 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) { // Sometimes, the `sprite.Play(TrapdoorAnimation.OPEN_ANIMATION, true)` function does not take // effect. Check for this case. const sprite = gridEntity.GetSprite(); const animation = sprite.GetAnimation(); // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison if (animation === TrapdoorAnimation_1.TrapdoorAnimation.CLOSED) { // Try it again. sprite.Play(TrapdoorAnimation_1.TrapdoorAnimation.OPEN_ANIMATION, true); } 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 name that identifies the type of custom trapdoor. It should * correspond to a local `CustomTrapdoorType` enum in your mod. It 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.)`); } destinationStage ??= this.stageHistory.getNextStageWithHistory(); 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); }