Make phaseQueue private

This commit is contained in:
Dean 2025-06-14 18:47:50 -07:00
parent 2ea180e94c
commit 08090a8c14
10 changed files with 68 additions and 89 deletions

View File

@ -0,0 +1,3 @@
import type { Phase } from "#app/phase";
export type PhaseConditionFunc = (phase: Phase) => boolean;

View File

@ -527,17 +527,7 @@ export class ShellTrapTag extends BattlerTag {
// Trap should only be triggered by opponent's Physical moves // Trap should only be triggered by opponent's Physical moves
if (phaseData?.move.category === MoveCategory.PHYSICAL && pokemon.isOpponent(phaseData.attacker)) { if (phaseData?.move.category === MoveCategory.PHYSICAL && pokemon.isOpponent(phaseData.attacker)) {
const shellTrapPhaseIndex = globalScene.phaseManager.phaseQueue.findIndex( globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === pokemon);
phase => phase.is("MovePhase") && phase.pokemon === pokemon,
);
const firstMovePhaseIndex = globalScene.phaseManager.phaseQueue.findIndex(phase => phase.is("MovePhase"));
// Only shift MovePhase timing if it's not already next up
if (shellTrapPhaseIndex !== -1 && shellTrapPhaseIndex !== firstMovePhaseIndex) {
const shellTrapMovePhase = globalScene.phaseManager.phaseQueue.splice(shellTrapPhaseIndex, 1)[0];
globalScene.phaseManager.prependToPhase(shellTrapMovePhase, "MovePhase");
}
this.activated = true; this.activated = true;
} }

View File

