From 301e12e92a7aba8d6987fd4b52a6cd62c697cea1 Mon Sep 17 00:00:00 2001 From: Dean Date: Sun, 22 Jun 2025 12:31:50 -0700 Subject: [PATCH] Automatically queue consecutive Pokemon phases as dynamic --- src/@types/phase-types.ts | 6 + src/data/abilities/ability.ts | 11 +- src/phase-manager.ts | 107 ++++++++++++------ src/phases/check-status-effect-phase.ts | 12 +- src/phases/encounter-phase.ts | 4 +- src/phases/post-summon-phase.ts | 5 - src/phases/turn-init-phase.ts | 1 + src/phases/turn-start-phase.ts | 8 -- src/queues/dynamic-queue-manager.ts | 42 ++++--- src/queues/pokemon-phase-priority-queue.ts | 6 +- .../post-summon-phase-priority-queue.ts | 10 +- 11 files changed, 132 insertions(+), 80 deletions(-) diff --git a/src/@types/phase-types.ts b/src/@types/phase-types.ts index 9abc3dadf8d..ca970f00cbf 100644 --- a/src/@types/phase-types.ts +++ b/src/@types/phase-types.ts @@ -1,3 +1,5 @@ +import type Pokemon from "#app/field/pokemon"; +import type { Phase } from "#app/phase"; import type { PhaseConstructorMap } from "#app/phase-manager"; // Intentionally export the types of everything in phase-manager, as this file is meant to be @@ -27,3 +29,7 @@ export type PhaseString = keyof PhaseMap; export type DynamicPhaseString = "PostSummonPhase" | "SwitchSummonPhase" | "MovePhase" | "MoveHeaderPhase"; export type StaticPhaseString = Exclude; + +export interface DynamicPhase extends Phase { + getPokemon(): Pokemon; +} diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 204793dbe4f..4f966f3422a 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -83,7 +83,7 @@ import type { Localizable } from "#app/@types/locales"; import { applyAbAttrs } from "./apply-ab-attrs"; import { MovePriorityModifier } from "#enums/move-priority-modifier"; import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; -import { MovePhase } from "#app/phases/move-phase"; +import type { MovePhase } from "#app/phases/move-phase"; export class Ability implements Localizable { public id: AbilityId; @@ -6102,7 +6102,14 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr { // If the move is an AttackMove or a StatusMove the Dancer must replicate the move on the source of the Dance if (move.getMove().is("AttackMove") || move.getMove().is("StatusMove")) { const target = this.getTarget(dancer, source, targets); - globalScene.phaseManager.pushNew("MovePhase", dancer, target, move, MoveUseMode.INDIRECT, MovePhaseTimingModifier.FIRST); + globalScene.phaseManager.pushNew( + "MovePhase", + dancer, + target, + move, + MoveUseMode.INDIRECT, + MovePhaseTimingModifier.FIRST, + ); } else if (move.getMove().is("SelfStatusMove")) { // If the move is a SelfStatusMove (ie. Swords Dance) the Dancer should replicate it on itself globalScene.phaseManager.pushNew( diff --git a/src/phase-manager.ts b/src/phase-manager.ts index 128ea4e4042..1243d6896c5 100644 --- a/src/phase-manager.ts +++ b/src/phase-manager.ts @@ -1,6 +1,6 @@ import type { Phase } from "#app/phase"; import type { default as Pokemon } from "#app/field/pokemon"; -import type { DynamicPhaseString, PhaseMap, PhaseString, StaticPhaseString } from "./@types/phase-types"; +import type { PhaseMap, PhaseString, DynamicPhase, StaticPhaseString } from "./@types/phase-types"; import { globalScene } from "#app/global-scene"; import { AddEnemyBuffModifierPhase } from "#app/phases/add-enemy-buff-modifier-phase"; import { AttemptCapturePhase } from "#app/phases/attempt-capture-phase"; @@ -214,6 +214,12 @@ const PHASES = Object.freeze({ /** Maps Phase strings to their constructors */ export type PhaseConstructorMap = typeof PHASES; +const turnEndPhases: PhaseString[] = ["WeatherEffectPhase", "BerryPhase", "CheckStatusEffectPhase", "TurnEndPhase"]; + +const ignorablePhases: PhaseString[] = ["ShowAbilityPhase", "HideAbilityPhase"]; +// TODO might be easier to define which phases should be dynamic instead +const nonDynamicPokemonPhases: PhaseString[] = ["SummonPhase", "CommandPhase"]; + /** * PhaseManager is responsible for managing the phases in the battle scene */ @@ -232,6 +238,8 @@ export class PhaseManager { private currentPhase: Phase | null = null; private standbyPhase: Phase | null = null; + public turnEnded = false; + /* Phase Functions */ getCurrentPhase(): Phase | null { return this.currentPhase; @@ -247,11 +255,11 @@ export class PhaseManager { * @param defer boolean on which queue to add to, defaults to false, and adds to phaseQueue */ pushPhase(phase: Phase, defer = false): void { - if (this.dynamicQueueManager.isDynamicPhase(phase.phaseName)) { + if (this.isDynamicPhase(phase) && this.dynamicQueueManager.activeQueueExists(phase.phaseName)) { this.dynamicQueueManager.queueDynamicPhase(phase); - } else { - (!defer ? this.phaseQueue : this.nextCommandPhaseQueue).push(phase); + return; } + (!defer ? this.phaseQueue : this.nextCommandPhaseQueue).push(phase); } /** @@ -259,6 +267,10 @@ export class PhaseManager { * @param phases {@linkcode Phase} the phase(s) to add */ unshiftPhase(...phases: Phase[]): void { + if (this.isDynamicPhase(phases[0]) && this.dynamicQueueManager.activeQueueExists(phases[0].phaseName)) { + phases.forEach((p: DynamicPhase) => this.dynamicQueueManager.queueDynamicPhase(p)); + return; + } if (this.phaseQueuePrependSpliceIndex === -1) { this.phaseQueuePrepend.push(...phases); } else { @@ -283,6 +295,7 @@ export class PhaseManager { this.dynamicQueueManager.clearQueues(); this.currentPhase = null; this.standbyPhase = null; + this.turnEnded = false; this.clearPhaseQueueSplice(); } @@ -323,14 +336,16 @@ export class PhaseManager { } } } - if (!this.phaseQueue.length) { - this.populatePhaseQueue(); + + this.queueDynamicPhasesAtFront(); + + if (this.phaseQueue.every(p => ignorablePhases.includes(p.phaseName))) { + this.startNextDynamicPhase(); } - if (this.phaseQueue[0].is("WeatherEffectPhase")) { - const dynamicPhase = this.dynamicQueueManager.popNextPhase(); - if (dynamicPhase) { - this.phaseQueue.unshift(dynamicPhase); + if (!this.turnEnded && (!this.phaseQueue.length || this.phaseQueue[0].is("BattleEndPhase"))) { + if (!this.startNextDynamicPhase()) { + this.turnEndSequence(); } } @@ -356,8 +371,8 @@ export class PhaseManager { } public hasPhaseOfType(type: PhaseString, condition?: PhaseConditionFunc): boolean { - if (this.dynamicQueueManager.isDynamicPhase(type)) { - return this.dynamicQueueManager.exists(type, condition); + if (this.dynamicQueueManager.exists(type, condition)) { + return true; } return [this.phaseQueue, this.phaseQueuePrepend].some((queue: Phase[]) => queue.find(phase => phase.is(type) && (!condition || condition(phase))), @@ -374,7 +389,7 @@ export class PhaseManager { phaseType: P, phaseFilter?: (phase: PhaseMap[P]) => boolean, ): PhaseMap[P] | undefined { - if (this.dynamicQueueManager.isDynamicPhase(phaseType)) { + if (this.dynamicQueueManager.exists(phaseType, phaseFilter)) { return this.dynamicQueueManager.findPhaseOfType(phaseType, phaseFilter) as PhaseMap[P]; } return this.phaseQueue.find(phase => phase.is(phaseType) && (!phaseFilter || phaseFilter(phase))) as PhaseMap[P]; @@ -457,17 +472,6 @@ export class PhaseManager { return false; } - /** - * Unshifts the top phase from the corresponding dynamic queue onto {@linkcode phaseQueue} - * @param type {@linkcode DynamicPhaseString} The type of dynamic phase to start - */ - public startNextDynamicPhase(type?: DynamicPhaseString): void { - const phase = this.dynamicQueueManager.popNextPhase(type); - if (phase) { - this.unshiftPhase(phase); - } - } - /** * Adds a MessagePhase, either to PhaseQueuePrepend or nextCommandPhaseQueue * @param message - string for MessagePhase @@ -518,12 +522,10 @@ export class PhaseManager { /** * Moves everything from nextCommandPhaseQueue to phaseQueue (keeping order) */ - private populatePhaseQueue(): void { - const dynamicPhase = this.dynamicQueueManager.popNextPhase(); - if (dynamicPhase) { - this.phaseQueue.unshift(dynamicPhase); - return; - } + private turnEndSequence(): void { + this.turnEnded = true; + this.dynamicQueueManager.clearQueues(); + this.queueTurnEndPhases(); if (this.nextCommandPhaseQueue.length) { this.phaseQueue.push(...this.nextCommandPhaseQueue); this.nextCommandPhaseQueue.splice(0, this.nextCommandPhaseQueue.length); @@ -565,10 +567,7 @@ export class PhaseManager { * @param phase - The name of the phase to create * @param args - The arguments to pass to the phase constructor */ - public unshiftNew( - phase: T, - ...args: ConstructorParameters - ): void { + public unshiftNew(phase: T, ...args: ConstructorParameters): void { this.unshiftPhase(this.create(phase, ...args)); } @@ -615,4 +614,44 @@ export class PhaseManager { public changePhaseMove(phaseCondition: PhaseConditionFunc, move: PokemonMove) { this.dynamicQueueManager.setMoveForPhase(phaseCondition, move); } + + public queueTurnEndPhases(): void { + turnEndPhases + .slice() + .reverse() + .forEach(p => this.phaseQueue.unshift(this.create(p))); + } + + private consecutivePokemonPhases(): DynamicPhase[] | undefined { + if (this.phaseQueue.length < 1 || !this.isDynamicPhase(this.phaseQueue[0])) { + return; + } + + let spliceLength = this.phaseQueue.findIndex(p => !p.is(this.phaseQueue[0].phaseName)); + spliceLength = spliceLength !== -1 ? spliceLength : this.phaseQueue.length; + if (spliceLength > 1) { + return this.phaseQueue.splice(0, spliceLength) as DynamicPhase[]; + } + } + + private queueDynamicPhasesAtFront(): void { + const dynamicPhases = this.consecutivePokemonPhases(); + if (dynamicPhases) { + dynamicPhases.forEach((p: DynamicPhase) => { + globalScene.phaseManager.dynamicQueueManager.queueDynamicPhase(p); + }); + } + } + + public startNextDynamicPhase(): boolean { + const dynamicPhase = this.dynamicQueueManager.popNextPhase(); + if (dynamicPhase) { + this.phaseQueue.unshift(dynamicPhase); + } + return !!dynamicPhase; + } + + private isDynamicPhase(phase: Phase): phase is DynamicPhase { + return typeof (phase as any).getPokemon === "function" && !nonDynamicPokemonPhases.includes(phase.phaseName); + } } diff --git a/src/phases/check-status-effect-phase.ts b/src/phases/check-status-effect-phase.ts index 43495e038e9..c757e13a68c 100644 --- a/src/phases/check-status-effect-phase.ts +++ b/src/phases/check-status-effect-phase.ts @@ -1,20 +1,14 @@ import { Phase } from "#app/phase"; -import type { BattlerIndex } from "#enums/battler-index"; import { globalScene } from "#app/global-scene"; export class CheckStatusEffectPhase extends Phase { public readonly phaseName = "CheckStatusEffectPhase"; - private order: BattlerIndex[]; - constructor(order: BattlerIndex[]) { - super(); - this.order = order; - } start() { const field = globalScene.getField(); - for (const o of this.order) { - if (field[o].status?.isPostTurn()) { - globalScene.phaseManager.unshiftNew("PostTurnStatusEffectPhase", o); + for (const p of field) { + if (p?.status?.isPostTurn()) { + globalScene.phaseManager.unshiftNew("PostTurnStatusEffectPhase", p.getBattlerIndex()); } } this.end(); diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index c35e6750a6b..8c769919e34 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -562,7 +562,6 @@ export class EncounterPhase extends BattlePhase { }); if (![BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(globalScene.currentBattle.battleType)) { - enemyField.map(p => globalScene.phaseManager.pushNew("PostSummonPhase", p.getBattlerIndex())); const ivScannerModifier = globalScene.findModifier(m => m instanceof IvScannerModifier); if (ivScannerModifier) { enemyField.map(p => globalScene.phaseManager.pushNew("ScanIvsPhase", p.getBattlerIndex())); @@ -603,6 +602,9 @@ export class EncounterPhase extends BattlePhase { } } } + if (![BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(globalScene.currentBattle.battleType)) { + enemyField.map(p => globalScene.phaseManager.pushNew("PostSummonPhase", p.getBattlerIndex())); + } handleTutorial(Tutorial.Access_Menu).then(() => super.end()); } diff --git a/src/phases/post-summon-phase.ts b/src/phases/post-summon-phase.ts index 4abeca9953e..14cef66e7f2 100644 --- a/src/phases/post-summon-phase.ts +++ b/src/phases/post-summon-phase.ts @@ -36,11 +36,6 @@ export class PostSummonPhase extends PokemonPhase { this.end(); } - override end() { - globalScene.phaseManager.startNextDynamicPhase("PostSummonPhase"); - super.end(); - } - public getPriority() { return 0; } diff --git a/src/phases/turn-init-phase.ts b/src/phases/turn-init-phase.ts index 8d0508c5ebb..0b1c5a4fc78 100644 --- a/src/phases/turn-init-phase.ts +++ b/src/phases/turn-init-phase.ts @@ -14,6 +14,7 @@ export class TurnInitPhase extends FieldPhase { start() { super.start(); + globalScene.phaseManager.turnEnded = false; globalScene.getPlayerField().forEach(p => { // If this pokemon is in play and evolved into something illegal under the current challenge, force a switch if (p.isOnField() && !p.isAllowedInBattle()) { diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index e9dfc4e2f48..dc7c426e243 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -163,14 +163,6 @@ export class TurnStartPhase extends FieldPhase { } } - phaseManager.pushNew("WeatherEffectPhase"); - phaseManager.pushNew("BerryPhase"); - - /** Add a new phase to check who should be taking status damage */ - phaseManager.pushNew("CheckStatusEffectPhase", moveOrder); - - phaseManager.pushNew("TurnEndPhase"); - /** * this.end() will call shiftPhase(), which dumps everything from PrependQueue (aka everything that is unshifted()) to the front * of the queue and dequeues to start the next phase diff --git a/src/queues/dynamic-queue-manager.ts b/src/queues/dynamic-queue-manager.ts index 14c45f2b6c6..eb6c1c08c5f 100644 --- a/src/queues/dynamic-queue-manager.ts +++ b/src/queues/dynamic-queue-manager.ts @@ -1,9 +1,8 @@ import type { PhaseConditionFunc } from "#app/@types/phase-condition"; -import type { DynamicPhaseString, PhaseString } from "#app/@types/phase-types"; +import type { PhaseString, DynamicPhase } from "#app/@types/phase-types"; import type { PokemonMove } from "#app/data/moves/pokemon-move"; import type Pokemon from "#app/field/pokemon"; import type { Phase } from "#app/phase"; -import type { MoveHeaderPhase } from "#app/phases/move-header-phase"; 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"; @@ -14,11 +13,12 @@ import type { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier" export class DynamicQueueManager { private dynamicPhaseMap: Map>; + private alwaysDynamic: PhaseString[] = ["SwitchSummonPhase", "PostSummonPhase", "MovePhase"]; + private popOrder: PhaseString[] = []; constructor() { this.dynamicPhaseMap = new Map(); this.dynamicPhaseMap.set("SwitchSummonPhase", new SwitchSummonPhasePriorityQueue()); - this.dynamicPhaseMap.set("MoveHeaderPhase", new PokemonPhasePriorityQueue()); this.dynamicPhaseMap.set("PostSummonPhase", new PostSummonPhasePriorityQueue()); this.dynamicPhaseMap.set("MovePhase", new MovePhasePriorityQueue()); } @@ -27,27 +27,39 @@ export class DynamicQueueManager { for (const queue of this.dynamicPhaseMap.values()) { queue.clear(); } + this.popOrder.splice(0, this.popOrder.length); } - public queueDynamicPhase(phase: Phase): void { + public queueDynamicPhase(phase: T): void { + if (!this.dynamicPhaseMap.has(phase.phaseName)) { + this.dynamicPhaseMap.set(phase.phaseName, new PokemonPhasePriorityQueue()); + } this.dynamicPhaseMap.get(phase.phaseName)?.push(phase); + this.popOrder.push(phase.phaseName); } - public popNextPhase(type?: DynamicPhaseString): Phase | undefined { - const queue = type - ? this.dynamicPhaseMap.get(type) - : [...this.dynamicPhaseMap.values()].find(queue => !queue.isEmpty()); - return queue?.pop(); - } - - public isDynamicPhase(type: PhaseString): boolean { - return this.dynamicPhaseMap.has(type); + public popNextPhase(): Phase | undefined { + const type = this.popOrder.pop(); + if (!type) { + return; + } + if (!this.alwaysDynamic.includes(type)) { + return this.dynamicPhaseMap.get(type)?.pop(); + } + return this.alwaysDynamic + .map((p: PhaseString) => this.dynamicPhaseMap.get(p)) + .find(q => q && !q.isEmpty()) + ?.pop(); } public findPhaseOfType(type: PhaseString, condition?: PhaseConditionFunc): Phase | undefined { return this.dynamicPhaseMap.get(type)?.findPhase(condition); } + public activeQueueExists(type: PhaseString) { + return this.alwaysDynamic.includes(type) || this.dynamicPhaseMap.get(type)?.isEmpty() === false; + } + public exists(type: PhaseString, condition?: PhaseConditionFunc): boolean { return !!this.dynamicPhaseMap.get(type)?.hasPhaseWithCondition(condition); } @@ -84,4 +96,8 @@ export class DynamicQueueManager { private getMovePhaseQueue(): MovePhasePriorityQueue { return this.dynamicPhaseMap.get("MovePhase") as MovePhasePriorityQueue; } + + public addPopType(type: PhaseString): void { + this.popOrder.push(type); + } } diff --git a/src/queues/pokemon-phase-priority-queue.ts b/src/queues/pokemon-phase-priority-queue.ts index 4dab3ffa6c3..21e2cf64f8e 100644 --- a/src/queues/pokemon-phase-priority-queue.ts +++ b/src/queues/pokemon-phase-priority-queue.ts @@ -1,12 +1,12 @@ -import type { PartyMemberPokemonPhase } from "#app/phases/party-member-pokemon-phase"; -import type { PokemonPhase } from "#app/phases/pokemon-phase"; +import type { DynamicPhase } from "#app/@types/phase-types"; import { PhasePriorityQueue } from "#app/queues/phase-priority-queue"; import { sortInSpeedOrder } from "#app/utils/speed-order"; import type { BattlerIndex } from "#enums/battler-index"; -export class PokemonPhasePriorityQueue extends PhasePriorityQueue { +export class PokemonPhasePriorityQueue extends PhasePriorityQueue { protected setOrder: BattlerIndex[] | undefined; public override reorder(): void { + this.queue = this.queue.filter(phase => phase.getPokemon()?.isActive(true)); if (this.setOrder) { this.queue.sort( (a, b) => diff --git a/src/queues/post-summon-phase-priority-queue.ts b/src/queues/post-summon-phase-priority-queue.ts index 95842a9fb64..8336ad4cc92 100644 --- a/src/queues/post-summon-phase-priority-queue.ts +++ b/src/queues/post-summon-phase-priority-queue.ts @@ -1,3 +1,4 @@ +import { globalScene } from "#app/global-scene"; import { PostSummonActivateAbilityPhase } from "#app/phases/post-summon-activate-ability-phase"; import type { PostSummonPhase } from "#app/phases/post-summon-phase"; import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue"; @@ -29,10 +30,9 @@ export class PostSummonPhasePriorityQueue extends PokemonPhasePriorityQueue - this.queue.push(new PostSummonActivateAbilityPhase(phasePokemon.getBattlerIndex(), priority, !!idx)), - ); + phasePokemon.getAbilityPriorities().forEach((priority, idx) => { + this.queue.push(new PostSummonActivateAbilityPhase(phasePokemon.getBattlerIndex(), priority, !!idx)); + globalScene.phaseManager.dynamicQueueManager.addPopType("PostSummonPhase"); + }); } }