From 5f098545f5de6bc8aaf66509f1d4d620139742af Mon Sep 17 00:00:00 2001 From: Dean Date: Sat, 14 Jun 2025 17:39:52 -0700 Subject: [PATCH 01/28] Add new priority queues --- src/data/abilities/ability.ts | 50 +++++++--- src/data/battler-tags.ts | 19 ++++ src/data/moves/move.ts | 8 ++ src/data/phase-priority-queue.ts | 97 ------------------- src/enums/battler-tag-type.ts | 1 + src/enums/move-priority-modifier.ts | 5 + src/modifier/modifier.ts | 21 +--- src/queues/move-phase-priority-queue.ts | 28 ++++++ src/queues/phase-priority-queue.ts | 43 ++++++++ src/queues/pokemon-phase-priority-queue.ts | 10 ++ .../post-summon-phase-priority-queue.ts | 42 ++++++++ src/utils/speed-order.ts | 46 +++++++++ 12 files changed, 244 insertions(+), 126 deletions(-) delete mode 100644 src/data/phase-priority-queue.ts create mode 100644 src/enums/move-priority-modifier.ts create mode 100644 src/queues/move-phase-priority-queue.ts create mode 100644 src/queues/phase-priority-queue.ts create mode 100644 src/queues/pokemon-phase-priority-queue.ts create mode 100644 src/queues/post-summon-phase-priority-queue.ts create mode 100644 src/utils/speed-order.ts diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index ef4529c361e..77aa74b90e4 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -79,6 +79,7 @@ import type { ArenaTrapTag, SuppressAbilitiesTag } from "#app/data/arena-tag"; import { noAbilityTypeOverrideMoves } from "../moves/invalid-moves"; import type { Localizable } from "#app/@types/locales"; import { applyAbAttrs } from "./apply-ab-attrs"; +import { MovePriorityModifier } from "#enums/move-priority-modifier"; export class Ability implements Localizable { public id: AbilityId; @@ -4978,6 +4979,31 @@ export class ChangeMovePriorityAbAttr extends AbAttr { } } +export class ChangeMovePriorityModifierAbAttr extends AbAttr { + private newModifier: MovePriorityModifier; + private moveFunc: (pokemon: Pokemon, move: Move) => boolean; + + constructor(moveFunc: (pokemon: Pokemon, move: Move) => boolean, newModifier: MovePriorityModifier) { + super(false); + this.newModifier = newModifier; + this.moveFunc = moveFunc; + } + + override canApply(pokemon: Pokemon, _passive: boolean, _simulated: boolean, args: any[]): boolean { + return this.moveFunc(pokemon, args[0] as Move); + } + + override apply( + _pokemon: Pokemon, + _passive: boolean, + _simulated: boolean, + _cancelled: BooleanHolder | null, + args: any[], + ): void { + (args[1] as NumberHolder).value = this.newModifier; + } +} + export class IgnoreContactAbAttr extends AbAttr {} export class PreWeatherEffectAbAttr extends AbAttr { @@ -7219,7 +7245,12 @@ export class BypassSpeedChanceAbAttr extends AbAttr { const move = turnCommand?.move?.move ? allMoves[turnCommand.move.move] : null; const isDamageMove = move?.category === MoveCategory.PHYSICAL || move?.category === MoveCategory.SPECIAL; return ( - !simulated && !bypassSpeed.value && pokemon.randBattleSeedInt(100) < this.chance && isCommandFight && isDamageMove + !simulated && + !bypassSpeed.value && + pokemon.randBattleSeedInt(100) < this.chance && + isCommandFight && + isDamageMove && + pokemon.canAddTag(BattlerTagType.BYPASS_SPEED) ); } @@ -7228,17 +7259,16 @@ export class BypassSpeedChanceAbAttr extends AbAttr { * @param {Pokemon} _pokemon {@linkcode Pokemon} the Pokemon applying this ability * @param {boolean} _passive N/A * @param {BooleanHolder} _cancelled N/A - * @param {any[]} args [0] {@linkcode BooleanHolder} set to true when the ability activated + * @param {any[]} _args N/A */ override apply( - _pokemon: Pokemon, + pokemon: Pokemon, _passive: boolean, _simulated: boolean, _cancelled: BooleanHolder, - args: any[], + _args: any[], ): void { - const bypassSpeed = args[0] as BooleanHolder; - bypassSpeed.value = true; + pokemon.addTag(BattlerTagType.BYPASS_SPEED); } getTriggerMessage(pokemon: Pokemon, _abilityName: string, ..._args: any[]): string { @@ -7270,7 +7300,6 @@ export class PreventBypassSpeedChanceAbAttr extends AbAttr { /** * @argument {boolean} bypassSpeed - determines if a Pokemon is able to bypass speed at the moment - * @argument {boolean} canCheckHeldItems - determines if a Pokemon has access to Quick Claw's effects or not */ override apply( _pokemon: Pokemon, @@ -7280,9 +7309,7 @@ export class PreventBypassSpeedChanceAbAttr extends AbAttr { args: any[], ): void { const bypassSpeed = args[0] as BooleanHolder; - const canCheckHeldItems = args[1] as BooleanHolder; bypassSpeed.value = false; - canCheckHeldItems.value = false; } } @@ -7814,6 +7841,7 @@ const AbilityAttrs = Object.freeze({ BlockStatusDamageAbAttr, BlockOneHitKOAbAttr, ChangeMovePriorityAbAttr, + ChangeMovePriorityModifierAbAttr, IgnoreContactAbAttr, PreWeatherEffectAbAttr, PreWeatherDamageAbAttr, @@ -8230,7 +8258,7 @@ export function initAbilities() { .attr(AlwaysHitAbAttr) .attr(DoubleBattleChanceAbAttr), new Ability(AbilityId.STALL, 4) - .attr(ChangeMovePriorityAbAttr, (_pokemon, _move: Move) => true, -0.2), + .attr(ChangeMovePriorityModifierAbAttr, (_pokemon, _move: Move) => true, MovePriorityModifier.LAST_IN_BRACKET), new Ability(AbilityId.TECHNICIAN, 4) .attr(MovePowerBoostAbAttr, (user, target, move) => { const power = new NumberHolder(move.power); @@ -8957,7 +8985,7 @@ export function initAbilities() { .attr(TypeImmunityHealAbAttr, PokemonType.GROUND) .ignorable(), new Ability(AbilityId.MYCELIUM_MIGHT, 9) - .attr(ChangeMovePriorityAbAttr, (_pokemon, move) => move.category === MoveCategory.STATUS, -0.2) + .attr(ChangeMovePriorityModifierAbAttr, (_pokemon, move) => move.category === MoveCategory.STATUS, MovePriorityModifier.LAST_IN_BRACKET) .attr(PreventBypassSpeedChanceAbAttr, (_pokemon, move) => move.category === MoveCategory.STATUS) .attr(MoveAbilityBypassAbAttr, (_pokemon, move: Move) => move.category === MoveCategory.STATUS), new Ability(AbilityId.MINDS_EYE, 9) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 89d5a76159f..8d817fffff3 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -3421,6 +3421,23 @@ export class GrudgeTag extends BattlerTag { } } +/** + * Tag to allow the affected Pokemon's move to go first in its priority bracket. + * Used for {@link https://bulbapedia.bulbagarden.net/wiki/Quick_Draw_(Ability) Quick Draw} + * and {@link https://bulbapedia.bulbagarden.net/wiki/Quick_Claw Quick Claw}. + */ +export class BypassSpeedTag extends BattlerTag { + constructor() { + super(BattlerTagType.BYPASS_SPEED, BattlerTagLapseType.TURN_END, 1); + } + + override canAdd(pokemon: Pokemon): boolean { + const cancelled = new BooleanHolder(false); + applyAbAttrs("PreventBypassSpeedChanceAbAttr", pokemon, null, false, cancelled); + return !cancelled.value; + } +} + /** * Tag used to heal the user of Psycho Shift of its status effect if Psycho Shift succeeds in transferring its status effect to the target Pokemon */ @@ -3668,6 +3685,8 @@ export function getBattlerTag( return new PsychoShiftTag(); case BattlerTagType.MAGIC_COAT: return new MagicCoatTag(); + case BattlerTagType.BYPASS_SPEED: + return new BypassSpeedTag(); case BattlerTagType.NONE: default: return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId); diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index e713020cf9c..7c3bb26161b 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -100,6 +100,7 @@ import { SelectBiomePhase } from "#app/phases/select-biome-phase"; import { ChargingMove, MoveAttrMap, MoveAttrString, MoveKindString, MoveClassMap } from "#app/@types/move-types"; import { applyMoveAttrs } from "./apply-attrs"; import { frenzyMissFunc, getMoveTargets } from "./move-utils"; +import { MovePriorityModifier } from "#enums/move-priority-modifier"; type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean; export type UserMoveConditionFunc = (user: Pokemon, move: Move) => boolean; @@ -867,6 +868,13 @@ export default abstract class Move implements Localizable { return priority.value; } + getPriorityModifier(user: Pokemon, simulated = true): MovePriorityModifier { + const modifierHolder = new NumberHolder(MovePriorityModifier.NORMAL); + applyAbAttrs("ChangeMovePriorityModifierAbAttr", user, null, simulated, this, modifierHolder); + modifierHolder.value = user.getTag(BattlerTagType.BYPASS_SPEED) ? MovePriorityModifier.FIRST_IN_BRACKET : modifierHolder.value; + return modifierHolder.value; + } + /** * Calculate the [Expected Power](https://en.wikipedia.org/wiki/Expected_value) per turn * of this move, taking into account multi hit moves, accuracy, and the number of turns it diff --git a/src/data/phase-priority-queue.ts b/src/data/phase-priority-queue.ts deleted file mode 100644 index b815a6ac34f..00000000000 --- a/src/data/phase-priority-queue.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { globalScene } from "#app/global-scene"; -import type { Phase } from "#app/phase"; -import { ActivatePriorityQueuePhase } from "#app/phases/activate-priority-queue-phase"; -import type { PostSummonPhase } from "#app/phases/post-summon-phase"; -import { PostSummonActivateAbilityPhase } from "#app/phases/post-summon-activate-ability-phase"; -import { Stat } from "#enums/stat"; -import { BooleanHolder } from "#app/utils/common"; -import { TrickRoomTag } from "#app/data/arena-tag"; -import { DynamicPhaseType } from "#enums/dynamic-phase-type"; - -/** - * 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); - } - - /** - * Removes all phases from the queue - */ - public clear(): void { - this.queue.splice(0, this.queue.length); - } -} - -/** - * 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)) * - (isTrickRoom() ? -1 : 1) - ); - } - - 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, idx) => { - this.queue.push(new PostSummonActivateAbilityPhase(phasePokemon.getBattlerIndex(), priority, !!idx)); - globalScene.phaseManager.appendToPhase( - new ActivatePriorityQueuePhase(DynamicPhaseType.POST_SUMMON), - "ActivatePriorityQueuePhase", - (p: ActivatePriorityQueuePhase) => p.getType() === DynamicPhaseType.POST_SUMMON, - ); - }); - } -} - -function isTrickRoom(): boolean { - const speedReversed = new BooleanHolder(false); - globalScene.arena.applyTags(TrickRoomTag, false, speedReversed); - return speedReversed.value; -} diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index 719b08c5b81..b7482bc9fb5 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -95,4 +95,5 @@ export enum BattlerTagType { ENDURE_TOKEN = "ENDURE_TOKEN", POWDER = "POWDER", MAGIC_COAT = "MAGIC_COAT", + BYPASS_SPEED = "BYPASS_SPEED" } diff --git a/src/enums/move-priority-modifier.ts b/src/enums/move-priority-modifier.ts new file mode 100644 index 00000000000..60d00ab2862 --- /dev/null +++ b/src/enums/move-priority-modifier.ts @@ -0,0 +1,5 @@ +export enum MovePriorityModifier { + LAST_IN_BRACKET = 0, + NORMAL, + FIRST_IN_BRACKET, +} \ No newline at end of file diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index e11f2c07ce8..687c789cad3 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -12,7 +12,6 @@ import { getPokemonNameWithAffix } from "#app/messages"; import Overrides from "#app/overrides"; import { LearnMoveType } from "#enums/learn-move-type"; import type { VoucherType } from "#app/system/voucher"; -import { Command } from "#enums/command"; import { addTextObject, TextStyle } from "#app/ui/text"; import { BooleanHolder, hslToHex, isNullOrUndefined, NumberHolder, randSeedFloat, toDmgValue } from "#app/utils/common"; import { BattlerTagType } from "#enums/battler-tag-type"; @@ -1588,30 +1587,16 @@ export class BypassSpeedChanceModifier extends PokemonHeldItemModifier { return new BypassSpeedChanceModifier(this.type, this.pokemonId, this.stackCount); } - /** - * Checks if {@linkcode BypassSpeedChanceModifier} should be applied - * @param pokemon the {@linkcode Pokemon} that holds the item - * @param doBypassSpeed {@linkcode BooleanHolder} that is `true` if speed should be bypassed - * @returns `true` if {@linkcode BypassSpeedChanceModifier} should be applied - */ - override shouldApply(pokemon?: Pokemon, doBypassSpeed?: BooleanHolder): boolean { - return super.shouldApply(pokemon, doBypassSpeed) && !!doBypassSpeed; - } - /** * Applies {@linkcode BypassSpeedChanceModifier} * @param pokemon the {@linkcode Pokemon} that holds the item - * @param doBypassSpeed {@linkcode BooleanHolder} that is `true` if speed should be bypassed * @returns `true` if {@linkcode BypassSpeedChanceModifier} has been applied */ - override apply(pokemon: Pokemon, doBypassSpeed: BooleanHolder): boolean { - if (!doBypassSpeed.value && pokemon.randBattleSeedInt(10) < this.getStackCount()) { - doBypassSpeed.value = true; - const isCommandFight = - globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()]?.command === Command.FIGHT; + override apply(pokemon: Pokemon): boolean { + if (pokemon.randBattleSeedInt(10) < this.getStackCount() && pokemon.addTag(BattlerTagType.BYPASS_SPEED)) { const hasQuickClaw = this.type.is("PokemonHeldItemModifierType") && this.type.id === "QUICK_CLAW"; - if (isCommandFight && hasQuickClaw) { + if (hasQuickClaw) { globalScene.phaseManager.queueMessage( i18next.t("modifier:bypassSpeedChanceApply", { pokemonName: getPokemonNameWithAffix(pokemon), diff --git a/src/queues/move-phase-priority-queue.ts b/src/queues/move-phase-priority-queue.ts new file mode 100644 index 00000000000..142271424b3 --- /dev/null +++ b/src/queues/move-phase-priority-queue.ts @@ -0,0 +1,28 @@ +import type { MovePhase } from "#app/phases/move-phase"; +import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue"; + +export class MovePhasePriorityQueue extends PokemonPhasePriorityQueue { + public override reorder(): void { + super.reorder(); + this.sortPostSpeed(); + } + + private sortPostSpeed(): void { + this.queue.sort((a: MovePhase, b: MovePhase) => { + const priority = [a, b].map(movePhase => { + const move = movePhase.move.getMove(); + return move.getPriority(movePhase.pokemon, true); + }); + + const priorityModifiers = [a, b].map(movePhase => + movePhase.move.getMove().getPriorityModifier(movePhase.pokemon), + ); + + if (priority[0] === priority[1] && priorityModifiers[0] !== priorityModifiers[1]) { + return priorityModifiers[0] - priorityModifiers[1]; + } + + return priority[0] - priority[1]; + }); + } +} diff --git a/src/queues/phase-priority-queue.ts b/src/queues/phase-priority-queue.ts new file mode 100644 index 00000000000..8ace2e6af59 --- /dev/null +++ b/src/queues/phase-priority-queue.ts @@ -0,0 +1,43 @@ +import type { Phase } from "#app/phase"; + +/** + * 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 queue: T[] = []; + + /** + * 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(): T | undefined { + this.reorder(); + return this.queue.shift(); + } + + /** + * Adds a phase to the queue + * @param phase The phase to add + */ + public push(phase: T): void { + this.queue.push(phase); + } + + /** + * Removes all phases from the queue + */ + public clear(): void { + this.queue.splice(0, this.queue.length); + } + + public isEmpty(): boolean { + return !this.queue.length; + } +} diff --git a/src/queues/pokemon-phase-priority-queue.ts b/src/queues/pokemon-phase-priority-queue.ts new file mode 100644 index 00000000000..cabba068001 --- /dev/null +++ b/src/queues/pokemon-phase-priority-queue.ts @@ -0,0 +1,10 @@ +import type { PartyMemberPokemonPhase } from "#app/phases/party-member-pokemon-phase"; +import type { PokemonPhase } from "#app/phases/pokemon-phase"; +import { PhasePriorityQueue } from "#app/queues/phase-priority-queue"; +import { sortInSpeedOrder } from "#app/utils/speed-order"; + +export class PokemonPhasePriorityQueue extends PhasePriorityQueue { + public override reorder(): void { + this.queue = sortInSpeedOrder(this.queue); + } +} diff --git a/src/queues/post-summon-phase-priority-queue.ts b/src/queues/post-summon-phase-priority-queue.ts new file mode 100644 index 00000000000..8843e5514a7 --- /dev/null +++ b/src/queues/post-summon-phase-priority-queue.ts @@ -0,0 +1,42 @@ +import { globalScene } from "#app/global-scene"; +import { ActivatePriorityQueuePhase } from "#app/phases/activate-priority-queue-phase"; +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"; + +/** + * 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 PokemonPhasePriorityQueue { + public override reorder(): void { + super.reorder(); + this.queue.sort((phaseA: PostSummonPhase, phaseB: PostSummonPhase) => { + 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, idx) => { + this.queue.push(new PostSummonActivateAbilityPhase(phasePokemon.getBattlerIndex(), priority, !!idx)); + globalScene.phaseManager.appendToPhase( + new ActivatePriorityQueuePhase("PostSummonPhase"), + "ActivatePriorityQueuePhase", + (p: ActivatePriorityQueuePhase) => p.getType() === "PostSummonPhase", + ); + }); + } +} diff --git a/src/utils/speed-order.ts b/src/utils/speed-order.ts new file mode 100644 index 00000000000..7916a471159 --- /dev/null +++ b/src/utils/speed-order.ts @@ -0,0 +1,46 @@ +import Pokemon from "#app/field/pokemon"; +import { globalScene } from "#app/global-scene"; +import { BooleanHolder, randSeedShuffle } from "#app/utils/common"; +import { ArenaTagType } from "#enums/arena-tag-type"; +import { Stat } from "#enums/stat"; + +export interface hasPokemon { + getPokemon(): Pokemon; +} + +export function sortInSpeedOrder(pokemonList: T[]): T[] { + pokemonList = shuffle(pokemonList); + sortBySpeed(pokemonList); + return pokemonList; +} + +/** Randomly shuffles the queue. */ +function shuffle(pokemonList: T[]): T[] { + // This is seeded with the current turn to prevent an inconsistency where it + // was varying based on how long since you last reloaded + globalScene.executeWithSeedOffset( + () => { + pokemonList = randSeedShuffle(pokemonList); + }, + globalScene.currentBattle.turn * 1000 + pokemonList.length, + globalScene.waveSeed, + ); + return pokemonList; +} + +function sortBySpeed(pokemonList: T[]): void { + pokemonList.sort((a, b) => { + const [aSpeed, bSpeed] = [a, b].map(pkmn => + pkmn instanceof Pokemon ? pkmn.getEffectiveStat(Stat.SPD) : pkmn.getPokemon().getEffectiveStat(Stat.SPD), + ); + return bSpeed - aSpeed; + }); + + /** 'true' if Trick Room is on the field. */ + const speedReversed = new BooleanHolder(false); + globalScene.arena.applyTags(ArenaTagType.TRICK_ROOM, false, speedReversed); + + if (speedReversed.value) { + pokemonList.reverse(); + } +} From 0121589d6f9e49fc7a3f2ce47bf77e1dc47de453 Mon Sep 17 00:00:00 2001 From: Dean Date: Sat, 14 Jun 2025 17:40:18 -0700 Subject: [PATCH 02/28] Add dynamic queue manager --- src/field/pokemon.ts | 2 + src/phase-manager.ts | 57 ++++----------------- src/phases/activate-priority-queue-phase.ts | 10 ++-- src/phases/move-phase.ts | 6 +-- src/phases/turn-start-phase.ts | 2 +- src/queues/dynamic-queue-manager.ts | 35 +++++++++++++ 6 files changed, 57 insertions(+), 55 deletions(-) create mode 100644 src/queues/dynamic-queue-manager.ts diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 834c65437af..f12b2d44fb7 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -193,6 +193,7 @@ import { AiType } from "#enums/ai-type"; import type { MoveResult } from "#enums/move-result"; import { PokemonMove } from "#app/data/moves/pokemon-move"; import type { AbAttrMap, AbAttrString } from "#app/@types/ability-types"; +import type { TurnCommand } from "#app/battle"; /** Base typeclass for damage parameter methods, used for DRY */ type damageParams = { @@ -6868,6 +6869,7 @@ export class PokemonWaveData { * Resets at the start of a new turn, as well as on switch. */ export class PokemonTurnData { + public turnCommand?: TurnCommand; public flinched = false; public acted = false; /** How many times the current move should hit the target(s) */ diff --git a/src/phase-manager.ts b/src/phase-manager.ts index 8c22a45758c..5e5a5491db6 100644 --- a/src/phase-manager.ts +++ b/src/phase-manager.ts @@ -12,9 +12,8 @@ import { CheckStatusEffectPhase } from "#app/phases/check-status-effect-phase"; import { CheckSwitchPhase } from "#app/phases/check-switch-phase"; import { CommandPhase } from "#app/phases/command-phase"; import { CommonAnimPhase } from "#app/phases/common-anim-phase"; -import { coerceArray, type Constructor } from "#app/utils/common"; +import { coerceArray } from "#app/utils/common"; import { DamageAnimPhase } from "#app/phases/damage-anim-phase"; -import type { DynamicPhaseType } from "#enums/dynamic-phase-type"; import { EggHatchPhase } from "#app/phases/egg-hatch-phase"; import { EggLapsePhase } from "#app/phases/egg-lapse-phase"; import { EggSummaryPhase } from "#app/phases/egg-summary-phase"; @@ -58,7 +57,6 @@ import { NextEncounterPhase } from "#app/phases/next-encounter-phase"; import { ObtainStatusEffectPhase } from "#app/phases/obtain-status-effect-phase"; import { PartyExpPhase } from "#app/phases/party-exp-phase"; import { PartyHealPhase } from "#app/phases/party-heal-phase"; -import { type PhasePriorityQueue, PostSummonPhasePriorityQueue } from "#app/data/phase-priority-queue"; import { PokemonAnimPhase } from "#app/phases/pokemon-anim-phase"; import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; import { PokemonTransformPhase } from "#app/phases/pokemon-transform-phase"; @@ -99,6 +97,7 @@ import { UnavailablePhase } from "#app/phases/unavailable-phase"; 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"; /** * Manager for phases used by battle scene. @@ -227,19 +226,11 @@ export class PhaseManager { private phaseQueuePrependSpliceIndex = -1; 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 dynamicQueueManager = new DynamicQueueManager(); private currentPhase: Phase | null = null; private standbyPhase: Phase | null = null; - constructor() { - this.dynamicPhaseQueues = [new PostSummonPhasePriorityQueue()]; - this.dynamicPhaseTypes = [PostSummonPhase]; - } - /* Phase Functions */ getCurrentPhase(): Phase | null { return this.currentPhase; @@ -269,7 +260,7 @@ 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.getDynamicPhaseType(phase) !== undefined) { + if (this.dynamicQueueManager.isDynamicPhase(phase.phaseName)) { this.pushDynamicPhase(phase); } else { (!defer ? this.phaseQueue : this.nextCommandPhaseQueue).push(phase); @@ -302,7 +293,7 @@ export class PhaseManager { for (const queue of [this.phaseQueue, this.phaseQueuePrepend, this.conditionalQueue, this.nextCommandPhaseQueue]) { queue.splice(0, queue.length); } - this.dynamicPhaseQueues.forEach(queue => queue.clear()); + this.dynamicQueueManager.clearQueues(); this.currentPhase = null; this.standbyPhase = null; this.clearPhaseQueueSplice(); @@ -470,22 +461,6 @@ export class PhaseManager { return false; } - /** - * 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` - */ - 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} * @@ -493,21 +468,16 @@ export class PhaseManager { * @param phase The phase to push */ public pushDynamicPhase(phase: Phase): void { - const type = this.getDynamicPhaseType(phase); - if (type === undefined) { - return; - } - - this.pushPhase(new ActivatePriorityQueuePhase(type)); - this.dynamicPhaseQueues[type].push(phase); + this.pushNew("ActivatePriorityQueuePhase", phase.phaseName); + this.dynamicQueueManager.queueDynamicPhase(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(); + public startNextDynamicPhaseOfType(type: PhaseString): void { + const phase = this.dynamicQueueManager.popNextPhaseOfType(type); if (phase) { this.unshiftPhase(phase); } @@ -523,13 +493,8 @@ export class PhaseManager { * @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); + this.unshiftNew("ActivatePriorityQueuePhase", phase.phaseName); + this.dynamicQueueManager.queueDynamicPhase(phase); } /** diff --git a/src/phases/activate-priority-queue-phase.ts b/src/phases/activate-priority-queue-phase.ts index df42c491676..b58e13be2e1 100644 --- a/src/phases/activate-priority-queue-phase.ts +++ b/src/phases/activate-priority-queue-phase.ts @@ -1,23 +1,23 @@ -import type { DynamicPhaseType } from "#enums/dynamic-phase-type"; +import type { PhaseString } from "#app/@types/phase-types"; import { globalScene } from "#app/global-scene"; import { Phase } from "#app/phase"; export class ActivatePriorityQueuePhase extends Phase { public readonly phaseName = "ActivatePriorityQueuePhase"; - private type: DynamicPhaseType; + private readonly type: PhaseString; - constructor(type: DynamicPhaseType) { + constructor(type: PhaseString) { super(); this.type = type; } override start() { super.start(); - globalScene.phaseManager.startDynamicPhaseType(this.type); + globalScene.phaseManager.startNextDynamicPhaseOfType(this.type); this.end(); } - public getType(): DynamicPhaseType { + public getType(): PhaseString { return this.type; } } diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index d72c7396f1f..2c36b922a7b 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -19,7 +19,6 @@ import type Pokemon from "#app/field/pokemon"; import { MoveResult } from "#enums/move-result"; import { getPokemonNameWithAffix } from "#app/messages"; import Overrides from "#app/overrides"; -import { BattlePhase } from "#app/phases/battle-phase"; import { NumberHolder } from "#app/utils/common"; import { AbilityId } from "#enums/ability-id"; import { ArenaTagType } from "#enums/arena-tag-type"; @@ -28,8 +27,9 @@ import { MoveId } from "#enums/move-id"; 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"; -export class MovePhase extends BattlePhase { +export class MovePhase extends PokemonPhase { public readonly phaseName = "MovePhase"; protected _pokemon: Pokemon; protected _move: PokemonMove; @@ -81,7 +81,7 @@ export class MovePhase extends BattlePhase { reflected = false, forcedLast = false, ) { - super(); + super(pokemon.getBattlerIndex()); this.pokemon = pokemon; this.targets = targets; diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index 6f062cb5fbe..14e8ec906ec 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -69,7 +69,7 @@ export class TurnStartPhase extends FieldPhase { applyAbAttrs("BypassSpeedChanceAbAttr", p, null, false, bypassSpeed); applyAbAttrs("PreventBypassSpeedChanceAbAttr", p, null, false, bypassSpeed, canCheckHeldItems); if (canCheckHeldItems.value) { - globalScene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p, bypassSpeed); + globalScene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p); } battlerBypassSpeed[p.getBattlerIndex()] = bypassSpeed; }); diff --git a/src/queues/dynamic-queue-manager.ts b/src/queues/dynamic-queue-manager.ts new file mode 100644 index 00000000000..3eebf107ed3 --- /dev/null +++ b/src/queues/dynamic-queue-manager.ts @@ -0,0 +1,35 @@ +import type { PhaseString } from "#app/@types/phase-types"; +import type { Phase } from "#app/phase"; +import type { SwitchSummonPhase } from "#app/phases/switch-summon-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"; +import { PostSummonPhasePriorityQueue } from "#app/queues/post-summon-phase-priority-queue"; +export class DynamicQueueManager { + private dynamicPhaseMap: Map>; + + constructor() { + this.dynamicPhaseMap = new Map(); + this.dynamicPhaseMap.set("SwitchSummonPhase", new PokemonPhasePriorityQueue()); + this.dynamicPhaseMap.set("PostSummonPhase", new PostSummonPhasePriorityQueue()); + this.dynamicPhaseMap.set("MovePhase", new MovePhasePriorityQueue()); + } + + public clearQueues(): void { + for (const queue of this.dynamicPhaseMap.values()) { + queue.clear(); + } + } + + public queueDynamicPhase(phase: Phase): void { + this.dynamicPhaseMap.get(phase.phaseName)?.push(phase); + } + + public popNextPhaseOfType(type: PhaseString): Phase | undefined { + return this.dynamicPhaseMap.get(type)?.pop(); + } + + public isDynamicPhase(type: PhaseString): boolean { + return this.dynamicPhaseMap.has(type); + } +} From 2ea180e94c9b4e04d843f4b8e8055332cd723dde Mon Sep 17 00:00:00 2001 From: Dean Date: Sat, 14 Jun 2025 18:46:28 -0700 Subject: [PATCH 03/28] Add timing modifier and fix post speed ordering --- src/enums/move-phase-timing-modifier.ts | 5 +++++ src/queues/move-phase-priority-queue.ts | 22 +++++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 src/enums/move-phase-timing-modifier.ts diff --git a/src/enums/move-phase-timing-modifier.ts b/src/enums/move-phase-timing-modifier.ts new file mode 100644 index 00000000000..219def57466 --- /dev/null +++ b/src/enums/move-phase-timing-modifier.ts @@ -0,0 +1,5 @@ +export enum MovePhaseTimingModifier { + LAST = 0, + NORMAL, + FIRST +} \ No newline at end of file diff --git a/src/queues/move-phase-priority-queue.ts b/src/queues/move-phase-priority-queue.ts index 142271424b3..b751b4bb608 100644 --- a/src/queues/move-phase-priority-queue.ts +++ b/src/queues/move-phase-priority-queue.ts @@ -1,5 +1,8 @@ +import type { PhaseConditionFunc } from "#app/@types/phase-condition"; import type { MovePhase } from "#app/phases/move-phase"; import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue"; +import { isNullOrUndefined } from "#app/utils/common"; +import type { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; export class MovePhasePriorityQueue extends PokemonPhasePriorityQueue { public override reorder(): void { @@ -7,6 +10,13 @@ export class MovePhasePriorityQueue extends PokemonPhasePriorityQueue this.sortPostSpeed(); } + public setTimingModifier(condition: PhaseConditionFunc, modifier: MovePhaseTimingModifier): void { + const phase = this.queue.find(phase => condition(phase)); + if (!isNullOrUndefined(phase)) { + phase.timingModifier = modifier; + } + } + private sortPostSpeed(): void { this.queue.sort((a: MovePhase, b: MovePhase) => { const priority = [a, b].map(movePhase => { @@ -18,11 +28,17 @@ export class MovePhasePriorityQueue extends PokemonPhasePriorityQueue movePhase.move.getMove().getPriorityModifier(movePhase.pokemon), ); - if (priority[0] === priority[1] && priorityModifiers[0] !== priorityModifiers[1]) { - return priorityModifiers[0] - priorityModifiers[1]; + const timingModifiers = [a, b].map(movePhase => movePhase.timingModifier); + + if (timingModifiers[0] !== timingModifiers[1]) { + return timingModifiers[1] - timingModifiers[0]; } - return priority[0] - priority[1]; + if (priority[0] === priority[1] && priorityModifiers[0] !== priorityModifiers[1]) { + return priorityModifiers[1] - priorityModifiers[0]; + } + + return priority[1] - priority[0]; }); } } From 08090a8c148b6261f620d0c0619ece77d4b74114 Mon Sep 17 00:00:00 2001 From: Dean Date: Sat, 14 Jun 2025 18:47:50 -0700 Subject: [PATCH 04/28] Make phaseQueue private --- src/@types/phase-condition.ts | 3 ++ src/data/battler-tags.ts | 12 +------- src/data/moves/move.ts | 44 +++-------------------------- src/phase-manager.ts | 28 ++++++++++++++++-- src/phases/battle-end-phase.ts | 22 ++++----------- src/phases/move-phase.ts | 22 +++++++-------- src/phases/new-battle-phase.ts | 7 +---- src/queues/dynamic-queue-manager.ts | 11 ++++++++ src/queues/phase-priority-queue.ts | 5 ++++ test/moves/focus_punch.test.ts | 3 +- 10 files changed, 68 insertions(+), 89 deletions(-) create mode 100644 src/@types/phase-condition.ts diff --git a/src/@types/phase-condition.ts b/src/@types/phase-condition.ts new file mode 100644 index 00000000000..e2e58ad1293 --- /dev/null +++ b/src/@types/phase-condition.ts @@ -0,0 +1,3 @@ +import type { Phase } from "#app/phase"; + +export type PhaseConditionFunc = (phase: Phase) => boolean; diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 8d817fffff3..630388b805b 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -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; } diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 7c3bb26161b..d526e7c4f8b 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -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((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(); diff --git a/src/phase-manager.ts b/src/phase-manager.ts index 5e5a5491db6..73526cc2448 100644 --- a/src/phase-manager.ts +++ b/src/phase-manager.ts @@ -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); + } } diff --git a/src/phases/battle-end-phase.ts b/src/phases/battle-end-phase.ts index e1bf4c2296c..a7c52bad11f 100644 --- a/src/phases/battle-end-phase.ts +++ b/src/phases/battle-end-phase.ts @@ -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 ( diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 2c36b922a7b..32889bae07a 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -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(); diff --git a/src/phases/new-battle-phase.ts b/src/phases/new-battle-phase.ts index 65ecc81df2d..4d4d1311992 100644 --- a/src/phases/new-battle-phase.ts +++ b/src/phases/new-battle-phase.ts @@ -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(); diff --git a/src/queues/dynamic-queue-manager.ts b/src/queues/dynamic-queue-manager.ts index 3eebf107ed3..3123c0b712b 100644 --- a/src/queues/dynamic-queue-manager.ts +++ b/src/queues/dynamic-queue-manager.ts @@ -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>; @@ -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); + } } diff --git a/src/queues/phase-priority-queue.ts b/src/queues/phase-priority-queue.ts index 8ace2e6af59..00816318fcd 100644 --- a/src/queues/phase-priority-queue.ts +++ b/src/queues/phase-priority-queue.ts @@ -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 { public isEmpty(): boolean { return !this.queue.length; } + + public hasPhaseWithCondition(condition?: PhaseConditionFunc): boolean { + return this.queue.find(phase => !condition || condition(phase)) !== undefined; + } } diff --git a/test/moves/focus_punch.test.ts b/test/moves/focus_punch.test.ts index 38b57b201c0..a94ae6ccdd0 100644 --- a/test/moves/focus_punch.test.ts +++ b/test/moves/focus_punch.test.ts @@ -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]); From 46c709ffad643929f85f659b0095a7cfa586c1f2 Mon Sep 17 00:00:00 2001 From: Dean Date: Sat, 14 Jun 2025 19:19:45 -0700 Subject: [PATCH 05/28] Fix gameManager.setTurnOrder --- src/phase-manager.ts | 2 +- src/queues/dynamic-queue-manager.ts | 5 +++++ src/queues/move-phase-priority-queue.ts | 5 +++++ src/queues/pokemon-phase-priority-queue.ts | 12 +++++++++++- test/testUtils/gameManager.ts | 2 +- 5 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/phase-manager.ts b/src/phase-manager.ts index 73526cc2448..6f576c29b08 100644 --- a/src/phase-manager.ts +++ b/src/phase-manager.ts @@ -228,7 +228,7 @@ export class PhaseManager { private phaseQueuePrependSpliceIndex = -1; private nextCommandPhaseQueue: Phase[] = []; - private dynamicQueueManager = new DynamicQueueManager(); + public dynamicQueueManager = new DynamicQueueManager(); private currentPhase: Phase | null = null; private standbyPhase: Phase | null = null; diff --git a/src/queues/dynamic-queue-manager.ts b/src/queues/dynamic-queue-manager.ts index 3123c0b712b..75680074fe1 100644 --- a/src/queues/dynamic-queue-manager.ts +++ b/src/queues/dynamic-queue-manager.ts @@ -6,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 { BattlerIndex } from "#enums/battler-index"; import type { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; export class DynamicQueueManager { private dynamicPhaseMap: Map>; @@ -43,4 +44,8 @@ export class DynamicQueueManager { const movePhaseQueue: MovePhasePriorityQueue = this.dynamicPhaseMap.get("MovePhase") as MovePhasePriorityQueue; movePhaseQueue.setTimingModifier(condition, modifier); } + + public setMoveOrder(order: BattlerIndex[]) { + (this.dynamicPhaseMap.get("MovePhase") as MovePhasePriorityQueue).setMoveOrder(order); + } } diff --git a/src/queues/move-phase-priority-queue.ts b/src/queues/move-phase-priority-queue.ts index b751b4bb608..cbd70d864c7 100644 --- a/src/queues/move-phase-priority-queue.ts +++ b/src/queues/move-phase-priority-queue.ts @@ -2,6 +2,7 @@ import type { PhaseConditionFunc } from "#app/@types/phase-condition"; import type { MovePhase } from "#app/phases/move-phase"; import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue"; import { isNullOrUndefined } from "#app/utils/common"; +import type { BattlerIndex } from "#enums/battler-index"; import type { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; export class MovePhasePriorityQueue extends PokemonPhasePriorityQueue { @@ -17,6 +18,10 @@ export class MovePhasePriorityQueue extends PokemonPhasePriorityQueue } } + public setMoveOrder(order: BattlerIndex[]) { + this.setOrder = order; + } + private sortPostSpeed(): void { this.queue.sort((a: MovePhase, b: MovePhase) => { const priority = [a, b].map(movePhase => { diff --git a/src/queues/pokemon-phase-priority-queue.ts b/src/queues/pokemon-phase-priority-queue.ts index cabba068001..4dab3ffa6c3 100644 --- a/src/queues/pokemon-phase-priority-queue.ts +++ b/src/queues/pokemon-phase-priority-queue.ts @@ -2,9 +2,19 @@ import type { PartyMemberPokemonPhase } from "#app/phases/party-member-pokemon-p import type { PokemonPhase } from "#app/phases/pokemon-phase"; 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 { + protected setOrder: BattlerIndex[] | undefined; public override reorder(): void { - this.queue = sortInSpeedOrder(this.queue); + if (this.setOrder) { + this.queue.sort( + (a, b) => + this.setOrder!.indexOf(a.getPokemon().getBattlerIndex()) - + this.setOrder!.indexOf(b.getPokemon().getBattlerIndex()), + ); + } else { + this.queue = sortInSpeedOrder(this.queue); + } } } diff --git a/test/testUtils/gameManager.ts b/test/testUtils/gameManager.ts index 5d3ed3b6c8c..b1822a95856 100644 --- a/test/testUtils/gameManager.ts +++ b/test/testUtils/gameManager.ts @@ -531,7 +531,7 @@ export default class GameManager { async setTurnOrder(order: BattlerIndex[]): Promise { await this.phaseInterceptor.to(TurnStartPhase, false); - vi.spyOn(this.scene.phaseManager.getCurrentPhase() as TurnStartPhase, "getSpeedOrder").mockReturnValue(order); + this.scene.phaseManager.dynamicQueueManager.setMoveOrder(order); } /** From 30ab803df9f35c8f9925047a94a4745b3ccdf9de Mon Sep 17 00:00:00 2001 From: Dean Date: Sat, 14 Jun 2025 21:47:10 -0700 Subject: [PATCH 06/28] Update findPhase to also check dynamic queues --- src/battle-scene.ts | 2 +- src/data/abilities/ability.ts | 2 +- src/data/battler-tags.ts | 2 +- src/data/moves/move.ts | 9 ++++----- src/phase-manager.ts | 20 +++++++++++++------- src/phases/egg-hatch-phase.ts | 2 +- src/phases/mystery-encounter-phases.ts | 2 +- src/phases/quiet-form-change-phase.ts | 5 +---- src/phases/stat-stage-change-phase.ts | 7 ++++--- src/queues/dynamic-queue-manager.ts | 4 ++++ src/queues/phase-priority-queue.ts | 4 ++++ 11 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 81c65d85e06..a3e51a734ac 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -861,8 +861,8 @@ export default class BattleScene extends SceneBase { let targetingMovePhase: MovePhase; do { targetingMovePhase = this.phaseManager.findPhase( + "MovePhase", mp => - mp.is("MovePhase") && mp.targets.length === 1 && mp.targets[0] === removedPokemon.getBattlerIndex() && mp.pokemon.isPlayer() !== allyPokemon.isPlayer(), diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 77aa74b90e4..066b8f538fb 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -8407,7 +8407,7 @@ export function initAbilities() { .ignorable(), new Ability(AbilityId.ANALYTIC, 5) .attr(MovePowerBoostAbAttr, (user, _target, _move) => { - const movePhase = globalScene.phaseManager.findPhase((phase) => phase.is("MovePhase") && phase.pokemon.id !== user?.id); + const movePhase = globalScene.phaseManager.findPhase("MovePhase", (phase) => phase.is("MovePhase") && phase.pokemon.id !== user?.id); return isNullOrUndefined(movePhase); }, 1.3), new Ability(AbilityId.ILLUSION, 5) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 630388b805b..2cbe960ddff 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1151,7 +1151,7 @@ export class EncoreTag extends MoveRestrictionBattlerTag { }), ); - const movePhase = globalScene.phaseManager.findPhase(m => m.is("MovePhase") && m.pokemon === pokemon); + const movePhase = globalScene.phaseManager.findPhase("MovePhase", (m: MovePhase) => m.pokemon === pokemon); if (movePhase) { const movesetMove = pokemon.getMoveset().find(m => m.moveId === this.moveId); if (movesetMove) { diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index d526e7c4f8b..ec92aadb504 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -3171,7 +3171,7 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr { const overridden = args[0] as BooleanHolder; - const allyMovePhase = globalScene.phaseManager.findPhase((phase) => phase.is("MovePhase") && phase.pokemon.isPlayer() === user.isPlayer()); + const allyMovePhase = globalScene.phaseManager.findPhase("MovePhase", (phase) => phase.pokemon.isPlayer() === user.isPlayer()); if (allyMovePhase) { const allyMove = allyMovePhase.move.getMove(); if (allyMove !== move && allyMove.hasAttr("AwaitCombinedPledgeAttr")) { @@ -4534,8 +4534,7 @@ export class CueNextRoundAttr extends MoveEffectAttr { } override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean { - const nextRoundPhase = globalScene.phaseManager.findPhase(phase => - phase.is("MovePhase") && phase.move.moveId === MoveId.ROUND + const nextRoundPhase = globalScene.phaseManager.findPhase("MovePhase", phase => phase.move.moveId === MoveId.ROUND ); if (!nextRoundPhase) { @@ -6217,7 +6216,7 @@ export class RevivalBlessingAttr extends MoveEffectAttr { globalScene.phaseManager.tryRemovePhase((phase: SwitchSummonPhase) => phase.is("SwitchSummonPhase") && phase.getPokemon() === pokemon); // If the pokemon being revived was alive earlier in the turn, cancel its move // (revived pokemon can't move in the turn they're brought back) - globalScene.phaseManager.findPhase((phase: MovePhase) => phase.pokemon === pokemon)?.cancel(); + globalScene.phaseManager.findPhase("MovePhase", (phase: MovePhase) => phase.pokemon === pokemon)?.cancel(); if (user.fieldPosition === FieldPosition.CENTER) { user.setFieldPosition(FieldPosition.LEFT); } @@ -7887,7 +7886,7 @@ export class AfterYouAttr extends MoveEffectAttr { globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:afterYou", { targetName: getPokemonNameWithAffix(target) })); //Will find next acting phase of the targeted pokémon, delete it and queue it next on successful delete. - const nextAttackPhase = globalScene.phaseManager.findPhase((phase) => phase.pokemon === target); + const nextAttackPhase = globalScene.phaseManager.findPhase("MovePhase", (phase) => phase.pokemon === target); if (nextAttackPhase && globalScene.phaseManager.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) { globalScene.phaseManager.prependNewToPhase("MovePhase", "MovePhase", target, [ ...nextAttackPhase.targets ], nextAttackPhase.move); } diff --git a/src/phase-manager.ts b/src/phase-manager.ts index 6f576c29b08..d38dd9fdbea 100644 --- a/src/phase-manager.ts +++ b/src/phase-manager.ts @@ -394,15 +394,21 @@ export class PhaseManager { /** * Find a specific {@linkcode Phase} in the phase queue. - * + * @param phaseType - A {@linkcode PhaseString} representing which type to search for * @param phaseFilter filter function to use to find the wanted phase * @returns the found phase or undefined if none found */ - findPhase

(phaseFilter: (phase: P) => boolean): P | undefined { - return this.phaseQueue.find(phaseFilter) as P; + findPhase

( + phaseType: P, + phaseFilter?: (phase: PhaseMap[P]) => boolean, + ): PhaseMap[P] | undefined { + if (this.dynamicQueueManager.isDynamicPhase(phaseType)) { + return this.dynamicQueueManager.findPhaseOfType(phaseType, phaseFilter) as PhaseMap[P]; + } + return this.phaseQueue.find(phase => phase.is(phaseType) && (!phaseFilter || phaseFilter(phase))) as PhaseMap[P]; } - tryReplacePhase(phaseFilter: (phase: Phase) => boolean, phase: Phase): boolean { + tryReplacePhase(phaseFilter: PhaseConditionFunc, phase: Phase): boolean { const phaseIndex = this.phaseQueue.findIndex(phaseFilter); if (phaseIndex > -1) { this.phaseQueue[phaseIndex] = phase; @@ -411,7 +417,7 @@ export class PhaseManager { return false; } - tryRemovePhase(phaseFilter: (phase: Phase) => boolean): boolean { + tryRemovePhase(phaseFilter: PhaseConditionFunc): boolean { const phaseIndex = this.phaseQueue.findIndex(phaseFilter); if (phaseIndex > -1) { this.phaseQueue.splice(phaseIndex, 1); @@ -424,7 +430,7 @@ export class PhaseManager { * Will search for a specific phase in {@linkcode phaseQueuePrepend} via filter, and remove the first result if a match is found. * @param phaseFilter filter function */ - tryRemoveUnshiftedPhase(phaseFilter: (phase: Phase) => boolean): boolean { + tryRemoveUnshiftedPhase(phaseFilter: PhaseConditionFunc): boolean { const phaseIndex = this.phaseQueuePrepend.findIndex(phaseFilter); if (phaseIndex > -1) { this.phaseQueuePrepend.splice(phaseIndex, 1); @@ -464,7 +470,7 @@ export class PhaseManager { * @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: PhaseString, condition?: (p: Phase) => boolean): boolean { + appendToPhase(phase: Phase | Phase[], targetPhase: PhaseString, condition?: PhaseConditionFunc): boolean { phase = coerceArray(phase); const target = PHASES[targetPhase]; const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof target && (!condition || condition(ph))); diff --git a/src/phases/egg-hatch-phase.ts b/src/phases/egg-hatch-phase.ts index d6c40a1510e..10beee1f222 100644 --- a/src/phases/egg-hatch-phase.ts +++ b/src/phases/egg-hatch-phase.ts @@ -225,7 +225,7 @@ export class EggHatchPhase extends Phase { } end() { - if (globalScene.phaseManager.findPhase(p => p.is("EggHatchPhase"))) { + if (globalScene.phaseManager.findPhase("EggHatchPhase")) { this.eggHatchHandler.clear(); } else { globalScene.time.delayedCall(250, () => globalScene.setModifiersVisible(true)); diff --git a/src/phases/mystery-encounter-phases.ts b/src/phases/mystery-encounter-phases.ts index 9aae796211f..14b961deb73 100644 --- a/src/phases/mystery-encounter-phases.ts +++ b/src/phases/mystery-encounter-phases.ts @@ -237,7 +237,7 @@ export class MysteryEncounterBattleStartCleanupPhase extends Phase { }); // Remove any status tick phases - while (globalScene.phaseManager.findPhase(p => p.is("PostTurnStatusEffectPhase"))) { + while (globalScene.phaseManager.findPhase("PostTurnStatusEffectPhase")) { globalScene.phaseManager.tryRemovePhase(p => p.is("PostTurnStatusEffectPhase")); } diff --git a/src/phases/quiet-form-change-phase.ts b/src/phases/quiet-form-change-phase.ts index e6a00c73756..388ca1fa1fd 100644 --- a/src/phases/quiet-form-change-phase.ts +++ b/src/phases/quiet-form-change-phase.ts @@ -11,7 +11,6 @@ import { BattlerTagType } from "#app/enums/battler-tag-type"; import type Pokemon from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { BattlePhase } from "./battle-phase"; -import type { MovePhase } from "./move-phase"; import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs"; export class QuietFormChangePhase extends BattlePhase { @@ -172,9 +171,7 @@ export class QuietFormChangePhase extends BattlePhase { this.pokemon.initBattleInfo(); this.pokemon.cry(); - const movePhase = globalScene.phaseManager.findPhase( - p => p.is("MovePhase") && p.pokemon === this.pokemon, - ) as MovePhase; + const movePhase = globalScene.phaseManager.findPhase("MovePhase", p => p.pokemon === this.pokemon); if (movePhase) { movePhase.cancel(); } diff --git a/src/phases/stat-stage-change-phase.ts b/src/phases/stat-stage-change-phase.ts index e73f72f7a63..7370f5f63b6 100644 --- a/src/phases/stat-stage-change-phase.ts +++ b/src/phases/stat-stage-change-phase.ts @@ -236,7 +236,8 @@ export class StatStageChangePhase extends PokemonPhase { // Look for any other stat change phases; if this is the last one, do White Herb check const existingPhase = globalScene.phaseManager.findPhase( - p => p.is("StatStageChangePhase") && p.battlerIndex === this.battlerIndex, + "StatStageChangePhase", + p => p.battlerIndex === this.battlerIndex, ); if (!existingPhase?.is("StatStageChangePhase")) { // Apply White Herb if needed @@ -316,8 +317,8 @@ export class StatStageChangePhase extends PokemonPhase { if (this.stats.length === 1) { while ( (existingPhase = globalScene.phaseManager.findPhase( + "StatStageChangePhase", p => - p.is("StatStageChangePhase") && p.battlerIndex === this.battlerIndex && p.stats.length === 1 && p.stats[0] === this.stats[0] && @@ -335,8 +336,8 @@ export class StatStageChangePhase extends PokemonPhase { } while ( (existingPhase = globalScene.phaseManager.findPhase( + "StatStageChangePhase", p => - p.is("StatStageChangePhase") && p.battlerIndex === this.battlerIndex && p.selfTarget === this.selfTarget && accEva.some(s => p.stats.includes(s)) === isAccEva && diff --git a/src/queues/dynamic-queue-manager.ts b/src/queues/dynamic-queue-manager.ts index 75680074fe1..e0b5ab3df6c 100644 --- a/src/queues/dynamic-queue-manager.ts +++ b/src/queues/dynamic-queue-manager.ts @@ -36,6 +36,10 @@ export class DynamicQueueManager { return this.dynamicPhaseMap.has(type); } + public findPhaseOfType(type: PhaseString, condition?: PhaseConditionFunc): Phase | undefined { + return this.dynamicPhaseMap.get(type)?.findPhase(condition); + } + public exists(type: PhaseString, condition?: PhaseConditionFunc): boolean { return !!this.dynamicPhaseMap.get(type)?.hasPhaseWithCondition(condition); } diff --git a/src/queues/phase-priority-queue.ts b/src/queues/phase-priority-queue.ts index 00816318fcd..2ca6a077154 100644 --- a/src/queues/phase-priority-queue.ts +++ b/src/queues/phase-priority-queue.ts @@ -42,6 +42,10 @@ export abstract class PhasePriorityQueue { return !this.queue.length; } + public findPhase(condition?: PhaseConditionFunc): Phase | undefined { + return this.queue.find(phase => !condition || condition(phase)); + } + public hasPhaseWithCondition(condition?: PhaseConditionFunc): boolean { return this.queue.find(phase => !condition || condition(phase)) !== undefined; } From 70edead47eec1fe43280ce66acafa8fb25f75eee Mon Sep 17 00:00:00 2001 From: Dean Date: Sun, 15 Jun 2025 11:17:39 -0700 Subject: [PATCH 07/28] Modify existing phase manager methods to check dyanmic queues --- src/@types/phase-types.ts | 4 ++ src/data/battler-tags.ts | 13 ++--- src/data/moves/move.ts | 12 ++--- src/enums/dynamic-phase-type.ts | 6 --- src/phase-manager.ts | 49 +++++++++++++------ src/phases/activate-priority-queue-phase.ts | 2 +- src/phases/move-phase.ts | 5 +- src/phases/party-member-pokemon-phase.ts | 4 ++ src/phases/summon-phase.ts | 4 ++ src/queues/dynamic-queue-manager.ts | 24 +++++++-- src/queues/move-phase-priority-queue.ts | 8 +++ src/queues/phase-priority-queue.ts | 9 ++++ .../switch-summon-phase-priority-queue.ts | 14 ++++++ 13 files changed, 105 insertions(+), 49 deletions(-) delete mode 100644 src/enums/dynamic-phase-type.ts create mode 100644 src/queues/switch-summon-phase-priority-queue.ts diff --git a/src/@types/phase-types.ts b/src/@types/phase-types.ts index 1d68c7921dd..dab8bfeea12 100644 --- a/src/@types/phase-types.ts +++ b/src/@types/phase-types.ts @@ -23,3 +23,7 @@ export type PhaseClass = PhaseConstructorMap[keyof PhaseConstructorMap]; * Union type of all phase names as strings. */ export type PhaseString = keyof PhaseMap; + +export type DynamicPhaseString = "PostSummonPhase" | "SwitchSummonPhase" | "MovePhase"; + +export type StaticPhaseString = Exclude; diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 2cbe960ddff..42643183eca 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1151,16 +1151,9 @@ export class EncoreTag extends MoveRestrictionBattlerTag { }), ); - const movePhase = globalScene.phaseManager.findPhase("MovePhase", (m: MovePhase) => m.pokemon === pokemon); - if (movePhase) { - const movesetMove = pokemon.getMoveset().find(m => m.moveId === this.moveId); - if (movesetMove) { - const lastMove = pokemon.getLastXMoves(1)[0]; - globalScene.phaseManager.tryReplacePhase( - m => m.is("MovePhase") && m.pokemon === pokemon, - globalScene.phaseManager.create("MovePhase", pokemon, lastMove.targets ?? [], movesetMove), - ); - } + const movesetMove = pokemon.getMoveset().find(m => m.moveId === this.moveId); + if (movesetMove) { + globalScene.phaseManager.changePhaseMove((phase: MovePhase) => phase.pokemon === pokemon, movesetMove); } } diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index ec92aadb504..2261f959f58 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -101,6 +101,7 @@ import { ChargingMove, MoveAttrMap, MoveAttrString, MoveKindString, MoveClassMap import { applyMoveAttrs } from "./apply-attrs"; import { frenzyMissFunc, getMoveTargets } from "./move-utils"; import { MovePriorityModifier } from "#enums/move-priority-modifier"; +import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean; export type UserMoveConditionFunc = (user: Pokemon, move: Move) => boolean; @@ -6212,8 +6213,6 @@ export class RevivalBlessingAttr extends MoveEffectAttr { if (globalScene.currentBattle.double && globalScene.getEnemyParty().length > 1 && !isNullOrUndefined(allyPokemon)) { // Handle cases where revived pokemon needs to get switched in on same turn if (allyPokemon.isFainted() || allyPokemon === pokemon) { - // Enemy switch phase should be removed and replaced with the revived pkmn switching in - globalScene.phaseManager.tryRemovePhase((phase: SwitchSummonPhase) => phase.is("SwitchSummonPhase") && phase.getPokemon() === pokemon); // If the pokemon being revived was alive earlier in the turn, cancel its move // (revived pokemon can't move in the turn they're brought back) globalScene.phaseManager.findPhase("MovePhase", (phase: MovePhase) => phase.pokemon === pokemon)?.cancel(); @@ -7086,7 +7085,7 @@ export class RepeatMoveAttr extends MoveEffectAttr { })); target.getMoveQueue().unshift({ move: lastMove.move, targets: moveTargets, ignorePP: false }); target.turnData.extraTurns++; - globalScene.phaseManager.appendNewToPhase("MoveEndPhase", "MovePhase", target, moveTargets, movesetMove); + globalScene.phaseManager.appendNewToPhase("MoveEndPhase", "MovePhase", target, moveTargets, movesetMove, false, false, false, MovePhaseTimingModifier.FIRST); return true; } @@ -7884,12 +7883,7 @@ export class AfterYouAttr extends MoveEffectAttr { */ override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean { globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:afterYou", { targetName: getPokemonNameWithAffix(target) })); - - //Will find next acting phase of the targeted pokémon, delete it and queue it next on successful delete. - const nextAttackPhase = globalScene.phaseManager.findPhase("MovePhase", (phase) => phase.pokemon === target); - if (nextAttackPhase && globalScene.phaseManager.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) { - globalScene.phaseManager.prependNewToPhase("MovePhase", "MovePhase", target, [ ...nextAttackPhase.targets ], nextAttackPhase.move); - } + globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === target); return true; } diff --git a/src/enums/dynamic-phase-type.ts b/src/enums/dynamic-phase-type.ts deleted file mode 100644 index a34ac371668..00000000000 --- a/src/enums/dynamic-phase-type.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Enum representation of the phase types held by implementations of {@linkcode PhasePriorityQueue} - */ -export enum DynamicPhaseType { - POST_SUMMON -} diff --git a/src/phase-manager.ts b/src/phase-manager.ts index d38dd9fdbea..fb9f7914b41 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 { PhaseMap, PhaseString } from "./@types/phase-types"; +import type { DynamicPhaseString, PhaseMap, PhaseString, StaticPhaseString } from "./@types/phase-types"; import { globalScene } from "#app/global-scene"; import { ActivatePriorityQueuePhase } from "#app/phases/activate-priority-queue-phase"; import { AddEnemyBuffModifierPhase } from "#app/phases/add-enemy-buff-modifier-phase"; @@ -12,7 +12,6 @@ import { CheckStatusEffectPhase } from "#app/phases/check-status-effect-phase"; import { CheckSwitchPhase } from "#app/phases/check-switch-phase"; import { CommandPhase } from "#app/phases/command-phase"; import { CommonAnimPhase } from "#app/phases/common-anim-phase"; -import { coerceArray } from "#app/utils/common"; import { DamageAnimPhase } from "#app/phases/damage-anim-phase"; import { EggHatchPhase } from "#app/phases/egg-hatch-phase"; import { EggLapsePhase } from "#app/phases/egg-lapse-phase"; @@ -100,6 +99,7 @@ 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"; +import type { PokemonMove } from "#app/data/moves/pokemon-move"; /** * Manager for phases used by battle scene. @@ -423,7 +423,7 @@ export class PhaseManager { this.phaseQueue.splice(phaseIndex, 1); return true; } - return false; + return this.dynamicQueueManager.removePhase(phaseFilter); } /** @@ -450,16 +450,23 @@ export class PhaseManager { * @param targetPhase - The phase to search for in phaseQueue * @returns boolean if a targetPhase was found and added */ - prependToPhase(phase: Phase | Phase[], targetPhase: PhaseString): boolean { - phase = coerceArray(phase); + prependToPhase(phase: Phase, targetPhase: PhaseString): boolean { + const insertPhase = this.dynamicQueueManager.isDynamicPhase(phase.phaseName) + ? new ActivatePriorityQueuePhase(phase.phaseName) + : phase; const target = PHASES[targetPhase]; const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof target); + if (this.dynamicQueueManager.isDynamicPhase(phase.phaseName)) { + this.dynamicQueueManager.queueDynamicPhase(phase); + } + if (targetIndex !== -1) { - this.phaseQueue.splice(targetIndex, 0, ...phase); + this.phaseQueue.splice(targetIndex, 0, insertPhase); return true; } - this.unshiftPhase(...phase); + this.unshiftPhase(insertPhase); + return false; } @@ -470,16 +477,22 @@ export class PhaseManager { * @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: PhaseString, condition?: PhaseConditionFunc): boolean { - phase = coerceArray(phase); + appendToPhase(phase: Phase, targetPhase: StaticPhaseString, condition?: PhaseConditionFunc): boolean { + const insertPhase = this.dynamicQueueManager.isDynamicPhase(phase.phaseName) + ? new ActivatePriorityQueuePhase(phase.phaseName) + : phase; const target = PHASES[targetPhase]; const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof target && (!condition || condition(ph))); + if (this.dynamicQueueManager.isDynamicPhase(phase.phaseName)) { + this.dynamicQueueManager.queueDynamicPhase(phase); + } + if (targetIndex !== -1 && this.phaseQueue.length > targetIndex) { - this.phaseQueue.splice(targetIndex + 1, 0, ...phase); + this.phaseQueue.splice(targetIndex + 1, 0, insertPhase); return true; } - this.unshiftPhase(...phase); + this.unshiftPhase(insertPhase); return false; } @@ -496,10 +509,10 @@ export class PhaseManager { /** * Unshifts the top phase from the corresponding dynamic queue onto {@linkcode phaseQueue} - * @param type {@linkcode DynamicPhaseType} The type of dynamic phase to start + * @param type {@linkcode DynamicPhaseString} The type of dynamic phase to start */ - public startNextDynamicPhaseOfType(type: PhaseString): void { - const phase = this.dynamicQueueManager.popNextPhaseOfType(type); + public startNextDynamicPhase(): void { + const phase = this.dynamicQueueManager.popNextPhase(); if (phase) { this.unshiftPhase(phase); } @@ -640,14 +653,14 @@ export class PhaseManager { * @returns `true` if a `targetPhase` was found to append to */ public appendNewToPhase( - targetPhase: PhaseString, + targetPhase: StaticPhaseString, phase: T, ...args: ConstructorParameters ): boolean { return this.appendToPhase(this.create(phase, ...args), targetPhase); } - public startNewDynamicPhase( + public startNewDynamicPhase( phase: T, ...args: ConstructorParameters ): void { @@ -661,4 +674,8 @@ export class PhaseManager { public forceMoveLast(phaseCondition: PhaseConditionFunc) { this.dynamicQueueManager.setMoveTimingModifier(phaseCondition, MovePhaseTimingModifier.LAST); } + + public changePhaseMove(phaseCondition: PhaseConditionFunc, move: PokemonMove) { + this.dynamicQueueManager.setMoveForPhase(phaseCondition, move); + } } diff --git a/src/phases/activate-priority-queue-phase.ts b/src/phases/activate-priority-queue-phase.ts index b58e13be2e1..4e467446055 100644 --- a/src/phases/activate-priority-queue-phase.ts +++ b/src/phases/activate-priority-queue-phase.ts @@ -13,7 +13,7 @@ export class ActivatePriorityQueuePhase extends Phase { override start() { super.start(); - globalScene.phaseManager.startNextDynamicPhaseOfType(this.type); + globalScene.phaseManager.startNextDynamicPhase(); this.end(); } diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 32889bae07a..416be08653d 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -54,7 +54,7 @@ export class MovePhase extends PokemonPhase { return this._move; } - protected set move(move: PokemonMove) { + public set move(move: PokemonMove) { this._move = move; } @@ -88,6 +88,7 @@ export class MovePhase extends PokemonPhase { followUp = false, ignorePp = false, reflected = false, + timingModifier = MovePhaseTimingModifier.NORMAL, ) { super(pokemon.getBattlerIndex()); @@ -97,7 +98,7 @@ export class MovePhase extends PokemonPhase { this.followUp = followUp; this.ignorePp = ignorePp; this.reflected = reflected; - this.timingModifier = MovePhaseTimingModifier.NORMAL; + this.timingModifier = timingModifier; } /** diff --git a/src/phases/party-member-pokemon-phase.ts b/src/phases/party-member-pokemon-phase.ts index a782eabda38..ac9de5a2b15 100644 --- a/src/phases/party-member-pokemon-phase.ts +++ b/src/phases/party-member-pokemon-phase.ts @@ -22,4 +22,8 @@ export abstract class PartyMemberPokemonPhase extends FieldPhase { getPokemon(): Pokemon { return this.getParty()[this.partyMemberIndex]; } + + isPlayer(): boolean { + return this.player; + } } diff --git a/src/phases/summon-phase.ts b/src/phases/summon-phase.ts index ad93452331f..8ed33e12870 100644 --- a/src/phases/summon-phase.ts +++ b/src/phases/summon-phase.ts @@ -296,4 +296,8 @@ export class SummonPhase extends PartyMemberPokemonPhase { super.end(); } + + public getFieldIndex(): number { + return this.fieldIndex; + } } diff --git a/src/queues/dynamic-queue-manager.ts b/src/queues/dynamic-queue-manager.ts index e0b5ab3df6c..c140c8bcc9e 100644 --- a/src/queues/dynamic-queue-manager.ts +++ b/src/queues/dynamic-queue-manager.ts @@ -1,19 +1,20 @@ import type { PhaseConditionFunc } from "#app/@types/phase-condition"; import type { PhaseString } from "#app/@types/phase-types"; +import type { PokemonMove } from "#app/data/moves/pokemon-move"; import type { Phase } from "#app/phase"; -import type { SwitchSummonPhase } from "#app/phases/switch-summon-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"; import { PostSummonPhasePriorityQueue } from "#app/queues/post-summon-phase-priority-queue"; +import { SwitchSummonPhasePriorityQueue } from "#app/queues/switch-summon-phase-priority-queue"; import type { BattlerIndex } from "#enums/battler-index"; import type { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; + export class DynamicQueueManager { private dynamicPhaseMap: Map>; constructor() { this.dynamicPhaseMap = new Map(); - this.dynamicPhaseMap.set("SwitchSummonPhase", new PokemonPhasePriorityQueue()); + this.dynamicPhaseMap.set("SwitchSummonPhase", new SwitchSummonPhasePriorityQueue()); this.dynamicPhaseMap.set("PostSummonPhase", new PostSummonPhasePriorityQueue()); this.dynamicPhaseMap.set("MovePhase", new MovePhasePriorityQueue()); } @@ -28,8 +29,8 @@ export class DynamicQueueManager { this.dynamicPhaseMap.get(phase.phaseName)?.push(phase); } - public popNextPhaseOfType(type: PhaseString): Phase | undefined { - return this.dynamicPhaseMap.get(type)?.pop(); + public popNextPhase(): Phase | undefined { + return [...this.dynamicPhaseMap.values()].find(queue => !queue.isEmpty())?.pop(); } public isDynamicPhase(type: PhaseString): boolean { @@ -44,11 +45,24 @@ export class DynamicQueueManager { return !!this.dynamicPhaseMap.get(type)?.hasPhaseWithCondition(condition); } + public removePhase(condition: PhaseConditionFunc) { + for (const queue of this.dynamicPhaseMap.values()) { + if (queue.remove(condition)) { + return true; + } + } + return false; + } + public setMoveTimingModifier(condition: PhaseConditionFunc, modifier: MovePhaseTimingModifier) { const movePhaseQueue: MovePhasePriorityQueue = this.dynamicPhaseMap.get("MovePhase") as MovePhasePriorityQueue; movePhaseQueue.setTimingModifier(condition, modifier); } + public setMoveForPhase(condition: PhaseConditionFunc, move: PokemonMove) { + (this.dynamicPhaseMap.get("MovePhase") as MovePhasePriorityQueue).setMoveForPhase(condition, move); + } + public setMoveOrder(order: BattlerIndex[]) { (this.dynamicPhaseMap.get("MovePhase") as MovePhasePriorityQueue).setMoveOrder(order); } diff --git a/src/queues/move-phase-priority-queue.ts b/src/queues/move-phase-priority-queue.ts index cbd70d864c7..81ec1120156 100644 --- a/src/queues/move-phase-priority-queue.ts +++ b/src/queues/move-phase-priority-queue.ts @@ -1,4 +1,5 @@ import type { PhaseConditionFunc } from "#app/@types/phase-condition"; +import type { PokemonMove } from "#app/data/moves/pokemon-move"; import type { MovePhase } from "#app/phases/move-phase"; import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue"; import { isNullOrUndefined } from "#app/utils/common"; @@ -18,6 +19,13 @@ export class MovePhasePriorityQueue extends PokemonPhasePriorityQueue } } + public setMoveForPhase(condition: PhaseConditionFunc, move: PokemonMove) { + const phase = this.queue.find(phase => condition(phase)); + if (!isNullOrUndefined(phase)) { + phase.move = move; + } + } + public setMoveOrder(order: BattlerIndex[]) { this.setOrder = order; } diff --git a/src/queues/phase-priority-queue.ts b/src/queues/phase-priority-queue.ts index 2ca6a077154..0b9a10b2e45 100644 --- a/src/queues/phase-priority-queue.ts +++ b/src/queues/phase-priority-queue.ts @@ -42,6 +42,15 @@ export abstract class PhasePriorityQueue { return !this.queue.length; } + public remove(condition: PhaseConditionFunc): boolean { + const phaseIndex = this.queue.findIndex(condition); + if (phaseIndex > -1) { + this.queue.splice(phaseIndex, 1); + return true; + } + return false; + } + public findPhase(condition?: PhaseConditionFunc): Phase | undefined { return this.queue.find(phase => !condition || condition(phase)); } diff --git a/src/queues/switch-summon-phase-priority-queue.ts b/src/queues/switch-summon-phase-priority-queue.ts new file mode 100644 index 00000000000..eb4ad1d7c28 --- /dev/null +++ b/src/queues/switch-summon-phase-priority-queue.ts @@ -0,0 +1,14 @@ +import type { SwitchSummonPhase } from "#app/phases/switch-summon-phase"; +import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue"; + +export class SwitchSummonPhasePriorityQueue extends PokemonPhasePriorityQueue { + public override push(phase: SwitchSummonPhase): void { + // The same pokemon or slot cannot be switched into at the same time + this.queue.filter( + old => + old.getPokemon() !== phase.getPokemon() && + !(old.isPlayer() === phase.isPlayer() && old.getFieldIndex() === phase.getFieldIndex()), + ); + super.push(phase); + } +} From 1441cf91711edeb7ca694e35fe57422abc5b1182 Mon Sep 17 00:00:00 2001 From: Dean Date: Sun, 15 Jun 2025 11:24:00 -0700 Subject: [PATCH 08/28] Fix move order persisting through tests --- src/queues/dynamic-queue-manager.ts | 11 +++++++---- src/queues/move-phase-priority-queue.ts | 5 +++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/queues/dynamic-queue-manager.ts b/src/queues/dynamic-queue-manager.ts index c140c8bcc9e..edd2afa35f6 100644 --- a/src/queues/dynamic-queue-manager.ts +++ b/src/queues/dynamic-queue-manager.ts @@ -55,15 +55,18 @@ export class DynamicQueueManager { } public setMoveTimingModifier(condition: PhaseConditionFunc, modifier: MovePhaseTimingModifier) { - const movePhaseQueue: MovePhasePriorityQueue = this.dynamicPhaseMap.get("MovePhase") as MovePhasePriorityQueue; - movePhaseQueue.setTimingModifier(condition, modifier); + this.getMovePhaseQueue().setTimingModifier(condition, modifier); } public setMoveForPhase(condition: PhaseConditionFunc, move: PokemonMove) { - (this.dynamicPhaseMap.get("MovePhase") as MovePhasePriorityQueue).setMoveForPhase(condition, move); + this.getMovePhaseQueue().setMoveForPhase(condition, move); } public setMoveOrder(order: BattlerIndex[]) { - (this.dynamicPhaseMap.get("MovePhase") as MovePhasePriorityQueue).setMoveOrder(order); + this.getMovePhaseQueue().setMoveOrder(order); + } + + private getMovePhaseQueue(): MovePhasePriorityQueue { + return this.dynamicPhaseMap.get("MovePhase") as MovePhasePriorityQueue; } } diff --git a/src/queues/move-phase-priority-queue.ts b/src/queues/move-phase-priority-queue.ts index 81ec1120156..dd01fcbb210 100644 --- a/src/queues/move-phase-priority-queue.ts +++ b/src/queues/move-phase-priority-queue.ts @@ -30,6 +30,11 @@ export class MovePhasePriorityQueue extends PokemonPhasePriorityQueue this.setOrder = order; } + public override clear(): void { + this.setOrder = undefined; + super.clear(); + } + private sortPostSpeed(): void { this.queue.sort((a: MovePhase, b: MovePhase) => { const priority = [a, b].map(movePhase => { From 16d848c7501a45d5bc5ce671d5678a2684f28b58 Mon Sep 17 00:00:00 2001 From: Dean Date: Sun, 15 Jun 2025 11:41:11 -0700 Subject: [PATCH 09/28] Fix magic coat/bounce --- src/phases/move-effect-phase.ts | 35 ++++++++++++++------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 770d9c79a2a..c89ea13473a 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -54,6 +54,7 @@ import { HitCheckResult } from "#enums/hit-check-result"; import type Move from "#app/data/moves/move"; import { isFieldTargeted } from "#app/data/moves/move-utils"; import { DamageAchv } from "#app/system/achv"; +import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; type HitCheckEntry = [HitCheckResult, TypeDamageMultiplier]; @@ -166,26 +167,23 @@ export class MoveEffectPhase extends PokemonPhase { : [user.getBattlerIndex()]; // TODO: ability displays should be handled by the ability if (!target.getTag(BattlerTagType.MAGIC_COAT)) { - this.queuedPhases.push( - globalScene.phaseManager.create( - "ShowAbilityPhase", - target.getBattlerIndex(), - target.getPassiveAbility().hasAttr("ReflectStatusMoveAbAttr"), - ), + globalScene.phaseManager.unshiftNew( + "ShowAbilityPhase", + target.getBattlerIndex(), + target.getPassiveAbility().hasAttr("ReflectStatusMoveAbAttr"), ); - this.queuedPhases.push(globalScene.phaseManager.create("HideAbilityPhase")); + globalScene.phaseManager.unshiftNew("HideAbilityPhase"); } - this.queuedPhases.push( - globalScene.phaseManager.create( - "MovePhase", - target, - newTargets, - new PokemonMove(this.move.id, 0, 0, true), - true, - true, - true, - ), + globalScene.phaseManager.startNewDynamicPhase( + "MovePhase", + target, + newTargets, + new PokemonMove(this.move.id, 0, 0, true), + true, + true, + true, + MovePhaseTimingModifier.FIRST, ); } @@ -372,9 +370,6 @@ export class MoveEffectPhase extends PokemonPhase { return; } - if (this.queuedPhases.length) { - globalScene.phaseManager.appendToPhase(this.queuedPhases, "MoveEndPhase"); - } const moveType = user.getMoveType(this.move, true); if (this.move.category !== MoveCategory.STATUS && !user.stellarTypesBoosted.includes(moveType)) { user.stellarTypesBoosted.push(moveType); From f629dbdc1099e235dec53d538fe9f4f7cdaa6659 Mon Sep 17 00:00:00 2001 From: Dean Date: Sun, 15 Jun 2025 11:45:41 -0700 Subject: [PATCH 10/28] Use append for magic coat/bounce --- src/phases/move-effect-phase.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index c89ea13473a..2af7e93a6ac 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -175,7 +175,8 @@ export class MoveEffectPhase extends PokemonPhase { globalScene.phaseManager.unshiftNew("HideAbilityPhase"); } - globalScene.phaseManager.startNewDynamicPhase( + globalScene.phaseManager.appendNewToPhase( + "MoveEndPhase", "MovePhase", target, newTargets, From b7200f9ecf6ce4f87dffa4222b3ed03958c552d5 Mon Sep 17 00:00:00 2001 From: Dean Date: Sun, 15 Jun 2025 13:38:50 -0700 Subject: [PATCH 11/28] Remove getSpeedOrder from TurnStartPhase, fix references to getCommandOrder in tests --- src/phases/turn-start-phase.ts | 135 ++++++-------------------- src/utils/speed-order.ts | 4 + test/abilities/mycelium_might.test.ts | 53 ++++------ test/abilities/stall.test.ts | 40 +++----- test/battle/battle-order.test.ts | 85 +++++++--------- test/testUtils/gameManager.ts | 2 +- 6 files changed, 101 insertions(+), 218 deletions(-) diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index 14e8ec906ec..e2681e0cf93 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -1,83 +1,34 @@ -import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs"; -import { allMoves } from "#app/data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { Stat } from "#app/enums/stat"; -import type Pokemon from "#app/field/pokemon"; import { PokemonMove } from "#app/data/moves/pokemon-move"; -import { BypassSpeedChanceModifier } from "#app/modifier/modifier"; import { Command } from "#enums/command"; -import { randSeedShuffle, BooleanHolder } from "#app/utils/common"; import { FieldPhase } from "./field-phase"; -import { BattlerIndex } from "#enums/battler-index"; -import { TrickRoomTag } from "#app/data/arena-tag"; +import type { BattlerIndex } from "#enums/battler-index"; import { SwitchType } from "#enums/switch-type"; import { globalScene } from "#app/global-scene"; +import { applyInSpeedOrder } from "#app/utils/speed-order"; +import type Pokemon from "#app/field/pokemon"; export class TurnStartPhase extends FieldPhase { public readonly phaseName = "TurnStartPhase"; /** - * This orders the active Pokemon on the field by speed into an BattlerIndex array and returns that array. - * It also checks for Trick Room and reverses the array if it is present. - * @returns {@linkcode BattlerIndex[]} the battle indices of all pokemon on the field ordered by speed - */ - getSpeedOrder(): BattlerIndex[] { - const playerField = globalScene.getPlayerField().filter(p => p.isActive()) as Pokemon[]; - const enemyField = globalScene.getEnemyField().filter(p => p.isActive()) as Pokemon[]; - - // We shuffle the list before sorting so speed ties produce random results - let orderedTargets: Pokemon[] = playerField.concat(enemyField); - // We seed it with the current turn to prevent an inconsistency where it - // was varying based on how long since you last reloaded - globalScene.executeWithSeedOffset( - () => { - orderedTargets = randSeedShuffle(orderedTargets); - }, - globalScene.currentBattle.turn, - globalScene.waveSeed, - ); - - // Next, a check for Trick Room is applied to determine sort order. - const speedReversed = new BooleanHolder(false); - globalScene.arena.applyTags(TrickRoomTag, false, speedReversed); - - // Adjust the sort function based on whether Trick Room is active. - orderedTargets.sort((a: Pokemon, b: Pokemon) => { - const aSpeed = a?.getEffectiveStat(Stat.SPD) ?? 0; - const bSpeed = b?.getEffectiveStat(Stat.SPD) ?? 0; - - return speedReversed.value ? aSpeed - bSpeed : bSpeed - aSpeed; - }); - - return orderedTargets.map(t => t.getFieldIndex() + (!t.isPlayer() ? BattlerIndex.ENEMY : BattlerIndex.PLAYER)); - } - - /** - * This takes the result of getSpeedOrder and applies priority / bypass speed attributes to it. - * This also considers the priority levels of various commands and changes the result of getSpeedOrder based on such. - * @returns {@linkcode BattlerIndex[]} the final sequence of commands for this turn + * Returns an ordering of the current field based on command priority + * @returns {@linkcode BattlerIndex[]} the sequence of commands for this turn */ getCommandOrder(): BattlerIndex[] { - let moveOrder = this.getSpeedOrder(); - // The creation of the battlerBypassSpeed object contains checks for the ability Quick Draw and the held item Quick Claw - // The ability Mycelium Might disables Quick Claw's activation when using a status move - // This occurs before the main loop because of battles with more than two Pokemon - const battlerBypassSpeed = {}; - - globalScene.getField(true).map(p => { - const bypassSpeed = new BooleanHolder(false); - const canCheckHeldItems = new BooleanHolder(true); - applyAbAttrs("BypassSpeedChanceAbAttr", p, null, false, bypassSpeed); - applyAbAttrs("PreventBypassSpeedChanceAbAttr", p, null, false, bypassSpeed, canCheckHeldItems); - if (canCheckHeldItems.value) { - globalScene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p); - } - battlerBypassSpeed[p.getBattlerIndex()] = bypassSpeed; - }); + const playerField = globalScene + .getPlayerField() + .filter(p => p.isActive()) + .map(p => p.getBattlerIndex()); + const enemyField = globalScene + .getEnemyField() + .filter(p => p.isActive()) + .map(p => p.getBattlerIndex()); + const orderedTargets: BattlerIndex[] = playerField.concat(enemyField); // The function begins sorting orderedTargets based on command priority, move priority, and possible speed bypasses. // Non-FIGHT commands (SWITCH, BALL, RUN) have a higher command priority and will always occur before any FIGHT commands. - moveOrder = moveOrder.slice(0); - moveOrder.sort((a, b) => { + orderedTargets.sort((a, b) => { const aCommand = globalScene.currentBattle.turnCommands[a]; const bCommand = globalScene.currentBattle.turnCommands[b]; @@ -88,40 +39,14 @@ export class TurnStartPhase extends FieldPhase { if (bCommand?.command === Command.FIGHT) { return -1; } - } else if (aCommand?.command === Command.FIGHT) { - const aMove = allMoves[aCommand.move!.move]; - const bMove = allMoves[bCommand!.move!.move]; - - const aUser = globalScene.getField(true).find(p => p.getBattlerIndex() === a)!; - const bUser = globalScene.getField(true).find(p => p.getBattlerIndex() === b)!; - - const aPriority = aMove.getPriority(aUser, false); - const bPriority = bMove.getPriority(bUser, false); - - // The game now checks for differences in priority levels. - // If the moves share the same original priority bracket, it can check for differences in battlerBypassSpeed and return the result. - // This conditional is used to ensure that Quick Claw can still activate with abilities like Stall and Mycelium Might (attack moves only) - // Otherwise, the game returns the user of the move with the highest priority. - const isSameBracket = Math.ceil(aPriority) - Math.ceil(bPriority) === 0; - if (aPriority !== bPriority) { - if (isSameBracket && battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value) { - return battlerBypassSpeed[a].value ? -1 : 1; - } - return aPriority < bPriority ? 1 : -1; - } } - // If there is no difference between the move's calculated priorities, the game checks for differences in battlerBypassSpeed and returns the result. - if (battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value) { - return battlerBypassSpeed[a].value ? -1 : 1; - } - - const aIndex = moveOrder.indexOf(a); - const bIndex = moveOrder.indexOf(b); + const aIndex = orderedTargets.indexOf(a); + const bIndex = orderedTargets.indexOf(b); return aIndex < bIndex ? -1 : aIndex > bIndex ? 1 : 0; }); - return moveOrder; + return orderedTargets; } start() { @@ -132,19 +57,21 @@ export class TurnStartPhase extends FieldPhase { let orderIndex = 0; - for (const o of this.getSpeedOrder()) { - const pokemon = field[o]; - const preTurnCommand = globalScene.currentBattle.preTurnCommands[o]; + applyInSpeedOrder( + field.filter(p => p?.isActive(true)), + (p: Pokemon) => { + const preTurnCommand = globalScene.currentBattle.preTurnCommands[p.getBattlerIndex()]; - if (preTurnCommand?.skip) { - continue; - } + if (preTurnCommand?.skip) { + return; + } - switch (preTurnCommand?.command) { - case Command.TERA: - globalScene.phaseManager.pushNew("TeraPhase", pokemon); - } - } + switch (preTurnCommand?.command) { + case Command.TERA: + globalScene.phaseManager.pushNew("TeraPhase", p); + } + }, + ); const phaseManager = globalScene.phaseManager; diff --git a/src/utils/speed-order.ts b/src/utils/speed-order.ts index 7916a471159..fcf5a396579 100644 --- a/src/utils/speed-order.ts +++ b/src/utils/speed-order.ts @@ -8,6 +8,10 @@ export interface hasPokemon { getPokemon(): Pokemon; } +export function applyInSpeedOrder(pokemonList: T[], callback: (pokemon: Pokemon) => void): void { + sortInSpeedOrder(pokemonList).forEach(pokemon => callback(pokemon)); +} + export function sortInSpeedOrder(pokemonList: T[]): T[] { pokemonList = shuffle(pokemonList); sortBySpeed(pokemonList); diff --git a/test/abilities/mycelium_might.test.ts b/test/abilities/mycelium_might.test.ts index 1f236f2c2fe..a9375fb022d 100644 --- a/test/abilities/mycelium_might.test.ts +++ b/test/abilities/mycelium_might.test.ts @@ -1,5 +1,3 @@ -import { TurnEndPhase } from "#app/phases/turn-end-phase"; -import { TurnStartPhase } from "#app/phases/turn-start-phase"; import GameManager from "#test/testUtils/gameManager"; import { AbilityId } from "#enums/ability-id"; import { Stat } from "#enums/stat"; @@ -45,65 +43,50 @@ describe("Abilities - Mycelium Might", () => { it("will move last in its priority bracket and ignore protective abilities", async () => { await game.classicMode.startBattle([SpeciesId.REGIELEKI]); - const enemyPokemon = game.scene.getEnemyPokemon(); - const playerIndex = game.scene.getPlayerPokemon()?.getBattlerIndex(); - const enemyIndex = enemyPokemon?.getBattlerIndex(); + const enemy = game.scene.getEnemyPokemon()!; + const player = game.scene.getPlayerPokemon()!; game.move.select(MoveId.BABY_DOLL_EYES); - await game.phaseInterceptor.to(TurnStartPhase, false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const speedOrder = phase.getSpeedOrder(); - const commandOrder = phase.getCommandOrder(); + await game.phaseInterceptor.to("MoveEndPhase", false); // The opponent Pokemon (without Mycelium Might) goes first despite having lower speed than the player Pokemon. // The player Pokemon (with Mycelium Might) goes last despite having higher speed than the opponent. - expect(speedOrder).toEqual([playerIndex, enemyIndex]); - expect(commandOrder).toEqual([enemyIndex, playerIndex]); - await game.phaseInterceptor.to(TurnEndPhase); + expect(player.hp).not.toEqual(player.getMaxHp()); + await game.phaseInterceptor.to("TurnEndPhase"); // Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced. - expect(enemyPokemon?.getStatStage(Stat.ATK)).toBe(-1); - }, 20000); + expect(enemy.getStatStage(Stat.ATK)).toBe(-1); + }); it("will still go first if a status move that is in a higher priority bracket than the opponent's move is used", async () => { game.override.enemyMoveset(MoveId.TACKLE); await game.classicMode.startBattle([SpeciesId.REGIELEKI]); - const enemyPokemon = game.scene.getEnemyPokemon(); - const playerIndex = game.scene.getPlayerPokemon()?.getBattlerIndex(); - const enemyIndex = enemyPokemon?.getBattlerIndex(); + const enemy = game.scene.getEnemyPokemon()!; + const player = game.scene.getPlayerPokemon()!; game.move.select(MoveId.BABY_DOLL_EYES); - await game.phaseInterceptor.to(TurnStartPhase, false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const speedOrder = phase.getSpeedOrder(); - const commandOrder = phase.getCommandOrder(); + await game.phaseInterceptor.to("MoveEndPhase", false); // The player Pokemon (with M.M.) goes first because its move is still within a higher priority bracket than its opponent. // The enemy Pokemon goes second because its move is in a lower priority bracket. - expect(speedOrder).toEqual([playerIndex, enemyIndex]); - expect(commandOrder).toEqual([playerIndex, enemyIndex]); - await game.phaseInterceptor.to(TurnEndPhase); + expect(player.hp).toEqual(player.getMaxHp()); + await game.phaseInterceptor.to("TurnEndPhase"); // Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced. - expect(enemyPokemon?.getStatStage(Stat.ATK)).toBe(-1); - }, 20000); + expect(enemy.getStatStage(Stat.ATK)).toBe(-1); + }); it("will not affect non-status moves", async () => { await game.classicMode.startBattle([SpeciesId.REGIELEKI]); - const playerIndex = game.scene.getPlayerPokemon()!.getBattlerIndex(); - const enemyIndex = game.scene.getEnemyPokemon()!.getBattlerIndex(); + const player = game.scene.getPlayerPokemon()!; game.move.select(MoveId.QUICK_ATTACK); - await game.phaseInterceptor.to(TurnStartPhase, false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const speedOrder = phase.getSpeedOrder(); - const commandOrder = phase.getCommandOrder(); + await game.phaseInterceptor.to("MoveEndPhase", false); // The player Pokemon (with M.M.) goes first because it has a higher speed and did not use a status move. // The enemy Pokemon (without M.M.) goes second because its speed is lower. // This means that the commandOrder should be identical to the speedOrder - expect(speedOrder).toEqual([playerIndex, enemyIndex]); - expect(commandOrder).toEqual([playerIndex, enemyIndex]); - }, 20000); + expect(player.hp).toEqual(player.getMaxHp()); + }); }); diff --git a/test/abilities/stall.test.ts b/test/abilities/stall.test.ts index df40bed3e90..ff36931dd50 100644 --- a/test/abilities/stall.test.ts +++ b/test/abilities/stall.test.ts @@ -4,7 +4,6 @@ import { SpeciesId } from "#enums/species-id"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -import { TurnStartPhase } from "#app/phases/turn-start-phase"; describe("Abilities - Stall", () => { let phaserGame: Phaser.Game; @@ -40,56 +39,41 @@ describe("Abilities - Stall", () => { it("Pokemon with Stall should move last in its priority bracket regardless of speed", async () => { await game.classicMode.startBattle([SpeciesId.SHUCKLE]); - const playerIndex = game.scene.getPlayerPokemon()!.getBattlerIndex(); - const enemyIndex = game.scene.getEnemyPokemon()!.getBattlerIndex(); + const player = game.scene.getPlayerPokemon()!; game.move.select(MoveId.QUICK_ATTACK); - await game.phaseInterceptor.to(TurnStartPhase, false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const speedOrder = phase.getSpeedOrder(); - const commandOrder = phase.getCommandOrder(); + await game.phaseInterceptor.to("MoveEndPhase", false); // The player Pokemon (without Stall) goes first despite having lower speed than the opponent. // The opponent Pokemon (with Stall) goes last despite having higher speed than the player Pokemon. - expect(speedOrder).toEqual([enemyIndex, playerIndex]); - expect(commandOrder).toEqual([playerIndex, enemyIndex]); - }, 20000); + expect(player.hp).toEqual(player.getMaxHp()); + }); it("Pokemon with Stall will go first if a move that is in a higher priority bracket than the opponent's move is used", async () => { await game.classicMode.startBattle([SpeciesId.SHUCKLE]); - const playerIndex = game.scene.getPlayerPokemon()!.getBattlerIndex(); - const enemyIndex = game.scene.getEnemyPokemon()!.getBattlerIndex(); + const player = game.scene.getPlayerPokemon()!; game.move.select(MoveId.TACKLE); - await game.phaseInterceptor.to(TurnStartPhase, false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const speedOrder = phase.getSpeedOrder(); - const commandOrder = phase.getCommandOrder(); + await game.phaseInterceptor.to("MoveEndPhase", false); // The opponent Pokemon (with Stall) goes first because its move is still within a higher priority bracket than its opponent. // The player Pokemon goes second because its move is in a lower priority bracket. - expect(speedOrder).toEqual([enemyIndex, playerIndex]); - expect(commandOrder).toEqual([enemyIndex, playerIndex]); - }, 20000); + expect(player.hp).not.toEqual(player.getMaxHp()); + }); it("If both Pokemon have stall and use the same move, speed is used to determine who goes first.", async () => { game.override.ability(AbilityId.STALL); await game.classicMode.startBattle([SpeciesId.SHUCKLE]); - const playerIndex = game.scene.getPlayerPokemon()!.getBattlerIndex(); - const enemyIndex = game.scene.getEnemyPokemon()!.getBattlerIndex(); + const player = game.scene.getPlayerPokemon()!; game.move.select(MoveId.TACKLE); - await game.phaseInterceptor.to(TurnStartPhase, false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const speedOrder = phase.getSpeedOrder(); - const commandOrder = phase.getCommandOrder(); + await game.phaseInterceptor.to("MoveEndPhase", false); // The opponent Pokemon (with Stall) goes first because it has a higher speed. // The player Pokemon (with Stall) goes second because its speed is lower. - expect(speedOrder).toEqual([enemyIndex, playerIndex]); - expect(commandOrder).toEqual([enemyIndex, playerIndex]); - }, 20000); + expect(player.hp).not.toEqual(player.getMaxHp()); + }); }); diff --git a/test/battle/battle-order.test.ts b/test/battle/battle-order.test.ts index c12760a7f30..06c4a7a2986 100644 --- a/test/battle/battle-order.test.ts +++ b/test/battle/battle-order.test.ts @@ -1,7 +1,6 @@ -import { EnemyCommandPhase } from "#app/phases/enemy-command-phase"; -import { SelectTargetPhase } from "#app/phases/select-target-phase"; -import { TurnStartPhase } from "#app/phases/turn-start-phase"; +import type { MovePhase } from "#app/phases/move-phase"; import { AbilityId } from "#enums/ability-id"; +import { BattlerIndex } from "#enums/battler-index"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import GameManager from "#test/testUtils/gameManager"; @@ -35,63 +34,60 @@ describe("Battle order", () => { await game.classicMode.startBattle([SpeciesId.BULBASAUR]); const playerPokemon = game.scene.getPlayerPokemon()!; + const playerStartHp = playerPokemon.hp; const enemyPokemon = game.scene.getEnemyPokemon()!; + const enemyStartHp = enemyPokemon.hp; + vi.spyOn(playerPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 50]); // set playerPokemon's speed to 50 vi.spyOn(enemyPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set enemyPokemon's speed to 150 - game.move.select(MoveId.TACKLE); - await game.phaseInterceptor.run(EnemyCommandPhase); - const playerPokemonIndex = playerPokemon.getBattlerIndex(); - const enemyPokemonIndex = enemyPokemon.getBattlerIndex(); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const order = phase.getCommandOrder(); - expect(order[0]).toBe(enemyPokemonIndex); - expect(order[1]).toBe(playerPokemonIndex); - }, 20000); + await game.phaseInterceptor.to("MoveEndPhase", false); + expect(playerPokemon.hp).not.toEqual(playerStartHp); + expect(enemyPokemon.hp).toEqual(enemyStartHp); + }); it("Player faster than opponent 150 vs 50", async () => { await game.classicMode.startBattle([SpeciesId.BULBASAUR]); const playerPokemon = game.scene.getPlayerPokemon()!; + const playerStartHp = playerPokemon.hp; const enemyPokemon = game.scene.getEnemyPokemon()!; + const enemyStartHp = enemyPokemon.hp; vi.spyOn(playerPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set playerPokemon's speed to 150 vi.spyOn(enemyPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 50]); // set enemyPokemon's speed to 50 game.move.select(MoveId.TACKLE); - await game.phaseInterceptor.run(EnemyCommandPhase); - const playerPokemonIndex = playerPokemon.getBattlerIndex(); - const enemyPokemonIndex = enemyPokemon.getBattlerIndex(); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const order = phase.getCommandOrder(); - expect(order[0]).toBe(playerPokemonIndex); - expect(order[1]).toBe(enemyPokemonIndex); - }, 20000); + await game.phaseInterceptor.to("MoveEndPhase", false); + expect(playerPokemon.hp).toEqual(playerStartHp); + expect(enemyPokemon.hp).not.toEqual(enemyStartHp); + }); it("double - both opponents faster than player 50/50 vs 150/150", async () => { game.override.battleStyle("double"); await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.BLASTOISE]); const playerPokemon = game.scene.getPlayerField(); + const playerHps = playerPokemon.map(p => p.hp); const enemyPokemon = game.scene.getEnemyField(); + const enemyHps = enemyPokemon.map(p => p.hp); playerPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 50])); // set both playerPokemons' speed to 50 enemyPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150])); // set both enemyPokemons' speed to 150 - const playerIndices = playerPokemon.map(p => p?.getBattlerIndex()); - const enemyIndices = enemyPokemon.map(p => p?.getBattlerIndex()); game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE, 1); - await game.phaseInterceptor.runFrom(SelectTargetPhase).to(TurnStartPhase, false); + await game.move.selectEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER); + await game.move.selectEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER_2); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const order = phase.getCommandOrder(); - expect(order.slice(0, 2).includes(enemyIndices[0])).toBe(true); - expect(order.slice(0, 2).includes(enemyIndices[1])).toBe(true); - expect(order.slice(2, 4).includes(playerIndices[0])).toBe(true); - expect(order.slice(2, 4).includes(playerIndices[1])).toBe(true); - }, 20000); + await game.phaseInterceptor.to("MoveEndPhase", true); + await game.phaseInterceptor.to("MoveEndPhase", false); + for (let i = 0; i < 2; i++) { + expect(playerPokemon[i].hp).not.toEqual(playerHps[i]); + expect(enemyPokemon[i].hp).toEqual(enemyHps[i]); + } + }); it("double - speed tie except 1 - 100/100 vs 100/150", async () => { game.override.battleStyle("double"); @@ -102,20 +98,14 @@ describe("Battle order", () => { playerPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100])); //set both playerPokemons' speed to 100 vi.spyOn(enemyPokemon[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100]); // set enemyPokemon's speed to 100 vi.spyOn(enemyPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set enemyPokemon's speed to 150 - const playerIndices = playerPokemon.map(p => p?.getBattlerIndex()); - const enemyIndices = enemyPokemon.map(p => p?.getBattlerIndex()); game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE, 1); - await game.phaseInterceptor.runFrom(SelectTargetPhase).to(TurnStartPhase, false); + await game.phaseInterceptor.to("MovePhase", false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const order = phase.getCommandOrder(); - expect(order[0]).toBe(enemyIndices[1]); - expect(order.slice(1, 4).includes(enemyIndices[0])).toBe(true); - expect(order.slice(1, 4).includes(playerIndices[0])).toBe(true); - expect(order.slice(1, 4).includes(playerIndices[1])).toBe(true); - }, 20000); + const phase = game.scene.phaseManager.getCurrentPhase() as MovePhase; + expect(phase.pokemon).toEqual(enemyPokemon[1]); + }); it("double - speed tie 100/150 vs 100/150", async () => { game.override.battleStyle("double"); @@ -127,18 +117,13 @@ describe("Battle order", () => { vi.spyOn(playerPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set other playerPokemon's speed to 150 vi.spyOn(enemyPokemon[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100]); // set one enemyPokemon's speed to 100 vi.spyOn(enemyPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set other enemyPokemon's speed to 150 - const playerIndices = playerPokemon.map(p => p?.getBattlerIndex()); - const enemyIndices = enemyPokemon.map(p => p?.getBattlerIndex()); game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE, 1); - await game.phaseInterceptor.runFrom(SelectTargetPhase).to(TurnStartPhase, false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const order = phase.getCommandOrder(); - expect(order.slice(0, 2).includes(playerIndices[1])).toBe(true); - expect(order.slice(0, 2).includes(enemyIndices[1])).toBe(true); - expect(order.slice(2, 4).includes(playerIndices[0])).toBe(true); - expect(order.slice(2, 4).includes(enemyIndices[0])).toBe(true); - }, 20000); + await game.phaseInterceptor.to("MovePhase", false); + + const phase = game.scene.phaseManager.getCurrentPhase() as MovePhase; + expect(enemyPokemon[1] === phase.pokemon || playerPokemon[1] === phase.pokemon); + }); }); diff --git a/test/testUtils/gameManager.ts b/test/testUtils/gameManager.ts index b1822a95856..41be0c7ca1d 100644 --- a/test/testUtils/gameManager.ts +++ b/test/testUtils/gameManager.ts @@ -519,7 +519,7 @@ export default class GameManager { } /** - * Intercepts `TurnStartPhase` and mocks {@linkcode TurnStartPhase.getSpeedOrder}'s return value. + * Modifies the queue manager to return move phases in a particular order * Used to manually modify Pokemon turn order. * Note: This *DOES NOT* account for priority. * @param order - The turn order to set as an array of {@linkcode BattlerIndex}es. From d8fb44240f0d311dbf262374cd305ad155f8c9fa Mon Sep 17 00:00:00 2001 From: Dean Date: Sun, 15 Jun 2025 13:39:01 -0700 Subject: [PATCH 12/28] Fix round queuing last instead of next --- src/data/moves/move.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 2261f959f58..8a0026a4ced 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -4542,7 +4542,7 @@ export class CueNextRoundAttr extends MoveEffectAttr { return false; } - globalScene.phaseManager.forceMoveLast((phase: MovePhase) => phase.is("MovePhase") && phase.move.moveId === MoveId.ROUND); + globalScene.phaseManager.forceMoveNext((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; From 2b6c918fb9dc857b16543bd24c77d1d337696f06 Mon Sep 17 00:00:00 2001 From: Dean Date: Sun, 15 Jun 2025 13:58:43 -0700 Subject: [PATCH 13/28] Add quick draw application --- src/data/abilities/ability.ts | 6 +---- src/phases/turn-start-phase.ts | 26 +++++++++--------- test/abilities/quick_draw.test.ts | 45 +++++++++++++------------------ 3 files changed, 33 insertions(+), 44 deletions(-) diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 066b8f538fb..2fc682954c6 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -7238,17 +7238,13 @@ export class BypassSpeedChanceAbAttr extends AbAttr { this.chance = chance; } - override canApply(pokemon: Pokemon, _passive: boolean, simulated: boolean, args: any[]): boolean { - const bypassSpeed = args[0] as BooleanHolder; + override canApply(pokemon: Pokemon, _passive: boolean, simulated: boolean, _args: any[]): boolean { const turnCommand = globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()]; - const isCommandFight = turnCommand?.command === Command.FIGHT; const move = turnCommand?.move?.move ? allMoves[turnCommand.move.move] : null; const isDamageMove = move?.category === MoveCategory.PHYSICAL || move?.category === MoveCategory.SPECIAL; return ( !simulated && - !bypassSpeed.value && pokemon.randBattleSeedInt(100) < this.chance && - isCommandFight && isDamageMove && pokemon.canAddTag(BattlerTagType.BYPASS_SPEED) ); diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index e2681e0cf93..8eba32003b2 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -8,6 +8,7 @@ import { SwitchType } from "#enums/switch-type"; import { globalScene } from "#app/global-scene"; import { applyInSpeedOrder } from "#app/utils/speed-order"; import type Pokemon from "#app/field/pokemon"; +import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs"; export class TurnStartPhase extends FieldPhase { public readonly phaseName = "TurnStartPhase"; @@ -53,27 +54,26 @@ export class TurnStartPhase extends FieldPhase { super.start(); const field = globalScene.getField(); + const activeField = globalScene.getField(true); const moveOrder = this.getCommandOrder(); let orderIndex = 0; - applyInSpeedOrder( - field.filter(p => p?.isActive(true)), - (p: Pokemon) => { - const preTurnCommand = globalScene.currentBattle.preTurnCommands[p.getBattlerIndex()]; + applyInSpeedOrder(activeField, (p: Pokemon) => { + const preTurnCommand = globalScene.currentBattle.preTurnCommands[p.getBattlerIndex()]; - if (preTurnCommand?.skip) { - return; - } + if (preTurnCommand?.skip) { + return; + } - switch (preTurnCommand?.command) { - case Command.TERA: - globalScene.phaseManager.pushNew("TeraPhase", p); - } - }, - ); + switch (preTurnCommand?.command) { + case Command.TERA: + globalScene.phaseManager.pushNew("TeraPhase", p); + } + }); const phaseManager = globalScene.phaseManager; + applyInSpeedOrder(activeField, (p: Pokemon) => applyAbAttrs("BypassSpeedChanceAbAttr", p, null)); for (const o of moveOrder) { const pokemon = field[o]; diff --git a/test/abilities/quick_draw.test.ts b/test/abilities/quick_draw.test.ts index 5e5e57fb056..6c7d4e08025 100644 --- a/test/abilities/quick_draw.test.ts +++ b/test/abilities/quick_draw.test.ts @@ -5,7 +5,7 @@ import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; describe("Abilities - Quick Draw", () => { let phaserGame: Phaser.Game; @@ -25,7 +25,6 @@ describe("Abilities - Quick Draw", () => { game = new GameManager(phaserGame); game.override.battleStyle("single"); - game.override.starterSpecies(SpeciesId.MAGIKARP); game.override.ability(AbilityId.QUICK_DRAW); game.override.moveset([MoveId.TACKLE, MoveId.TAIL_WHIP]); @@ -41,8 +40,8 @@ describe("Abilities - Quick Draw", () => { ).mockReturnValue(100); }); - test("makes pokemon going first in its priority bracket", async () => { - await game.classicMode.startBattle(); + it("makes pokemon go first in its priority bracket", async () => { + await game.classicMode.startBattle([SpeciesId.MAGIKARP]); const pokemon = game.scene.getPlayerPokemon()!; const enemy = game.scene.getEnemyPokemon()!; @@ -56,35 +55,29 @@ describe("Abilities - Quick Draw", () => { expect(pokemon.isFainted()).toBe(false); expect(enemy.isFainted()).toBe(true); expect(pokemon.waveData.abilitiesApplied).contain(AbilityId.QUICK_DRAW); - }, 20000); + }); - test( - "does not triggered by non damage moves", - { - retry: 5, - }, - async () => { - await game.classicMode.startBattle(); + it("is not triggered by non damaging moves", async () => { + await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - const pokemon = game.scene.getPlayerPokemon()!; - const enemy = game.scene.getEnemyPokemon()!; + const pokemon = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; - pokemon.hp = 1; - enemy.hp = 1; + pokemon.hp = 1; + enemy.hp = 1; - game.move.select(MoveId.TAIL_WHIP); - await game.phaseInterceptor.to(FaintPhase, false); + game.move.select(MoveId.TAIL_WHIP); + await game.phaseInterceptor.to(FaintPhase, false); - expect(pokemon.isFainted()).toBe(true); - expect(enemy.isFainted()).toBe(false); - expect(pokemon.waveData.abilitiesApplied).not.contain(AbilityId.QUICK_DRAW); - }, - ); + expect(pokemon.isFainted()).toBe(true); + expect(enemy.isFainted()).toBe(false); + expect(pokemon.waveData.abilitiesApplied).not.contain(AbilityId.QUICK_DRAW); + }); - test("does not increase priority", async () => { + it("does not increase priority", async () => { game.override.enemyMoveset([MoveId.EXTREME_SPEED]); - await game.classicMode.startBattle(); + await game.classicMode.startBattle([SpeciesId.MAGIKARP]); const pokemon = game.scene.getPlayerPokemon()!; const enemy = game.scene.getEnemyPokemon()!; @@ -98,5 +91,5 @@ describe("Abilities - Quick Draw", () => { expect(pokemon.isFainted()).toBe(true); expect(enemy.isFainted()).toBe(false); expect(pokemon.waveData.abilitiesApplied).contain(AbilityId.QUICK_DRAW); - }, 20000); + }); }); From ace1dab86f029d56d710240e27559da48d2866d6 Mon Sep 17 00:00:00 2001 From: Dean Date: Sun, 15 Jun 2025 14:03:38 -0700 Subject: [PATCH 14/28] Add quick claw activation --- src/phases/turn-start-phase.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index 8eba32003b2..887076b37a5 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -9,6 +9,7 @@ import { globalScene } from "#app/global-scene"; import { applyInSpeedOrder } from "#app/utils/speed-order"; import type Pokemon from "#app/field/pokemon"; import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs"; +import { BypassSpeedChanceModifier } from "#app/modifier/modifier"; export class TurnStartPhase extends FieldPhase { public readonly phaseName = "TurnStartPhase"; @@ -73,7 +74,10 @@ export class TurnStartPhase extends FieldPhase { }); const phaseManager = globalScene.phaseManager; - applyInSpeedOrder(activeField, (p: Pokemon) => applyAbAttrs("BypassSpeedChanceAbAttr", p, null)); + applyInSpeedOrder(activeField, (p: Pokemon) => { + applyAbAttrs("BypassSpeedChanceAbAttr", p, null); + globalScene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p); + }); for (const o of moveOrder) { const pokemon = field[o]; From acbcd3e308b9828f7cceaa73af572fdb33f63069 Mon Sep 17 00:00:00 2001 From: Dean Date: Sun, 15 Jun 2025 20:10:26 -0700 Subject: [PATCH 15/28] Fix tests --- test/abilities/neutralizing_gas.test.ts | 2 +- test/moves/rage_fist.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/abilities/neutralizing_gas.test.ts b/test/abilities/neutralizing_gas.test.ts index 51d2bed3ff0..764c96a461d 100644 --- a/test/abilities/neutralizing_gas.test.ts +++ b/test/abilities/neutralizing_gas.test.ts @@ -58,7 +58,7 @@ describe("Abilities - Neutralizing Gas", () => { expect(game.scene.getPlayerPokemon()?.getStatStage(Stat.ATK)).toBe(1); }); - it.todo("should activate before other abilities", async () => { + it("should activate before other abilities", async () => { game.override.enemySpecies(SpeciesId.ACCELGOR).enemyLevel(100).enemyAbility(AbilityId.INTIMIDATE); await game.classicMode.startBattle([SpeciesId.FEEBAS]); diff --git a/test/moves/rage_fist.test.ts b/test/moves/rage_fist.test.ts index 31dd987cb87..a5994457e3d 100644 --- a/test/moves/rage_fist.test.ts +++ b/test/moves/rage_fist.test.ts @@ -166,7 +166,7 @@ describe("Moves - Rage Fist", () => { // Charizard hit game.move.select(MoveId.SPLASH); - await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.toNextTurn(); expect(getPartyHitCount()).toEqual([1, 0]); From 3aa1940a8d42a61e84c4f8b57081c86c2842802e Mon Sep 17 00:00:00 2001 From: Dean Date: Sun, 15 Jun 2025 20:12:26 -0700 Subject: [PATCH 16/28] Remove APQP --- src/data/abilities/ability.ts | 15 +++- src/data/moves/move.ts | 8 +- src/phase-manager.ts | 86 ++++++------------- src/phases/activate-priority-queue-phase.ts | 23 ----- src/phases/move-effect-phase.ts | 3 +- src/phases/post-summon-phase.ts | 9 +- src/phases/summon-phase.ts | 1 + src/phases/switch-phase.ts | 2 +- src/phases/switch-summon-phase.ts | 2 +- src/phases/turn-start-phase.ts | 2 +- src/queues/dynamic-queue-manager.ts | 9 +- src/queues/move-phase-priority-queue.ts | 1 + .../post-summon-phase-priority-queue.ts | 13 +-- 13 files changed, 69 insertions(+), 105 deletions(-) delete mode 100644 src/phases/activate-priority-queue-phase.ts diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 2fc682954c6..faa9a8afafe 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -80,6 +80,7 @@ import { noAbilityTypeOverrideMoves } from "../moves/invalid-moves"; 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"; export class Ability implements Localizable { public id: AbilityId; @@ -4016,6 +4017,7 @@ export class CommanderAbAttr extends AbAttr { return ( globalScene.currentBattle?.double && !isNullOrUndefined(ally) && + ally.isActive(true) && ally.species.speciesId === SpeciesId.DONDOZO && !(ally.isFainted() || ally.getTag(BattlerTagType.COMMANDED)) ); @@ -6093,10 +6095,19 @@ 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); - phaseManager.unshiftNew("MovePhase", dancer, target, move, true, true); + phaseManager.pushNew("MovePhase", dancer, target, move, true, true, false, MovePhaseTimingModifier.FIRST); } else if (move.getMove().is("SelfStatusMove")) { // If the move is a SelfStatusMove (ie. Swords Dance) the Dancer should replicate it on itself - phaseManager.unshiftNew("MovePhase", dancer, [dancer.getBattlerIndex()], move, true, true); + phaseManager.pushNew( + "MovePhase", + dancer, + [dancer.getBattlerIndex()], + move, + true, + true, + false, + MovePhaseTimingModifier.FIRST, + ); } } } diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 8a0026a4ced..227f47e31b9 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -6219,7 +6219,7 @@ export class RevivalBlessingAttr extends MoveEffectAttr { if (user.fieldPosition === FieldPosition.CENTER) { user.setFieldPosition(FieldPosition.LEFT); } - globalScene.phaseManager.unshiftNew("SwitchSummonPhase", SwitchType.SWITCH, allyPokemon.getFieldIndex(), slotIndex, false, false); + globalScene.phaseManager.pushNew("SwitchSummonPhase", SwitchType.SWITCH, allyPokemon.getFieldIndex(), slotIndex, false, false); } } return true; @@ -6769,7 +6769,7 @@ class CallMoveAttr extends OverrideMoveEffectAttr { : [ this.hasTarget ? target.getBattlerIndex() : moveTargets.targets[user.randBattleSeedInt(moveTargets.targets.length)] ]; // account for Mirror Move having a target already user.getMoveQueue().push({ move: move.id, targets: targets, virtual: true, ignorePP: true }); globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", move.id); - globalScene.phaseManager.unshiftNew("MovePhase", user, targets, new PokemonMove(move.id, 0, 0, true), true, true); + globalScene.phaseManager.pushNew("MovePhase", user, targets, new PokemonMove(move.id, 0, 0, true), true, true, false, MovePhaseTimingModifier.FIRST); return true; } } @@ -6998,7 +6998,7 @@ export class NaturePowerAttr extends OverrideMoveEffectAttr { user.getMoveQueue().push({ move: moveId, targets: [ target.getBattlerIndex() ], ignorePP: true }); globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", moveId); - globalScene.phaseManager.unshiftNew("MovePhase", user, [ target.getBattlerIndex() ], new PokemonMove(moveId, 0, 0, true), true); + globalScene.phaseManager.pushNew("MovePhase", user, [ target.getBattlerIndex() ], new PokemonMove(moveId, 0, 0, true), true); return true; } } @@ -7085,7 +7085,7 @@ export class RepeatMoveAttr extends MoveEffectAttr { })); target.getMoveQueue().unshift({ move: lastMove.move, targets: moveTargets, ignorePP: false }); target.turnData.extraTurns++; - globalScene.phaseManager.appendNewToPhase("MoveEndPhase", "MovePhase", target, moveTargets, movesetMove, false, false, false, MovePhaseTimingModifier.FIRST); + globalScene.phaseManager.pushNew("MovePhase", target, moveTargets, movesetMove, false, false, false, MovePhaseTimingModifier.FIRST); return true; } diff --git a/src/phase-manager.ts b/src/phase-manager.ts index fb9f7914b41..cd67f144496 100644 --- a/src/phase-manager.ts +++ b/src/phase-manager.ts @@ -2,7 +2,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 { globalScene } from "#app/global-scene"; -import { ActivatePriorityQueuePhase } from "#app/phases/activate-priority-queue-phase"; import { AddEnemyBuffModifierPhase } from "#app/phases/add-enemy-buff-modifier-phase"; import { AttemptCapturePhase } from "#app/phases/attempt-capture-phase"; import { AttemptRunPhase } from "#app/phases/attempt-run-phase"; @@ -116,7 +115,6 @@ import type { PokemonMove } from "#app/data/moves/pokemon-move"; * This allows for easy creation of new phases without needing to import each phase individually. */ const PHASES = Object.freeze({ - ActivatePriorityQueuePhase, AddEnemyBuffModifierPhase, AttemptCapturePhase, AttemptRunPhase, @@ -263,7 +261,7 @@ export class PhaseManager { */ pushPhase(phase: Phase, defer = false): void { if (this.dynamicQueueManager.isDynamicPhase(phase.phaseName)) { - this.pushDynamicPhase(phase); + this.dynamicQueueManager.queueDynamicPhase(phase); } else { (!defer ? this.phaseQueue : this.nextCommandPhaseQueue).push(phase); } @@ -344,6 +342,13 @@ export class PhaseManager { this.conditionalQueue = []; } + if (this.phaseQueue[0].is("WeatherEffectPhase")) { + const dynamicPhase = this.dynamicQueueManager.popNextPhase(); + if (dynamicPhase) { + this.phaseQueue.unshift(dynamicPhase); + } + } + this.currentPhase = this.phaseQueue.shift() ?? null; const unactivatedConditionalPhases: [() => boolean, Phase][] = []; @@ -418,6 +423,9 @@ export class PhaseManager { } tryRemovePhase(phaseFilter: PhaseConditionFunc): boolean { + if (this.dynamicQueueManager.removePhase(phaseFilter)) { + return true; + } const phaseIndex = this.phaseQueue.findIndex(phaseFilter); if (phaseIndex > -1) { this.phaseQueue.splice(phaseIndex, 1); @@ -450,22 +458,15 @@ export class PhaseManager { * @param targetPhase - The phase to search for in phaseQueue * @returns boolean if a targetPhase was found and added */ - prependToPhase(phase: Phase, targetPhase: PhaseString): boolean { - const insertPhase = this.dynamicQueueManager.isDynamicPhase(phase.phaseName) - ? new ActivatePriorityQueuePhase(phase.phaseName) - : phase; + prependToPhase(phase: Phase, targetPhase: StaticPhaseString): boolean { const target = PHASES[targetPhase]; const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof target); - if (this.dynamicQueueManager.isDynamicPhase(phase.phaseName)) { - this.dynamicQueueManager.queueDynamicPhase(phase); - } - if (targetIndex !== -1) { - this.phaseQueue.splice(targetIndex, 0, insertPhase); + this.phaseQueue.splice(targetIndex, 0, phase); return true; } - this.unshiftPhase(insertPhase); + this.unshiftPhase(phase); return false; } @@ -478,60 +479,28 @@ export class PhaseManager { * @returns `true` if a `targetPhase` was found to append to */ appendToPhase(phase: Phase, targetPhase: StaticPhaseString, condition?: PhaseConditionFunc): boolean { - const insertPhase = this.dynamicQueueManager.isDynamicPhase(phase.phaseName) - ? new ActivatePriorityQueuePhase(phase.phaseName) - : phase; const target = PHASES[targetPhase]; const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof target && (!condition || condition(ph))); - if (this.dynamicQueueManager.isDynamicPhase(phase.phaseName)) { - this.dynamicQueueManager.queueDynamicPhase(phase); - } - if (targetIndex !== -1 && this.phaseQueue.length > targetIndex) { - this.phaseQueue.splice(targetIndex + 1, 0, insertPhase); + this.phaseQueue.splice(targetIndex + 1, 0, phase); return true; } - this.unshiftPhase(insertPhase); + this.unshiftPhase(phase); return false; } - /** - * 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 { - this.pushNew("ActivatePriorityQueuePhase", phase.phaseName); - this.dynamicQueueManager.queueDynamicPhase(phase); - } - /** * 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(): void { - const phase = this.dynamicQueueManager.popNextPhase(); + public startNextDynamicPhase(type?: DynamicPhaseString): void { + const phase = this.dynamicQueueManager.popNextPhase(type); 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 { - this.unshiftNew("ActivatePriorityQueuePhase", phase.phaseName); - this.dynamicQueueManager.queueDynamicPhase(phase); - } - /** * Adds a MessagePhase, either to PhaseQueuePrepend or nextCommandPhaseQueue * @param message - string for MessagePhase @@ -583,6 +552,11 @@ 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; + } if (this.nextCommandPhaseQueue.length) { this.phaseQueue.push(...this.nextCommandPhaseQueue); this.nextCommandPhaseQueue.splice(0, this.nextCommandPhaseQueue.length); @@ -624,7 +598,10 @@ 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)); } @@ -637,7 +614,7 @@ export class PhaseManager { * @returns `true` if a `targetPhase` was found to prepend to */ public prependNewToPhase( - targetPhase: PhaseString, + targetPhase: StaticPhaseString, phase: T, ...args: ConstructorParameters ): boolean { @@ -660,13 +637,6 @@ export class PhaseManager { return this.appendToPhase(this.create(phase, ...args), targetPhase); } - public startNewDynamicPhase( - phase: T, - ...args: ConstructorParameters - ): void { - this.startDynamicPhase(this.create(phase, ...args)); - } - public forceMoveNext(phaseCondition: PhaseConditionFunc) { this.dynamicQueueManager.setMoveTimingModifier(phaseCondition, MovePhaseTimingModifier.FIRST); } diff --git a/src/phases/activate-priority-queue-phase.ts b/src/phases/activate-priority-queue-phase.ts deleted file mode 100644 index 4e467446055..00000000000 --- a/src/phases/activate-priority-queue-phase.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { PhaseString } from "#app/@types/phase-types"; -import { globalScene } from "#app/global-scene"; -import { Phase } from "#app/phase"; - -export class ActivatePriorityQueuePhase extends Phase { - public readonly phaseName = "ActivatePriorityQueuePhase"; - private readonly type: PhaseString; - - constructor(type: PhaseString) { - super(); - this.type = type; - } - - override start() { - super.start(); - globalScene.phaseManager.startNextDynamicPhase(); - this.end(); - } - - public getType(): PhaseString { - return this.type; - } -} diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 2af7e93a6ac..6a3384f99ed 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -175,8 +175,7 @@ export class MoveEffectPhase extends PokemonPhase { globalScene.phaseManager.unshiftNew("HideAbilityPhase"); } - globalScene.phaseManager.appendNewToPhase( - "MoveEndPhase", + globalScene.phaseManager.pushNew( "MovePhase", target, newTargets, diff --git a/src/phases/post-summon-phase.ts b/src/phases/post-summon-phase.ts index 26fffd1b024..4abeca9953e 100644 --- a/src/phases/post-summon-phase.ts +++ b/src/phases/post-summon-phase.ts @@ -28,12 +28,19 @@ export class PostSummonPhase extends PokemonPhase { const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); for (const p of field) { - applyAbAttrs("CommanderAbAttr", p, null, false); + if (p.isActive(true)) { + applyAbAttrs("CommanderAbAttr", p, null, false); + } } this.end(); } + override end() { + globalScene.phaseManager.startNextDynamicPhase("PostSummonPhase"); + super.end(); + } + public getPriority() { return 0; } diff --git a/src/phases/summon-phase.ts b/src/phases/summon-phase.ts index 8ed33e12870..dc916a67ebe 100644 --- a/src/phases/summon-phase.ts +++ b/src/phases/summon-phase.ts @@ -289,6 +289,7 @@ export class SummonPhase extends PartyMemberPokemonPhase { queuePostSummon(): void { globalScene.phaseManager.pushNew("PostSummonPhase", this.getPokemon().getBattlerIndex()); + globalScene.phaseManager.startNextDynamicPhase("PostSummonPhase"); } end() { diff --git a/src/phases/switch-phase.ts b/src/phases/switch-phase.ts index 8d18e29e6a6..94c4800c823 100644 --- a/src/phases/switch-phase.ts +++ b/src/phases/switch-phase.ts @@ -79,7 +79,7 @@ export class SwitchPhase extends BattlePhase { p => p.is("PostSummonPhase") && p.player && p.fieldIndex === this.fieldIndex, ); const switchType = option === PartyOption.PASS_BATON ? SwitchType.BATON_PASS : this.switchType; - globalScene.phaseManager.unshiftNew("SwitchSummonPhase", switchType, fieldIndex, slotIndex, this.doReturn); + globalScene.phaseManager.pushNew("SwitchSummonPhase", switchType, fieldIndex, slotIndex, this.doReturn); } globalScene.ui.setMode(UiMode.MESSAGE).then(() => super.end()); }, diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index af03cc42b54..7d1403b8de9 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -245,7 +245,7 @@ export class SwitchSummonPhase extends SummonPhase { } queuePostSummon(): void { - globalScene.phaseManager.startNewDynamicPhase("PostSummonPhase", this.getPokemon().getBattlerIndex()); + globalScene.phaseManager.pushNew("PostSummonPhase", this.getPokemon().getBattlerIndex()); } /** diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index 887076b37a5..c9a9b2a57a4 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -132,7 +132,7 @@ export class TurnStartPhase extends FieldPhase { case Command.POKEMON: { const switchType = turnCommand.args?.[0] ? SwitchType.BATON_PASS : SwitchType.SWITCH; - phaseManager.unshiftNew( + phaseManager.pushNew( "SwitchSummonPhase", switchType, pokemon.getFieldIndex(), diff --git a/src/queues/dynamic-queue-manager.ts b/src/queues/dynamic-queue-manager.ts index edd2afa35f6..1e8972863d4 100644 --- a/src/queues/dynamic-queue-manager.ts +++ b/src/queues/dynamic-queue-manager.ts @@ -1,5 +1,5 @@ import type { PhaseConditionFunc } from "#app/@types/phase-condition"; -import type { PhaseString } from "#app/@types/phase-types"; +import type { DynamicPhaseString, PhaseString } from "#app/@types/phase-types"; import type { PokemonMove } from "#app/data/moves/pokemon-move"; import type { Phase } from "#app/phase"; import { MovePhasePriorityQueue } from "#app/queues/move-phase-priority-queue"; @@ -29,8 +29,11 @@ export class DynamicQueueManager { this.dynamicPhaseMap.get(phase.phaseName)?.push(phase); } - public popNextPhase(): Phase | undefined { - return [...this.dynamicPhaseMap.values()].find(queue => !queue.isEmpty())?.pop(); + 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 { diff --git a/src/queues/move-phase-priority-queue.ts b/src/queues/move-phase-priority-queue.ts index dd01fcbb210..eb9d834c7c0 100644 --- a/src/queues/move-phase-priority-queue.ts +++ b/src/queues/move-phase-priority-queue.ts @@ -10,6 +10,7 @@ export class MovePhasePriorityQueue extends PokemonPhasePriorityQueue public override reorder(): void { super.reorder(); this.sortPostSpeed(); + console.log(this.queue.map(p => p.getPokemon().name)); } public setTimingModifier(condition: PhaseConditionFunc, modifier: MovePhaseTimingModifier): void { diff --git a/src/queues/post-summon-phase-priority-queue.ts b/src/queues/post-summon-phase-priority-queue.ts index 8843e5514a7..39de4ce0193 100644 --- a/src/queues/post-summon-phase-priority-queue.ts +++ b/src/queues/post-summon-phase-priority-queue.ts @@ -1,5 +1,3 @@ -import { globalScene } from "#app/global-scene"; -import { ActivatePriorityQueuePhase } from "#app/phases/activate-priority-queue-phase"; 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"; @@ -30,13 +28,10 @@ export class PostSummonPhasePriorityQueue extends PokemonPhasePriorityQueue { - this.queue.push(new PostSummonActivateAbilityPhase(phasePokemon.getBattlerIndex(), priority, !!idx)); - globalScene.phaseManager.appendToPhase( - new ActivatePriorityQueuePhase("PostSummonPhase"), - "ActivatePriorityQueuePhase", - (p: ActivatePriorityQueuePhase) => p.getType() === "PostSummonPhase", + phasePokemon + .getAbilityPriorities() + .forEach((priority, idx) => + this.queue.push(new PostSummonActivateAbilityPhase(phasePokemon.getBattlerIndex(), priority, !!idx)), ); - }); } } From c790f2e84fce5201d95bec3d0a996aa135a1b5a7 Mon Sep 17 00:00:00 2001 From: Dean Date: Sun, 15 Jun 2025 23:05:09 -0700 Subject: [PATCH 17/28] Fix turn order tracking --- src/data/moves/move.ts | 23 +---------------------- src/phases/turn-end-phase.ts | 1 + src/queues/dynamic-queue-manager.ts | 9 +++++++++ src/queues/move-phase-priority-queue.ts | 21 +++++++++++++++++++++ test/moves/fusion_flare_bolt.test.ts | 14 +++++++------- 5 files changed, 39 insertions(+), 29 deletions(-) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 227f47e31b9..a55e8c94803 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -4429,28 +4429,7 @@ export class LastMoveDoublePowerAttr extends VariablePowerAttr { */ apply(user: Pokemon, _target: Pokemon, _move: Move, args: any[]): boolean { const power = args[0] as NumberHolder; - const enemy = user.getOpponent(0); - const pokemonActed: Pokemon[] = []; - - if (enemy?.turnData.acted) { - pokemonActed.push(enemy); - } - - if (globalScene.currentBattle.double) { - const userAlly = user.getAlly(); - const enemyAlly = enemy?.getAlly(); - - if (userAlly?.turnData.acted) { - pokemonActed.push(userAlly); - } - if (enemyAlly?.turnData.acted) { - pokemonActed.push(enemyAlly); - } - } - - pokemonActed.sort((a, b) => b.turnData.order - a.turnData.order); - - for (const p of pokemonActed) { + for (const p of globalScene.phaseManager.dynamicQueueManager.getLastTurnOrder().slice(0, -1).reverse()) { const [ lastMove ] = p.getLastXMoves(1); if (lastMove.result !== MoveResult.FAIL) { if ((lastMove.result === MoveResult.SUCCESS) && (lastMove.move === this.move)) { diff --git a/src/phases/turn-end-phase.ts b/src/phases/turn-end-phase.ts index ab46292c1d2..18374045871 100644 --- a/src/phases/turn-end-phase.ts +++ b/src/phases/turn-end-phase.ts @@ -23,6 +23,7 @@ export class TurnEndPhase extends FieldPhase { globalScene.currentBattle.incrementTurn(); globalScene.eventTarget.dispatchEvent(new TurnEndEvent(globalScene.currentBattle.turn)); + globalScene.phaseManager.dynamicQueueManager.clearLastTurnOrder(); globalScene.phaseManager.hideAbilityBar(); diff --git a/src/queues/dynamic-queue-manager.ts b/src/queues/dynamic-queue-manager.ts index 1e8972863d4..c2e5d52ca01 100644 --- a/src/queues/dynamic-queue-manager.ts +++ b/src/queues/dynamic-queue-manager.ts @@ -1,6 +1,7 @@ import type { PhaseConditionFunc } from "#app/@types/phase-condition"; import type { DynamicPhaseString, PhaseString } 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 { MovePhasePriorityQueue } from "#app/queues/move-phase-priority-queue"; import type { PhasePriorityQueue } from "#app/queues/phase-priority-queue"; @@ -69,6 +70,14 @@ export class DynamicQueueManager { this.getMovePhaseQueue().setMoveOrder(order); } + public getLastTurnOrder(): Pokemon[] { + return this.getMovePhaseQueue().getTurnOrder(); + } + + public clearLastTurnOrder(): void { + this.getMovePhaseQueue().clearTurnOrder(); + } + private getMovePhaseQueue(): MovePhasePriorityQueue { return this.dynamicPhaseMap.get("MovePhase") as MovePhasePriorityQueue; } diff --git a/src/queues/move-phase-priority-queue.ts b/src/queues/move-phase-priority-queue.ts index eb9d834c7c0..eee61e77c46 100644 --- a/src/queues/move-phase-priority-queue.ts +++ b/src/queues/move-phase-priority-queue.ts @@ -1,5 +1,6 @@ import type { PhaseConditionFunc } from "#app/@types/phase-condition"; import type { PokemonMove } from "#app/data/moves/pokemon-move"; +import type Pokemon from "#app/field/pokemon"; import type { MovePhase } from "#app/phases/move-phase"; import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue"; import { isNullOrUndefined } from "#app/utils/common"; @@ -7,6 +8,8 @@ import type { BattlerIndex } from "#enums/battler-index"; import type { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; export class MovePhasePriorityQueue extends PokemonPhasePriorityQueue { + private lastTurnOrder: Pokemon[] = []; + public override reorder(): void { super.reorder(); this.sortPostSpeed(); @@ -31,8 +34,26 @@ export class MovePhasePriorityQueue extends PokemonPhasePriorityQueue this.setOrder = order; } + public override pop(): MovePhase | undefined { + this.reorder(); + const phase = this.queue.shift(); + if (phase) { + this.lastTurnOrder.push(phase.pokemon); + } + return phase; + } + + public getTurnOrder(): Pokemon[] { + return this.lastTurnOrder; + } + + public clearTurnOrder(): void { + this.lastTurnOrder = []; + } + public override clear(): void { this.setOrder = undefined; + this.lastTurnOrder = []; super.clear(); } diff --git a/test/moves/fusion_flare_bolt.test.ts b/test/moves/fusion_flare_bolt.test.ts index f10ede8717c..f9e55b72f6c 100644 --- a/test/moves/fusion_flare_bolt.test.ts +++ b/test/moves/fusion_flare_bolt.test.ts @@ -64,7 +64,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id); await game.phaseInterceptor.to(DamageAnimPhase, false); expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(200); - }, 20000); + }); it("FUSION_BOLT should double power of subsequent FUSION_FLARE", async () => { await game.classicMode.startBattle([SpeciesId.ZEKROM, SpeciesId.ZEKROM]); @@ -84,7 +84,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id); await game.phaseInterceptor.to(DamageAnimPhase, false); expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200); - }, 20000); + }); it("FUSION_FLARE should double power of subsequent FUSION_BOLT if a move failed in between", async () => { await game.classicMode.startBattle([SpeciesId.ZEKROM, SpeciesId.ZEKROM]); @@ -109,7 +109,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id); await game.phaseInterceptor.to(DamageAnimPhase, false); expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(200); - }, 20000); + }); it("FUSION_FLARE should not double power of subsequent FUSION_BOLT if a move succeeded in between", async () => { game.override.enemyMoveset(MoveId.SPLASH); @@ -134,7 +134,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id); await game.phaseInterceptor.to(DamageAnimPhase, false); expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(100); - }, 20000); + }); it("FUSION_FLARE should double power of subsequent FUSION_BOLT if moves are aimed at allies", async () => { await game.classicMode.startBattle([SpeciesId.ZEKROM, SpeciesId.RESHIRAM]); @@ -154,7 +154,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id); await game.phaseInterceptor.to(DamageAnimPhase, false); expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200); - }, 20000); + }); it("FUSION_FLARE and FUSION_BOLT alternating throughout turn should double power of subsequent moves", async () => { game.override.enemyMoveset(fusionFlare.id); @@ -208,7 +208,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id); await game.phaseInterceptor.to(DamageAnimPhase, false); expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200); - }, 20000); + }); it("FUSION_FLARE and FUSION_BOLT alternating throughout turn should double power of subsequent moves if moves are aimed at allies", async () => { game.override.enemyMoveset(fusionFlare.id); @@ -262,5 +262,5 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id); await game.phaseInterceptor.to(DamageAnimPhase, false); expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200); - }, 20000); + }); }); From 0128d673b5d167dbf04b21cc11faed0f88db7877 Mon Sep 17 00:00:00 2001 From: Dean Date: Sun, 15 Jun 2025 23:05:31 -0700 Subject: [PATCH 18/28] Add move header queue to fix ordering --- src/@types/phase-types.ts | 2 +- src/field/pokemon.ts | 1 - src/phases/move-header-phase.ts | 16 +++++++--------- src/phases/turn-start-phase.ts | 5 +---- src/queues/dynamic-queue-manager.ts | 3 +++ 5 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/@types/phase-types.ts b/src/@types/phase-types.ts index dab8bfeea12..9abc3dadf8d 100644 --- a/src/@types/phase-types.ts +++ b/src/@types/phase-types.ts @@ -24,6 +24,6 @@ export type PhaseClass = PhaseConstructorMap[keyof PhaseConstructorMap]; */ export type PhaseString = keyof PhaseMap; -export type DynamicPhaseString = "PostSummonPhase" | "SwitchSummonPhase" | "MovePhase"; +export type DynamicPhaseString = "PostSummonPhase" | "SwitchSummonPhase" | "MovePhase" | "MoveHeaderPhase"; export type StaticPhaseString = Exclude; diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index f12b2d44fb7..67b04b6b49e 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -6883,7 +6883,6 @@ export class PokemonTurnData { public singleHitDamageDealt = 0; public damageTaken = 0; public attacksReceived: AttackMoveResult[] = []; - public order: number; public statStagesIncreased = false; public statStagesDecreased = false; public moveEffectiveness: TypeDamageMultiplier | null = null; diff --git a/src/phases/move-header-phase.ts b/src/phases/move-header-phase.ts index 8c2d184c3f5..a5bf6083898 100644 --- a/src/phases/move-header-phase.ts +++ b/src/phases/move-header-phase.ts @@ -1,29 +1,27 @@ import { applyMoveAttrs } from "#app/data/moves/apply-attrs"; import type { PokemonMove } from "#app/data/moves/pokemon-move"; -import type Pokemon from "#app/field/pokemon"; -import { BattlePhase } from "./battle-phase"; +import { PokemonPhase } from "#app/phases/pokemon-phase"; +import type { BattlerIndex } from "#enums/battler-index"; -export class MoveHeaderPhase extends BattlePhase { +export class MoveHeaderPhase extends PokemonPhase { public readonly phaseName = "MoveHeaderPhase"; - public pokemon: Pokemon; public move: PokemonMove; - constructor(pokemon: Pokemon, move: PokemonMove) { - super(); + constructor(battlerIndex: BattlerIndex, move: PokemonMove) { + super(battlerIndex); - this.pokemon = pokemon; this.move = move; } canMove(): boolean { - return this.pokemon.isActive(true) && this.move.isUsable(this.pokemon); + return this.getPokemon().isActive(true) && this.move.isUsable(this.getPokemon()); } start() { super.start(); if (this.canMove()) { - applyMoveAttrs("MoveHeaderAttr", this.pokemon, null, this.move.getMove()); + applyMoveAttrs("MoveHeaderAttr", this.getPokemon(), null, this.move.getMove()); } this.end(); } diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index c9a9b2a57a4..b1070243c4e 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -58,8 +58,6 @@ export class TurnStartPhase extends FieldPhase { const activeField = globalScene.getField(true); const moveOrder = this.getCommandOrder(); - let orderIndex = 0; - applyInSpeedOrder(activeField, (p: Pokemon) => { const preTurnCommand = globalScene.currentBattle.preTurnCommands[p.getBattlerIndex()]; @@ -91,7 +89,6 @@ export class TurnStartPhase extends FieldPhase { case Command.FIGHT: { const queuedMove = turnCommand.move; - pokemon.turnData.order = orderIndex++; if (!queuedMove) { continue; } @@ -99,7 +96,7 @@ export class TurnStartPhase extends FieldPhase { pokemon.getMoveset().find(m => m.moveId === queuedMove.move && m.ppUsed < m.getMovePp()) || new PokemonMove(queuedMove.move); if (move.getMove().hasAttr("MoveHeaderAttr")) { - phaseManager.unshiftNew("MoveHeaderPhase", pokemon, move); + phaseManager.pushNew("MoveHeaderPhase", pokemon.getBattlerIndex(), move); } if (pokemon.isPlayer()) { if (turnCommand.cursor === -1) { diff --git a/src/queues/dynamic-queue-manager.ts b/src/queues/dynamic-queue-manager.ts index c2e5d52ca01..14c45f2b6c6 100644 --- a/src/queues/dynamic-queue-manager.ts +++ b/src/queues/dynamic-queue-manager.ts @@ -3,8 +3,10 @@ import type { DynamicPhaseString, PhaseString } 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"; import { PostSummonPhasePriorityQueue } from "#app/queues/post-summon-phase-priority-queue"; import { SwitchSummonPhasePriorityQueue } from "#app/queues/switch-summon-phase-priority-queue"; import type { BattlerIndex } from "#enums/battler-index"; @@ -16,6 +18,7 @@ export class DynamicQueueManager { 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()); } From 78f0b5d85332341273a87e255fe9c3bb9a47e1ed Mon Sep 17 00:00:00 2001 From: Dean Date: Sun, 15 Jun 2025 23:13:16 -0700 Subject: [PATCH 19/28] Fix abilities activating immediately on summon --- src/phases/summon-phase.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/phases/summon-phase.ts b/src/phases/summon-phase.ts index dc916a67ebe..8ed33e12870 100644 --- a/src/phases/summon-phase.ts +++ b/src/phases/summon-phase.ts @@ -289,7 +289,6 @@ export class SummonPhase extends PartyMemberPokemonPhase { queuePostSummon(): void { globalScene.phaseManager.pushNew("PostSummonPhase", this.getPokemon().getBattlerIndex()); - globalScene.phaseManager.startNextDynamicPhase("PostSummonPhase"); } end() { From 9b9cd3a5229fa52eea6135bfa85b7957c6964c45 Mon Sep 17 00:00:00 2001 From: Dean Date: Sun, 15 Jun 2025 23:20:10 -0700 Subject: [PATCH 20/28] Fix postsummonphases being shuffled (need to handle speed ties differently here) --- src/queues/post-summon-phase-priority-queue.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/queues/post-summon-phase-priority-queue.ts b/src/queues/post-summon-phase-priority-queue.ts index 39de4ce0193..95842a9fb64 100644 --- a/src/queues/post-summon-phase-priority-queue.ts +++ b/src/queues/post-summon-phase-priority-queue.ts @@ -1,6 +1,7 @@ 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"; +import { sortInSpeedOrder } from "#app/utils/speed-order"; /** * Priority Queue for {@linkcode PostSummonPhase} and {@linkcode PostSummonActivateAbilityPhase} @@ -10,7 +11,7 @@ import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-qu export class PostSummonPhasePriorityQueue extends PokemonPhasePriorityQueue { public override reorder(): void { - super.reorder(); + this.queue = sortInSpeedOrder(this.queue, false); this.queue.sort((phaseA: PostSummonPhase, phaseB: PostSummonPhase) => { return phaseB.getPriority() - phaseA.getPriority(); }); From 88e447fe7e208fb88e881644ff76f69b76b8aaeb Mon Sep 17 00:00:00 2001 From: Dean Date: Sun, 15 Jun 2025 23:20:33 -0700 Subject: [PATCH 21/28] Update speed order function --- src/utils/speed-order.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/speed-order.ts b/src/utils/speed-order.ts index fcf5a396579..301c19701d1 100644 --- a/src/utils/speed-order.ts +++ b/src/utils/speed-order.ts @@ -12,8 +12,8 @@ export function applyInSpeedOrder(pokemonList: T[], callback: sortInSpeedOrder(pokemonList).forEach(pokemon => callback(pokemon)); } -export function sortInSpeedOrder(pokemonList: T[]): T[] { - pokemonList = shuffle(pokemonList); +export function sortInSpeedOrder(pokemonList: T[], shuffleFirst = true): T[] { + pokemonList = shuffleFirst ? shuffle(pokemonList) : pokemonList; sortBySpeed(pokemonList); return pokemonList; } From 87ed788e8f014d689fcd26f2e26e7eeb186b2e8a Mon Sep 17 00:00:00 2001 From: Dean Date: Mon, 16 Jun 2025 10:46:15 -0700 Subject: [PATCH 22/28] Add StaticSwitchSummonPhase --- src/data/abilities/ability.ts | 2 +- src/data/moves/move.ts | 10 ++++++---- src/field/pokemon.ts | 2 +- src/phase-manager.ts | 2 ++ src/phases/faint-phase.ts | 9 ++++++++- src/phases/revival-blessing-phase.ts | 4 ++-- src/phases/static-switch-summon-phase.ts | 5 +++++ src/phases/summon-phase.ts | 7 ++++++- src/phases/switch-phase.ts | 8 +++++++- src/phases/switch-summon-phase.ts | 2 +- src/queues/switch-summon-phase-priority-queue.ts | 2 +- 11 files changed, 40 insertions(+), 13 deletions(-) create mode 100644 src/phases/static-switch-summon-phase.ts diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index faa9a8afafe..621dd011650 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -7451,7 +7451,7 @@ class ForceSwitchOutHelper { : 0; globalScene.phaseManager.prependNewToPhase( "MoveEndPhase", - "SwitchSummonPhase", + "StaticSwitchSummonPhase", this.switchType, switchOutTarget.getFieldIndex(), summonIndex, diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index a55e8c94803..79656c7fc40 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -6192,13 +6192,15 @@ export class RevivalBlessingAttr extends MoveEffectAttr { if (globalScene.currentBattle.double && globalScene.getEnemyParty().length > 1 && !isNullOrUndefined(allyPokemon)) { // Handle cases where revived pokemon needs to get switched in on same turn if (allyPokemon.isFainted() || allyPokemon === pokemon) { + // Enemy switch phase should be removed and replaced with the revived pkmn switching in + globalScene.phaseManager.tryRemovePhase((phase: SwitchSummonPhase) => phase.is("StaticSwitchSummonPhase") && phase.getPokemon() === pokemon); // If the pokemon being revived was alive earlier in the turn, cancel its move // (revived pokemon can't move in the turn they're brought back) globalScene.phaseManager.findPhase("MovePhase", (phase: MovePhase) => phase.pokemon === pokemon)?.cancel(); if (user.fieldPosition === FieldPosition.CENTER) { user.setFieldPosition(FieldPosition.LEFT); } - globalScene.phaseManager.pushNew("SwitchSummonPhase", SwitchType.SWITCH, allyPokemon.getFieldIndex(), slotIndex, false, false); + globalScene.phaseManager.unshiftNew("StaticSwitchSummonPhase", SwitchType.SWITCH, allyPokemon.getFieldIndex(), slotIndex, false, false); } } return true; @@ -6278,7 +6280,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { const slotIndex = eligibleNewIndices[user.randBattleSeedInt(eligibleNewIndices.length)]; globalScene.phaseManager.prependNewToPhase( "MoveEndPhase", - "SwitchSummonPhase", + "StaticSwitchSummonPhase", this.switchType, switchOutTarget.getFieldIndex(), slotIndex, @@ -6317,7 +6319,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { switchOutTarget.leaveField(true); const slotIndex = eligibleNewIndices[user.randBattleSeedInt(eligibleNewIndices.length)]; globalScene.phaseManager.prependNewToPhase("MoveEndPhase", - "SwitchSummonPhase", + "StaticSwitchSummonPhase", this.switchType, switchOutTarget.getFieldIndex(), slotIndex, @@ -6327,7 +6329,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { } else { switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); globalScene.phaseManager.prependNewToPhase("MoveEndPhase", - "SwitchSummonPhase", + "StaticSwitchSummonPhase", this.switchType, switchOutTarget.getFieldIndex(), (globalScene.currentBattle.trainer ? globalScene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0), diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 67b04b6b49e..c393c809742 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -5598,7 +5598,7 @@ export class PlayerPokemon extends Pokemon { if (slotIndex >= globalScene.currentBattle.getBattlerCount() && slotIndex < 6) { globalScene.phaseManager.prependNewToPhase( "MoveEndPhase", - "SwitchSummonPhase", + "StaticSwitchSummonPhase", switchType, this.getFieldIndex(), slotIndex, diff --git a/src/phase-manager.ts b/src/phase-manager.ts index cd67f144496..f0a138a02f5 100644 --- a/src/phase-manager.ts +++ b/src/phase-manager.ts @@ -99,6 +99,7 @@ import { DynamicQueueManager } from "#app/queues/dynamic-queue-manager"; import type { PhaseConditionFunc } from "#app/@types/phase-condition"; import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; import type { PokemonMove } from "#app/data/moves/pokemon-move"; +import { StaticSwitchSummonPhase } from "#app/phases/static-switch-summon-phase"; /** * Manager for phases used by battle scene. @@ -189,6 +190,7 @@ const PHASES = Object.freeze({ ShowAbilityPhase, ShowPartyExpBarPhase, ShowTrainerPhase, + StaticSwitchSummonPhase, StatStageChangePhase, SummonMissingPhase, SummonPhase, diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index 675a198d096..9e28829b27b 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -183,7 +183,14 @@ export class FaintPhase extends PokemonPhase { .filter(p => p.isActive() && !p.isOnField() && p.trainerSlot === (pokemon as EnemyPokemon).trainerSlot) .length; if (hasReservePartyMember) { - globalScene.phaseManager.pushNew("SwitchSummonPhase", SwitchType.SWITCH, this.fieldIndex, -1, false, false); + globalScene.phaseManager.pushNew( + "StaticSwitchSummonPhase", + SwitchType.SWITCH, + this.fieldIndex, + -1, + false, + false, + ); } } } diff --git a/src/phases/revival-blessing-phase.ts b/src/phases/revival-blessing-phase.ts index e3e69f7ef25..fcbc276ea71 100644 --- a/src/phases/revival-blessing-phase.ts +++ b/src/phases/revival-blessing-phase.ts @@ -50,7 +50,7 @@ export class RevivalBlessingPhase extends BattlePhase { if (slotIndex <= 1) { // Revived ally pokemon globalScene.phaseManager.unshiftNew( - "SwitchSummonPhase", + "StaticSwitchSummonPhase", SwitchType.SWITCH, pokemon.getFieldIndex(), slotIndex, @@ -61,7 +61,7 @@ export class RevivalBlessingPhase extends BattlePhase { } else if (allyPokemon.isFainted()) { // Revived party pokemon, and ally pokemon is fainted globalScene.phaseManager.unshiftNew( - "SwitchSummonPhase", + "StaticSwitchSummonPhase", SwitchType.SWITCH, allyPokemon.getFieldIndex(), slotIndex, diff --git a/src/phases/static-switch-summon-phase.ts b/src/phases/static-switch-summon-phase.ts new file mode 100644 index 00000000000..5fe8ef17bd0 --- /dev/null +++ b/src/phases/static-switch-summon-phase.ts @@ -0,0 +1,5 @@ +import { SwitchSummonPhase } from "#app/phases/switch-summon-phase"; + +export class StaticSwitchSummonPhase extends SwitchSummonPhase { + public readonly phaseName = "StaticSwitchSummonPhase"; +} diff --git a/src/phases/summon-phase.ts b/src/phases/summon-phase.ts index 8ed33e12870..2c95e7a273f 100644 --- a/src/phases/summon-phase.ts +++ b/src/phases/summon-phase.ts @@ -15,7 +15,12 @@ import { globalScene } from "#app/global-scene"; export class SummonPhase extends PartyMemberPokemonPhase { // The union type is needed to keep typescript happy as these phases extend from SummonPhase - public readonly phaseName: "SummonPhase" | "SummonMissingPhase" | "SwitchSummonPhase" | "ReturnPhase" = "SummonPhase"; + public readonly phaseName: + | "SummonPhase" + | "SummonMissingPhase" + | "SwitchSummonPhase" + | "ReturnPhase" + | "StaticSwitchSummonPhase" = "SummonPhase"; private loaded: boolean; constructor(fieldIndex: number, player = true, loaded = false) { diff --git a/src/phases/switch-phase.ts b/src/phases/switch-phase.ts index 94c4800c823..49d84909895 100644 --- a/src/phases/switch-phase.ts +++ b/src/phases/switch-phase.ts @@ -79,7 +79,13 @@ export class SwitchPhase extends BattlePhase { p => p.is("PostSummonPhase") && p.player && p.fieldIndex === this.fieldIndex, ); const switchType = option === PartyOption.PASS_BATON ? SwitchType.BATON_PASS : this.switchType; - globalScene.phaseManager.pushNew("SwitchSummonPhase", switchType, fieldIndex, slotIndex, this.doReturn); + globalScene.phaseManager.unshiftNew( + "StaticSwitchSummonPhase", + switchType, + fieldIndex, + slotIndex, + this.doReturn, + ); } globalScene.ui.setMode(UiMode.MESSAGE).then(() => super.end()); }, diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index 7d1403b8de9..175621918a2 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -14,7 +14,7 @@ import { SubstituteTag } from "#app/data/battler-tags"; import { SwitchType } from "#enums/switch-type"; export class SwitchSummonPhase extends SummonPhase { - public readonly phaseName: "SwitchSummonPhase" | "ReturnPhase" = "SwitchSummonPhase"; + public readonly phaseName: "SwitchSummonPhase" | "ReturnPhase" | "StaticSwitchSummonPhase" = "SwitchSummonPhase"; private readonly switchType: SwitchType; private readonly slotIndex: number; private readonly doReturn: boolean; diff --git a/src/queues/switch-summon-phase-priority-queue.ts b/src/queues/switch-summon-phase-priority-queue.ts index eb4ad1d7c28..adc1508c275 100644 --- a/src/queues/switch-summon-phase-priority-queue.ts +++ b/src/queues/switch-summon-phase-priority-queue.ts @@ -4,7 +4,7 @@ import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-qu export class SwitchSummonPhasePriorityQueue extends PokemonPhasePriorityQueue { public override push(phase: SwitchSummonPhase): void { // The same pokemon or slot cannot be switched into at the same time - this.queue.filter( + this.queue = this.queue.filter( old => old.getPokemon() !== phase.getPokemon() && !(old.isPlayer() === phase.isPlayer() && old.getFieldIndex() === phase.getFieldIndex()), From f4d3bed3eede585c2749d4af4e26297201dd2ece Mon Sep 17 00:00:00 2001 From: Dean Date: Mon, 16 Jun 2025 19:03:10 -0700 Subject: [PATCH 23/28] Fix magic coat/bounce error from conflict resolution --- src/phases/move-effect-phase.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index fd5e4b6c693..b4b94db40db 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -162,7 +162,7 @@ export class MoveEffectPhase extends PokemonPhase { } /** - * Queue the phaes that should occur when the target reflects the move back to the user + * Queue the phases that should occur when the target reflects the move back to the user * @param user - The {@linkcode Pokemon} using this phase's invoked move * @param target - The {@linkcode Pokemon} that is reflecting the move * TODO: Rework this to use `onApply` of Magic Coat @@ -181,15 +181,13 @@ export class MoveEffectPhase extends PokemonPhase { globalScene.phaseManager.unshiftNew("HideAbilityPhase"); } - this.queuedPhases.push( - globalScene.phaseManager.create( - "MovePhase", - target, - newTargets, - new PokemonMove(this.move.id), - MoveUseMode.REFLECTED, - MovePhaseTimingModifier.FIRST - ), + globalScene.phaseManager.pushNew( + "MovePhase", + target, + newTargets, + new PokemonMove(this.move.id), + MoveUseMode.REFLECTED, + MovePhaseTimingModifier.FIRST, ); } From 8bc6b4b9faaafcc88295b06f7d6db3716fdab72a Mon Sep 17 00:00:00 2001 From: Dean Date: Mon, 16 Jun 2025 20:40:17 -0700 Subject: [PATCH 24/28] Remove conditional queue --- src/phase-manager.ts | 37 +---------------------------------- src/phases/encounter-phase.ts | 24 +---------------------- 2 files changed, 2 insertions(+), 59 deletions(-) diff --git a/src/phase-manager.ts b/src/phase-manager.ts index f0a138a02f5..128ea4e4042 100644 --- a/src/phase-manager.ts +++ b/src/phase-manager.ts @@ -220,7 +220,6 @@ export type PhaseConstructorMap = typeof PHASES; export class PhaseManager { /** PhaseQueue: dequeue/remove the first element to get the next phase */ private phaseQueue: Phase[] = []; - private conditionalQueue: Array<[() => boolean, Phase]> = []; /** PhaseQueuePrepend: is a temp storage of what will be added to PhaseQueue */ private phaseQueuePrepend: Phase[] = []; @@ -242,20 +241,6 @@ export class PhaseManager { return this.standbyPhase; } - /** - * Adds a phase to the conditional queue and ensures it is executed only when the specified condition is met. - * - * This method allows deferring the execution of a phase until certain conditions are met, which is useful for handling - * situations like abilities and entry hazards that depend on specific game states. - * - * @param phase - The phase to be added to the conditional queue. - * @param condition - A function that returns a boolean indicating whether the phase should be executed. - * - */ - pushConditionalPhase(phase: Phase, condition: () => boolean): void { - this.conditionalQueue.push([condition, phase]); - } - /** * Adds a phase to nextCommandPhaseQueue, as long as boolean passed in is false * @param phase {@linkcode Phase} the phase to add @@ -292,7 +277,7 @@ export class PhaseManager { * Clears all phase-related stuff, including all phase queues, the current and standby phases, and a splice index */ clearAllPhases(): void { - for (const queue of [this.phaseQueue, this.phaseQueuePrepend, this.conditionalQueue, this.nextCommandPhaseQueue]) { + for (const queue of [this.phaseQueue, this.phaseQueuePrepend, this.nextCommandPhaseQueue]) { queue.splice(0, queue.length); } this.dynamicQueueManager.clearQueues(); @@ -340,8 +325,6 @@ export class PhaseManager { } if (!this.phaseQueue.length) { this.populatePhaseQueue(); - // Clear the conditionalQueue if there are no phases left in the phaseQueue - this.conditionalQueue = []; } if (this.phaseQueue[0].is("WeatherEffectPhase")) { @@ -353,24 +336,6 @@ export class PhaseManager { this.currentPhase = this.phaseQueue.shift() ?? null; - const unactivatedConditionalPhases: [() => boolean, Phase][] = []; - // Check if there are any conditional phases queued - while (this.conditionalQueue?.length) { - // Retrieve the first conditional phase from the queue - const conditionalPhase = this.conditionalQueue.shift(); - // Evaluate the condition associated with the phase - if (conditionalPhase?.[0]()) { - // If the condition is met, add the phase to the phase queue - this.pushPhase(conditionalPhase[1]); - } else if (conditionalPhase) { - // If the condition is not met, re-add the phase back to the front of the conditional queue - unactivatedConditionalPhases.push(conditionalPhase); - } else { - console.warn("condition phase is undefined/null!", conditionalPhase); - } - } - this.conditionalQueue.push(...unactivatedConditionalPhases); - if (this.currentPhase) { console.log(`%cStart Phase ${this.currentPhase.constructor.name}`, "color:green;"); this.currentPhase.start(); diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index f2c23384627..c35e6750a6b 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -562,29 +562,7 @@ export class EncounterPhase extends BattlePhase { }); if (![BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(globalScene.currentBattle.battleType)) { - enemyField.map(p => - globalScene.phaseManager.pushConditionalPhase( - globalScene.phaseManager.create("PostSummonPhase", p.getBattlerIndex()), - () => { - // if there is not a player party, we can't continue - if (!globalScene.getPlayerParty().length) { - return false; - } - // how many player pokemon are on the field ? - const pokemonsOnFieldCount = globalScene.getPlayerParty().filter(p => p.isOnField()).length; - // if it's a 2vs1, there will never be a 2nd pokemon on our field even - const requiredPokemonsOnField = Math.min( - globalScene.getPlayerParty().filter(p => !p.isFainted()).length, - 2, - ); - // if it's a double, there should be 2, otherwise 1 - if (globalScene.currentBattle.double) { - return pokemonsOnFieldCount === requiredPokemonsOnField; - } - return pokemonsOnFieldCount === 1; - }, - ), - ); + 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())); From 78401189db9944cd0c5caae59f9334627c26aced Mon Sep 17 00:00:00 2001 From: Dean Date: Mon, 16 Jun 2025 20:55:15 -0700 Subject: [PATCH 25/28] Bandaid fix forecast --- src/field/arena.ts | 8 +++++++- src/phases/switch-summon-phase.ts | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/field/arena.ts b/src/field/arena.ts index 8d7e5037852..82f7f5c857b 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -380,9 +380,15 @@ export class Arena { /** * Function to trigger all weather based form changes + * @param source - The Pokemon causing the changes by removing itself from the field */ - triggerWeatherBasedFormChanges(): void { + triggerWeatherBasedFormChanges(source?: Pokemon): void { globalScene.getField(true).forEach(p => { + // TODO - This is a bandaid. Abilities leaving the field needs a better approach than + // calling this method for every switch out that happens + if (p === source) { + return; + } const isCastformWithForecast = p.hasAbility(AbilityId.FORECAST) && p.species.speciesId === SpeciesId.CASTFORM; const isCherrimWithFlowerGift = p.hasAbility(AbilityId.FLOWER_GIFT) && p.species.speciesId === SpeciesId.CHERRIM; diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index 175621918a2..3a78c97c014 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -241,7 +241,7 @@ export class SwitchSummonPhase extends SummonPhase { globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); // Reverts to weather-based forms when weather suppressors (Cloud Nine/Air Lock) are switched out - globalScene.arena.triggerWeatherBasedFormChanges(); + globalScene.arena.triggerWeatherBasedFormChanges(pokemon); } queuePostSummon(): void { From 6e762ad80a0ba764a3a6e6d89bd7890c950063a4 Mon Sep 17 00:00:00 2001 From: Dean Date: Mon, 16 Jun 2025 21:06:53 -0700 Subject: [PATCH 26/28] Fix dancer and baton pass tests --- test/abilities/dancer.test.ts | 4 +++- test/moves/baton_pass.test.ts | 7 +------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/test/abilities/dancer.test.ts b/test/abilities/dancer.test.ts index 2a4a3c36bcc..a2a3ca47288 100644 --- a/test/abilities/dancer.test.ts +++ b/test/abilities/dancer.test.ts @@ -35,6 +35,7 @@ describe("Abilities - Dancer", () => { await game.classicMode.startBattle([SpeciesId.ORICORIO, SpeciesId.FEEBAS]); const [oricorio, feebas] = game.scene.getPlayerField(); + const [magikarp1] = game.scene.getEnemyField(); game.move.changeMoveset(oricorio, [MoveId.SWORDS_DANCE, MoveId.VICTORY_DANCE, MoveId.SPLASH]); game.move.changeMoveset(feebas, [MoveId.SWORDS_DANCE, MoveId.SPLASH]); @@ -44,8 +45,9 @@ describe("Abilities - Dancer", () => { await game.phaseInterceptor.to("MovePhase"); // feebas uses swords dance await game.phaseInterceptor.to("MovePhase", false); // oricorio copies swords dance + // Dancer order will be Magikarp, Oricorio, Magikarp based on set turn order let currentPhase = game.scene.phaseManager.getCurrentPhase() as MovePhase; - expect(currentPhase.pokemon).toBe(oricorio); + expect(currentPhase.pokemon).toBe(magikarp1); expect(currentPhase.move.moveId).toBe(MoveId.SWORDS_DANCE); await game.phaseInterceptor.to("MoveEndPhase"); // end oricorio's move diff --git a/test/moves/baton_pass.test.ts b/test/moves/baton_pass.test.ts index 1b1b0620133..486bb6adb90 100644 --- a/test/moves/baton_pass.test.ts +++ b/test/moves/baton_pass.test.ts @@ -76,12 +76,7 @@ describe("Moves - Baton Pass", () => { expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.SPATK)).toEqual(2); // confirm that a switch actually happened. can't use species because I // can't find a way to override trainer parties with more than 1 pokemon species - expect(game.phaseInterceptor.log.slice(-4)).toEqual([ - "MoveEffectPhase", - "SwitchSummonPhase", - "SummonPhase", - "PostSummonPhase", - ]); + expect(game.scene.getEnemyPokemon()?.summonData.moveHistory.length).toEqual(0); }); it("doesn't transfer effects that aren't transferrable", async () => { From 301e12e92a7aba8d6987fd4b52a6cd62c697cea1 Mon Sep 17 00:00:00 2001 From: Dean Date: Sun, 22 Jun 2025 12:31:50 -0700 Subject: [PATCH 27/28] 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"); + }); } } From 1dd94c45a0c2ccf2ece5df34ab5f0fb4c7d931cb Mon Sep 17 00:00:00 2001 From: Dean Date: Wed, 2 Jul 2025 19:02:28 -0700 Subject: [PATCH 28/28] Merge fixes --- src/data/abilities/ability.ts | 11 +++-------- src/data/moves/move.ts | 2 +- src/phases/turn-start-phase.ts | 3 +-- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index baf19ee3601..d30a2fc18e4 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -5983,11 +5983,6 @@ export class IllusionPostBattleAbAttr extends PostBattleAbAttr { } } -export interface BypassSpeedChanceAbAttrParams extends AbAttrBaseParams { - /** Holds whether the speed check is bypassed after ability application */ - bypass: BooleanHolder; -} - /** * If a Pokémon with this Ability selects a damaging move, it has a 30% chance of going first in its priority bracket. If the Ability activates, this is announced at the start of the turn (after move selection). * @sealed @@ -6003,7 +5998,7 @@ export class BypassSpeedChanceAbAttr extends AbAttr { this.chance = chance; } - override canApply({ bypass, simulated, pokemon }: BypassSpeedChanceAbAttrParams): boolean { + override canApply({ simulated, pokemon }: AbAttrBaseParams): boolean { // TODO: Consider whether we can move the simulated check to the `apply` method // May be difficult as we likely do not want to modify the randBattleSeed const turnCommand = globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()]; @@ -6020,11 +6015,11 @@ export class BypassSpeedChanceAbAttr extends AbAttr { /** * bypass move order in their priority bracket when pokemon choose damaging move */ - override apply({ pokemon }: BypassSpeedChanceAbAttrParams): void { + override apply({ pokemon }: AbAttrBaseParams): void { pokemon.addTag(BattlerTagType.BYPASS_SPEED); } - override getTriggerMessage({ pokemon }: BypassSpeedChanceAbAttrParams, _abilityName: string): string { + override getTriggerMessage({ pokemon }: AbAttrBaseParams, _abilityName: string): string { return i18next.t("abilityTriggers:quickDraw", { pokemonName: getPokemonNameWithAffix(pokemon) }); } } diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 17b22c46d9a..071ff6e6337 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -874,7 +874,7 @@ export default abstract class Move implements Localizable { getPriorityModifier(user: Pokemon, simulated = true): MovePriorityModifier { const modifierHolder = new NumberHolder(MovePriorityModifier.NORMAL); - applyAbAttrs("ChangeMovePriorityModifierAbAttr", user, null, simulated, this, modifierHolder); + applyAbAttrs("ChangeMovePriorityModifierAbAttr", {pokemon: user, simulated: simulated, move: this, priority: modifierHolder}); modifierHolder.value = user.getTag(BattlerTagType.BYPASS_SPEED) ? MovePriorityModifier.FIRST_IN_BRACKET : modifierHolder.value; return modifierHolder.value; } diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index 3e70d79cd06..fde4bc9ae58 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -1,4 +1,3 @@ -import { Stat } from "#app/enums/stat"; import { PokemonMove } from "#app/data/moves/pokemon-move"; import { Command } from "#enums/command"; import { FieldPhase } from "./field-phase"; @@ -74,7 +73,7 @@ export class TurnStartPhase extends FieldPhase { const phaseManager = globalScene.phaseManager; applyInSpeedOrder(activeField, (p: Pokemon) => { - applyAbAttrs("BypassSpeedChanceAbAttr", p, null); + applyAbAttrs("BypassSpeedChanceAbAttr", { pokemon: p }); globalScene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p); });