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);
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(

View File

@ -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) {

View File

@ -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<PhaseClass, PhaseStub> = {};
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<void> {
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<void> {
const targetName = typeof phaseTarget === "string" ? phaseTarget : phaseTarget.name;
this.scene.moveAnimations = null; // Mandatory to avoid crash
private run(): Promise<void> {
// @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<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.
*
@ -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<void> {
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<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.
*/
@ -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);