isaacscript-common
Version:
Helper functions and features for IsaacScript mods.
248 lines (217 loc) • 8.95 kB
text/typescript
import type { LevelStage, StageType } from "isaac-typescript-definitions";
import { CollectibleType, ModCallback } from "isaac-typescript-definitions";
import { game } from "../../../core/cachedClasses";
import { Exported } from "../../../decorators";
import { onGameFrame, onRenderFrame } from "../../../functions/frames";
import type { PostGameStartedReordered } from "../../callbacks/PostGameStartedReordered";
import type { PostGameStartedReorderedLast } from "../../callbacks/PostGameStartedReorderedLast";
import type { PostNewLevelReordered } from "../../callbacks/PostNewLevelReordered";
import type { PostNewRoomReordered } from "../../callbacks/PostNewRoomReordered";
import { Feature } from "../../private/Feature";
/**
* By default, callbacks fire in the following order:
* - `POST_NEW_ROOM` --> `POST_NEW_LEVEL` --> `POST_GAME_STARTED`
*
* It is easier to write mod code if the callbacks run in a more logical order:
* - `POST_GAME_STARTED` --> `POST_NEW_LEVEL` --> `POST_NEW_ROOM`
*
* `isaacscript-common` provides three new callbacks that change the order to this:
* - `POST_GAME_STARTED_REORDERED`
* - `POST_NEW_LEVEL_REORDERED`
* - `POST_NEW_ROOM_REORDERED`
*
* Additionally, there are some helper functions listed below that can deal with some edge cases
* that you may run into with these callbacks.
*/
export class GameReorderedCallbacks extends Feature {
/** Used to detect a player resuming a saved run. */
private renderFrameRunStarted: int | null = null;
private currentStage: int | null = null;
private currentStageType: int | null = null;
private usedGlowingHourGlass = false;
private forceNewLevel = false;
private forceNewRoom = false;
private readonly postGameStartedReordered: PostGameStartedReordered;
private readonly postNewLevelReordered: PostNewLevelReordered;
private readonly postNewRoomReordered: PostNewRoomReordered;
private readonly postGameStartedReorderedLast: PostGameStartedReorderedLast;
/** @internal */
constructor(
postGameStartedReordered: PostGameStartedReordered,
postNewLevelReordered: PostNewLevelReordered,
postNewRoomReordered: PostNewRoomReordered,
postGameStartedReorderedLast: PostGameStartedReorderedLast,
) {
super();
this.callbacksUsed = [
// 3
[
ModCallback.POST_USE_ITEM,
this.postUseItemGlowingHourGlass,
[CollectibleType.GLOWING_HOUR_GLASS],
],
// 9
[ModCallback.POST_PLAYER_INIT, this.postPlayerInit],
// 15
// eslint-disable-next-line @typescript-eslint/no-deprecated
[ModCallback.POST_GAME_STARTED, this.postGameStarted],
// 17
[ModCallback.PRE_GAME_EXIT, this.preGameExit],
// 18
// eslint-disable-next-line @typescript-eslint/no-deprecated
[ModCallback.POST_NEW_LEVEL, this.postNewLevel],
// 19
// eslint-disable-next-line @typescript-eslint/no-deprecated
[ModCallback.POST_NEW_ROOM, this.postNewRoom],
];
this.postGameStartedReordered = postGameStartedReordered;
this.postNewLevelReordered = postNewLevelReordered;
this.postNewRoomReordered = postNewRoomReordered;
this.postGameStartedReorderedLast = postGameStartedReorderedLast;
}
// ModCallback.POST_USE_ITEM (3)
// CollectibleType.GLOWING_HOUR_GLASS (422)
private readonly postUseItemGlowingHourGlass = (): boolean | undefined => {
// If Glowing Hourglass is used on the first room of a floor, it will send the player to the
// previous floor without triggering the `POST_NEW_LEVEL` callback. Manually check for this.
this.usedGlowingHourGlass = true;
return undefined;
};
// ModCallback.POST_PLAYER_INIT (9)
private readonly postPlayerInit = (_player: EntityPlayer): void => {
this.renderFrameRunStarted ??= Isaac.GetFrameCount();
};
// ModCallback.POST_GAME_STARTED (15)
private readonly postGameStarted = (isContinued: boolean): void => {
const level = game.GetLevel();
const stage = level.GetStage();
const stageType = level.GetStageType();
const room = game.GetRoom();
const roomType = room.GetType();
this.recordCurrentStage();
this.postGameStartedReordered.fire(isContinued);
this.postGameStartedReorderedLast.fire(isContinued);
if (!isContinued) {
// The vanilla `POST_NEW_LEVEL` callback only fires on non-continued runs, which makes sense,
// because we do not want to blow away level variables in this case.
this.postNewLevelReordered.fire(stage, stageType);
}
this.postNewRoomReordered.fire(roomType);
};
// ModCallback.PRE_GAME_EXIT (17)
private readonly preGameExit = (): void => {
this.renderFrameRunStarted = null;
};
// ModCallback.POST_NEW_LEVEL (18)
private readonly postNewLevel = (): void => {
const level = game.GetLevel();
const stage = level.GetStage();
const stageType = level.GetStageType();
const room = game.GetRoom();
const roomType = room.GetType();
if (onGameFrame(0) && !this.forceNewLevel) {
// Wait for the `POST_GAME_STARTED` callback to fire.
return;
}
this.forceNewLevel = false;
this.recordCurrentStage();
this.postNewLevelReordered.fire(stage, stageType);
this.postNewRoomReordered.fire(roomType);
};
// ModCallback.POST_NEW_ROOM (19)
private readonly postNewRoom = (): void => {
const level = game.GetLevel();
const stage = level.GetStage();
const stageType = level.GetStageType();
const room = game.GetRoom();
const roomType = room.GetType();
if (this.usedGlowingHourGlass) {
this.usedGlowingHourGlass = false;
if (this.currentStage !== stage || this.currentStageType !== stageType) {
// The player has used the Glowing Hourglass to take them to the previous floor (which does
// not trigger the `POST_NEW_LEVEL` callback). Emulate what happens in the `POST_NEW_LEVEL`
// callback.
this.recordCurrentStage();
this.postNewLevelReordered.fire(stage, stageType);
this.postNewRoomReordered.fire(roomType);
return;
}
}
if (
(onGameFrame(0)
|| onRenderFrame(this.renderFrameRunStarted)
|| this.currentStage !== stage
|| this.currentStageType !== stageType)
&& !this.forceNewRoom
) {
return;
}
this.forceNewRoom = false;
this.postNewRoomReordered.fire(roomType);
};
private recordCurrentStage(): void {
const level = game.GetLevel();
const stage = level.GetStage();
const stageType = level.GetStageType();
this.currentStage = stage;
this.currentStageType = stageType;
}
/**
* Helper function to tell the `POST_NEW_LEVEL_REORDERED` callback that it should always fire on
* the next `POST_NEW_LEVEL`.
*
* If some specific cases, mods can change the current level during run initialization on the 0th
* frame. (For example, if you had a mod that made the player start the run in Caves instead of
* Basement.) However, due to how the callback reordering works, the `POST_NEW_LEVEL_REORDERED`
* callback will never fire on the 0th frame. To get around this, call this function before
* changing levels to temporarily force the callback to fire.
*
* In order to use this function, you must upgrade your mod with
* `ISCFeature.GAME_REORDERED_CALLBACKS`.
*
* @public
*/
public forceNewLevelCallback(): void {
this.forceNewLevel = true;
}
/**
* Helper function to tell the `POST_NEW_ROOM_REORDERED` callback that it should always fire on
* the next `POST_NEW_ROOM`.
*
* If some specific cases, mods can change the current room during run initialization on the 0th
* frame. (For example, if you had a mod that made the player start the Treasure Room of Basement
* 1 instead of the normal starting room.) However, due to how the callback reordering works, the
* `POST_NEW_ROOM_REORDERED` callback will never fire on the 0th frame. To get around this, call
* this function before changing rooms to temporarily force the callback to fire.
*
* In order to use this function, you must upgrade your mod with
* `ISCFeature.GAME_REORDERED_CALLBACKS`.
*
* @public
*/
public forceNewRoomCallback(): void {
this.forceNewRoom = true;
}
/**
* Helper function to manually set the variables that the reordered callback logic uses to track
* the current stage and stage type.
*
* This is useful because if the stage is changed with the `Game.SetStage` method (or the
* `setStage` helper function), the reordered callbacks will stop working.
*
* In order to use this function, you must upgrade your mod with
* `ISCFeature.GAME_REORDERED_CALLBACKS`.
*
* @public
*/
public reorderedCallbacksSetStage(
stage: LevelStage,
stageType: StageType,
): void {
this.currentStage = stage;
this.currentStageType = stageType;
}
}