Added minor docs to the phase manager + renamed shift to shiftPhase

This commit is contained in:
Bertie690 2025-08-01 18:57:31 -04:00
parent a1a3526c17
commit bca9560f55
3 changed files with 119 additions and 167 deletions

View File

@ -241,7 +241,7 @@ describe("SelectModifierPhase", () => {
const selectModifierPhase = new SelectModifierPhase(0, undefined, customModifiers); const selectModifierPhase = new SelectModifierPhase(0, undefined, customModifiers);
scene.phaseManager.unshiftPhase(selectModifierPhase); scene.phaseManager.unshiftPhase(selectModifierPhase);
game.move.select(MoveId.SPLASH); game.move.select(MoveId.SPLASH);
await game.phaseInterceptor.run(SelectModifierPhase); await game.phaseInterceptor.to("SelectModifierPhase");
expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT);
const modifierSelectHandler = scene.ui.handlers.find( const modifierSelectHandler = scene.ui.handlers.find(
@ -265,7 +265,7 @@ describe("SelectModifierPhase", () => {
const selectModifierPhase = new SelectModifierPhase(0, undefined, customModifiers); const selectModifierPhase = new SelectModifierPhase(0, undefined, customModifiers);
scene.phaseManager.unshiftPhase(selectModifierPhase); scene.phaseManager.unshiftPhase(selectModifierPhase);
game.move.select(MoveId.SPLASH); game.move.select(MoveId.SPLASH);
await game.phaseInterceptor.run(SelectModifierPhase); await game.phaseInterceptor.to("SelectModifierPhase");
expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT);
const modifierSelectHandler = scene.ui.handlers.find( const modifierSelectHandler = scene.ui.handlers.find(

View File

@ -57,7 +57,7 @@ export class ReloadHelper extends GameManagerHelper {
this.game.scene.modifiers = []; this.game.scene.modifiers = [];
} }
titlePhase.loadSaveSlot(-1); // Load the desired session data 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 // Run through prompts for switching Pokemon, copied from classicModeHelper.ts
if (this.game.scene.battleStyle === BattleStyle.SWITCH) { if (this.game.scene.battleStyle === BattleStyle.SWITCH) {

View File

@ -1,3 +1,4 @@
import type { BattleScene } from "#app/battle-scene";
import { Phase } from "#app/phase"; import { Phase } from "#app/phase";
import { UiMode } from "#enums/ui-mode"; import { UiMode } from "#enums/ui-mode";
import { AttemptRunPhase } from "#phases/attempt-run-phase"; import { AttemptRunPhase } from "#phases/attempt-run-phase";
@ -64,6 +65,7 @@ import { UnlockPhase } from "#phases/unlock-phase";
import { VictoryPhase } from "#phases/victory-phase"; import { VictoryPhase } from "#phases/victory-phase";
import { ErrorInterceptor } from "#test/test-utils/error-interceptor"; import { ErrorInterceptor } from "#test/test-utils/error-interceptor";
import type { PhaseClass, PhaseString } from "#types/phase-types"; import type { PhaseClass, PhaseString } from "#types/phase-types";
import type { AwaitableUiHandler } from "#ui/awaitable-ui-handler";
import { UI } from "#ui/ui"; import { UI } from "#ui/ui";
export interface PromptHandler { export interface PromptHandler {
@ -76,20 +78,29 @@ export interface PromptHandler {
type PhaseInterceptorPhase = PhaseClass | PhaseString; type PhaseInterceptorPhase = PhaseClass | PhaseString;
interface PhaseStub {
start(): void;
endBySetMode: boolean;
}
interface InProgressStub {
name: string;
callback(): void;
onError(error: any): void;
}
export class PhaseInterceptor { export class PhaseInterceptor {
public scene; public scene: BattleScene;
public phases = {}; public phases: Record<PhaseClass, PhaseStub> = {};
public log: string[]; public log: PhaseString[];
private onHold; private onHold: PhaseClass[];
private interval; private interval: NodeJS.Timeout;
private promptInterval; private promptInterval: NodeJS.Timeout;
private intervalRun; private intervalRun: NodeJS.Timeout;
private prompts: PromptHandler[]; private prompts: PromptHandler[];
private phaseFrom; private inProgress?: InProgressStub;
private inProgress; private originalSetMode: UI["setMode"];
private originalSetMode; private originalSuperEnd: Phase["end"];
private originalSetOverlayMode;
private originalSuperEnd;
/** /**
* List of phases with their corresponding start methods. * 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. * `initPhases()` so that its subclasses can use `super.start()` properly.
*/ */
private PHASES = [ private PHASES = [
[LoginPhase, this.startPhase], LoginPhase,
[TitlePhase, this.startPhase], TitlePhase,
[SelectGenderPhase, this.startPhase], SelectGenderPhase,
[NewBiomeEncounterPhase, this.startPhase], NewBiomeEncounterPhase,
[SelectStarterPhase, this.startPhase], SelectStarterPhase,
[PostSummonPhase, this.startPhase], PostSummonPhase,
[SummonPhase, this.startPhase], SummonPhase,
[ToggleDoublePositionPhase, this.startPhase], ToggleDoublePositionPhase,
[CheckSwitchPhase, this.startPhase], CheckSwitchPhase,
[ShowAbilityPhase, this.startPhase], ShowAbilityPhase,
[MessagePhase, this.startPhase], MessagePhase,
[TurnInitPhase, this.startPhase], TurnInitPhase,
[CommandPhase, this.startPhase], CommandPhase,
[EnemyCommandPhase, this.startPhase], EnemyCommandPhase,
[TurnStartPhase, this.startPhase], TurnStartPhase,
[MovePhase, this.startPhase], MovePhase,
[MoveEffectPhase, this.startPhase], MoveEffectPhase,
[DamageAnimPhase, this.startPhase], DamageAnimPhase,
[FaintPhase, this.startPhase], FaintPhase,
[BerryPhase, this.startPhase], BerryPhase,
[TurnEndPhase, this.startPhase], TurnEndPhase,
[BattleEndPhase, this.startPhase], BattleEndPhase,
[EggLapsePhase, this.startPhase], EggLapsePhase,
[SelectModifierPhase, this.startPhase], SelectModifierPhase,
[NextEncounterPhase, this.startPhase], NextEncounterPhase,
[NewBattlePhase, this.startPhase], NewBattlePhase,
[VictoryPhase, this.startPhase], VictoryPhase,
[LearnMovePhase, this.startPhase], LearnMovePhase,
[MoveEndPhase, this.startPhase], MoveEndPhase,
[StatStageChangePhase, this.startPhase], StatStageChangePhase,
[ShinySparklePhase, this.startPhase], ShinySparklePhase,
[SelectTargetPhase, this.startPhase], SelectTargetPhase,
[UnavailablePhase, this.startPhase], UnavailablePhase,
[QuietFormChangePhase, this.startPhase], QuietFormChangePhase,
[SwitchPhase, this.startPhase], SwitchPhase,
[SwitchSummonPhase, this.startPhase], SwitchSummonPhase,
[PartyHealPhase, this.startPhase], PartyHealPhase,
[FormChangePhase, this.startPhase], FormChangePhase,
[EvolutionPhase, this.startPhase], EvolutionPhase,
[EndEvolutionPhase, this.startPhase], EndEvolutionPhase,
[LevelCapPhase, this.startPhase], LevelCapPhase,
[AttemptRunPhase, this.startPhase], AttemptRunPhase,
[SelectBiomePhase, this.startPhase], SelectBiomePhase,
[PositionalTagPhase, this.startPhase], PositionalTagPhase,
[PokemonTransformPhase, this.startPhase], PokemonTransformPhase,
[MysteryEncounterPhase, this.startPhase], MysteryEncounterPhase,
[MysteryEncounterOptionSelectedPhase, this.startPhase], MysteryEncounterOptionSelectedPhase,
[MysteryEncounterBattlePhase, this.startPhase], MysteryEncounterBattlePhase,
[MysteryEncounterRewardsPhase, this.startPhase], MysteryEncounterRewardsPhase,
[PostMysteryEncounterPhase, this.startPhase], PostMysteryEncounterPhase,
[RibbonModifierRewardPhase, this.startPhase], RibbonModifierRewardPhase,
[GameOverModifierRewardPhase, this.startPhase], GameOverModifierRewardPhase,
[ModifierRewardPhase, this.startPhase], ModifierRewardPhase,
[PartyExpPhase, this.startPhase], PartyExpPhase,
[ExpPhase, this.startPhase], ExpPhase,
[EncounterPhase, this.startPhase], EncounterPhase,
[GameOverPhase, this.startPhase], GameOverPhase,
[UnlockPhase, this.startPhase], UnlockPhase,
[PostGameOverPhase, this.startPhase], PostGameOverPhase,
[RevivalBlessingPhase, this.startPhase], RevivalBlessingPhase,
]; ];
private endBySetMode = [ private endBySetMode = [
@ -175,7 +186,7 @@ export class PhaseInterceptor {
* Constructor to initialize the scene and properties, and to start the phase handling. * Constructor to initialize the scene and properties, and to start the phase handling.
* @param scene - The scene to be managed. * @param scene - The scene to be managed.
*/ */
constructor(scene) { constructor(scene: BattleScene) {
this.scene = scene; this.scene = scene;
this.onHold = []; this.onHold = [];
this.prompts = []; 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. * Method to transition to a target phase.
* @param phaseTo - The phase to transition to. * @param phaseTo - The phase to transition to.
@ -219,59 +220,49 @@ export class PhaseInterceptor {
async to(phaseTo: PhaseInterceptorPhase, runTarget = true): Promise<void> { async to(phaseTo: PhaseInterceptorPhase, runTarget = true): Promise<void> {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
ErrorInterceptor.getInstance().add(this); 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; const targetName = typeof phaseTo === "string" ? phaseTo : phaseTo.name;
this.intervalRun = setInterval(async () => { this.intervalRun = setInterval(async () => {
const currentPhase = this.onHold?.length && this.onHold[0]; const currentPhase = this.onHold?.length && this.onHold[0];
if (currentPhase && currentPhase.name === targetName) { if (!currentPhase) {
clearInterval(this.intervalRun); // No current phase = interrupted by prompt; wait for phase to finish
if (!runTarget) { return;
return resolve(); }
}
await this.run(currentPhase).catch(e => { // If current phase is different, do nothing.
if (currentPhase.name !== targetName) {
await this.run().catch(e => {
clearInterval(this.intervalRun); clearInterval(this.intervalRun);
return reject(e); return reject(e);
}); });
return;
}
// Hit target phase; run it and resolve
clearInterval(this.intervalRun);
if (!runTarget) {
return resolve(); return resolve();
} }
if (currentPhase && currentPhase.name !== targetName) { await this.run().catch(e => {
await this.run(currentPhase).catch(e => { clearInterval(this.intervalRun);
clearInterval(this.intervalRun); return reject(e);
return reject(e); });
}); return resolve();
}
}); });
}); });
} }
/** /**
* Method to run a phase with an optional skip function. * Method to run the current 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. * @returns A promise that resolves when the phase is run.
*/ */
run(phaseTarget: PhaseInterceptorPhase, skipFn?: (className: PhaseClass) => boolean): Promise<void> { private run(): Promise<void> {
const targetName = typeof phaseTarget === "string" ? phaseTarget : phaseTarget.name; // @ts-expect-error: This is apparently mandatory to avoid a crash; review if this is needed
this.scene.moveAnimations = null; // Mandatory to avoid crash this.scene.moveAnimations = null;
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
ErrorInterceptor.getInstance().add(this); ErrorInterceptor.getInstance().add(this);
const interval = setInterval(async () => { const interval = setInterval(async () => {
const currentPhase = this.onHold.shift(); const currentPhase = this.onHold.shift();
if (currentPhase) { 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); clearInterval(interval);
this.inProgress = { this.inProgress = {
name: currentPhase.name, name: currentPhase.name,
@ -281,32 +272,12 @@ export class PhaseInterceptor {
}, },
onError: error => reject(error), onError: error => reject(error),
}; };
currentPhase.call(); this.phases[currentPhase.name].start.call(this.scene.phaseManager.getCurrentPhase());
} }
}); });
}); });
} }
whenAboutToRun(phaseTarget: PhaseInterceptorPhase, _skipFn?: (className: PhaseClass) => boolean): Promise<void> {
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. * 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. * @param shouldRun Whether or not the current scene should also be run.
*/ */
shift(shouldRun = false): void { shiftPhase(shouldRun = false): void {
this.onHold.shift(); this.onHold.shift();
if (shouldRun) { if (shouldRun) {
this.scene.phaseManager.shiftPhase(); this.scene.phaseManager.shiftPhase();
@ -328,17 +299,16 @@ export class PhaseInterceptor {
*/ */
initPhases() { initPhases() {
this.originalSetMode = UI.prototype.setMode; this.originalSetMode = UI.prototype.setMode;
this.originalSetOverlayMode = UI.prototype.setOverlayMode;
this.originalSuperEnd = Phase.prototype.end; this.originalSuperEnd = Phase.prototype.end;
UI.prototype.setMode = (mode, ...args) => this.setMode.call(this, mode, ...args); UI.prototype.setMode = (mode, ...args) => this.setMode.call(this, mode, ...args);
Phase.prototype.end = () => this.superEndPhase.call(this); 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; const originalStart = phase.prototype.start;
this.phases[phase.name] = { this.phases[phase.name] = {
start: originalStart, start: originalStart,
endBySetMode: this.endBySetMode.some(elm => elm.name === phase.name), 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. * @param phase - The phase to start.
*/ */
startPhase(phase: PhaseClass) { startPhase(phase: PhaseClass) {
this.log.push(phase.name); this.log.push(phase.name as PhaseString);
const instance = this.scene.phaseManager.getCurrentPhase(); const instance = this.scene.phaseManager.getCurrentPhase();
this.onHold.push({ this.onHold.push({
name: phase.name, 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. * Method to end a phase and log it.
* @param phase - The phase to start. * @param phase - The phase to start.
*/ */
superEndPhase() { private superEndPhase() {
const instance = this.scene.phaseManager.getCurrentPhase(); const instance = this.scene.phaseManager.getCurrentPhase();
this.originalSuperEnd.apply(instance); this.originalSuperEnd.apply(instance);
this.inProgress?.callback(); this.inProgress?.callback();
@ -379,7 +344,7 @@ export class PhaseInterceptor {
* @param args - Additional arguments to pass to the original method. * @param args - Additional arguments to pass to the original method.
*/ */
setMode(mode: UiMode, ...args: unknown[]): Promise<void> { setMode(mode: UiMode, ...args: unknown[]): Promise<void> {
const currentPhase = this.scene.phaseManager.getCurrentPhase(); const currentPhase = this.scene.phaseManager.getCurrentPhase()!;
const instance = this.scene.ui; const instance = this.scene.ui;
console.log("setMode", `${UiMode[mode]} (=${mode})`, args); console.log("setMode", `${UiMode[mode]} (=${mode})`, args);
const ret = this.originalSetMode.apply(instance, [mode, ...args]); const ret = this.originalSetMode.apply(instance, [mode, ...args]);
@ -395,18 +360,6 @@ export class PhaseInterceptor {
return ret; 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<void> {
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. * Method to start the prompt handler.
*/ */
@ -425,7 +378,7 @@ export class PhaseInterceptor {
currentPhase === actionForNextPrompt.phaseTarget && currentPhase === actionForNextPrompt.phaseTarget &&
currentHandler.active && currentHandler.active &&
(!actionForNextPrompt.awaitingActionInput || (!actionForNextPrompt.awaitingActionInput ||
(actionForNextPrompt.awaitingActionInput && currentHandler.awaitingActionInput)) (actionForNextPrompt.awaitingActionInput && (currentHandler as AwaitableUiHandler)["awaitingActionInput"]))
) { ) {
const prompt = this.prompts.shift(); const prompt = this.prompts.shift();
if (prompt?.callback) { if (prompt?.callback) {
@ -467,11 +420,10 @@ export class PhaseInterceptor {
* function stored in `this.phases`. Additionally, it clears the `promptInterval` and `interval`. * function stored in `this.phases`. Additionally, it clears the `promptInterval` and `interval`.
*/ */
restoreOg() { restoreOg() {
for (const [phase] of this.PHASES) { for (const phase of this.PHASES) {
phase.prototype.start = this.phases[phase.name].start; phase.prototype.start = this.phases[phase.name].start;
} }
UI.prototype.setMode = this.originalSetMode; UI.prototype.setMode = this.originalSetMode;
UI.prototype.setOverlayMode = this.originalSetOverlayMode;
Phase.prototype.end = this.originalSuperEnd; Phase.prototype.end = this.originalSuperEnd;
clearInterval(this.promptInterval); clearInterval(this.promptInterval);
clearInterval(this.interval); clearInterval(this.interval);