@ -3184,11 +3184,7 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr {
})); }));
// Move the ally's MovePhase (if needed) so that the ally moves next // Move the ally's MovePhase (if needed) so that the ally moves next
const allyMovePhaseIndex = globalScene.phaseManager.phaseQueue.indexOf(allyMovePhase); globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === user.getAlly());
const firstMovePhaseIndex = globalScene.phaseManager.phaseQueue.findIndex((phase) => phase.is("MovePhase"));
if (allyMovePhaseIndex !== firstMovePhaseIndex) {
globalScene.phaseManager.prependToPhase(globalScene.phaseManager.phaseQueue.splice(allyMovePhaseIndex, 1)[0], "MovePhase");
}
overridden.value = true; overridden.value = true;
return true; return true;
@ -4546,12 +4542,7 @@ export class CueNextRoundAttr extends MoveEffectAttr {
return false; return false;
} }
// Update the phase queue so that the next Pokemon using Round moves next globalScene.phaseManager.forceMoveLast((phase: MovePhase) => phase.is("MovePhase") && phase.move.moveId === MoveId.ROUND);
const nextRoundIndex = globalScene.phaseManager.phaseQueue.indexOf(nextRoundPhase);
const nextMoveIndex = globalScene.phaseManager.phaseQueue.findIndex(phase => phase.is("MovePhase"));
if (nextRoundIndex !== nextMoveIndex) {
globalScene.phaseManager.prependToPhase(globalScene.phaseManager.phaseQueue.splice(nextRoundIndex, 1)[0], "MovePhase");
}
// Mark the corresponding Pokemon as having "joined the Round" (for doubling power later) // Mark the corresponding Pokemon as having "joined the Round" (for doubling power later)
nextRoundPhase.pokemon.turnData.joinedRound = true; nextRoundPhase.pokemon.turnData.joinedRound = true;
@ -7923,38 +7914,11 @@ export class ForceLastAttr extends MoveEffectAttr {
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean { override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:forceLast", { targetPokemonName: getPokemonNameWithAffix(target) })); globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:forceLast", { targetPokemonName: getPokemonNameWithAffix(target) }));
const targetMovePhase = globalScene.phaseManager.findPhase<MovePhase>((phase) => phase.pokemon === target); globalScene.phaseManager.forceMoveLast((phase: MovePhase) => phase.pokemon === target);
if (targetMovePhase && !targetMovePhase.isForcedLast() && globalScene.phaseManager.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) {
// Finding the phase to insert the move in front of -
// Either the end of the turn or in front of another, slower move which has also been forced last
const prependPhase = globalScene.phaseManager.findPhase((phase) =>
[ MovePhase, MoveEndPhase ].every(cls => !(phase instanceof cls))
|| (phase.is("MovePhase")) && phaseForcedSlower(phase, target, !!globalScene.arena.getTag(ArenaTagType.TRICK_ROOM))
);
if (prependPhase) {
globalScene.phaseManager.phaseQueue.splice(
globalScene.phaseManager.phaseQueue.indexOf(prependPhase),
0,
globalScene.phaseManager.create("MovePhase", target, [ ...targetMovePhase.targets ], targetMovePhase.move, false, false, false, true)
);
}
}
return true; return true;
} }
} }
/** Returns whether a {@linkcode MovePhase} has been forced last and the corresponding pokemon is slower than {@linkcode target} */
const phaseForcedSlower = (phase: MovePhase, target: Pokemon, trickRoom: boolean): boolean => {
let slower: boolean;
// quashed pokemon still have speed ties
if (phase.pokemon.getEffectiveStat(Stat.SPD) === target.getEffectiveStat(Stat.SPD)) {
slower = !!target.randBattleSeedInt(2);
} else {
slower = !trickRoom ? phase.pokemon.getEffectiveStat(Stat.SPD) < target.getEffectiveStat(Stat.SPD) : phase.pokemon.getEffectiveStat(Stat.SPD) > target.getEffectiveStat(Stat.SPD);
}
return phase.isForcedLast() && slower;
};
const failOnGravityCondition: MoveConditionFunc = (user, target, move) => !globalScene.arena.getTag(ArenaTagType.GRAVITY); const failOnGravityCondition: MoveConditionFunc = (user, target, move) => !globalScene.arena.getTag(ArenaTagType.GRAVITY);
const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune(); const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune();
@ -7975,7 +7939,7 @@ const userSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target:
const targetSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => target.status?.effect === StatusEffect.SLEEP || target.hasAbility(AbilityId.COMATOSE); const targetSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => target.status?.effect === StatusEffect.SLEEP || target.hasAbility(AbilityId.COMATOSE);
const failIfLastCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => globalScene.phaseManager.phaseQueue.find(phase => phase.is("MovePhase")) !== undefined; const failIfLastCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => globalScene.phaseManager.hasPhaseOfType("MovePhase");
const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => { const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => {
const party: Pokemon[] = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty(); const party: Pokemon[] = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty();

View File

@ -98,6 +98,8 @@ import { UnlockPhase } from "#app/phases/unlock-phase";
import { VictoryPhase } from "#app/phases/victory-phase"; import { VictoryPhase } from "#app/phases/victory-phase";
import { WeatherEffectPhase } from "#app/phases/weather-effect-phase"; import { WeatherEffectPhase } from "#app/phases/weather-effect-phase";
import { DynamicQueueManager } from "#app/queues/dynamic-queue-manager"; import { DynamicQueueManager } from "#app/queues/dynamic-queue-manager";
import type { PhaseConditionFunc } from "#app/@types/phase-condition";
import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
/** /**
* Manager for phases used by battle scene. * Manager for phases used by battle scene.
@ -217,8 +219,8 @@ export type PhaseConstructorMap = typeof PHASES;
*/ */
export class PhaseManager { export class PhaseManager {
/** PhaseQueue: dequeue/remove the first element to get the next phase */ /** PhaseQueue: dequeue/remove the first element to get the next phase */
public phaseQueue: Phase[] = []; private phaseQueue: Phase[] = [];
public conditionalQueue: Array<[() => boolean, Phase]> = []; private conditionalQueue: Array<[() => boolean, Phase]> = [];
/** PhaseQueuePrepend: is a temp storage of what will be added to PhaseQueue */ /** PhaseQueuePrepend: is a temp storage of what will be added to PhaseQueue */
private phaseQueuePrepend: Phase[] = []; private phaseQueuePrepend: Phase[] = [];
@ -381,6 +383,15 @@ export class PhaseManager {
return true; return true;
} }
public hasPhaseOfType(type: PhaseString, condition?: PhaseConditionFunc): boolean {
if (this.dynamicQueueManager.isDynamicPhase(type)) {
return this.dynamicQueueManager.exists(type, condition);
}
return [this.phaseQueue, this.phaseQueuePrepend].some((queue: Phase[]) =>
queue.find(phase => phase.is(type) && (!condition || condition(phase))),
);
}
/** /**
* Find a specific {@linkcode Phase} in the phase queue. * Find a specific {@linkcode Phase} in the phase queue.
* *
@ -422,6 +433,11 @@ export class PhaseManager {
return false; return false;
} }
public removeAllPhasesOfType(type: PhaseString): void {
this.phaseQueue = this.phaseQueue.filter(phase => !phase.is(type));
this.phaseQueuePrepend = this.phaseQueuePrepend.filter(phase => !phase.is(type));
}
/** /**
* Tries to add the input phase to index before target phase in the phaseQueue, else simply calls unshiftPhase() * Tries to add the input phase to index before target phase in the phaseQueue, else simply calls unshiftPhase()
* @param phase - The phase to be added * @param phase - The phase to be added
@ -631,4 +647,12 @@ export class PhaseManager {
): void { ): void {
this.startDynamicPhase(this.create(phase, ...args)); this.startDynamicPhase(this.create(phase, ...args));
} }
public forceMoveNext(phaseCondition: PhaseConditionFunc) {
this.dynamicQueueManager.setMoveTimingModifier(phaseCondition, MovePhaseTimingModifier.FIRST);
}
public forceMoveLast(phaseCondition: PhaseConditionFunc) {
this.dynamicQueueManager.setMoveTimingModifier(phaseCondition, MovePhaseTimingModifier.LAST);
}
} }

View File

@ -18,23 +18,11 @@ export class BattleEndPhase extends BattlePhase {
super.start(); super.start();
// cull any extra `BattleEnd` phases from the queue. // cull any extra `BattleEnd` phases from the queue.
globalScene.phaseManager.phaseQueue = globalScene.phaseManager.phaseQueue.filter(phase => { this.isVictory ||= globalScene.phaseManager.hasPhaseOfType(
if (phase.is("BattleEndPhase")) { "BattleEndPhase",
this.isVictory ||= phase.isVictory; (phase: BattleEndPhase) => phase.isVictory,
return false; );
} globalScene.phaseManager.removeAllPhasesOfType("BattleEndPhase");
return true;
});
// `phaseQueuePrepend` is private, so we have to use this inefficient loop.
while (
globalScene.phaseManager.tryRemoveUnshiftedPhase(phase => {
if (phase.is("BattleEndPhase")) {
this.isVictory ||= phase.isVictory;
return true;
}
return false;
})
) {}
globalScene.gameData.gameStats.battles++; globalScene.gameData.gameStats.battles++;
if ( if (

View File

@ -28,6 +28,7 @@ import { StatusEffect } from "#enums/status-effect";
import i18next from "i18next"; import i18next from "i18next";
import { frenzyMissFunc } from "#app/data/moves/move-utils"; import { frenzyMissFunc } from "#app/data/moves/move-utils";
import { PokemonPhase } from "#app/phases/pokemon-phase"; import { PokemonPhase } from "#app/phases/pokemon-phase";
import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
export class MovePhase extends PokemonPhase { export class MovePhase extends PokemonPhase {
public readonly phaseName = "MovePhase"; public readonly phaseName = "MovePhase";
@ -36,7 +37,7 @@ export class MovePhase extends PokemonPhase {
protected _targets: BattlerIndex[]; protected _targets: BattlerIndex[];
protected followUp: boolean; protected followUp: boolean;
protected ignorePp: boolean; protected ignorePp: boolean;
protected forcedLast: boolean; protected _timingModifier: MovePhaseTimingModifier;
protected failed = false; protected failed = false;
protected cancelled = false; protected cancelled = false;
protected reflected = false; protected reflected = false;
@ -65,6 +66,14 @@ export class MovePhase extends PokemonPhase {
this._targets = targets; this._targets = targets;
} }
public get timingModifier(): MovePhaseTimingModifier {
return this._timingModifier;
}
public set timingModifier(modifier: MovePhaseTimingModifier) {
this._timingModifier = modifier;
}
/** /**
* @param followUp Indicates that the move being used is a "follow-up" - for example, a move being used by Metronome or Dancer. * @param followUp Indicates that the move being used is a "follow-up" - for example, a move being used by Metronome or Dancer.
* Follow-ups bypass a few failure conditions, including flinches, sleep/paralysis/freeze and volatile status checks, etc. * Follow-ups bypass a few failure conditions, including flinches, sleep/paralysis/freeze and volatile status checks, etc.
@ -79,7 +88,6 @@ export class MovePhase extends PokemonPhase {
followUp = false, followUp = false,
ignorePp = false, ignorePp = false,
reflected = false, reflected = false,
forcedLast = false,
) { ) {
super(pokemon.getBattlerIndex()); super(pokemon.getBattlerIndex());
@ -89,7 +97,7 @@ export class MovePhase extends PokemonPhase {
this.followUp = followUp; this.followUp = followUp;
this.ignorePp = ignorePp; this.ignorePp = ignorePp;
this.reflected = reflected; this.reflected = reflected;
this.forcedLast = forcedLast; this.timingModifier = MovePhaseTimingModifier.NORMAL;
} }
/** /**
@ -115,14 +123,6 @@ export class MovePhase extends PokemonPhase {
this.cancelled = true; this.cancelled = true;
} }
/**
* Shows whether the current move has been forced to the end of the turn
* Needed for speed order, see {@linkcode MoveId.QUASH}
* */
public isForcedLast(): boolean {
return this.forcedLast;
}
public start(): void { public start(): void {
super.start(); super.start();

View File

@ -6,12 +6,7 @@ export class NewBattlePhase extends BattlePhase {
start() { start() {
super.start(); super.start();
// cull any extra `NewBattle` phases from the queue. globalScene.phaseManager.removeAllPhasesOfType("NewBattlePhase");
globalScene.phaseManager.phaseQueue = globalScene.phaseManager.phaseQueue.filter(
phase => !phase.is("NewBattlePhase"),
);
// `phaseQueuePrepend` is private, so we have to use this inefficient loop.
while (globalScene.phaseManager.tryRemoveUnshiftedPhase(phase => phase.is("NewBattlePhase"))) {}
globalScene.newBattle(); globalScene.newBattle();

View File

@ -1,3 +1,4 @@
import type { PhaseConditionFunc } from "#app/@types/phase-condition";
import type { PhaseString } from "#app/@types/phase-types"; import type { PhaseString } from "#app/@types/phase-types";
import type { Phase } from "#app/phase"; import type { Phase } from "#app/phase";
import type { SwitchSummonPhase } from "#app/phases/switch-summon-phase"; import type { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
@ -5,6 +6,7 @@ import { MovePhasePriorityQueue } from "#app/queues/move-phase-priority-queue";
import type { PhasePriorityQueue } from "#app/queues/phase-priority-queue"; import type { PhasePriorityQueue } from "#app/queues/phase-priority-queue";
import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue"; import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue";
import { PostSummonPhasePriorityQueue } from "#app/queues/post-summon-phase-priority-queue"; import { PostSummonPhasePriorityQueue } from "#app/queues/post-summon-phase-priority-queue";
import type { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
export class DynamicQueueManager { export class DynamicQueueManager {
private dynamicPhaseMap: Map<PhaseString, PhasePriorityQueue<Phase>>; private dynamicPhaseMap: Map<PhaseString, PhasePriorityQueue<Phase>>;
@ -32,4 +34,13 @@ export class DynamicQueueManager {
public isDynamicPhase(type: PhaseString): boolean { public isDynamicPhase(type: PhaseString): boolean {
return this.dynamicPhaseMap.has(type); return this.dynamicPhaseMap.has(type);
} }
public exists(type: PhaseString, condition?: PhaseConditionFunc): boolean {
return !!this.dynamicPhaseMap.get(type)?.hasPhaseWithCondition(condition);
}
public setMoveTimingModifier(condition: PhaseConditionFunc, modifier: MovePhaseTimingModifier) {
const movePhaseQueue: MovePhasePriorityQueue = this.dynamicPhaseMap.get("MovePhase") as MovePhasePriorityQueue;
movePhaseQueue.setTimingModifier(condition, modifier);
}
} }

View File

@ -1,3 +1,4 @@
import type { PhaseConditionFunc } from "#app/@types/phase-condition";
import type { Phase } from "#app/phase"; import type { Phase } from "#app/phase";
/** /**
@ -40,4 +41,8 @@ export abstract class PhasePriorityQueue<T extends Phase> {
public isEmpty(): boolean { public isEmpty(): boolean {
return !this.queue.length; return !this.queue.length;
} }
public hasPhaseWithCondition(condition?: PhaseConditionFunc): boolean {
return this.queue.find(phase => !condition || condition(phase)) !== undefined;
}
} }

View File

@ -1,6 +1,5 @@
import { BerryPhase } from "#app/phases/berry-phase"; import { BerryPhase } from "#app/phases/berry-phase";
import { MessagePhase } from "#app/phases/message-phase"; import { MessagePhase } from "#app/phases/message-phase";
import { MoveHeaderPhase } from "#app/phases/move-header-phase";
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase"; import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
import { TurnStartPhase } from "#app/phases/turn-start-phase"; import { TurnStartPhase } from "#app/phases/turn-start-phase";
import { AbilityId } from "#enums/ability-id"; import { AbilityId } from "#enums/ability-id";
@ -116,7 +115,7 @@ describe("Moves - Focus Punch", () => {
await game.phaseInterceptor.to(TurnStartPhase); await game.phaseInterceptor.to(TurnStartPhase);
expect(game.scene.phaseManager.getCurrentPhase() instanceof SwitchSummonPhase).toBeTruthy(); expect(game.scene.phaseManager.getCurrentPhase() instanceof SwitchSummonPhase).toBeTruthy();
expect(game.scene.phaseManager.phaseQueue.find(phase => phase instanceof MoveHeaderPhase)).toBeDefined(); expect(game.scene.phaseManager.hasPhaseOfType("MoveHeaderPhase")).toBeTruthy();
}); });
it("should replace the 'but it failed' text when the user gets hit", async () => { it("should replace the 'but it failed' text when the user gets hit", async () => {
game.override.enemyMoveset([MoveId.TACKLE]); game.override.enemyMoveset([MoveId.TACKLE]);