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
if (phaseData?.move.category === MoveCategory.PHYSICAL && pokemon.isOpponent(phaseData.attacker)) {
const shellTrapPhaseIndex = globalScene.phaseManager.phaseQueue.findIndex(
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");
}
globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === pokemon);
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
const allyMovePhaseIndex = globalScene.phaseManager.phaseQueue.indexOf(allyMovePhase);
const firstMovePhaseIndex = globalScene.phaseManager.phaseQueue.findIndex((phase) => phase.is("MovePhase"));
if (allyMovePhaseIndex !== firstMovePhaseIndex) {
globalScene.phaseManager.prependToPhase(globalScene.phaseManager.phaseQueue.splice(allyMovePhaseIndex, 1)[0], "MovePhase");
}
globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === user.getAlly());
overridden.value = true;
return true;
@ -4546,12 +4542,7 @@ export class CueNextRoundAttr extends MoveEffectAttr {
return false;
}
// Update the phase queue so that the next Pokemon using Round moves next
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");
}
globalScene.phaseManager.forceMoveLast((phase: MovePhase) => phase.is("MovePhase") && phase.move.moveId === MoveId.ROUND);
// Mark the corresponding Pokemon as having "joined the Round" (for doubling power later)
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 {
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:forceLast", { targetPokemonName: getPokemonNameWithAffix(target) }));
const targetMovePhase = globalScene.phaseManager.findPhase<MovePhase>((phase) => 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)
);
}
}
globalScene.phaseManager.forceMoveLast((phase: MovePhase) => phase.pokemon === target);
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 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 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 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 { WeatherEffectPhase } from "#app/phases/weather-effect-phase";
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.
@ -217,8 +219,8 @@ export type PhaseConstructorMap = typeof PHASES;
*/
export class PhaseManager {
/** PhaseQueue: dequeue/remove the first element to get the next phase */
public phaseQueue: Phase[] = [];
public conditionalQueue: Array<[() => boolean, Phase]> = [];
private phaseQueue: Phase[] = [];
private conditionalQueue: Array<[() => boolean, Phase]> = [];
/** PhaseQueuePrepend: is a temp storage of what will be added to PhaseQueue */
private phaseQueuePrepend: Phase[] = [];
@ -381,6 +383,15 @@ export class PhaseManager {
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.
*
@ -422,6 +433,11 @@ export class PhaseManager {
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()
* @param phase - The phase to be added
@ -631,4 +647,12 @@ export class PhaseManager {
): void {
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();
// cull any extra `BattleEnd` phases from the queue.
globalScene.phaseManager.phaseQueue = globalScene.phaseManager.phaseQueue.filter(phase => {
if (phase.is("BattleEndPhase")) {
this.isVictory ||= phase.isVictory;
return false;
}
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;
})
) {}
this.isVictory ||= globalScene.phaseManager.hasPhaseOfType(
"BattleEndPhase",
(phase: BattleEndPhase) => phase.isVictory,
);
globalScene.phaseManager.removeAllPhasesOfType("BattleEndPhase");
globalScene.gameData.gameStats.battles++;
if (

View File

@ -28,6 +28,7 @@ import { StatusEffect } from "#enums/status-effect";
import i18next from "i18next";
import { frenzyMissFunc } from "#app/data/moves/move-utils";
import { PokemonPhase } from "#app/phases/pokemon-phase";
import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
export class MovePhase extends PokemonPhase {
public readonly phaseName = "MovePhase";
@ -36,7 +37,7 @@ export class MovePhase extends PokemonPhase {
protected _targets: BattlerIndex[];
protected followUp: boolean;
protected ignorePp: boolean;
protected forcedLast: boolean;
protected _timingModifier: MovePhaseTimingModifier;
protected failed = false;
protected cancelled = false;
protected reflected = false;
@ -65,6 +66,14 @@ export class MovePhase extends PokemonPhase {
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.
* 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,
ignorePp = false,
reflected = false,
forcedLast = false,
) {
super(pokemon.getBattlerIndex());
@ -89,7 +97,7 @@ export class MovePhase extends PokemonPhase {
this.followUp = followUp;
this.ignorePp = ignorePp;
this.reflected = reflected;
this.forcedLast = forcedLast;
this.timingModifier = MovePhaseTimingModifier.NORMAL;
}
/**
@ -115,14 +123,6 @@ export class MovePhase extends PokemonPhase {
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 {
super.start();

View File

@ -6,12 +6,7 @@ export class NewBattlePhase extends BattlePhase {
start() {
super.start();
// cull any extra `NewBattle` phases from the queue.
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.phaseManager.removeAllPhasesOfType("NewBattlePhase");
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 { Phase } from "#app/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 { PokemonPhasePriorityQueue } from "#app/queues/pokemon-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 {
private dynamicPhaseMap: Map<PhaseString, PhasePriorityQueue<Phase>>;
@ -32,4 +34,13 @@ export class DynamicQueueManager {
public isDynamicPhase(type: PhaseString): boolean {
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";
/**
@ -40,4 +41,8 @@ export abstract class PhasePriorityQueue<T extends Phase> {
public isEmpty(): boolean {
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 { MessagePhase } from "#app/phases/message-phase";
import { MoveHeaderPhase } from "#app/phases/move-header-phase";
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
import { TurnStartPhase } from "#app/phases/turn-start-phase";
import { AbilityId } from "#enums/ability-id";
@ -116,7 +115,7 @@ describe("Moves - Focus Punch", () => {
await game.phaseInterceptor.to(TurnStartPhase);
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 () => {
game.override.enemyMoveset([MoveId.TACKLE]);