diff --git a/test/phases/select-modifier-phase.test.ts b/test/phases/select-modifier-phase.test.ts index ae4cebb1866..b77e31e931f 100644 --- a/test/phases/select-modifier-phase.test.ts +++ b/test/phases/select-modifier-phase.test.ts @@ -241,7 +241,7 @@ describe("SelectModifierPhase", () => { const selectModifierPhase = new SelectModifierPhase(0, undefined, customModifiers); scene.phaseManager.unshiftPhase(selectModifierPhase); game.move.select(MoveId.SPLASH); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( @@ -265,7 +265,7 @@ describe("SelectModifierPhase", () => { const selectModifierPhase = new SelectModifierPhase(0, undefined, customModifiers); scene.phaseManager.unshiftPhase(selectModifierPhase); game.move.select(MoveId.SPLASH); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( diff --git a/test/test-utils/helpers/reload-helper.ts b/test/test-utils/helpers/reload-helper.ts index c537f5ca15d..7166f1b6cf9 100644 --- a/test/test-utils/helpers/reload-helper.ts +++ b/test/test-utils/helpers/reload-helper.ts @@ -57,7 +57,7 @@ export class ReloadHelper extends GameManagerHelper { this.game.scene.modifiers = []; } titlePhase.loadSaveSlot(-1); // Load the desired session data - this.game.scene.phaseManager.shiftPhase(); // Loading the save slot also ended TitlePhase, clean it up + this.game.phaseInterceptor.shiftPhase(); // Loading the save slot also ended TitlePhase, clean it up // Run through prompts for switching Pokemon, copied from classicModeHelper.ts if (this.game.scene.battleStyle === BattleStyle.SWITCH) { diff --git a/test/test-utils/phase-interceptor.ts b/test/test-utils/phase-interceptor.ts index 50de7e9f047..3dd64a751b2 100644 --- a/test/test-utils/phase-interceptor.ts +++ b/test/test-utils/phase-interceptor.ts @@ -1,3 +1,4 @@ +import type { BattleScene } from "#app/battle-scene"; import { Phase } from "#app/phase"; import { UiMode } from "#enums/ui-mode"; import { AttemptRunPhase } from "#phases/attempt-run-phase"; @@ -64,6 +65,7 @@ import { UnlockPhase } from "#phases/unlock-phase"; import { VictoryPhase } from "#phases/victory-phase"; import { ErrorInterceptor } from "#test/test-utils/error-interceptor"; import type { PhaseClass, PhaseString } from "#types/phase-types"; +import type { AwaitableUiHandler } from "#ui/awaitable-ui-handler"; import { UI } from "#ui/ui"; export interface PromptHandler { @@ -76,20 +78,29 @@ export interface PromptHandler { type PhaseInterceptorPhase = PhaseClass | PhaseString; +interface PhaseStub { + start(): void; + endBySetMode: boolean; +} + +interface InProgressStub { + name: string; + callback(): void; + onError(error: any): void; +} + export class PhaseInterceptor { - public scene; - public phases = {}; - public log: string[]; - private onHold; - private interval; - private promptInterval; - private intervalRun; + public scene: BattleScene; + public phases: Record = {}; + public log: PhaseString[]; + private onHold: PhaseClass[]; + private interval: NodeJS.Timeout; + private promptInterval: NodeJS.Timeout; + private intervalRun: NodeJS.Timeout; private prompts: PromptHandler[]; - private phaseFrom; - private inProgress; - private originalSetMode; - private originalSetOverlayMode; - private originalSuperEnd; + private inProgress?: InProgressStub; + private originalSetMode: UI["setMode"]; + private originalSuperEnd: Phase["end"]; /** * List of phases with their corresponding start methods. @@ -100,66 +111,66 @@ export class PhaseInterceptor { * `initPhases()` so that its subclasses can use `super.start()` properly. */ private PHASES = [ - [LoginPhase, this.startPhase], - [TitlePhase, this.startPhase], - [SelectGenderPhase, this.startPhase], - [NewBiomeEncounterPhase, this.startPhase], - [SelectStarterPhase, this.startPhase], - [PostSummonPhase, this.startPhase], - [SummonPhase, this.startPhase], - [ToggleDoublePositionPhase, this.startPhase], - [CheckSwitchPhase, this.startPhase], - [ShowAbilityPhase, this.startPhase], - [MessagePhase, this.startPhase], - [TurnInitPhase, this.startPhase], - [CommandPhase, this.startPhase], - [EnemyCommandPhase, this.startPhase], - [TurnStartPhase, this.startPhase], - [MovePhase, this.startPhase], - [MoveEffectPhase, this.startPhase], - [DamageAnimPhase, this.startPhase], - [FaintPhase, this.startPhase], - [BerryPhase, this.startPhase], - [TurnEndPhase, this.startPhase], - [BattleEndPhase, this.startPhase], - [EggLapsePhase, this.startPhase], - [SelectModifierPhase, this.startPhase], - [NextEncounterPhase, this.startPhase], - [NewBattlePhase, this.startPhase], - [VictoryPhase, this.startPhase], - [LearnMovePhase, this.startPhase], - [MoveEndPhase, this.startPhase], - [StatStageChangePhase, this.startPhase], - [ShinySparklePhase, this.startPhase], - [SelectTargetPhase, this.startPhase], - [UnavailablePhase, this.startPhase], - [QuietFormChangePhase, this.startPhase], - [SwitchPhase, this.startPhase], - [SwitchSummonPhase, this.startPhase], - [PartyHealPhase, this.startPhase], - [FormChangePhase, this.startPhase], - [EvolutionPhase, this.startPhase], - [EndEvolutionPhase, this.startPhase], - [LevelCapPhase, this.startPhase], - [AttemptRunPhase, this.startPhase], - [SelectBiomePhase, this.startPhase], - [PositionalTagPhase, this.startPhase], - [PokemonTransformPhase, this.startPhase], - [MysteryEncounterPhase, this.startPhase], - [MysteryEncounterOptionSelectedPhase, this.startPhase], - [MysteryEncounterBattlePhase, this.startPhase], - [MysteryEncounterRewardsPhase, this.startPhase], - [PostMysteryEncounterPhase, this.startPhase], - [RibbonModifierRewardPhase, this.startPhase], - [GameOverModifierRewardPhase, this.startPhase], - [ModifierRewardPhase, this.startPhase], - [PartyExpPhase, this.startPhase], - [ExpPhase, this.startPhase], - [EncounterPhase, this.startPhase], - [GameOverPhase, this.startPhase], - [UnlockPhase, this.startPhase], - [PostGameOverPhase, this.startPhase], - [RevivalBlessingPhase, this.startPhase], + LoginPhase, + TitlePhase, + SelectGenderPhase, + NewBiomeEncounterPhase, + SelectStarterPhase, + PostSummonPhase, + SummonPhase, + ToggleDoublePositionPhase, + CheckSwitchPhase, + ShowAbilityPhase, + MessagePhase, + TurnInitPhase, + CommandPhase, + EnemyCommandPhase, + TurnStartPhase, + MovePhase, + MoveEffectPhase, + DamageAnimPhase, + FaintPhase, + BerryPhase, + TurnEndPhase, + BattleEndPhase, + EggLapsePhase, + SelectModifierPhase, + NextEncounterPhase, + NewBattlePhase, + VictoryPhase, + LearnMovePhase, + MoveEndPhase, + StatStageChangePhase, + ShinySparklePhase, + SelectTargetPhase, + UnavailablePhase, + QuietFormChangePhase, + SwitchPhase, + SwitchSummonPhase, + PartyHealPhase, + FormChangePhase, + EvolutionPhase, + EndEvolutionPhase, + LevelCapPhase, + AttemptRunPhase, + SelectBiomePhase, + PositionalTagPhase, + PokemonTransformPhase, + MysteryEncounterPhase, + MysteryEncounterOptionSelectedPhase, + MysteryEncounterBattlePhase, + MysteryEncounterRewardsPhase, + PostMysteryEncounterPhase, + RibbonModifierRewardPhase, + GameOverModifierRewardPhase, + ModifierRewardPhase, + PartyExpPhase, + ExpPhase, + EncounterPhase, + GameOverPhase, + UnlockPhase, + PostGameOverPhase, + RevivalBlessingPhase, ]; private endBySetMode = [ @@ -175,7 +186,7 @@ export class PhaseInterceptor { * Constructor to initialize the scene and properties, and to start the phase handling. * @param scene - The scene to be managed. */ - constructor(scene) { + constructor(scene: BattleScene) { this.scene = scene; this.onHold = []; this.prompts = []; @@ -200,16 +211,6 @@ export class PhaseInterceptor { } } - /** - * Method to set the starting phase. - * @param phaseFrom - The phase to start from. - * @returns The instance of the PhaseInterceptor. - */ - runFrom(phaseFrom: PhaseInterceptorPhase): PhaseInterceptor { - this.phaseFrom = phaseFrom; - return this; - } - /** * Method to transition to a target phase. * @param phaseTo - The phase to transition to. @@ -219,59 +220,49 @@ export class PhaseInterceptor { async to(phaseTo: PhaseInterceptorPhase, runTarget = true): Promise { return new Promise(async (resolve, reject) => { ErrorInterceptor.getInstance().add(this); - if (this.phaseFrom) { - await this.run(this.phaseFrom).catch(e => reject(e)); - this.phaseFrom = null; - } const targetName = typeof phaseTo === "string" ? phaseTo : phaseTo.name; this.intervalRun = setInterval(async () => { const currentPhase = this.onHold?.length && this.onHold[0]; - if (currentPhase && currentPhase.name === targetName) { - clearInterval(this.intervalRun); - if (!runTarget) { - return resolve(); - } - await this.run(currentPhase).catch(e => { + if (!currentPhase) { + // No current phase = interrupted by prompt; wait for phase to finish + return; + } + + // If current phase is different, do nothing. + if (currentPhase.name !== targetName) { + await this.run().catch(e => { clearInterval(this.intervalRun); return reject(e); }); + return; + } + + // Hit target phase; run it and resolve + clearInterval(this.intervalRun); + if (!runTarget) { return resolve(); } - if (currentPhase && currentPhase.name !== targetName) { - await this.run(currentPhase).catch(e => { - clearInterval(this.intervalRun); - return reject(e); - }); - } + await this.run().catch(e => { + clearInterval(this.intervalRun); + return reject(e); + }); + return resolve(); }); }); } /** - * Method to run a phase with an optional skip function. - * @param phaseTarget - The phase to run. - * @param skipFn - Optional skip function. + * Method to run the current phase with an optional skip function. * @returns A promise that resolves when the phase is run. */ - run(phaseTarget: PhaseInterceptorPhase, skipFn?: (className: PhaseClass) => boolean): Promise { - const targetName = typeof phaseTarget === "string" ? phaseTarget : phaseTarget.name; - this.scene.moveAnimations = null; // Mandatory to avoid crash + private run(): Promise { + // @ts-expect-error: This is apparently mandatory to avoid a crash; review if this is needed + this.scene.moveAnimations = null; return new Promise(async (resolve, reject) => { ErrorInterceptor.getInstance().add(this); const interval = setInterval(async () => { const currentPhase = this.onHold.shift(); if (currentPhase) { - if (currentPhase.name !== targetName) { - clearInterval(interval); - const skip = skipFn?.(currentPhase.name); - if (skip) { - this.onHold.unshift(currentPhase); - ErrorInterceptor.getInstance().remove(this); - return resolve(); - } - clearInterval(interval); - return reject(`Wrong phase: this is ${currentPhase.name} and not ${targetName}`); - } clearInterval(interval); this.inProgress = { name: currentPhase.name, @@ -281,32 +272,12 @@ export class PhaseInterceptor { }, onError: error => reject(error), }; - currentPhase.call(); + this.phases[currentPhase.name].start.call(this.scene.phaseManager.getCurrentPhase()); } }); }); } - whenAboutToRun(phaseTarget: PhaseInterceptorPhase, _skipFn?: (className: PhaseClass) => boolean): Promise { - const targetName = typeof phaseTarget === "string" ? phaseTarget : phaseTarget.name; - this.scene.moveAnimations = null; // Mandatory to avoid crash - return new Promise(async (resolve, _reject) => { - ErrorInterceptor.getInstance().add(this); - const interval = setInterval(async () => { - const currentPhase = this.onHold[0]; - if (currentPhase?.name === targetName) { - clearInterval(interval); - resolve(); - } - }); - }); - } - - pop() { - this.onHold.pop(); - this.scene.phaseManager.shiftPhase(); - } - /** * Remove the current phase from the phase interceptor. * @@ -316,7 +287,7 @@ export class PhaseInterceptor { * * @param shouldRun Whether or not the current scene should also be run. */ - shift(shouldRun = false): void { + shiftPhase(shouldRun = false): void { this.onHold.shift(); if (shouldRun) { this.scene.phaseManager.shiftPhase(); @@ -328,17 +299,16 @@ export class PhaseInterceptor { */ initPhases() { this.originalSetMode = UI.prototype.setMode; - this.originalSetOverlayMode = UI.prototype.setOverlayMode; this.originalSuperEnd = Phase.prototype.end; UI.prototype.setMode = (mode, ...args) => this.setMode.call(this, mode, ...args); Phase.prototype.end = () => this.superEndPhase.call(this); - for (const [phase, methodStart] of this.PHASES) { + for (const phase of this.PHASES) { const originalStart = phase.prototype.start; this.phases[phase.name] = { start: originalStart, endBySetMode: this.endBySetMode.some(elm => elm.name === phase.name), }; - phase.prototype.start = () => methodStart.call(this, phase); + phase.prototype.start = () => this.startPhase.call(this, phase); } } @@ -347,7 +317,7 @@ export class PhaseInterceptor { * @param phase - The phase to start. */ startPhase(phase: PhaseClass) { - this.log.push(phase.name); + this.log.push(phase.name as PhaseString); const instance = this.scene.phaseManager.getCurrentPhase(); this.onHold.push({ name: phase.name, @@ -357,16 +327,11 @@ export class PhaseInterceptor { }); } - unlock() { - this.inProgress?.callback(); - this.inProgress = undefined; - } - /** * Method to end a phase and log it. * @param phase - The phase to start. */ - superEndPhase() { + private superEndPhase() { const instance = this.scene.phaseManager.getCurrentPhase(); this.originalSuperEnd.apply(instance); this.inProgress?.callback(); @@ -379,7 +344,7 @@ export class PhaseInterceptor { * @param args - Additional arguments to pass to the original method. */ setMode(mode: UiMode, ...args: unknown[]): Promise { - const currentPhase = this.scene.phaseManager.getCurrentPhase(); + const currentPhase = this.scene.phaseManager.getCurrentPhase()!; const instance = this.scene.ui; console.log("setMode", `${UiMode[mode]} (=${mode})`, args); const ret = this.originalSetMode.apply(instance, [mode, ...args]); @@ -395,18 +360,6 @@ export class PhaseInterceptor { return ret; } - /** - * mock to set overlay mode - * @param mode - The {@linkcode Mode} to set. - * @param args - Additional arguments to pass to the original method. - */ - setOverlayMode(mode: UiMode, ...args: unknown[]): Promise { - const instance = this.scene.ui; - console.log("setOverlayMode", `${UiMode[mode]} (=${mode})`, args); - const ret = this.originalSetOverlayMode.apply(instance, [mode, ...args]); - return ret; - } - /** * Method to start the prompt handler. */ @@ -425,7 +378,7 @@ export class PhaseInterceptor { currentPhase === actionForNextPrompt.phaseTarget && currentHandler.active && (!actionForNextPrompt.awaitingActionInput || - (actionForNextPrompt.awaitingActionInput && currentHandler.awaitingActionInput)) + (actionForNextPrompt.awaitingActionInput && (currentHandler as AwaitableUiHandler)["awaitingActionInput"])) ) { const prompt = this.prompts.shift(); if (prompt?.callback) { @@ -467,11 +420,10 @@ export class PhaseInterceptor { * function stored in `this.phases`. Additionally, it clears the `promptInterval` and `interval`. */ restoreOg() { - for (const [phase] of this.PHASES) { + for (const phase of this.PHASES) { phase.prototype.start = this.phases[phase.name].start; } UI.prototype.setMode = this.originalSetMode; - UI.prototype.setOverlayMode = this.originalSetOverlayMode; Phase.prototype.end = this.originalSuperEnd; clearInterval(this.promptInterval); clearInterval(this.interval);