From b458d512f11ab7553181b03eaf494423ac9251f5 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Wed, 18 Jun 2025 18:24:43 -0400 Subject: [PATCH] Fixed up phase interceptor --- test/testUtils/errorInterceptor.ts | 49 --- test/testUtils/gameManager.ts | 11 +- test/testUtils/helpers/reloadHelper.ts | 2 +- test/testUtils/phaseInterceptor.ts | 557 +++++++------------------ test/vitest.setup.ts | 2 - tsconfig.json | 1 + 6 files changed, 156 insertions(+), 466 deletions(-) delete mode 100644 test/testUtils/errorInterceptor.ts diff --git a/test/testUtils/errorInterceptor.ts b/test/testUtils/errorInterceptor.ts deleted file mode 100644 index a8fb3284b78..00000000000 --- a/test/testUtils/errorInterceptor.ts +++ /dev/null @@ -1,49 +0,0 @@ -export default class ErrorInterceptor { - private static instance: ErrorInterceptor; - public running; - - constructor() { - this.running = []; - } - - public static getInstance(): ErrorInterceptor { - if (!ErrorInterceptor.instance) { - ErrorInterceptor.instance = new ErrorInterceptor(); - } - return ErrorInterceptor.instance; - } - - clear() { - this.running = []; - } - - add(obj) { - this.running.push(obj); - } - - remove(obj) { - const index = this.running.indexOf(obj); - if (index !== -1) { - this.running.splice(index, 1); - } - } -} - -process.on("uncaughtException", error => { - console.log(error); - const toStop = ErrorInterceptor.getInstance().running; - for (const elm of toStop) { - elm.rejectAll(error); - } - global.testFailed = true; -}); - -// Global error handler for unhandled promise rejections -process.on("unhandledRejection", (reason, _promise) => { - console.log(reason); - const toStop = ErrorInterceptor.getInstance().running; - for (const elm of toStop) { - elm.rejectAll(reason); - } - global.testFailed = true; -}); diff --git a/test/testUtils/gameManager.ts b/test/testUtils/gameManager.ts index b9f499d4e0c..f18ff6e98f7 100644 --- a/test/testUtils/gameManager.ts +++ b/test/testUtils/gameManager.ts @@ -13,13 +13,11 @@ import { CheckSwitchPhase } from "#app/phases/check-switch-phase"; import { CommandPhase } from "#app/phases/command-phase"; import { EncounterPhase } from "#app/phases/encounter-phase"; import { FaintPhase } from "#app/phases/faint-phase"; -import { LoginPhase } from "#app/phases/login-phase"; import { MovePhase } from "#app/phases/move-phase"; import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; import { NewBattlePhase } from "#app/phases/new-battle-phase"; import { SelectStarterPhase } from "#app/phases/select-starter-phase"; import type { SelectTargetPhase } from "#app/phases/select-target-phase"; -import { TitlePhase } from "#app/phases/title-phase"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { TurnInitPhase } from "#app/phases/turn-init-phase"; import { TurnStartPhase } from "#app/phases/turn-start-phase"; @@ -129,6 +127,8 @@ export default class GameManager { // Disables Mystery Encounters on all tests (can be overridden at test level) this.override.mysteryEncounterChance(0); + this.scene.moveAnimations = false; // Disable move animations + global.fetch = vi.fn(MockFetch) as any; } @@ -182,9 +182,10 @@ export default class GameManager { * @returns A promise that resolves when the title phase is reached. */ async runToTitle(): Promise { - await this.phaseInterceptor.whenAboutToRun(LoginPhase); - this.phaseInterceptor.pop(); - await this.phaseInterceptor.run(TitlePhase); + // Go to login phase and skip past it + await this.phaseInterceptor.to("LoginPhase", false); + this.phaseInterceptor.shiftPhase(); + await this.phaseInterceptor.to("TitlePhase"); this.scene.gameSpeed = 5; this.scene.moveAnimations = false; diff --git a/test/testUtils/helpers/reloadHelper.ts b/test/testUtils/helpers/reloadHelper.ts index 4f9d6c810f8..5a10b0a7235 100644 --- a/test/testUtils/helpers/reloadHelper.ts +++ b/test/testUtils/helpers/reloadHelper.ts @@ -57,7 +57,7 @@ export class ReloadHelper extends GameManagerHelper { this.game.scene.modifiers = []; } titlePhase.loadSaveSlot(-1); // Load the desired session data - this.game.phaseInterceptor.shift(); // Loading the save slot also ended TitlePhase, clean it up + this.game.scene.phaseManager.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/testUtils/phaseInterceptor.ts b/test/testUtils/phaseInterceptor.ts index 9d046fc85ba..b14b943db8e 100644 --- a/test/testUtils/phaseInterceptor.ts +++ b/test/testUtils/phaseInterceptor.ts @@ -1,184 +1,49 @@ -import { Phase } from "#app/phase"; -import ErrorInterceptor from "#test/testUtils/errorInterceptor"; -import { AttemptRunPhase } from "#app/phases/attempt-run-phase"; -import { BattleEndPhase } from "#app/phases/battle-end-phase"; -import { BerryPhase } from "#app/phases/berry-phase"; -import { CheckSwitchPhase } from "#app/phases/check-switch-phase"; -import { CommandPhase } from "#app/phases/command-phase"; -import { DamageAnimPhase } from "#app/phases/damage-anim-phase"; -import { EggLapsePhase } from "#app/phases/egg-lapse-phase"; -import { EncounterPhase } from "#app/phases/encounter-phase"; -import { EndEvolutionPhase } from "#app/phases/end-evolution-phase"; -import { EnemyCommandPhase } from "#app/phases/enemy-command-phase"; -import { EvolutionPhase } from "#app/phases/evolution-phase"; -import { FaintPhase } from "#app/phases/faint-phase"; -import { FormChangePhase } from "#app/phases/form-change-phase"; -import { LearnMovePhase } from "#app/phases/learn-move-phase"; -import { LevelCapPhase } from "#app/phases/level-cap-phase"; -import { LoginPhase } from "#app/phases/login-phase"; -import { MessagePhase } from "#app/phases/message-phase"; -import { MoveEffectPhase } from "#app/phases/move-effect-phase"; -import { MoveEndPhase } from "#app/phases/move-end-phase"; -import { MovePhase } from "#app/phases/move-phase"; -import { NewBattlePhase } from "#app/phases/new-battle-phase"; -import { NewBiomeEncounterPhase } from "#app/phases/new-biome-encounter-phase"; -import { NextEncounterPhase } from "#app/phases/next-encounter-phase"; -import { PostSummonPhase } from "#app/phases/post-summon-phase"; -import { QuietFormChangePhase } from "#app/phases/quiet-form-change-phase"; -import { SelectGenderPhase } from "#app/phases/select-gender-phase"; -import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; -import { SelectStarterPhase } from "#app/phases/select-starter-phase"; -import { SelectTargetPhase } from "#app/phases/select-target-phase"; -import { ShinySparklePhase } from "#app/phases/shiny-sparkle-phase"; -import { ShowAbilityPhase } from "#app/phases/show-ability-phase"; -import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; -import { SummonPhase } from "#app/phases/summon-phase"; -import { SwitchPhase } from "#app/phases/switch-phase"; -import { SwitchSummonPhase } from "#app/phases/switch-summon-phase"; -import { TitlePhase } from "#app/phases/title-phase"; -import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase"; -import { TurnEndPhase } from "#app/phases/turn-end-phase"; -import { TurnInitPhase } from "#app/phases/turn-init-phase"; -import { TurnStartPhase } from "#app/phases/turn-start-phase"; -import { UnavailablePhase } from "#app/phases/unavailable-phase"; -import { VictoryPhase } from "#app/phases/victory-phase"; -import { PartyHealPhase } from "#app/phases/party-heal-phase"; -import UI from "#app/ui/ui"; +import type BattleScene from "#app/battle-scene"; +import type { Phase } from "#app/phase"; import { UiMode } from "#enums/ui-mode"; -import { SelectBiomePhase } from "#app/phases/select-biome-phase"; -import { - MysteryEncounterBattlePhase, - MysteryEncounterOptionSelectedPhase, - MysteryEncounterPhase, - MysteryEncounterRewardsPhase, - PostMysteryEncounterPhase, -} from "#app/phases/mystery-encounter-phases"; -import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase"; -import { PartyExpPhase } from "#app/phases/party-exp-phase"; -import { ExpPhase } from "#app/phases/exp-phase"; -import { GameOverPhase } from "#app/phases/game-over-phase"; -import { RibbonModifierRewardPhase } from "#app/phases/ribbon-modifier-reward-phase"; -import { GameOverModifierRewardPhase } from "#app/phases/game-over-modifier-reward-phase"; -import { UnlockPhase } from "#app/phases/unlock-phase"; -import { PostGameOverPhase } from "#app/phases/post-game-over-phase"; -import { RevivalBlessingPhase } from "#app/phases/revival-blessing-phase"; +import UI from "#app/ui/ui"; +import type { PhaseString } from "#app/@types/phase-types"; +import { vi } from "vitest"; -import type { PhaseClass, PhaseString } from "#app/@types/phase-types"; - -export interface PromptHandler { - phaseTarget?: string; +interface PromptHandler { + phaseTarget?: PhaseString; mode?: UiMode; callback?: () => void; - expireFn?: () => void; + expireFn?: () => boolean; awaitingActionInput?: boolean; } -type PhaseInterceptorPhase = PhaseClass | PhaseString; +/** Array of phases which end via player input. */ +const endBySetMode: Set = new Set([ + "TitlePhase", + "SelectGenderPhase", + "CommandPhase", + "SelectModifierPhase", + "MysteryEncounterPhase", + "PostMysteryEncounterPhase", +]); +/** + * The PhaseInterceptor is a wrapper around the `BattleScene`'s {@linkcode PhaseManager}. + * It allows tests to exert finer control over the phase system, providing logging, + */ export default class PhaseInterceptor { - public scene; - public phases = {}; - public log: string[]; - private onHold; - private interval; - private promptInterval; - private intervalRun; - private prompts: PromptHandler[]; - private phaseFrom; - private inProgress; - private originalSetMode; - private originalSetOverlayMode; - private originalSuperEnd; - - /** - * List of phases with their corresponding start methods. - * - * CAUTION: If a phase and its subclasses (if any) both appear in this list, - * make sure that this list contains said phase AFTER all of its subclasses. - * This way, the phase's `prototype.start` is properly preserved during - * `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], - [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], - ]; - - private endBySetMode = [ - TitlePhase, - SelectGenderPhase, - CommandPhase, - SelectModifierPhase, - MysteryEncounterPhase, - PostMysteryEncounterPhase, - ]; + private scene: BattleScene; + /** A log of phases having been executed. */ + public log: PhaseString[] = []; + private promptInterval: NodeJS.Timeout; + private intervalRun: NodeJS.Timeout; + // TODO: Move prompts to a separate class + private prompts: PromptHandler[] = []; /** * 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 = []; - this.clearLogs(); + this.initMocks(); this.startPromptHandler(); - this.initPhases(); } /** @@ -188,264 +53,148 @@ export default class PhaseInterceptor { this.log = []; } - rejectAll(error) { - if (this.inProgress) { - clearInterval(this.promptInterval); - clearInterval(this.interval); - clearInterval(this.intervalRun); - this.inProgress.onError(error); - } - } - /** - * Method to set the starting phase. - * @param phaseFrom - The phase to start from. - * @returns The instance of the PhaseInterceptor. + * Method to initialize various mocks for intercepting phases. */ - runFrom(phaseFrom: PhaseInterceptorPhase): PhaseInterceptor { - this.phaseFrom = phaseFrom; - return this; - } + initMocks() { + const originalSetMode = UI.prototype.setMode; + vi.spyOn(UI.prototype, "setMode").mockImplementation((mode, ...args) => + this.setMode(originalSetMode, mode, ...args), + ); - /** - * Method to transition to a target phase. - * @param phaseTo - The phase to transition to. - * @param runTarget - Whether or not to run the target phase; default `true`. - * @returns A promise that resolves when the transition is complete. - */ - 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 => { - clearInterval(this.intervalRun); - return reject(e); - }); - return resolve(); - } - if (currentPhase && currentPhase.name !== targetName) { - await this.run(currentPhase).catch(e => { - clearInterval(this.intervalRun); - return reject(e); - }); - } - }); - }); - } - - /** - * Method to run a phase with an optional skip function. - * @param phaseTarget - The phase to run. - * @param skipFn - 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 - 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, - callback: () => { - ErrorInterceptor.getInstance().remove(this); - resolve(); - }, - onError: error => reject(error), - }; - currentPhase.call(); - } - }); - }); - } - - 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. - * - * Do not call this unless absolutely necessary. This function is intended - * for cleaning up the phase interceptor when, for whatever reason, a phase - * is manually ended without using the phase interceptor. - * - * @param shouldRun Whether or not the current scene should also be run. - */ - shift(shouldRun = false): void { - this.onHold.shift(); - if (shouldRun) { - this.scene.phaseManager.shiftPhase(); - } - } - - /** - * Method to initialize phases and their corresponding methods. - */ - 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) { - 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); - } - } - - /** - * Method to start a phase and log it. - * @param phase - The phase to start. - */ - startPhase(phase: PhaseClass) { - this.log.push(phase.name); - const instance = this.scene.phaseManager.getCurrentPhase(); - this.onHold.push({ - name: phase.name, - call: () => { - this.phases[phase.name].start.apply(instance); - }, - }); - } - - unlock() { - this.inProgress?.callback(); - this.inProgress = undefined; - } - - /** - * Method to end a phase and log it. - * @param phase - The phase to start. - */ - superEndPhase() { - const instance = this.scene.phaseManager.getCurrentPhase(); - this.originalSuperEnd.apply(instance); - this.inProgress?.callback(); - this.inProgress = undefined; - } - - /** - * m2m to set mode. - * @param mode - The {@linkcode UiMode} to set. - * @param args - Additional arguments to pass to the original method. - */ - setMode(mode: UiMode, ...args: unknown[]): Promise { - 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]); - if (!this.phases[currentPhase.constructor.name]) { - throw new Error( - `missing ${currentPhase.constructor.name} in phaseInterceptor PHASES list --- Add it to PHASES inside of /test/utils/phaseInterceptor.ts`, - ); - } - if (this.phases[currentPhase.constructor.name].endBySetMode) { - this.inProgress?.callback(); - this.inProgress = undefined; - } - 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; + // Mock the private startCurrentPhase method to do nothing to let us + // handle starting phases manually in the `run` method. + vi.fn(this.scene.phaseManager["startCurrentPhase"]).mockImplementation(() => {}); } /** * Method to start the prompt handler. */ - startPromptHandler() { + private startPromptHandler() { this.promptInterval = setInterval(() => { - if (this.prompts.length) { - const actionForNextPrompt = this.prompts[0]; - const expireFn = actionForNextPrompt.expireFn?.(); - const currentMode = this.scene.ui.getMode(); - const currentPhase = this.scene.phaseManager.getCurrentPhase()?.constructor.name; - const currentHandler = this.scene.ui.getHandler(); - if (expireFn) { - this.prompts.shift(); - } else if ( - currentMode === actionForNextPrompt.mode && - currentPhase === actionForNextPrompt.phaseTarget && - currentHandler.active && - (!actionForNextPrompt.awaitingActionInput || - (actionForNextPrompt.awaitingActionInput && currentHandler.awaitingActionInput)) - ) { - const prompt = this.prompts.shift(); - if (prompt?.callback) { - prompt.callback(); - } + const actionForNextPrompt = this.prompts[0] as PromptHandler | undefined; + if (!actionForNextPrompt) { + return; + } + const expireFn = actionForNextPrompt.expireFn?.(); + const currentMode = this.scene.ui.getMode(); + const currentPhase = this.scene.phaseManager.getCurrentPhase()?.phaseName; + const currentHandler = this.scene.ui.getHandler(); + if (expireFn) { + this.prompts.shift(); + } else if ( + currentMode === actionForNextPrompt.mode && + currentPhase === actionForNextPrompt.phaseTarget && + currentHandler.active && + (!actionForNextPrompt.awaitingActionInput || currentHandler["awaitingActionInput"]) + ) { + const prompt = this.prompts.shift(); + if (prompt?.callback) { + prompt.callback(); } } }); } + /** + * Method to log the start of a phase. + * @param phaseName - The name of the phase to log. + */ + private logPhase(phaseName: PhaseString) { + // Exclude normal green highlighting due to issues with consoles + console.log(`Start Phase ${phaseName}`); + this.log.push(phaseName); + } + + /** + * Method to transition to a target phase. + * @param targetPhase - The name of the phase to transition to. + * @param runTarget - Whether or not to run the target phase; default `true`. + * @returns A promise that resolves when the transition is complete. + */ + public async to(targetPhase: PhaseString, runTarget = true): Promise { + this.intervalRun = setInterval(async () => { + const currentPhase = this.scene.phaseManager.getCurrentPhase(); + if (!currentPhase) { + console.log("No phases left to run; resolving."); + return; + } + + if (!currentPhase.is(targetPhase)) { + // Current phase is different; run and wait for it to finish + await this.run(currentPhase); + return; + } + + // target phase reached; run as applicable and resolve + clearInterval(this.intervalRun); + if (!runTarget) { + console.log(`PhaseInterceptor.to: Stopping on run of ${targetPhase}`); + this.scene.phaseManager.unshiftPhase(currentPhase); + return; + } + await this.run(currentPhase); + return; + }); + } + + /** + * Method to run a phase with an optional skip function. + * @param phaseTarget - The {@linkcode Phase} to run. + * @returns A Promise that resolves when the phase is run. + */ + private async run(phaseTarget: Phase): Promise { + const currentPhase = this.scene.phaseManager.getCurrentPhase(); + if (!currentPhase?.is(phaseTarget.phaseName)) { + throw new Error( + `Wrong phase passed to PhaseInterceptor.run;\nthis is ${currentPhase?.phaseName} and not ${phaseTarget.phaseName}`, + ); + } + + try { + this.logPhase(currentPhase.phaseName); + currentPhase.start(); + } catch (error: unknown) { + throw error instanceof Error + ? error + : new Error(`Unknown error ${error} occurred while running phase ${currentPhase?.phaseName}!`); + } + } + + /** Alias for {@linkcode PhaseManager.shiftPhase()} */ + shiftPhase() { + return this.scene.phaseManager.shiftPhase(); + } + + /** + * Method to set mode. + * @param originalSetMode - The original setMode method from the UI. + * @param mode - The {@linkcode UiMode} to set. + * @param args - Additional arguments to pass to the original method. + */ + private async setMode(originalSetMode: typeof UI.prototype.setMode, mode: UiMode, ...args: unknown[]): Promise { + const currentPhase = this.scene.phaseManager.getCurrentPhase(); + console.log("setMode", `${UiMode[mode]} (=${mode})`, args); + const ret = originalSetMode.apply(this.scene.ui, [mode, ...args]); + if (currentPhase && endBySetMode.has(currentPhase.phaseName)) { + await this.run(currentPhase); + } + return ret; + } + /** * Method to add an action to the next prompt. * @param phaseTarget - The target phase for the prompt. * @param mode - The mode of the UI. * @param callback - The callback function to execute. * @param expireFn - The function to determine if the prompt has expired. - * @param awaitingActionInput - ???; default `false` + * @param awaitingActionInput */ addToNextPrompt( - phaseTarget: string, + phaseTarget: PhaseString, mode: UiMode, callback: () => void, - expireFn?: () => void, + expireFn?: () => boolean, awaitingActionInput = false, ) { this.prompts.push({ @@ -459,19 +208,9 @@ export default class PhaseInterceptor { /** * Restores the original state of phases and clears intervals. - * - * This function iterates through all phases and resets their `start` method to the original - * function stored in `this.phases`. Additionally, it clears the `promptInterval` and `interval`. */ restoreOg() { - 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); clearInterval(this.intervalRun); } } diff --git a/test/vitest.setup.ts b/test/vitest.setup.ts index 93b439e540f..f6d49100ef3 100644 --- a/test/vitest.setup.ts +++ b/test/vitest.setup.ts @@ -49,8 +49,6 @@ vi.mock("i18next", async importOriginal => { return await importOriginal(); }); -global.testFailed = false; - beforeAll(() => { initTestFile(); }); diff --git a/tsconfig.json b/tsconfig.json index 6af3e9ce650..dce71e23536 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,7 @@ "paths": { "#enums/*": ["./enums/*.ts"], "#app/*": ["*.ts"], + "#phases/*": ["../src/phases/*.ts"], "#test/*": ["../test/*.ts"] }, "outDir": "./build",