From aca6d2ce8d3b1068f4f2662309c73f481ab0653f Mon Sep 17 00:00:00 2001 From: Dean Date: Mon, 17 Mar 2025 21:22:30 -0700 Subject: [PATCH] Switch to priority queue approach --- src/battle-scene.ts | 95 +++++++++++++--- src/data/phase-priority-queue.ts | 84 ++++++++++++++ src/phases/activate-priority-queue-phase.ts | 22 ++++ .../post-summon-activate-ability-phase.ts | 31 ------ src/phases/post-summon-phase.ts | 104 +++++------------- src/phases/switch-summon-phase.ts | 2 +- 6 files changed, 213 insertions(+), 125 deletions(-) create mode 100644 src/data/phase-priority-queue.ts create mode 100644 src/phases/activate-priority-queue-phase.ts delete mode 100644 src/phases/post-summon-activate-ability-phase.ts diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 743b09805af..f82abde6476 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -170,6 +170,13 @@ import { StatusEffect } from "#enums/status-effect"; import { initGlobalScene } from "#app/global-scene"; import { ShowAbilityPhase } from "#app/phases/show-ability-phase"; import { HideAbilityPhase } from "#app/phases/hide-ability-phase"; +import { + type DynamicPhaseType, + type PhasePriorityQueue, + PostSummonPhasePriorityQueue, +} from "#app/data/phase-priority-queue"; +import { PostSummonPhase } from "#app/phases/post-summon-phase"; +import { ActivatePriorityQueuePhase } from "#app/phases/activate-priority-queue-phase"; export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1"; @@ -302,6 +309,10 @@ export default class BattleScene extends SceneBase { /** overrides default of inserting phases to end of phaseQueuePrepend array, useful or inserting Phases "out of order" */ private phaseQueuePrependSpliceIndex: number; private nextCommandPhaseQueue: Phase[]; + /** Storage for {@linkcode PhasePriorityQueue}s which hold phases whose order dynamically changes */ + private dynamicPhaseQueues: PhasePriorityQueue[]; + /** Parallel array to {@linkcode dynamicPhaseQueues} - matches phase types to their queues */ + private dynamicPhaseTypes: Constructor[]; private currentPhase: Phase | null; private standbyPhase: Phase | null; @@ -397,6 +408,8 @@ export default class BattleScene extends SceneBase { this.conditionalQueue = []; this.phaseQueuePrependSpliceIndex = -1; this.nextCommandPhaseQueue = []; + this.dynamicPhaseQueues = [new PostSummonPhasePriorityQueue()]; + this.dynamicPhaseTypes = [PostSummonPhase]; this.eventManager = new TimedEventManager(); this.updateGameInfo(); initGlobalScene(this); @@ -2693,12 +2706,16 @@ export default class BattleScene extends SceneBase { } /** - * Adds a phase to nextCommandPhaseQueue, as long as boolean passed in is false + * Adds a phase to the end of the appropriate queue (dynamic or {@linkcode phaseQueue} / {@linkcode nextCommandPhaseQueue}) * @param phase {@linkcode Phase} the phase to add - * @param defer boolean on which queue to add to, defaults to false, and adds to phaseQueue + * @param defer If `true`, add to {@linkcode nextCommandPhaseQueue} instead of {@linkcode phaseQueue} */ pushPhase(phase: Phase, defer = false): void { - (!defer ? this.phaseQueue : this.nextCommandPhaseQueue).push(phase); + if (this.getDynamicPhaseType(phase) !== undefined) { + this.pushDynamicPhase(phase); + } else { + (!defer ? this.phaseQueue : this.nextCommandPhaseQueue).push(phase); + } } /** @@ -2867,13 +2884,14 @@ export default class BattleScene extends SceneBase { * Tries to add the input phase(s) to index after target phase in the {@linkcode phaseQueue}, else simply calls {@linkcode unshiftPhase()} * @param phase {@linkcode Phase} the phase(s) to be added * @param targetPhase {@linkcode Phase} the type of phase to search for in {@linkcode phaseQueue} + * @param condition Condition the target phase must meet to be appended to * @returns `true` if a `targetPhase` was found to append to */ - appendToPhase(phase: Phase | Phase[], targetPhase: Constructor): boolean { + appendToPhase(phase: Phase | Phase[], targetPhase: Constructor, condition?: (p: Phase) => boolean): boolean { if (!Array.isArray(phase)) { phase = [phase]; } - const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof targetPhase); + const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof targetPhase && (!condition || condition(ph))); if (targetIndex !== -1 && this.phaseQueue.length > targetIndex) { this.phaseQueue.splice(targetIndex + 1, 0, ...phase); @@ -2884,22 +2902,65 @@ export default class BattleScene extends SceneBase { } /** - * Sorts the first consecutive set of occurences of {@linkcode targetPhase} in {@linkcode phaseQueue} - * @param targetPhase The type of phase to search for and sort - * @param by A function to compare the phases with - * @see {@linkcode Array.sort} for the comparison function + * Checks a phase and returns the matching {@linkcode DynamicPhaseType}, or undefined if it does not match one + * @param phase The phase to check + * @returns The corresponding {@linkcode DynamicPhaseType} or `undefined` */ - sortPhaseType(targetPhase: Constructor, by: (a: Phase, b: Phase) => number): void { - const startIndex = this.phaseQueue.findIndex(phase => phase instanceof targetPhase); - if (startIndex === -1) { + public getDynamicPhaseType(phase: Phase | null): DynamicPhaseType | undefined { + let phaseType: DynamicPhaseType | undefined; + this.dynamicPhaseTypes.forEach((cls, index) => { + if (phase instanceof cls) { + phaseType = index; + } + }); + + return phaseType; + } + + /** + * Pushes a phase onto its corresponding dynamic queue and marks the activation point in {@linkcode phaseQueue} + * + * The {@linkcode ActivatePriorityQueuePhase} will run the top phase in the dynamic queue (not necessarily {@linkcode phase}) + * @param phase The phase to push + */ + public pushDynamicPhase(phase: Phase): void { + const type = this.getDynamicPhaseType(phase); + if (type === undefined) { return; } - const endIndex = this.phaseQueue.findIndex((phase, index) => index > startIndex && !(phase instanceof targetPhase)); - const sortedSubset = this.phaseQueue - .slice(startIndex, endIndex !== -1 ? endIndex + 1 : this.phaseQueue.length) - .sort(by); - this.phaseQueue.splice(startIndex, sortedSubset.length, ...sortedSubset); + this.pushPhase(new ActivatePriorityQueuePhase(type)); + this.dynamicPhaseQueues[type].push(phase); + } + + /** + * Unshifts the top phase from the corresponding dynamic queue onto {@linkcode phaseQueue} + * @param type {@linkcode DynamicPhaseType} The type of dynamic phase to start + */ + public startDynamicPhaseType(type: DynamicPhaseType): void { + const phase = this.dynamicPhaseQueues[type].pop(); + if (phase) { + this.unshiftPhase(phase); + } + } + + /** + * Unshifts an {@linkcode ActivatePriorityQueuePhase} for {@linkcode phase}, then pushes {@linkcode phase} to its dynamic queue + * + * This is the same as {@linkcode pushDynamicPhase}, except the activation phase is unshifted + * + * {@linkcode phase} is not guaranteed to be the next phase from the queue to run (if the queue is not empty) + * @param phase The phase to add + * @returns + */ + public startDynamicPhase(phase: Phase): void { + const type = this.getDynamicPhaseType(phase); + if (type === undefined) { + return; + } + + this.unshiftPhase(new ActivatePriorityQueuePhase(type)); + this.dynamicPhaseQueues[type].push(phase); } /** diff --git a/src/data/phase-priority-queue.ts b/src/data/phase-priority-queue.ts new file mode 100644 index 00000000000..55d2d20b2fb --- /dev/null +++ b/src/data/phase-priority-queue.ts @@ -0,0 +1,84 @@ +import { globalScene } from "#app/global-scene"; +import type { Phase } from "#app/phase"; +import { ActivatePriorityQueuePhase } from "#app/phases/activate-priority-queue-phase"; +import { type PostSummonPhase, PostSummonActivateAbilityPhase } from "#app/phases/post-summon-phase"; +import { Stat } from "#enums/stat"; + +/** + * Stores a list of {@linkcode Phase}s + * + * Dynamically updates ordering to always pop the highest "priority", based on implementation of {@linkcode reorder} + */ +export abstract class PhasePriorityQueue { + protected abstract queue: Phase[]; + + /** + * Sorts the elements in the queue + */ + public abstract reorder(): void; + + /** + * Calls {@linkcode reorder} and shifts the queue + * @returns The front element of the queue after sorting + */ + public pop(): Phase | undefined { + this.reorder(); + return this.queue.shift(); + } + + /** + * Adds a phase to the queue + * @param phase The phase to add + */ + public push(phase: Phase): void { + this.queue.push(phase); + } +} + +/** + * Priority Queue for {@linkcode PostSummonPhase} and {@linkcode PostSummonActivateAbilityPhase} + * + * Orders phases first by ability priority, then by the {@linkcode Pokemon}'s effective speed + */ +export class PostSummonPhasePriorityQueue extends PhasePriorityQueue { + protected override queue: PostSummonPhase[] = []; + + public override reorder(): void { + this.queue.sort((phaseA: PostSummonPhase, phaseB: PostSummonPhase) => { + if (phaseA.getPriority() === phaseB.getPriority()) { + return phaseB.getPokemon().getEffectiveStat(Stat.SPD) - phaseA.getPokemon().getEffectiveStat(Stat.SPD); + } + + return phaseB.getPriority() - phaseA.getPriority(); + }); + } + + public override push(phase: PostSummonPhase): void { + super.push(phase); + this.queueAbilityPhase(phase); + } + + /** + * Queues all necessary {@linkcode PostSummonActivateAbilityPhase}s for each pushed {@linkcode PostSummonPhase} + * @param phase The {@linkcode PostSummonPhase} that was pushed onto the queue + */ + private queueAbilityPhase(phase: PostSummonPhase): void { + const phasePokemon = phase.getPokemon(); + + phasePokemon.getAbilityPriorities().forEach(priority => { + this.queue.push(new PostSummonActivateAbilityPhase(phasePokemon.getBattlerIndex(), priority)); + globalScene.appendToPhase( + new ActivatePriorityQueuePhase(DynamicPhaseType.POST_SUMMON), + ActivatePriorityQueuePhase, + (p: ActivatePriorityQueuePhase) => p.getType() === DynamicPhaseType.POST_SUMMON, + ); + }); + } +} + +/** + * Enum representation of the phase types held by implementations of {@linkcode PhasePriorityQueue} + */ +export enum DynamicPhaseType { + POST_SUMMON, +} diff --git a/src/phases/activate-priority-queue-phase.ts b/src/phases/activate-priority-queue-phase.ts new file mode 100644 index 00000000000..b4f4a6f55a1 --- /dev/null +++ b/src/phases/activate-priority-queue-phase.ts @@ -0,0 +1,22 @@ +import type { DynamicPhaseType } from "#app/data/phase-priority-queue"; +import { globalScene } from "#app/global-scene"; +import { Phase } from "#app/phase"; + +export class ActivatePriorityQueuePhase extends Phase { + private type: DynamicPhaseType; + + constructor(type: DynamicPhaseType) { + super(); + this.type = type; + } + + override start() { + super.start(); + globalScene.startDynamicPhaseType(this.type); + this.end(); + } + + public getType(): DynamicPhaseType { + return this.type; + } +} diff --git a/src/phases/post-summon-activate-ability-phase.ts b/src/phases/post-summon-activate-ability-phase.ts deleted file mode 100644 index fb1c3294a63..00000000000 --- a/src/phases/post-summon-activate-ability-phase.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { BattlerIndex } from "#app/battle"; -import { applyPostSummonAbAttrs, PostSummonAbAttr } from "#app/data/ability"; -import { PokemonPhase } from "#app/phases/pokemon-phase"; - -/** - * Phase to apply (post-summon) ability attributes for abilities with nonzero priority - * - * Priority abilities activate before others and before hazards - * - * @see Example - {@link https://bulbapedia.bulbagarden.net/wiki/Neutralizing_Gas_(Ability) | Neutralizing Gas} - */ -export class PostSummonActivateAbilityPhase extends PokemonPhase { - private priority: number; - - constructor(battlerIndex: BattlerIndex, priority: number) { - super(battlerIndex); - this.priority = priority; - } - - start() { - super.start(); - - applyPostSummonAbAttrs(PostSummonAbAttr, this.getPokemon(), false, (p: number) => p === this.priority); - - this.end(); - } - - public getPriority() { - return this.priority; - } -} diff --git a/src/phases/post-summon-phase.ts b/src/phases/post-summon-phase.ts index 0500d63698f..173390fbdbd 100644 --- a/src/phases/post-summon-phase.ts +++ b/src/phases/post-summon-phase.ts @@ -6,42 +6,12 @@ import { StatusEffect } from "#app/enums/status-effect"; import { PokemonPhase } from "./pokemon-phase"; import { MysteryEncounterPostSummonTag } from "#app/data/battler-tags"; import { BattlerTagType } from "#enums/battler-tag-type"; -import { Stat } from "#enums/stat"; -import { PostSummonActivateAbilityPhase } from "#app/phases/post-summon-activate-ability-phase"; export class PostSummonPhase extends PokemonPhase { - /** Represents whether or not this phase has already been placed in the correct (speed) order */ - private ordered: boolean; - - constructor(battlerIndex?: BattlerIndex, ordered = false) { - super(battlerIndex); - - this.ordered = ordered; - } - start() { super.start(); const pokemon = this.getPokemon(); - let indexAfterPostSummon = globalScene.phaseQueue.findIndex(phase => !(phase instanceof PostSummonPhase)); - indexAfterPostSummon = indexAfterPostSummon === -1 ? globalScene.phaseQueue.length : indexAfterPostSummon; - - if ( - !this.ordered && - globalScene.findPhase(phase => phase instanceof PostSummonPhase && phase.getPokemon() !== pokemon) - ) { - globalScene.phaseQueue.splice(indexAfterPostSummon++, 0, new PostSummonPhase(pokemon.getBattlerIndex(), true)); - - this.orderPostSummonPhases(); - this.queueAbilityActivationPhases(indexAfterPostSummon); - - this.end(); - return; - } - - if (!this.ordered) { - applyPostSummonAbAttrs(PostSummonAbAttr, pokemon, false, (p: number) => p > 0); - } if (pokemon.status?.effect === StatusEffect.TOXIC) { pokemon.status.toxicTurnCount = 0; @@ -56,10 +26,6 @@ export class PostSummonPhase extends PokemonPhase { pokemon.lapseTag(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON); } - if (!this.ordered) { - applyPostSummonAbAttrs(PostSummonAbAttr, pokemon, false, (p: number) => p <= 0); - } - const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); for (const p of field) { applyAbAttrs(CommanderAbAttr, p, null, false); @@ -68,47 +34,33 @@ export class PostSummonPhase extends PokemonPhase { this.end(); } - /** - * Sorts the {@linkcode PostSummonPhase}s in the queue by effective speed - */ - private orderPostSummonPhases() { - globalScene.sortPhaseType( - PostSummonPhase, - (phaseA: PostSummonPhase, phaseB: PostSummonPhase) => - phaseB.getPokemon().getEffectiveStat(Stat.SPD) - phaseA.getPokemon().getEffectiveStat(Stat.SPD), - ); - - for (let i = 0; i < globalScene.phaseQueue.length && globalScene.phaseQueue[i] instanceof PostSummonPhase; i++) { - (globalScene.phaseQueue[i] as PostSummonPhase).ordered = true; - } - } - - /** - * Adds {@linkcode PostSummonActivateAbilityPhase}s for all {@linkcode PostSummonPhase}s in the queue - * @param endIndex The index of the first non-{@linkcode PostSummonPhase} Phase in the queue, or the length if none exists - */ - private queueAbilityActivationPhases(endIndex: number) { - const abilityPhases: PostSummonActivateAbilityPhase[] = []; - - globalScene.phaseQueue.slice(0, endIndex).forEach((phase: PostSummonPhase) => { - const phasePokemon = phase.getPokemon(); - - phasePokemon - .getAbilityPriorities() - .forEach(priority => - abilityPhases.push(new PostSummonActivateAbilityPhase(phasePokemon.getBattlerIndex(), priority)), - ); - }); - - abilityPhases.sort( - (phaseA: PostSummonActivateAbilityPhase, phaseB: PostSummonActivateAbilityPhase) => - phaseB.getPriority() - phaseA.getPriority(), - ); - - let zeroIndex = abilityPhases.findIndex(phase => phase.getPriority() === 0); - zeroIndex = zeroIndex === -1 ? abilityPhases.length : zeroIndex; - - globalScene.unshiftPhase(...abilityPhases.slice(0, zeroIndex)); - globalScene.phaseQueue.splice(endIndex, 0, ...abilityPhases.slice(zeroIndex)); + public getPriority() { + return 0; + } +} + +/** + * Phase to apply (post-summon) ability attributes for abilities with nonzero priority + * + * Priority abilities activate before others and before hazards + * + * @see Example - {@link https://bulbapedia.bulbagarden.net/wiki/Neutralizing_Gas_(Ability) | Neutralizing Gas} + */ +export class PostSummonActivateAbilityPhase extends PostSummonPhase { + private priority: number; + + constructor(battlerIndex: BattlerIndex, priority: number) { + super(battlerIndex); + this.priority = priority; + } + + start() { + applyPostSummonAbAttrs(PostSummonAbAttr, this.getPokemon(), false, (p: number) => p === this.priority); + + this.end(); + } + + public override getPriority() { + return this.priority; } } diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index 48bcd0c4ebd..7c14383c62e 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -246,6 +246,6 @@ export class SwitchSummonPhase extends SummonPhase { } queuePostSummon(): void { - globalScene.unshiftPhase(new PostSummonPhase(this.getPokemon().getBattlerIndex())); + globalScene.startDynamicPhase(new PostSummonPhase(this.getPokemon().getBattlerIndex())); } }