isaacscript-common
Version:
Helper functions and features for IsaacScript mods.
603 lines (602 loc) • 31.8 kB
JavaScript
"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);
}