): void;
+ /**
+ * Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}.
+ * @param expectedType - The {@linkcode ArenaTagType} of the desired tag
+ * @param side - The {@linkcode ArenaTagSide | side(s) of the field} the tag should affect; default {@linkcode ArenaTagSide.BOTH}
+ */
+ toHaveArenaTag(expectedType: ArenaTagType, side?: ArenaTagSide): void;
+
+ /**
+ * Check whether the current {@linkcode Arena} contains the given {@linkcode PositionalTag}.
+ * @param expectedTag - A partially-filled `PositionalTag` containing the desired properties
+ */
+ toHavePositionalTag(expectedTag: toHavePositionalTagOptions
): void;
+ /**
+ * Check whether the current {@linkcode Arena} contains the given number of {@linkcode PositionalTag}s.
+ * @param expectedType - The {@linkcode PositionalTagType} of the desired tag
+ * @param count - The number of instances of `expectedType` that should be active;
+ * defaults to `1` and must be within the range `[0, 4]`
+ */
+ toHavePositionalTag(expectedType: PositionalTagType, count?: number): void;
+}
+
+// #endregion Arena Matchers
+
+// #region Pokemon Matchers
+interface PokemonMatchers {
+ /**
+ * Check whether a {@linkcode Pokemon}'s current typing includes the given types.
+ * @param expectedTypes - The expected {@linkcode PokemonType}s to check against; must have length `>0`
+ * @param options - The {@linkcode toHaveTypesOptions | options} passed to the matcher
+ */
+ toHaveTypes(expectedTypes: PokemonType[], options?: toHaveTypesOptions): void;
+
+ /**
+ * Check whether a {@linkcode Pokemon} has used a move matching the given criteria.
+ * @param expectedMove - The {@linkcode MoveId} the Pokemon is expected to have used,
+ * or a partially filled {@linkcode TurnMove} containing the desired properties to check
+ * @param index - The index of the move history entry to check, in order from most recent to least recent; default `0`
+ * @see {@linkcode Pokemon.getLastXMoves}
+ */
+ toHaveUsedMove(expectedMove: MoveId | AtLeastOne, index?: number): void;
+
+ /**
+ * Check whether a {@linkcode Pokemon}'s effective stat is as expected
+ * (checked after all stat value modifications).
+ * @param stat - The {@linkcode EffectiveStat} to check
+ * @param expectedValue - The expected value of {@linkcode stat}
+ * @param options - The {@linkcode toHaveEffectiveStatOptions | options} passed to the matcher
+ * @remarks
+ * If you want to check the stat **before** modifiers are applied, use {@linkcode Pokemon.getStat} instead.
+ */
+ toHaveEffectiveStat(stat: EffectiveStat, expectedValue: number, options?: toHaveEffectiveStatOptions): void;
+
+ /**
+ * Check whether a {@linkcode Pokemon} has a specific {@linkcode StatusEffect | non-volatile status effect}.
+ * @param expectedStatusEffect - The {@linkcode StatusEffect} the Pokemon is expected to have,
+ * or a partially filled {@linkcode Status} containing the desired properties
+ */
+ toHaveStatusEffect(expectedStatusEffect: expectedStatusType): void;
+
+ /**
+ * Check whether a {@linkcode Pokemon} has a specific {@linkcode Stat} stage.
+ * @param stat - The {@linkcode BattleStat} to check
+ * @param expectedStage - The expected stat stage value of {@linkcode stat}
+ */
+ toHaveStatStage(stat: BattleStat, expectedStage: number): void;
+
+ /**
+ * Check whether a {@linkcode Pokemon} has the given {@linkcode BattlerTag}.
+ * @param expectedTag - A partially-filled {@linkcode BattlerTag} containing the desired properties
+ */
+ toHaveBattlerTag(expectedTag: toHaveBattlerTagOptions): void;
+ /**
+ * Check whether a {@linkcode Pokemon} has the given {@linkcode BattlerTag}.
+ * @param expectedType - The expected {@linkcode BattlerTagType}
+ */
+ toHaveBattlerTag(expectedType: BattlerTagType): void;
+
+ /**
+ * Check whether a {@linkcode Pokemon} has applied a specific {@linkcode AbilityId}.
+ * @param expectedAbilityId - The `AbilityId` to check for
+ */
+ toHaveAbilityApplied(expectedAbilityId: AbilityId): void;
+
+ /**
+ * Check whether a {@linkcode Pokemon} has a specific amount of {@linkcode Stat.HP | HP}.
+ * @param expectedHp - The expected amount of {@linkcode Stat.HP | HP} to have
+ */
+ toHaveHp(expectedHp: number): void;
+
+ /**
+ * Check whether a {@linkcode Pokemon} has taken a specific amount of damage.
+ * @param expectedDamageTaken - The expected amount of damage taken
+ * @param roundDown - Whether to round down `expectedDamageTaken` with {@linkcode toDmgValue}; default `true`
+ */
+ toHaveTakenDamage(expectedDamageTaken: number, roundDown?: boolean): void;
+
+ /**
+ * Check whether a {@linkcode Pokemon} is currently fainted (as determined by {@linkcode Pokemon.isFainted}).
+ * @remarks
+ * When checking whether an enemy wild Pokemon is fainted, one must store a reference to it in a variable _before_ the fainting effect occurs.
+ * Otherwise, the Pokemon will be removed from the field and garbage collected.
+ */
+ toHaveFainted(): void;
+
+ /**
+ * Check whether a {@linkcode Pokemon} is at full HP.
+ */
+ toHaveFullHp(): void;
+
+ /**
+ * Check whether a {@linkcode Pokemon} has consumed the given amount of PP for one of its moves.
+ * @param moveId - The {@linkcode MoveId} corresponding to the {@linkcode PokemonMove} that should have consumed PP
+ * @param ppUsed - The numerical amount of PP that should have been consumed,
+ * or `all` to indicate the move should be _out_ of PP
+ * @remarks
+ * If the Pokemon's moveset has been set via {@linkcode Overrides.MOVESET_OVERRIDE}/{@linkcode Overrides.ENEMY_MOVESET_OVERRIDE}
+ * or does not contain exactly one copy of `moveId`, this will fail the test.
+ */
+ toHaveUsedPP(moveId: MoveId, ppUsed: number | "all"): void;
+}
+// #endregion Pokemon Matchers
diff --git a/test/setup/matchers.setup.ts b/test/setup/matchers.setup.ts
index 88ca0a5c6bc..8ad14c8679a 100644
--- a/test/setup/matchers.setup.ts
+++ b/test/setup/matchers.setup.ts
@@ -1,5 +1,11 @@
+/**
+ * Setup file for custom matchers.
+ * Make sure to define the call signatures in `#test/@types/vitest.d.ts` too!
+ * @module
+ */
+
import { toBeAtPhase } from "#test/test-utils/matchers/to-be-at-phase";
-import { toEqualArrayUnsorted } from "#test/test-utils/matchers/to-equal-array-unsorted";
+import { toEqualUnsorted } from "#test/test-utils/matchers/to-equal-unsorted";
import { toHaveAbilityApplied } from "#test/test-utils/matchers/to-have-ability-applied";
import { toHaveArenaTag } from "#test/test-utils/matchers/to-have-arena-tag";
import { toHaveBattlerTag } from "#test/test-utils/matchers/to-have-battler-tag";
@@ -7,6 +13,7 @@ import { toHaveEffectiveStat } from "#test/test-utils/matchers/to-have-effective
import { toHaveFainted } from "#test/test-utils/matchers/to-have-fainted";
import { toHaveFullHp } from "#test/test-utils/matchers/to-have-full-hp";
import { toHaveHp } from "#test/test-utils/matchers/to-have-hp";
+import { toHaveKey } from "#test/test-utils/matchers/to-have-key";
import { toHavePositionalTag } from "#test/test-utils/matchers/to-have-positional-tag";
import { toHaveShownMessage } from "#test/test-utils/matchers/to-have-shown-message";
import { toHaveStatStage } from "#test/test-utils/matchers/to-have-stat-stage";
@@ -19,13 +26,9 @@ import { toHaveUsedPP } from "#test/test-utils/matchers/to-have-used-pp";
import { toHaveWeather } from "#test/test-utils/matchers/to-have-weather";
import { expect } from "vitest";
-/*
- * Setup file for custom matchers.
- * Make sure to define the call signatures in `#test/@types/vitest.d.ts` too!
- */
-
expect.extend({
- toEqualArrayUnsorted,
+ toEqualUnsorted,
+ toHaveKey,
toHaveShownMessage,
toBeAtPhase,
toHaveWeather,
diff --git a/test/test-utils/helpers/modifiers-helper.ts b/test/test-utils/helpers/modifiers-helper.ts
index bfda35427fa..7d3e29c420f 100644
--- a/test/test-utils/helpers/modifiers-helper.ts
+++ b/test/test-utils/helpers/modifiers-helper.ts
@@ -40,10 +40,7 @@ export class ModifierHelper extends GameManagerHelper {
* @returns `this`
*/
testCheck(modifier: ModifierTypeKeys, expectToBePreset: boolean): this {
- if (expectToBePreset) {
- expect(itemPoolChecks.get(modifier)).toBeTruthy();
- }
- expect(itemPoolChecks.get(modifier)).toBeFalsy();
+ (expectToBePreset ? expect(itemPoolChecks) : expect(itemPoolChecks).not).toHaveKey(modifier);
return this;
}
diff --git a/test/test-utils/matchers/to-equal-array-unsorted.ts b/test/test-utils/matchers/to-equal-unsorted.ts
similarity index 92%
rename from test/test-utils/matchers/to-equal-array-unsorted.ts
rename to test/test-utils/matchers/to-equal-unsorted.ts
index 97398689032..c3d85288815 100644
--- a/test/test-utils/matchers/to-equal-array-unsorted.ts
+++ b/test/test-utils/matchers/to-equal-unsorted.ts
@@ -8,11 +8,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
* @param expected - The array to check equality with
* @returns Whether the matcher passed
*/
-export function toEqualArrayUnsorted(
- this: MatcherState,
- received: unknown,
- expected: unknown[],
-): SyncExpectationResult {
+export function toEqualUnsorted(this: MatcherState, received: unknown, expected: unknown[]): SyncExpectationResult {
if (!Array.isArray(received)) {
return {
pass: this.isNot,
diff --git a/test/test-utils/matchers/to-have-key.ts b/test/test-utils/matchers/to-have-key.ts
new file mode 100644
index 00000000000..73d442fc979
--- /dev/null
+++ b/test/test-utils/matchers/to-have-key.ts
@@ -0,0 +1,47 @@
+import { getOnelineDiffStr } from "#test/test-utils/string-utils";
+import { receivedStr } from "#test/test-utils/test-utils";
+import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
+
+/**
+ * Matcher that checks if a {@linkcode Map} contains the given key, regardless of its value.
+ * @param received - The received value. Should be a Map
+ * @param expectedKey - The key whose inclusion in the map is being checked
+ * @returns Whether the matcher passed
+ */
+export function toHaveKey(this: MatcherState, received: unknown, expectedKey: unknown): SyncExpectationResult {
+ if (!(received instanceof Map)) {
+ return {
+ pass: this.isNot,
+ message: () => `Expected to receive a Map, but got ${receivedStr(received)}!`,
+ };
+ }
+
+ if (received.size === 0) {
+ return {
+ pass: this.isNot,
+ message: () => "Expected to receive a non-empty Map, but received map was empty!",
+ expected: expectedKey,
+ actual: received,
+ };
+ }
+
+ const keys = [...received.keys()];
+ const pass = this.equals(keys, expectedKey, [
+ ...this.customTesters,
+ this.utils.iterableEquality,
+ this.utils.subsetEquality,
+ ]);
+
+ const actualStr = getOnelineDiffStr.call(this, received);
+ const expectedStr = getOnelineDiffStr.call(this, expectedKey);
+
+ return {
+ pass,
+ message: () =>
+ pass
+ ? `Expected ${actualStr} to NOT have the key ${expectedStr}, but it did!`
+ : `Expected ${actualStr} to have the key ${expectedStr}, but it didn't!`,
+ expected: expectedKey,
+ actual: keys,
+ };
+}
diff --git a/test/test-utils/matchers/to-have-terrain.ts b/test/test-utils/matchers/to-have-terrain.ts
index f951abed0b3..9b6939168f0 100644
--- a/test/test-utils/matchers/to-have-terrain.ts
+++ b/test/test-utils/matchers/to-have-terrain.ts
@@ -8,8 +8,8 @@ import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils"
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
/**
- * Matcher that checks if the {@linkcode TerrainType} is as expected
- * @param received - The object to check. Should be an instance of {@linkcode GameManager}.
+ * Matcher that checks if the current {@linkcode TerrainType} is as expected.
+ * @param received - The object to check. Should be the current {@linkcode GameManager}.
* @param expectedTerrainType - The expected {@linkcode TerrainType}, or {@linkcode TerrainType.NONE} if no terrain should be active
* @returns Whether the matcher passed
*/
diff --git a/test/test-utils/matchers/to-have-weather.ts b/test/test-utils/matchers/to-have-weather.ts
index ffb1e0aad97..7604cd5f890 100644
--- a/test/test-utils/matchers/to-have-weather.ts
+++ b/test/test-utils/matchers/to-have-weather.ts
@@ -8,8 +8,8 @@ import { toTitleCase } from "#utils/strings";
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
/**
- * Matcher that checks if the {@linkcode WeatherType} is as expected
- * @param received - The object to check. Expects an instance of {@linkcode GameManager}.
+ * Matcher that checks if the current {@linkcode WeatherType} is as expected.
+ * @param received - The object to check. Should be the current {@linkcode GameManager}
* @param expectedWeatherType - The expected {@linkcode WeatherType}
* @returns Whether the matcher passed
*/
From d02980dd4efb6237205375c1f786a47e4e5be342 Mon Sep 17 00:00:00 2001
From: Dean <69436131+emdeann@users.noreply.github.com>
Date: Sat, 20 Sep 2025 15:32:31 -0700
Subject: [PATCH 33/42] [Move] Fully implement Healing wish (/Lunar Dance) and
remove `nextCommandPhaseQueue` (#6027)
* Remove NCPQ
* Implement PendingHealTag
* Fix test
* Code review
* Use message directly instead of as key in tag
* Update tag for serialization
* Update test import
* Update src/data/arena-tag.ts
Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com>
* Remove isNullOrUndefined uses
* Fix arena tag type(o)
* Fix pendinghealtag
* Fix hwish tests
* Arena tag denesting
---------
Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com>
Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com>
---
src/data/arena-tag.ts | 143 ++++++++++++
src/data/moves/move.ts | 29 +--
src/enums/arena-tag-type.ts | 1 +
src/phase-manager.ts | 12 +-
src/phases/pokemon-heal-phase.ts | 3 +-
src/phases/post-summon-phase.ts | 4 +
src/ui/containers/arena-flyout.ts | 6 +
test/moves/healing-wish-lunar-dance.test.ts | 245 ++++++++++++++++++++
test/moves/lunar-dance.test.ts | 73 ------
9 files changed, 414 insertions(+), 102 deletions(-)
create mode 100644 test/moves/healing-wish-lunar-dance.test.ts
delete mode 100644 test/moves/lunar-dance.test.ts
diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts
index 7d78076e06b..fd64e271758 100644
--- a/src/data/arena-tag.ts
+++ b/src/data/arena-tag.ts
@@ -56,6 +56,7 @@ import { allMoves } from "#data/data-lists";
import { AbilityId } from "#enums/ability-id";
import { ArenaTagSide } from "#enums/arena-tag-side";
import { ArenaTagType } from "#enums/arena-tag-type";
+import type { BattlerIndex } from "#enums/battler-index";
import { BattlerTagType } from "#enums/battler-tag-type";
import { HitResult } from "#enums/hit-result";
import { CommonAnim } from "#enums/move-anims-common";
@@ -1597,6 +1598,145 @@ export class SuppressAbilitiesTag extends SerializableArenaTag {
}
}
+/**
+ * Interface containing data related to a queued healing effect from
+ * {@link https://bulbapedia.bulbagarden.net/wiki/Healing_Wish_(move) | Healing Wish}
+ * or {@link https://bulbapedia.bulbagarden.net/wiki/Lunar_Dance_(move) | Lunar Dance}.
+ */
+interface PendingHealEffect {
+ /** The {@linkcode Pokemon.id | PID} of the {@linkcode Pokemon} that created the effect. */
+ readonly sourceId: number;
+ /** The {@linkcode MoveId} of the move that created the effect. */
+ readonly moveId: MoveId;
+ /** If `true`, also restores the target's PP when the effect activates. */
+ readonly restorePP: boolean;
+ /** The message to display when the effect activates */
+ readonly healMessage: string;
+}
+
+/**
+ * Arena tag to contain stored healing effects, namely from
+ * {@link https://bulbapedia.bulbagarden.net/wiki/Healing_Wish_(move) | Healing Wish}
+ * and {@link https://bulbapedia.bulbagarden.net/wiki/Lunar_Dance_(move) | Lunar Dance}.
+ * When a damaged Pokemon first enters the effect's {@linkcode BattlerIndex | field position},
+ * their HP is fully restored, and they are cured of any non-volatile status condition.
+ * If the effect is from Lunar Dance, their PP is also restored.
+ */
+export class PendingHealTag extends SerializableArenaTag {
+ public readonly tagType = ArenaTagType.PENDING_HEAL;
+ /** All pending healing effects, organized by {@linkcode BattlerIndex} */
+ public readonly pendingHeals: Partial> = {};
+
+ constructor() {
+ super(0);
+ }
+
+ /**
+ * Adds a pending healing effect to the field. Effects under the same move *and*
+ * target index as an existing effect are ignored.
+ * @param targetIndex - The {@linkcode BattlerIndex} under which the effect applies
+ * @param healEffect - The {@linkcode PendingHealEffect | data} for the pending heal effect
+ */
+ public queueHeal(targetIndex: BattlerIndex, healEffect: PendingHealEffect): void {
+ const existingHealEffects = this.pendingHeals[targetIndex];
+ if (existingHealEffects) {
+ if (!existingHealEffects.some(he => he.moveId === healEffect.moveId)) {
+ existingHealEffects.push(healEffect);
+ }
+ } else {
+ this.pendingHeals[targetIndex] = [healEffect];
+ }
+ }
+
+ /** Removes default on-remove message */
+ override onRemove(_arena: Arena): void {}
+
+ /** This arena tag is removed at the end of the turn if no pending healing effects are on the field */
+ override lapse(_arena: Arena): boolean {
+ for (const key in this.pendingHeals) {
+ if (this.pendingHeals[key].length > 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Applies a pending healing effect on the given target index. If an effect is found for
+ * the index, the Pokemon at that index is healed to full HP, is cured of any non-volatile status,
+ * and has its PP fully restored (if the effect is from Lunar Dance).
+ * @param arena - The {@linkcode Arena} containing this tag
+ * @param simulated - If `true`, suppresses changes to game state
+ * @param pokemon - The {@linkcode Pokemon} receiving the healing effect
+ * @returns `true` if the target Pokemon was healed by this effect
+ * @todo This should also be called when a Pokemon moves into a new position via Ally Switch
+ */
+ override apply(arena: Arena, simulated: boolean, pokemon: Pokemon): boolean {
+ const targetIndex = pokemon.getBattlerIndex();
+ const targetEffects = this.pendingHeals[targetIndex];
+
+ if (targetEffects == null || targetEffects.length === 0) {
+ return false;
+ }
+
+ const healEffect = targetEffects.find(effect => this.canApply(effect, pokemon));
+
+ if (healEffect == null) {
+ return false;
+ }
+
+ if (simulated) {
+ return true;
+ }
+
+ const { sourceId, moveId, restorePP, healMessage } = healEffect;
+ const sourcePokemon = globalScene.getPokemonById(sourceId);
+ if (!sourcePokemon) {
+ console.warn(`Source of pending ${allMoves[moveId].name} effect is undefined!`);
+ targetEffects.splice(targetEffects.indexOf(healEffect), 1);
+ // Re-evaluate after the invalid heal effect is removed
+ return this.apply(arena, simulated, pokemon);
+ }
+
+ globalScene.phaseManager.unshiftNew(
+ "PokemonHealPhase",
+ targetIndex,
+ pokemon.getMaxHp(),
+ healMessage,
+ true,
+ false,
+ false,
+ true,
+ false,
+ restorePP,
+ );
+
+ targetEffects.splice(targetEffects.indexOf(healEffect), 1);
+
+ return healEffect != null;
+ }
+
+ /**
+ * Determines if the given {@linkcode PendingHealEffect} can immediately heal
+ * the given target {@linkcode Pokemon}.
+ * @param healEffect - The {@linkcode PendingHealEffect} to evaluate
+ * @param pokemon - The {@linkcode Pokemon} to evaluate against
+ * @returns `true` if the Pokemon can be healed by the effect
+ */
+ private canApply(healEffect: PendingHealEffect, pokemon: Pokemon): boolean {
+ return (
+ !pokemon.isFullHp()
+ || pokemon.status != null
+ || (healEffect.restorePP && pokemon.getMoveset().some(mv => mv.ppUsed > 0))
+ );
+ }
+
+ override loadTag(source: BaseArenaTag & Pick): void {
+ super.loadTag(source);
+ (this as Mutable).pendingHeals = source.pendingHeals;
+ }
+}
+
// TODO: swap `sourceMove` and `sourceId` and make `sourceMove` an optional parameter
export function getArenaTag(
tagType: ArenaTagType,
@@ -1660,6 +1800,8 @@ export function getArenaTag(
return new FairyLockTag(turnCount, sourceId);
case ArenaTagType.NEUTRALIZING_GAS:
return new SuppressAbilitiesTag(sourceId);
+ case ArenaTagType.PENDING_HEAL:
+ return new PendingHealTag();
default:
return null;
}
@@ -1708,5 +1850,6 @@ export type ArenaTagTypeMap = {
[ArenaTagType.GRASS_WATER_PLEDGE]: GrassWaterPledgeTag;
[ArenaTagType.FAIRY_LOCK]: FairyLockTag;
[ArenaTagType.NEUTRALIZING_GAS]: SuppressAbilitiesTag;
+ [ArenaTagType.PENDING_HEAL]: PendingHealTag;
[ArenaTagType.NONE]: NoneTag;
};
diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts
index 0fdb0d01e43..72376b7934f 100644
--- a/src/data/moves/move.ts
+++ b/src/data/moves/move.ts
@@ -6,7 +6,7 @@ import { loggedInUser } from "#app/account";
import type { GameMode } from "#app/game-mode";
import { globalScene } from "#app/global-scene";
import { getPokemonNameWithAffix } from "#app/messages";
-import type { EntryHazardTag } from "#data/arena-tag";
+import type { EntryHazardTag, PendingHealTag } from "#data/arena-tag";
import { WeakenMoveTypeTag } from "#data/arena-tag";
import { MoveChargeAnim } from "#data/battle-anims";
import {
@@ -2150,24 +2150,15 @@ export class SacrificialFullRestoreAttr extends SacrificialAttr {
return false;
}
- // We don't know which party member will be chosen, so pick the highest max HP in the party
- const party = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty();
- const maxPartyMemberHp = party.map(p => p.getMaxHp()).reduce((maxHp: number, hp: number) => Math.max(hp, maxHp), 0);
-
- const pm = globalScene.phaseManager;
-
- pm.pushPhase(
- pm.create("PokemonHealPhase",
- user.getBattlerIndex(),
- maxPartyMemberHp,
- i18next.t(this.moveMessage, { pokemonName: getPokemonNameWithAffix(user) }),
- true,
- false,
- false,
- true,
- false,
- this.restorePP),
- true);
+ // Add a tag to the field if it doesn't already exist, then queue a delayed healing effect in the user's current slot.
+ globalScene.arena.addTag(ArenaTagType.PENDING_HEAL, 0, move.id, user.id); // Arguments after first go completely unused
+ const tag = globalScene.arena.getTag(ArenaTagType.PENDING_HEAL) as PendingHealTag;
+ tag.queueHeal(user.getBattlerIndex(), {
+ sourceId: user.id,
+ moveId: move.id,
+ restorePP: this.restorePP,
+ healMessage: i18next.t(this.moveMessage, { pokemonName: getPokemonNameWithAffix(user) }),
+ });
return true;
}
diff --git a/src/enums/arena-tag-type.ts b/src/enums/arena-tag-type.ts
index 30f053b98bd..717845cf2d9 100644
--- a/src/enums/arena-tag-type.ts
+++ b/src/enums/arena-tag-type.ts
@@ -34,4 +34,5 @@ export enum ArenaTagType {
GRASS_WATER_PLEDGE = "GRASS_WATER_PLEDGE",
FAIRY_LOCK = "FAIRY_LOCK",
NEUTRALIZING_GAS = "NEUTRALIZING_GAS",
+ PENDING_HEAL = "PENDING_HEAL",
}
diff --git a/src/phase-manager.ts b/src/phase-manager.ts
index 3fbf68de60d..125ca00786b 100644
--- a/src/phase-manager.ts
+++ b/src/phase-manager.ts
@@ -233,7 +233,6 @@ export class PhaseManager {
/** overrides default of inserting phases to end of phaseQueuePrepend array. Useful for inserting Phases "out of order" */
private phaseQueuePrependSpliceIndex = -1;
- private nextCommandPhaseQueue: Phase[] = [];
/** Storage for {@linkcode PhasePriorityQueue}s which hold phases whose order dynamically changes */
private dynamicPhaseQueues: PhasePriorityQueue[];
@@ -291,13 +290,12 @@ export class PhaseManager {
/**
* Adds a phase to nextCommandPhaseQueue, as long as boolean passed in is false
* @param phase {@linkcode Phase} the phase to add
- * @param defer boolean on which queue to add to, defaults to false, and adds to phaseQueue
*/
- pushPhase(phase: Phase, defer = false): void {
+ pushPhase(phase: Phase): void {
if (this.getDynamicPhaseType(phase) !== undefined) {
this.pushDynamicPhase(phase);
} else {
- (!defer ? this.phaseQueue : this.nextCommandPhaseQueue).push(phase);
+ this.phaseQueue.push(phase);
}
}
@@ -324,7 +322,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.conditionalQueue]) {
queue.splice(0, queue.length);
}
this.dynamicPhaseQueues.forEach(queue => queue.clear());
@@ -623,10 +621,6 @@ export class PhaseManager {
* Moves everything from nextCommandPhaseQueue to phaseQueue (keeping order)
*/
private populatePhaseQueue(): void {
- if (this.nextCommandPhaseQueue.length > 0) {
- this.phaseQueue.push(...this.nextCommandPhaseQueue);
- this.nextCommandPhaseQueue.splice(0, this.nextCommandPhaseQueue.length);
- }
this.phaseQueue.push(new TurnInitPhase());
}
diff --git a/src/phases/pokemon-heal-phase.ts b/src/phases/pokemon-heal-phase.ts
index 02bb3a0b968..258ddb0b624 100644
--- a/src/phases/pokemon-heal-phase.ts
+++ b/src/phases/pokemon-heal-phase.ts
@@ -64,7 +64,8 @@ export class PokemonHealPhase extends CommonAnimPhase {
}
const hasMessage = !!this.message;
- const healOrDamage = !pokemon.isFullHp() || this.hpHealed < 0;
+ const canRestorePP = this.fullRestorePP && pokemon.getMoveset().some(mv => mv.ppUsed > 0);
+ const healOrDamage = !pokemon.isFullHp() || this.hpHealed < 0 || canRestorePP;
const healBlock = pokemon.getTag(BattlerTagType.HEAL_BLOCK) as HealBlockTag;
let lastStatusEffect = StatusEffect.NONE;
diff --git a/src/phases/post-summon-phase.ts b/src/phases/post-summon-phase.ts
index 5de068f2ae5..5f66cf91eca 100644
--- a/src/phases/post-summon-phase.ts
+++ b/src/phases/post-summon-phase.ts
@@ -2,6 +2,7 @@ import { applyAbAttrs } from "#abilities/apply-ab-attrs";
import { globalScene } from "#app/global-scene";
import { EntryHazardTag } from "#data/arena-tag";
import { MysteryEncounterPostSummonTag } from "#data/battler-tags";
+import { ArenaTagType } from "#enums/arena-tag-type";
import { BattlerTagType } from "#enums/battler-tag-type";
import { StatusEffect } from "#enums/status-effect";
import { PokemonPhase } from "#phases/pokemon-phase";
@@ -16,6 +17,9 @@ export class PostSummonPhase extends PokemonPhase {
if (pokemon.status?.effect === StatusEffect.TOXIC) {
pokemon.status.toxicTurnCount = 0;
}
+
+ globalScene.arena.applyTags(ArenaTagType.PENDING_HEAL, false, pokemon);
+
globalScene.arena.applyTags(EntryHazardTag, false, pokemon);
// If this is mystery encounter and has post summon phase tag, apply post summon effects
diff --git a/src/ui/containers/arena-flyout.ts b/src/ui/containers/arena-flyout.ts
index ab95d1a3e7a..355f3edb293 100644
--- a/src/ui/containers/arena-flyout.ts
+++ b/src/ui/containers/arena-flyout.ts
@@ -285,6 +285,12 @@ export class ArenaFlyout extends Phaser.GameObjects.Container {
switch (arenaEffectChangedEvent.constructor) {
case TagAddedEvent: {
const tagAddedEvent = arenaEffectChangedEvent as TagAddedEvent;
+
+ const excludedTags = [ArenaTagType.PENDING_HEAL];
+ if (excludedTags.includes(tagAddedEvent.arenaTagType)) {
+ return;
+ }
+
const isArenaTrapTag = globalScene.arena.getTag(tagAddedEvent.arenaTagType) instanceof EntryHazardTag;
let arenaEffectType: ArenaEffectType;
diff --git a/test/moves/healing-wish-lunar-dance.test.ts b/test/moves/healing-wish-lunar-dance.test.ts
new file mode 100644
index 00000000000..0dcf993aeac
--- /dev/null
+++ b/test/moves/healing-wish-lunar-dance.test.ts
@@ -0,0 +1,245 @@
+import { AbilityId } from "#enums/ability-id";
+import { ArenaTagType } from "#enums/arena-tag-type";
+import { Challenges } from "#enums/challenges";
+import { MoveId } from "#enums/move-id";
+import { MoveResult } from "#enums/move-result";
+import { PokemonType } from "#enums/pokemon-type";
+import { SpeciesId } from "#enums/species-id";
+import { StatusEffect } from "#enums/status-effect";
+import { GameManager } from "#test/test-utils/game-manager";
+import Phaser from "phaser";
+import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
+
+describe("Moves - Lunar Dance and Healing Wish", () => {
+ let phaserGame: Phaser.Game;
+ let game: GameManager;
+
+ beforeAll(() => {
+ phaserGame = new Phaser.Game({
+ type: Phaser.HEADLESS,
+ });
+ });
+
+ afterEach(() => {
+ game.phaseInterceptor.restoreOg();
+ });
+
+ beforeEach(() => {
+ game = new GameManager(phaserGame);
+ game.override.battleStyle("double").enemyAbility(AbilityId.BALL_FETCH).enemyMoveset(MoveId.SPLASH);
+ });
+
+ describe.each([
+ { moveName: "Healing Wish", moveId: MoveId.HEALING_WISH },
+ { moveName: "Lunar Dance", moveId: MoveId.LUNAR_DANCE },
+ ])("$moveName", ({ moveId }) => {
+ it("should sacrifice the user to restore the switched in Pokemon's HP", async () => {
+ await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER, SpeciesId.SQUIRTLE]);
+
+ const [bulbasaur, charmander, squirtle] = game.scene.getPlayerParty();
+ squirtle.hp = 1;
+
+ game.move.use(MoveId.SPLASH, 0);
+ game.move.use(moveId, 1);
+ game.doSelectPartyPokemon(2);
+
+ await game.toNextTurn();
+
+ expect(bulbasaur.isFullHp()).toBe(true);
+ expect(charmander.isFainted()).toBe(true);
+ expect(squirtle.isFullHp()).toBe(true);
+ });
+
+ it("should sacrifice the user to cure the switched in Pokemon's status", async () => {
+ game.override.statusEffect(StatusEffect.BURN);
+
+ await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER, SpeciesId.SQUIRTLE]);
+ const [bulbasaur, charmander, squirtle] = game.scene.getPlayerParty();
+
+ game.move.use(MoveId.SPLASH, 0);
+ game.move.use(moveId, 1);
+ game.doSelectPartyPokemon(2);
+
+ await game.toNextTurn();
+
+ expect(bulbasaur.status?.effect).toBe(StatusEffect.BURN);
+ expect(charmander.isFainted()).toBe(true);
+ expect(squirtle.status?.effect).toBeUndefined();
+ });
+
+ it("should fail if the user has no non-fainted allies in their party", async () => {
+ game.override.battleStyle("single");
+
+ await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER]);
+ const [bulbasaur, charmander] = game.scene.getPlayerParty();
+
+ game.move.use(MoveId.MEMENTO);
+ game.doSelectPartyPokemon(1);
+
+ await game.toNextTurn();
+
+ expect(bulbasaur.isFainted()).toBe(true);
+ expect(charmander.isActive(true)).toBe(true);
+
+ game.move.use(moveId);
+
+ await game.toEndOfTurn();
+
+ expect(charmander.isFullHp());
+ expect(charmander.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
+ });
+
+ it("should fail if the user has no challenge-eligible allies", async () => {
+ game.override.battleStyle("single");
+ // Mono normal challenge
+ game.challengeMode.addChallenge(Challenges.SINGLE_TYPE, PokemonType.NORMAL + 1, 0);
+ await game.challengeMode.startBattle([SpeciesId.RATICATE, SpeciesId.ODDISH]);
+
+ const raticate = game.field.getPlayerPokemon();
+
+ game.move.use(moveId);
+ await game.toNextTurn();
+
+ expect(raticate.isFullHp()).toBe(true);
+ expect(raticate.getLastXMoves()[0].result).toEqual(MoveResult.FAIL);
+ });
+
+ it("should store its effect if the switched-in Pokemon would be unaffected", async () => {
+ game.override.battleStyle("single");
+
+ await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER, SpeciesId.SQUIRTLE]);
+
+ const [bulbasaur, charmander, squirtle] = game.scene.getPlayerParty();
+ squirtle.hp = 1;
+
+ game.move.use(moveId);
+ game.doSelectPartyPokemon(1);
+
+ await game.toNextTurn();
+
+ // Bulbasaur fainted and stored a healing effect
+ expect(bulbasaur.isFainted()).toBe(true);
+ expect(charmander.isFullHp()).toBe(true);
+ expect(game.phaseInterceptor.log).not.toContain("PokemonHealPhase");
+ expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeDefined();
+
+ // Switch to damaged Squirtle. HW/LD's effect should activate
+ game.doSwitchPokemon(2);
+
+ await game.toEndOfTurn();
+ expect(squirtle.isFullHp()).toBe(true);
+ expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeUndefined();
+
+ // Set Charmander's HP to 1, then switch back to Charmander.
+ // HW/LD shouldn't activate again
+ charmander.hp = 1;
+ game.doSwitchPokemon(2);
+
+ await game.toEndOfTurn();
+ expect(charmander.hp).toBe(1);
+ });
+
+ it("should only store one charge of the effect at a time", async () => {
+ game.override.battleStyle("single");
+
+ await game.classicMode.startBattle([
+ SpeciesId.BULBASAUR,
+ SpeciesId.CHARMANDER,
+ SpeciesId.SQUIRTLE,
+ SpeciesId.PIKACHU,
+ ]);
+
+ const [bulbasaur, charmander, squirtle, pikachu] = game.scene.getPlayerParty();
+ [squirtle, pikachu].forEach(p => (p.hp = 1));
+
+ // Use HW/LD and send in Charmander. HW/LD's effect should be stored
+ game.move.use(moveId);
+ game.doSelectPartyPokemon(1);
+
+ await game.toNextTurn();
+ expect(bulbasaur.isFainted()).toBe(true);
+ expect(charmander.isFullHp()).toBe(true);
+ expect(charmander.isFullHp());
+ expect(game.phaseInterceptor.log).not.toContain("PokemonHealPhase");
+ expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeDefined();
+
+ // Use HW/LD again, sending in Squirtle. HW/LD should activate and heal Squirtle
+ game.move.use(moveId);
+ game.doSelectPartyPokemon(2);
+
+ await game.toNextTurn();
+ expect(charmander.isFainted()).toBe(true);
+ expect(squirtle.isFullHp()).toBe(true);
+ expect(squirtle.isFullHp());
+
+ // Switch again to Pikachu. HW/LD's effect shouldn't be present
+ game.doSwitchPokemon(3);
+
+ expect(pikachu.isFullHp()).toBe(false);
+ });
+ });
+
+ it("Lunar Dance should sacrifice the user to restore the switched in Pokemon's PP", async () => {
+ game.override.battleStyle("single");
+
+ await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER]);
+
+ const [bulbasaur, charmander] = game.scene.getPlayerParty();
+
+ game.move.use(MoveId.SPLASH);
+ await game.toNextTurn();
+
+ game.doSwitchPokemon(1);
+ await game.toNextTurn();
+
+ game.move.use(MoveId.LUNAR_DANCE);
+ game.doSelectPartyPokemon(1);
+
+ await game.toNextTurn();
+ expect(charmander.isFainted()).toBeTruthy();
+ bulbasaur.getMoveset().forEach(mv => expect(mv.ppUsed).toBe(0));
+ });
+
+ it("should stack with each other", async () => {
+ game.override.battleStyle("single");
+
+ await game.classicMode.startBattle([
+ SpeciesId.BULBASAUR,
+ SpeciesId.CHARMANDER,
+ SpeciesId.SQUIRTLE,
+ SpeciesId.PIKACHU,
+ ]);
+
+ const [bulbasaur, charmander, squirtle, pikachu] = game.scene.getPlayerParty();
+ [squirtle, pikachu].forEach(p => {
+ p.hp = 1;
+ p.getMoveset().forEach(mv => (mv.ppUsed = 1));
+ });
+
+ game.move.use(MoveId.LUNAR_DANCE);
+ game.doSelectPartyPokemon(1);
+
+ await game.toNextTurn();
+ expect(bulbasaur.isFainted()).toBe(true);
+ expect(charmander.isFullHp()).toBe(true);
+ expect(game.phaseInterceptor.log).not.toContain("PokemonHealPhase");
+ expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeDefined();
+
+ game.move.use(MoveId.HEALING_WISH);
+ game.doSelectPartyPokemon(2);
+
+ // Lunar Dance should apply first since it was used first, restoring Squirtle's HP and PP
+ await game.toNextTurn();
+ expect(squirtle.isFullHp()).toBe(true);
+ squirtle.getMoveset().forEach(mv => expect(mv.ppUsed).toBe(0));
+ expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeDefined();
+
+ game.doSwitchPokemon(3);
+
+ // Healing Wish should apply on the next switch, restoring Pikachu's HP
+ await game.toEndOfTurn();
+ expect(pikachu.isFullHp()).toBe(true);
+ pikachu.getMoveset().forEach(mv => expect(mv.ppUsed).toBe(1));
+ expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeUndefined();
+ });
+});
diff --git a/test/moves/lunar-dance.test.ts b/test/moves/lunar-dance.test.ts
deleted file mode 100644
index 7386d15079b..00000000000
--- a/test/moves/lunar-dance.test.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import { AbilityId } from "#enums/ability-id";
-import { MoveId } from "#enums/move-id";
-import { SpeciesId } from "#enums/species-id";
-import { StatusEffect } from "#enums/status-effect";
-import { GameManager } from "#test/test-utils/game-manager";
-import Phaser from "phaser";
-import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
-
-describe("Moves - Lunar Dance", () => {
- let phaserGame: Phaser.Game;
- let game: GameManager;
-
- beforeAll(() => {
- phaserGame = new Phaser.Game({
- type: Phaser.HEADLESS,
- });
- });
-
- afterEach(() => {
- game.phaseInterceptor.restoreOg();
- });
-
- beforeEach(() => {
- game = new GameManager(phaserGame);
- game.override
- .statusEffect(StatusEffect.BURN)
- .battleStyle("double")
- .enemyAbility(AbilityId.BALL_FETCH)
- .enemyMoveset(MoveId.SPLASH);
- });
-
- it("should full restore HP, PP and status of switched in pokemon, then fail second use because no remaining backup pokemon in party", async () => {
- await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.ODDISH, SpeciesId.RATTATA]);
-
- const [bulbasaur, oddish, rattata] = game.scene.getPlayerParty();
- game.move.changeMoveset(bulbasaur, [MoveId.LUNAR_DANCE, MoveId.SPLASH]);
- game.move.changeMoveset(oddish, [MoveId.LUNAR_DANCE, MoveId.SPLASH]);
- game.move.changeMoveset(rattata, [MoveId.LUNAR_DANCE, MoveId.SPLASH]);
-
- game.move.select(MoveId.SPLASH, 0);
- game.move.select(MoveId.SPLASH, 1);
- await game.toNextTurn();
-
- // Bulbasaur should still be burned and have used a PP for splash and not at max hp
- expect(bulbasaur.status?.effect).toBe(StatusEffect.BURN);
- expect(bulbasaur.moveset[1]?.ppUsed).toBe(1);
- expect(bulbasaur.hp).toBeLessThan(bulbasaur.getMaxHp());
-
- // Switch out Bulbasaur for Rattata so we can swtich bulbasaur back in with lunar dance
- game.doSwitchPokemon(2);
- game.move.select(MoveId.SPLASH, 1);
- await game.toNextTurn();
-
- game.move.select(MoveId.SPLASH, 0);
- game.move.select(MoveId.LUNAR_DANCE);
- game.doSelectPartyPokemon(2);
- await game.phaseInterceptor.to("SwitchPhase", false);
- await game.toNextTurn();
-
- // Bulbasaur should NOT have any status and have full PP for splash and be at max hp
- expect(bulbasaur.status?.effect).toBeUndefined();
- expect(bulbasaur.moveset[1]?.ppUsed).toBe(0);
- expect(bulbasaur.isFullHp()).toBe(true);
-
- game.move.select(MoveId.SPLASH, 0);
- game.move.select(MoveId.LUNAR_DANCE);
- await game.toNextTurn();
-
- // Using Lunar dance again should fail because nothing in party and rattata should be alive
- expect(rattata.status?.effect).toBe(StatusEffect.BURN);
- expect(rattata.hp).toBeLessThan(rattata.getMaxHp());
- });
-});
From 87e6095a001f24e81118e65957f760e7d247834d Mon Sep 17 00:00:00 2001
From: Dean <69436131+emdeann@users.noreply.github.com>
Date: Sat, 20 Sep 2025 15:49:40 -0700
Subject: [PATCH 34/42] [Misc/Feature] Add dynamic turn order (#6036)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Add new priority queues
* Add dynamic queue manager
* Add timing modifier and fix post speed ordering
* Make `phaseQueue` private
* Fix `gameManager.setTurnOrder`
* Update `findPhase` to also check dynamic queues
* Modify existing phase manager methods to check dynamic queues
* Fix move order persisting through tests
* Fix magic coat/bounce
* Use append for magic coat/bounce
* Remove `getSpeedOrder` from `TurnStartPhase`, fix references to `getCommandOrder` in tests
* Fix round queuing last instead of next
* Add quick draw application
* Add quick claw activation
* Fix turn order tracking
* Add move header queue to fix ordering
* Fix abilities activating immediately on summon
* Fix `postsummonphases` being shuffled (need to handle speed ties differently here)
* Update speed order function
* Add `StaticSwitchSummonPhase`
* Fix magic coat/bounce error from conflict resolution
* Remove conditional queue
* Fix dancer and baton pass tests
* Automatically queue consecutive Pokémon phases as dynamic
* Move turn end phases queuing back to `TurnStartPhase`
* Fix `LearnMovePhase`
* Remove `PrependSplice`
* Move DQM to phase manager
* Fix various phases being pushed instead of unshifted
* Remove `StaticSwitchSummonPhase`
* Ensure the top queue is always at length - 1
* Fix encounter `PostSummonPhase`s and Revival Blessing
* Fix move headers
* Remove implicit ordering from DQM
* Fix `PostSummonPhase`s in encounters running too early
* Fix `tryRemovePhase` usages
* Add `MovePhase` after `MoveEndPhase` automatically
* Implement an `inSpeedOrder` function
* Merge fixes
* Fix encounter rewards
* Defer `FaintPhase`s where splice was used previously
* Separate speed order utils to avoid circular imports
* Temporarily disable lunar dance test
* Simplify deferral
* Remove move priority modifier
* Fix TS errors in code files
* Fix ts errors in tests
* Fix more test files
* Fix postsummon + checkswitch ability activations
* Fix `removeAll`
* Reposition `positionalTagPhase`
* Re-add `startCurrentPhase`
* Avoid overwriting `currentPhase` after `turnStart`
* Delete `switchSummonPhasePriorityQueue`
* Update `phase-manager.ts`
* Remove uses of `isNullOrUndefined`
* Rename deferral methods
* Update docs and use `getPlayerField(true)` in turn start phase
* Use `.getEnemyField(true)`
* Update docs for post summon phase priority queue (psppq)
* Update speed order utils
* Remove null from `nextPhase`
* Update move phase timing modifier docs
* Remove mention of phases from base priority queue class
* Remove and replace `applyInSpeedOrder`
* Don't sort weather effect phases
* Order priority queues before removing
- Add some `readonly` and `public` modifiers
- Remove unused `queuedPhases` field from `MoveEffectPhase`
* Fix linting in `phase-manager.ts`
* Remove unnecessary turn order modification in Rage Fist test
---------
Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com>
Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com>
Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
---
src/@types/phase-types.ts | 13 +
src/battle-scene.ts | 31 +-
src/data/abilities/ability.ts | 48 +-
src/data/abilities/apply-ab-attrs.ts | 1 -
src/data/battler-tags.ts | 53 +-
src/data/moves/move.ts | 113 +---
.../encounters/fun-and-games-encounter.ts | 2 +-
.../the-expert-pokemon-breeder-encounter.ts | 1 -
.../utils/encounter-phase-utils.ts | 9 +-
src/data/phase-priority-queue.ts | 125 -----
src/dynamic-queue-manager.ts | 182 +++++++
src/enums/arena-tag-side.ts | 1 +
src/enums/battler-tag-type.ts | 1 +
src/enums/dynamic-phase-type.ts | 7 -
src/enums/move-phase-timing-modifier.ts | 16 +
src/field/arena.ts | 8 +-
src/field/pokemon.ts | 13 +-
src/modifier/modifier.ts | 21 +-
src/phase-manager.ts | 504 +++++++-----------
src/phase-tree.ts | 206 +++++++
src/phases/activate-priority-queue-phase.ts | 23 -
src/phases/battle-end-phase.ts | 22 +-
src/phases/check-status-effect-phase.ts | 12 +-
src/phases/check-switch-phase.ts | 18 +-
src/phases/dynamic-phase-marker.ts | 17 +
src/phases/egg-hatch-phase.ts | 2 +-
src/phases/encounter-phase.ts | 53 +-
src/phases/game-over-phase.ts | 18 +-
src/phases/learn-move-phase.ts | 4 +-
src/phases/move-charge-phase.ts | 2 +-
src/phases/move-effect-phase.ts | 45 +-
src/phases/move-header-phase.ts | 6 +-
src/phases/move-phase.ts | 41 +-
src/phases/mystery-encounter-phases.ts | 26 +-
src/phases/new-battle-phase.ts | 7 +-
src/phases/party-member-pokemon-phase.ts | 4 +
.../post-summon-activate-ability-phase.ts | 4 +-
src/phases/post-summon-phase.ts | 15 +-
src/phases/quiet-form-change-phase.ts | 8 +-
src/phases/stat-stage-change-phase.ts | 48 +-
src/phases/summon-phase.ts | 22 +-
src/phases/switch-phase.ts | 9 -
src/phases/switch-summon-phase.ts | 4 +-
src/phases/title-phase.ts | 22 +-
src/phases/turn-end-phase.ts | 1 +
src/phases/turn-start-phase.ts | 127 +----
src/queues/move-phase-priority-queue.ts | 103 ++++
src/queues/pokemon-phase-priority-queue.ts | 20 +
src/queues/pokemon-priority-queue.ts | 10 +
.../post-summon-phase-priority-queue.ts | 45 ++
src/queues/priority-queue.ts | 78 +++
src/utils/speed-order-generator.ts | 39 ++
src/utils/speed-order.ts | 57 ++
test/abilities/dancer.test.ts | 5 +-
test/abilities/mycelium-might.test.ts | 47 +-
test/abilities/neutralizing-gas.test.ts | 2 +-
test/abilities/quick-draw.test.ts | 41 +-
test/abilities/stall.test.ts | 34 +-
test/battle/battle-order.test.ts | 71 ++-
test/moves/baton-pass.test.ts | 7 +-
test/moves/delayed-attack.test.ts | 2 +-
test/moves/focus-punch.test.ts | 3 +-
test/moves/rage-fist.test.ts | 1 -
test/moves/revival-blessing.test.ts | 7 +-
test/moves/shell-trap.test.ts | 2 +-
test/moves/trick-room.test.ts | 12 +-
test/moves/wish.test.ts | 2 +-
.../mystery-encounter/encounter-test-utils.ts | 2 -
.../the-winstrate-challenge-encounter.test.ts | 1 -
test/test-utils/game-manager.ts | 7 +-
70 files changed, 1340 insertions(+), 1173 deletions(-)
delete mode 100644 src/data/phase-priority-queue.ts
create mode 100644 src/dynamic-queue-manager.ts
delete mode 100644 src/enums/dynamic-phase-type.ts
create mode 100644 src/enums/move-phase-timing-modifier.ts
create mode 100644 src/phase-tree.ts
delete mode 100644 src/phases/activate-priority-queue-phase.ts
create mode 100644 src/phases/dynamic-phase-marker.ts
create mode 100644 src/queues/move-phase-priority-queue.ts
create mode 100644 src/queues/pokemon-phase-priority-queue.ts
create mode 100644 src/queues/pokemon-priority-queue.ts
create mode 100644 src/queues/post-summon-phase-priority-queue.ts
create mode 100644 src/queues/priority-queue.ts
create mode 100644 src/utils/speed-order-generator.ts
create mode 100644 src/utils/speed-order.ts
diff --git a/src/@types/phase-types.ts b/src/@types/phase-types.ts
index 91673053747..d396375c5fa 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";
import type { ObjectValues } from "#types/type-helpers";
@@ -24,3 +26,14 @@ export type PhaseClass = ObjectValues;
* Union type of all phase names as strings.
*/
export type PhaseString = keyof PhaseMap;
+
+/** Type for predicate functions operating on a specific type of {@linkcode Phase}. */
+
+export type PhaseConditionFunc = (phase: PhaseMap[T]) => boolean;
+
+/**
+ * Interface type representing the assumption that all phases with pokemon associated are dynamic
+ */
+export interface DynamicPhase extends Phase {
+ getPokemon(): Pokemon;
+}
diff --git a/src/battle-scene.ts b/src/battle-scene.ts
index cbda368782e..289c9a8f051 100644
--- a/src/battle-scene.ts
+++ b/src/battle-scene.ts
@@ -104,7 +104,6 @@ import {
import { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterSaveData } from "#mystery-encounters/mystery-encounter-save-data";
import { allMysteryEncounters, mysteryEncountersByBiome } from "#mystery-encounters/mystery-encounters";
-import type { MovePhase } from "#phases/move-phase";
import { expSpriteKeys } from "#sprites/sprite-keys";
import { hasExpSprite } from "#sprites/sprite-utils";
import type { Variant } from "#sprites/variant";
@@ -787,12 +786,14 @@ export class BattleScene extends SceneBase {
/**
* Returns an array of EnemyPokemon of length 1 or 2 depending on if in a double battle or not.
- * Does not actually check if the pokemon are on the field or not.
+ * @param active - (Default `false`) Whether to consider only {@linkcode Pokemon.isActive | active} on-field pokemon
* @returns array of {@linkcode EnemyPokemon}
*/
- public getEnemyField(): EnemyPokemon[] {
+ public getEnemyField(active = false): EnemyPokemon[] {
const party = this.getEnemyParty();
- return party.slice(0, Math.min(party.length, this.currentBattle?.double ? 2 : 1));
+ return party
+ .slice(0, Math.min(party.length, this.currentBattle?.double ? 2 : 1))
+ .filter(p => !active || p.isActive());
}
/**
@@ -817,25 +818,7 @@ export class BattleScene extends SceneBase {
* @param allyPokemon - The {@linkcode Pokemon} allied with the removed Pokemon; will have moves redirected to it
*/
redirectPokemonMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void {
- // failsafe: if not a double battle just return
- if (this.currentBattle.double === false) {
- return;
- }
- if (allyPokemon?.isActive(true)) {
- let targetingMovePhase: MovePhase;
- do {
- targetingMovePhase = this.phaseManager.findPhase(
- mp =>
- mp.is("MovePhase")
- && mp.targets.length === 1
- && mp.targets[0] === removedPokemon.getBattlerIndex()
- && mp.pokemon.isPlayer() !== allyPokemon.isPlayer(),
- ) as MovePhase;
- if (targetingMovePhase && targetingMovePhase.targets[0] !== allyPokemon.getBattlerIndex()) {
- targetingMovePhase.targets[0] = allyPokemon.getBattlerIndex();
- }
- } while (targetingMovePhase);
- }
+ this.phaseManager.redirectMoves(removedPokemon, allyPokemon);
}
/**
@@ -1433,7 +1416,7 @@ export class BattleScene extends SceneBase {
}
if (lastBattle?.double && !newDouble) {
- this.phaseManager.tryRemovePhase((p: Phase) => p.is("SwitchPhase"));
+ this.phaseManager.tryRemovePhase("SwitchPhase");
for (const p of this.getPlayerField()) {
p.lapseTag(BattlerTagType.COMMANDED);
}
diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts
index ebe8b816e5e..f6494548b99 100644
--- a/src/data/abilities/ability.ts
+++ b/src/data/abilities/ability.ts
@@ -33,6 +33,7 @@ import { CommonAnim } from "#enums/move-anims-common";
import { MoveCategory } from "#enums/move-category";
import { MoveFlags } from "#enums/move-flags";
import { MoveId } from "#enums/move-id";
+import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
import { MoveResult } from "#enums/move-result";
import { MoveTarget } from "#enums/move-target";
import { MoveUseMode } from "#enums/move-use-mode";
@@ -2555,7 +2556,7 @@ export class PostIntimidateStatStageChangeAbAttr extends AbAttr {
override apply({ pokemon, simulated, cancelled }: AbAttrParamsWithCancel): void {
if (!simulated) {
- globalScene.phaseManager.pushNew(
+ globalScene.phaseManager.unshiftNew(
"StatStageChangePhase",
pokemon.getBattlerIndex(),
false,
@@ -3240,6 +3241,7 @@ export class CommanderAbAttr extends AbAttr {
return (
globalScene.currentBattle?.double
&& ally != null
+ && ally.isActive(true)
&& ally.species.speciesId === SpeciesId.DONDOZO
&& !(ally.isFainted() || ally.getTag(BattlerTagType.COMMANDED))
);
@@ -3254,7 +3256,7 @@ export class CommanderAbAttr extends AbAttr {
// Apply boosts from this effect to the ally Dondozo
pokemon.getAlly()?.addTag(BattlerTagType.COMMANDED, 0, MoveId.NONE, pokemon.id);
// Cancel the source Pokemon's next move (if a move is queued)
- globalScene.phaseManager.tryRemovePhase(phase => phase.is("MovePhase") && phase.pokemon === pokemon);
+ globalScene.phaseManager.tryRemovePhase("MovePhase", phase => phase.pokemon === pokemon);
}
}
}
@@ -5004,7 +5006,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(pokemon, source, targets);
- globalScene.phaseManager.unshiftNew("MovePhase", pokemon, target, move, MoveUseMode.INDIRECT);
+ globalScene.phaseManager.unshiftNew(
+ "MovePhase",
+ pokemon,
+ 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.unshiftNew(
@@ -5013,6 +5022,7 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr {
[pokemon.getBattlerIndex()],
move,
MoveUseMode.INDIRECT,
+ MovePhaseTimingModifier.FIRST,
);
}
}
@@ -6028,11 +6038,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
@@ -6048,26 +6053,28 @@ 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()];
- 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 && !bypass.value && pokemon.randBattleSeedInt(100) < this.chance && isCommandFight && isDamageMove
+ !simulated
+ && pokemon.randBattleSeedInt(100) < this.chance
+ && isDamageMove
+ && pokemon.canAddTag(BattlerTagType.BYPASS_SPEED)
);
}
/**
* bypass move order in their priority bracket when pokemon choose damaging move
*/
- override apply({ bypass }: BypassSpeedChanceAbAttrParams): void {
- bypass.value = true;
+ 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) });
}
}
@@ -6075,8 +6082,6 @@ export class BypassSpeedChanceAbAttr extends AbAttr {
export interface PreventBypassSpeedChanceAbAttrParams extends AbAttrBaseParams {
/** Holds whether the speed check is bypassed after ability application */
bypass: BooleanHolder;
- /** Holds whether the Pokemon can check held items for Quick Claw's effects */
- canCheckHeldItems: BooleanHolder;
}
/**
@@ -6103,9 +6108,8 @@ export class PreventBypassSpeedChanceAbAttr extends AbAttr {
return isCommandFight && this.condition(pokemon, move!);
}
- override apply({ bypass, canCheckHeldItems }: PreventBypassSpeedChanceAbAttrParams): void {
+ override apply({ bypass }: PreventBypassSpeedChanceAbAttrParams): void {
bypass.value = false;
- canCheckHeldItems.value = false;
}
}
@@ -6203,8 +6207,7 @@ class ForceSwitchOutHelper {
if (switchOutTarget.hp > 0) {
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
- globalScene.phaseManager.prependNewToPhase(
- "MoveEndPhase",
+ globalScene.phaseManager.queueDeferred(
"SwitchPhase",
this.switchType,
switchOutTarget.getFieldIndex(),
@@ -6226,8 +6229,7 @@ class ForceSwitchOutHelper {
const summonIndex = globalScene.currentBattle.trainer
? globalScene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot)
: 0;
- globalScene.phaseManager.prependNewToPhase(
- "MoveEndPhase",
+ globalScene.phaseManager.queueDeferred(
"SwitchSummonPhase",
this.switchType,
switchOutTarget.getFieldIndex(),
@@ -7161,7 +7163,7 @@ export function initAbilities() {
new Ability(AbilityId.ANALYTIC, 5)
.attr(MovePowerBoostAbAttr, (user) =>
// Boost power if all other Pokemon have already moved (no other moves are slated to execute)
- !globalScene.phaseManager.findPhase((phase) => phase.is("MovePhase") && phase.pokemon.id !== user?.id),
+ !globalScene.phaseManager.hasPhaseOfType("MovePhase", phase => phase.pokemon.id !== user?.id),
1.3),
new Ability(AbilityId.ILLUSION, 5)
// The Pokemon generate an illusion if it's available
diff --git a/src/data/abilities/apply-ab-attrs.ts b/src/data/abilities/apply-ab-attrs.ts
index 58f63c5924a..23b16a4cac7 100644
--- a/src/data/abilities/apply-ab-attrs.ts
+++ b/src/data/abilities/apply-ab-attrs.ts
@@ -74,7 +74,6 @@ function applyAbAttrsInternal(
for (const passive of [false, true]) {
params.passive = passive;
applySingleAbAttrs(attrType, params, gainedMidTurn, messages);
- globalScene.phaseManager.clearPhaseQueueSplice();
}
// We need to restore passive to its original state in the case that it was undefined on entry
// this is necessary in case this method is called with an object that is reused.
diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts
index b6c3cf2b5a6..8abd98f4683 100644
--- a/src/data/battler-tags.ts
+++ b/src/data/battler-tags.ts
@@ -606,17 +606,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;
}
@@ -1279,22 +1269,9 @@ export class EncoreTag extends MoveRestrictionBattlerTag {
}),
);
- const movePhase = globalScene.phaseManager.findPhase(m => m.is("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,
- MoveUseMode.NORMAL,
- ),
- );
- }
+ const movesetMove = pokemon.getMoveset().find(m => m.moveId === this.moveId);
+ if (movesetMove) {
+ globalScene.phaseManager.changePhaseMove((phase: MovePhase) => phase.pokemon === pokemon, movesetMove);
}
}
@@ -3578,6 +3555,25 @@ export class GrudgeTag extends SerializableBattlerTag {
}
}
+/**
+ * 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 {
+ public override readonly tagType = BattlerTagType.BYPASS_SPEED;
+
+ constructor() {
+ super(BattlerTagType.BYPASS_SPEED, BattlerTagLapseType.TURN_END, 1);
+ }
+
+ override canAdd(pokemon: Pokemon): boolean {
+ const bypass = new BooleanHolder(true);
+ applyAbAttrs("PreventBypassSpeedChanceAbAttr", { pokemon, bypass });
+ return bypass.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
*/
@@ -3863,6 +3859,8 @@ export function getBattlerTag(
return new MagicCoatTag();
case BattlerTagType.SUPREME_OVERLORD:
return new SupremeOverlordTag();
+ case BattlerTagType.BYPASS_SPEED:
+ return new BypassSpeedTag();
}
}
@@ -3998,4 +3996,5 @@ export type BattlerTagTypeMap = {
[BattlerTagType.PSYCHO_SHIFT]: PsychoShiftTag;
[BattlerTagType.MAGIC_COAT]: MagicCoatTag;
[BattlerTagType.SUPREME_OVERLORD]: SupremeOverlordTag;
+ [BattlerTagType.BYPASS_SPEED]: BypassSpeedTag;
};
diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts
index 72376b7934f..075876d8ddd 100644
--- a/src/data/moves/move.ts
+++ b/src/data/moves/move.ts
@@ -81,10 +81,8 @@ import { applyMoveAttrs } from "#moves/apply-attrs";
import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSketchMoves, invalidSleepTalkMoves } from "#moves/invalid-moves";
import { frenzyMissFunc, getMoveTargets } from "#moves/move-utils";
import { PokemonMove } from "#moves/pokemon-move";
-import { MoveEndPhase } from "#phases/move-end-phase";
import { MovePhase } from "#phases/move-phase";
import { PokemonHealPhase } from "#phases/pokemon-heal-phase";
-import { SwitchSummonPhase } from "#phases/switch-summon-phase";
import type { AttackMoveResult } from "#types/attack-move-result";
import type { Localizable } from "#types/locales";
import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString, MoveMessageFunc } from "#types/move-types";
@@ -94,6 +92,7 @@ import { getEnumValues } from "#utils/enums";
import { toCamelCase, toTitleCase } from "#utils/strings";
import i18next from "i18next";
import { applyChallenges } from "#utils/challenge-utils";
+import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
import type { AbstractConstructor } from "#types/type-helpers";
/**
@@ -891,6 +890,10 @@ export abstract class Move implements Localizable {
applyMoveAttrs("IncrementMovePriorityAttr", user, null, this, priority);
applyAbAttrs("ChangeMovePriorityAbAttr", {pokemon: user, simulated, move: this, priority});
+ if (user.getTag(BattlerTagType.BYPASS_SPEED)) {
+ priority.value += 0.2;
+ }
+
return priority.value;
}
@@ -3298,7 +3301,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.getMovePhase((phase) => phase.pokemon.isPlayer() === user.isPlayer());
if (allyMovePhase) {
const allyMove = allyMovePhase.move.getMove();
if (allyMove !== move && allyMove.hasAttr("AwaitCombinedPledgeAttr")) {
@@ -3311,11 +3314,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;
@@ -4550,28 +4549,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)) {
@@ -4653,20 +4631,13 @@ 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.getMovePhase(phase => phase.move.moveId === MoveId.ROUND);
if (!nextRoundPhase) {
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.forceMoveNext(phase => phase.move.moveId === MoveId.ROUND);
// Mark the corresponding Pokemon as having "joined the Round" (for doubling power later)
nextRoundPhase.pokemon.turnData.joinedRound = true;
@@ -6291,11 +6262,11 @@ export class RevivalBlessingAttr extends MoveEffectAttr {
// 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);
+ globalScene.phaseManager.tryRemovePhase("SwitchSummonPhase", phase => phase.getFieldIndex() === slotIndex);
// 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)
// TODO: might make sense to move this to `FaintPhase` after checking for Rev Seed (rather than handling it in the move)
- globalScene.phaseManager.findPhase((phase: MovePhase) => phase.pokemon === pokemon)?.cancel();
+ globalScene.phaseManager.getMovePhase((phase: MovePhase) => phase.pokemon === pokemon)?.cancel();
if (user.fieldPosition === FieldPosition.CENTER) {
user.setFieldPosition(FieldPosition.LEFT);
}
@@ -6376,8 +6347,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
if (this.switchType === SwitchType.FORCE_SWITCH) {
switchOutTarget.leaveField(true);
const slotIndex = eligibleNewIndices[user.randBattleSeedInt(eligibleNewIndices.length)];
- globalScene.phaseManager.prependNewToPhase(
- "MoveEndPhase",
+ globalScene.phaseManager.queueDeferred(
"SwitchSummonPhase",
this.switchType,
switchOutTarget.getFieldIndex(),
@@ -6387,7 +6357,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
);
} else {
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
- globalScene.phaseManager.prependNewToPhase("MoveEndPhase",
+ globalScene.phaseManager.queueDeferred(
"SwitchPhase",
this.switchType,
switchOutTarget.getFieldIndex(),
@@ -6416,7 +6386,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
if (this.switchType === SwitchType.FORCE_SWITCH) {
switchOutTarget.leaveField(true);
const slotIndex = eligibleNewIndices[user.randBattleSeedInt(eligibleNewIndices.length)];
- globalScene.phaseManager.prependNewToPhase("MoveEndPhase",
+ globalScene.phaseManager.queueDeferred(
"SwitchSummonPhase",
this.switchType,
switchOutTarget.getFieldIndex(),
@@ -6426,7 +6396,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
);
} else {
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
- globalScene.phaseManager.prependNewToPhase("MoveEndPhase",
+ globalScene.phaseManager.queueDeferred(
"SwitchSummonPhase",
this.switchType,
switchOutTarget.getFieldIndex(),
@@ -6857,7 +6827,7 @@ class CallMoveAttr extends OverrideMoveEffectAttr {
: moveTargets.targets[user.randBattleSeedInt(moveTargets.targets.length)]];
globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", move.id);
- globalScene.phaseManager.unshiftNew("MovePhase", user, targets, new PokemonMove(move.id), MoveUseMode.FOLLOW_UP);
+ globalScene.phaseManager.unshiftNew("MovePhase", user, targets, new PokemonMove(move.id), MoveUseMode.FOLLOW_UP, MovePhaseTimingModifier.FIRST);
return true;
}
}
@@ -7089,7 +7059,7 @@ export class NaturePowerAttr extends OverrideMoveEffectAttr {
// Load the move's animation if we didn't already and unshift a new usage phase
globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", moveId);
- globalScene.phaseManager.unshiftNew("MovePhase", user, [ target.getBattlerIndex() ], new PokemonMove(moveId), MoveUseMode.FOLLOW_UP);
+ globalScene.phaseManager.unshiftNew("MovePhase", user, [ target.getBattlerIndex() ], new PokemonMove(moveId), MoveUseMode.FOLLOW_UP, MovePhaseTimingModifier.FIRST);
return true;
}
}
@@ -7173,7 +7143,7 @@ export class RepeatMoveAttr extends MoveEffectAttr {
targetPokemonName: getPokemonNameWithAffix(target)
}));
target.turnData.extraTurns++;
- globalScene.phaseManager.appendNewToPhase("MoveEndPhase", "MovePhase", target, moveTargets, movesetMove, MoveUseMode.NORMAL);
+ globalScene.phaseManager.unshiftNew("MovePhase", target, moveTargets, movesetMove, MoveUseMode.NORMAL, MovePhaseTimingModifier.FIRST);
return true;
}
@@ -7946,12 +7916,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 right after us.
- const targetNextPhase = globalScene.phaseManager.findPhase(phase => phase.pokemon === target);
- if (targetNextPhase && globalScene.phaseManager.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) {
- globalScene.phaseManager.prependToPhase(targetNextPhase, "MovePhase");
- }
+ globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === target);
return true;
}
@@ -7974,45 +7939,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) }));
- // TODO: Refactor this to be more readable and less janky
- 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, targetMovePhase.useMode, 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}.
-
- * TODO:
- - Make this a class method
- - Make this look at speed order from TurnStartPhase
-*/
-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();
@@ -8036,7 +7967,7 @@ const userSleptOrComatoseCondition: MoveConditionFunc = (user) => user.status?.e
const targetSleptOrComatoseCondition: MoveConditionFunc = (_user: Pokemon, target: Pokemon, _move: Move) => target.status?.effect === StatusEffect.SLEEP || target.hasAbility(AbilityId.COMATOSE);
-const failIfLastCondition: MoveConditionFunc = () => globalScene.phaseManager.findPhase(phase => phase.is("MovePhase")) !== undefined;
+const failIfLastCondition: MoveConditionFunc = () => 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/data/mystery-encounters/encounters/fun-and-games-encounter.ts b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts
index d883fdbb567..f2363ade500 100644
--- a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts
+++ b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts
@@ -414,7 +414,7 @@ function summonPlayerPokemonAnimation(pokemon: PlayerPokemon): Promise {
pokemon.resetTurnData();
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true);
- globalScene.phaseManager.pushNew("PostSummonPhase", pokemon.getBattlerIndex());
+ globalScene.phaseManager.unshiftNew("PostSummonPhase", pokemon.getBattlerIndex());
resolve();
});
},
diff --git a/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts b/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts
index b5084743613..67e778d8c4b 100644
--- a/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts
+++ b/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts
@@ -669,7 +669,6 @@ function onGameOver() {
// Clear any leftover battle phases
globalScene.phaseManager.clearPhaseQueue();
- globalScene.phaseManager.clearPhaseQueueSplice();
// Return enemy Pokemon
const pokemon = globalScene.getEnemyPokemon();
diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts
index 86cd3fa3a32..0ba0dec896a 100644
--- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts
+++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts
@@ -738,7 +738,7 @@ export function setEncounterRewards(
if (customShopRewards) {
globalScene.phaseManager.unshiftNew("SelectModifierPhase", 0, undefined, customShopRewards);
} else {
- globalScene.phaseManager.tryRemovePhase(p => p.is("MysteryEncounterRewardsPhase"));
+ globalScene.phaseManager.removeAllPhasesOfType("MysteryEncounterRewardsPhase");
}
if (eggRewards) {
@@ -812,8 +812,7 @@ export function leaveEncounterWithoutBattle(
encounterMode: MysteryEncounterMode = MysteryEncounterMode.NO_BATTLE,
) {
globalScene.currentBattle.mysteryEncounter!.encounterMode = encounterMode;
- globalScene.phaseManager.clearPhaseQueue();
- globalScene.phaseManager.clearPhaseQueueSplice();
+ globalScene.phaseManager.clearPhaseQueue(true);
handleMysteryEncounterVictory(addHealPhase);
}
@@ -826,7 +825,7 @@ export function handleMysteryEncounterVictory(addHealPhase = false, doNotContinu
const allowedPkm = globalScene.getPlayerParty().filter(pkm => pkm.isAllowedInBattle());
if (allowedPkm.length === 0) {
- globalScene.phaseManager.clearPhaseQueue();
+ globalScene.phaseManager.clearPhaseQueue(true);
globalScene.phaseManager.unshiftNew("GameOverPhase");
return;
}
@@ -869,7 +868,7 @@ export function handleMysteryEncounterBattleFailed(addHealPhase = false, doNotCo
const allowedPkm = globalScene.getPlayerParty().filter(pkm => pkm.isAllowedInBattle());
if (allowedPkm.length === 0) {
- globalScene.phaseManager.clearPhaseQueue();
+ globalScene.phaseManager.clearPhaseQueue(true);
globalScene.phaseManager.unshiftNew("GameOverPhase");
return;
}
diff --git a/src/data/phase-priority-queue.ts b/src/data/phase-priority-queue.ts
deleted file mode 100644
index 2c83348cc7b..00000000000
--- a/src/data/phase-priority-queue.ts
+++ /dev/null
@@ -1,125 +0,0 @@
-import { globalScene } from "#app/global-scene";
-import type { Phase } from "#app/phase";
-import { TrickRoomTag } from "#data/arena-tag";
-import { DynamicPhaseType } from "#enums/dynamic-phase-type";
-import { Stat } from "#enums/stat";
-import { ActivatePriorityQueuePhase } from "#phases/activate-priority-queue-phase";
-import { PostSummonActivateAbilityPhase } from "#phases/post-summon-activate-ability-phase";
-import type { PostSummonPhase } from "#phases/post-summon-phase";
-import { BooleanHolder } from "#utils/common";
-
-/**
- * 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);
- }
-
- /**
- * Attempt to remove one or more Phases from the current queue.
- * @param phaseFilter - The function to select phases for removal
- * @param removeCount - The maximum number of phases to remove, or `all` to remove all matching phases;
- * default `1`
- * @returns The number of successfully removed phases
- * @todo Remove this eventually once the patchwork bug this is used for is fixed
- */
- public tryRemovePhase(phaseFilter: (phase: Phase) => boolean, removeCount: number | "all" = 1): number {
- if (removeCount === "all") {
- removeCount = this.queue.length;
- } else if (removeCount < 1) {
- return 0;
- }
- let numRemoved = 0;
-
- do {
- const phaseIndex = this.queue.findIndex(phaseFilter);
- if (phaseIndex === -1) {
- break;
- }
- this.queue.splice(phaseIndex, 1);
- numRemoved++;
- } while (numRemoved < removeCount && this.queue.length > 0);
-
- return numRemoved;
- }
-}
-
-/**
- * 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/dynamic-queue-manager.ts b/src/dynamic-queue-manager.ts
new file mode 100644
index 00000000000..7356f67bc1d
--- /dev/null
+++ b/src/dynamic-queue-manager.ts
@@ -0,0 +1,182 @@
+import type { DynamicPhase, PhaseConditionFunc, 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 { MovePhase } from "#app/phases/move-phase";
+import { MovePhasePriorityQueue } from "#app/queues/move-phase-priority-queue";
+import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue";
+import { PostSummonPhasePriorityQueue } from "#app/queues/post-summon-phase-priority-queue";
+import type { PriorityQueue } from "#app/queues/priority-queue";
+import type { BattlerIndex } from "#enums/battler-index";
+import type { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
+
+// TODO: might be easier to define which phases should be dynamic instead
+/** All phases which have defined a `getPokemon` method but should not be sorted dynamically */
+const nonDynamicPokemonPhases: readonly PhaseString[] = [
+ "SummonPhase",
+ "CommandPhase",
+ "LearnMovePhase",
+ "MoveEffectPhase",
+ "MoveEndPhase",
+ "FaintPhase",
+ "DamageAnimPhase",
+ "VictoryPhase",
+ "PokemonHealPhase",
+ "WeatherEffectPhase",
+] as const;
+
+/**
+ * The dynamic queue manager holds priority queues for phases which are queued as dynamic.
+ *
+ * Dynamic phases are generally those which hold a pokemon and are unshifted, not pushed. \
+ * Queues work by sorting their entries in speed order (and possibly with more complex ordering) before each time a phase is popped.
+ *
+ * As the holder, this structure is also used to access and modify queued phases.
+ * This is mostly used in redirection, cancellation, etc. of {@linkcode MovePhase}s.
+ */
+export class DynamicQueueManager {
+ /** Maps phase types to their corresponding queues */
+ private readonly dynamicPhaseMap: Map>;
+
+ constructor() {
+ this.dynamicPhaseMap = new Map();
+ // PostSummon and Move phases have specialized queues
+ this.dynamicPhaseMap.set("PostSummonPhase", new PostSummonPhasePriorityQueue());
+ this.dynamicPhaseMap.set("MovePhase", new MovePhasePriorityQueue());
+ }
+
+ /** Removes all phases from the manager */
+ public clearQueues(): void {
+ for (const queue of this.dynamicPhaseMap.values()) {
+ queue.clear();
+ }
+ }
+
+ /**
+ * Adds a new phase to the manager and creates the priority queue for it if one does not exist.
+ * @param phase - The {@linkcode Phase} to add
+ * @returns `true` if the phase was added, or `false` if it is not dynamic
+ */
+ public queueDynamicPhase(phase: T): boolean {
+ if (!this.isDynamicPhase(phase)) {
+ return false;
+ }
+
+ if (!this.dynamicPhaseMap.has(phase.phaseName)) {
+ // TS can't figure out that T is dynamic at this point, but it does know that `typeof phase` is
+ this.dynamicPhaseMap.set(phase.phaseName, new PokemonPhasePriorityQueue());
+ }
+ this.dynamicPhaseMap.get(phase.phaseName)?.push(phase);
+ return true;
+ }
+
+ /**
+ * Returns the highest-priority (generally by speed) {@linkcode Phase} of the specified type
+ * @param type - The {@linkcode PhaseString | type} to pop
+ * @returns The popped {@linkcode Phase}, or `undefined` if none of the specified type exist
+ */
+ public popNextPhase(type: PhaseString): Phase | undefined {
+ return this.dynamicPhaseMap.get(type)?.pop();
+ }
+
+ /**
+ * Determines if there is a queued dynamic {@linkcode Phase} meeting the conditions
+ * @param type - The {@linkcode PhaseString | type} of phase to search for
+ * @param condition - An optional {@linkcode PhaseConditionFunc} to add conditions to the search
+ * @returns Whether a matching phase exists
+ */
+ public exists(type: T, condition?: PhaseConditionFunc): boolean {
+ return !!this.dynamicPhaseMap.get(type)?.has(condition);
+ }
+
+ /**
+ * Finds and removes a single queued {@linkcode Phase}
+ * @param type - The {@linkcode PhaseString | type} of phase to search for
+ * @param phaseFilter - A {@linkcode PhaseConditionFunc} to specify conditions for the phase
+ * @returns Whether a removal occurred
+ */
+ public removePhase(type: T, condition?: PhaseConditionFunc): boolean {
+ return !!this.dynamicPhaseMap.get(type)?.remove(condition);
+ }
+
+ /**
+ * Sets the timing modifier of a move (i.e. to force it first or last)
+ * @param condition - A {@linkcode PhaseConditionFunc} to specify conditions for the move
+ * @param modifier - The {@linkcode MovePhaseTimingModifier} to switch the move to
+ */
+ public setMoveTimingModifier(condition: PhaseConditionFunc<"MovePhase">, modifier: MovePhaseTimingModifier): void {
+ this.getMovePhaseQueue().setTimingModifier(condition, modifier);
+ }
+
+ /**
+ * Finds the {@linkcode MovePhase} meeting the condition and changes its move
+ * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function
+ * @param move - The {@linkcode PokemonMove | move} to use in replacement
+ */
+ public setMoveForPhase(condition: PhaseConditionFunc<"MovePhase">, move: PokemonMove): void {
+ this.getMovePhaseQueue().setMoveForPhase(condition, move);
+ }
+
+ /**
+ * Redirects moves which were targeted at a {@linkcode Pokemon} that has been removed
+ * @param removedPokemon - The removed {@linkcode Pokemon}
+ * @param allyPokemon - The ally of the removed pokemon
+ */
+ public redirectMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void {
+ this.getMovePhaseQueue().redirectMoves(removedPokemon, allyPokemon);
+ }
+
+ /**
+ * Finds a {@linkcode MovePhase} meeting the condition
+ * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function
+ * @returns The MovePhase, or `undefined` if it does not exist
+ */
+ public getMovePhase(condition: PhaseConditionFunc<"MovePhase">): MovePhase | undefined {
+ return this.getMovePhaseQueue().find(condition);
+ }
+
+ /**
+ * Finds and cancels a {@linkcode MovePhase} meeting the condition
+ * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function
+ */
+ public cancelMovePhase(condition: PhaseConditionFunc<"MovePhase">): void {
+ this.getMovePhaseQueue().cancelMove(condition);
+ }
+
+ /**
+ * Sets the move order to a static array rather than a dynamic queue
+ * @param order - The order of {@linkcode BattlerIndex}s
+ */
+ public setMoveOrder(order: BattlerIndex[]): void {
+ this.getMovePhaseQueue().setMoveOrder(order);
+ }
+
+ /**
+ * @returns An in-order array of {@linkcode Pokemon}, representing the turn order as played out in the most recent turn
+ */
+ public getLastTurnOrder(): Pokemon[] {
+ return this.getMovePhaseQueue().getTurnOrder();
+ }
+
+ /** Clears the stored `Move` turn order */
+ public clearLastTurnOrder(): void {
+ this.getMovePhaseQueue().clearTurnOrder();
+ }
+
+ /** Internal helper to get the {@linkcode MovePhasePriorityQueue} */
+ private getMovePhaseQueue(): MovePhasePriorityQueue {
+ return this.dynamicPhaseMap.get("MovePhase") as MovePhasePriorityQueue;
+ }
+
+ /**
+ * Internal helper to determine if a phase is dynamic.
+ * @param phase - The {@linkcode Phase} to check
+ * @returns Whether `phase` is dynamic
+ * @privateRemarks
+ * Currently, this checks that `phase` has a `getPokemon` method
+ * and is not blacklisted in `nonDynamicPokemonPhases`.
+ */
+ private isDynamicPhase(phase: Phase): phase is DynamicPhase {
+ return typeof (phase as any).getPokemon === "function" && !nonDynamicPokemonPhases.includes(phase.phaseName);
+ }
+}
diff --git a/src/enums/arena-tag-side.ts b/src/enums/arena-tag-side.ts
index 5f25a74ab36..50741751fbb 100644
--- a/src/enums/arena-tag-side.ts
+++ b/src/enums/arena-tag-side.ts
@@ -1,3 +1,4 @@
+// TODO: rename to something else (this isn't used only for arena tags)
export enum ArenaTagSide {
BOTH,
PLAYER,
diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts
index 7956e506886..4f0ac491e8b 100644
--- a/src/enums/battler-tag-type.ts
+++ b/src/enums/battler-tag-type.ts
@@ -95,4 +95,5 @@ export enum BattlerTagType {
POWDER = "POWDER",
MAGIC_COAT = "MAGIC_COAT",
SUPREME_OVERLORD = "SUPREME_OVERLORD",
+ BYPASS_SPEED = "BYPASS_SPEED",
}
diff --git a/src/enums/dynamic-phase-type.ts b/src/enums/dynamic-phase-type.ts
deleted file mode 100644
index 3146b136dac..00000000000
--- a/src/enums/dynamic-phase-type.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * Enum representation of the phase types held by implementations of {@linkcode PhasePriorityQueue}.
- */
-// TODO: We currently assume these are in order
-export enum DynamicPhaseType {
- POST_SUMMON,
-}
diff --git a/src/enums/move-phase-timing-modifier.ts b/src/enums/move-phase-timing-modifier.ts
new file mode 100644
index 00000000000..a452d37e7ff
--- /dev/null
+++ b/src/enums/move-phase-timing-modifier.ts
@@ -0,0 +1,16 @@
+import type { ObjectValues } from "#types/type-helpers";
+
+/**
+ * Enum representing modifiers for the timing of MovePhases.
+ *
+ * @remarks
+ * This system is entirely independent of and takes precedence over move priority
+ */
+export const MovePhaseTimingModifier = Object.freeze({
+ /** Used when moves go last regardless of speed and priority (i.e. Quash) */
+ LAST: 0,
+ NORMAL: 1,
+ /** Used to trigger moves immediately (i.e. ones that were called through Instruct). */
+ FIRST: 2,
+});
+export type MovePhaseTimingModifier = ObjectValues;
diff --git a/src/field/arena.ts b/src/field/arena.ts
index 5ab50e540ee..3e214ff1ea7 100644
--- a/src/field/arena.ts
+++ b/src/field/arena.ts
@@ -371,9 +371,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/field/pokemon.ts b/src/field/pokemon.ts
index 3154f273cf5..ec813e52e56 100644
--- a/src/field/pokemon.ts
+++ b/src/field/pokemon.ts
@@ -3890,15 +3890,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
damage = Math.min(damage, this.hp);
this.hp = this.hp - damage;
if (this.isFainted() && !ignoreFaintPhase) {
- /**
- * When adding the FaintPhase, want to toggle future unshiftPhase() and queueMessage() calls
- * to appear before the FaintPhase (as FaintPhase will potentially end the encounter and add Phases such as
- * GameOverPhase, VictoryPhase, etc.. that will interfere with anything else that happens during this MoveEffectPhase)
- *
- * Once the MoveEffectPhase is over (and calls it's .end() function, shiftPhase() will reset the PhaseQueueSplice via clearPhaseQueueSplice() )
- */
- globalScene.phaseManager.setPhaseQueueSplice();
- globalScene.phaseManager.unshiftNew("FaintPhase", this.getBattlerIndex(), preventEndure);
+ globalScene.phaseManager.queueFaintPhase(this.getBattlerIndex(), preventEndure);
this.destroySubstitute();
this.lapseTag(BattlerTagType.COMMANDED);
}
@@ -5842,8 +5834,7 @@ export class PlayerPokemon extends Pokemon {
this.getFieldIndex(),
(slotIndex: number, _option: PartyOption) => {
if (slotIndex >= globalScene.currentBattle.getBattlerCount() && slotIndex < 6) {
- globalScene.phaseManager.prependNewToPhase(
- "MoveEndPhase",
+ globalScene.phaseManager.queueDeferred(
"SwitchSummonPhase",
switchType,
this.getFieldIndex(),
diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts
index b94c479e96e..19ddc77d436 100644
--- a/src/modifier/modifier.ts
+++ b/src/modifier/modifier.ts
@@ -13,7 +13,6 @@ import { getStatusEffectHealText } from "#data/status-effect";
import { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type";
import { Color, ShadowColor } from "#enums/color";
-import { Command } from "#enums/command";
import type { FormChangeItem } from "#enums/form-change-item";
import { LearnMoveType } from "#enums/learn-move-type";
import type { MoveId } from "#enums/move-id";
@@ -1542,30 +1541,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/phase-manager.ts b/src/phase-manager.ts
index 125ca00786b..350e77e52eb 100644
--- a/src/phase-manager.ts
+++ b/src/phase-manager.ts
@@ -8,12 +8,14 @@
*/
import { PHASE_START_COLOR } from "#app/constants/colors";
+import { DynamicQueueManager } from "#app/dynamic-queue-manager";
import { globalScene } from "#app/global-scene";
import type { Phase } from "#app/phase";
-import { type PhasePriorityQueue, PostSummonPhasePriorityQueue } from "#data/phase-priority-queue";
-import type { DynamicPhaseType } from "#enums/dynamic-phase-type";
+import { PhaseTree } from "#app/phase-tree";
+import { BattleType } from "#enums/battle-type";
+import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
import type { Pokemon } from "#field/pokemon";
-import { ActivatePriorityQueuePhase } from "#phases/activate-priority-queue-phase";
+import type { PokemonMove } from "#moves/pokemon-move";
import { AddEnemyBuffModifierPhase } from "#phases/add-enemy-buff-modifier-phase";
import { AttemptCapturePhase } from "#phases/attempt-capture-phase";
import { AttemptRunPhase } from "#phases/attempt-run-phase";
@@ -25,6 +27,7 @@ import { CheckSwitchPhase } from "#phases/check-switch-phase";
import { CommandPhase } from "#phases/command-phase";
import { CommonAnimPhase } from "#phases/common-anim-phase";
import { DamageAnimPhase } from "#phases/damage-anim-phase";
+import { DynamicPhaseMarker } from "#phases/dynamic-phase-marker";
import { EggHatchPhase } from "#phases/egg-hatch-phase";
import { EggLapsePhase } from "#phases/egg-lapse-phase";
import { EggSummaryPhase } from "#phases/egg-summary-phase";
@@ -109,8 +112,7 @@ import { UnavailablePhase } from "#phases/unavailable-phase";
import { UnlockPhase } from "#phases/unlock-phase";
import { VictoryPhase } from "#phases/victory-phase";
import { WeatherEffectPhase } from "#phases/weather-effect-phase";
-import type { PhaseMap, PhaseString } from "#types/phase-types";
-import { type Constructor, coerceArray } from "#utils/common";
+import type { PhaseConditionFunc, PhaseMap, PhaseString } from "#types/phase-types";
/**
* Object that holds all of the phase constructors.
@@ -121,7 +123,6 @@ import { type Constructor, coerceArray } from "#utils/common";
* This allows for easy creation of new phases without needing to import each phase individually.
*/
const PHASES = Object.freeze({
- ActivatePriorityQueuePhase,
AddEnemyBuffModifierPhase,
AttemptCapturePhase,
AttemptRunPhase,
@@ -133,6 +134,7 @@ const PHASES = Object.freeze({
CommandPhase,
CommonAnimPhase,
DamageAnimPhase,
+ DynamicPhaseMarker,
EggHatchPhase,
EggLapsePhase,
EggSummaryPhase,
@@ -221,32 +223,30 @@ const PHASES = Object.freeze({
/** Maps Phase strings to their constructors */
export type PhaseConstructorMap = typeof PHASES;
+/** Phases pushed at the end of each {@linkcode TurnStartPhase} */
+const turnEndPhases: readonly PhaseString[] = [
+ "WeatherEffectPhase",
+ "PositionalTagPhase",
+ "BerryPhase",
+ "CheckStatusEffectPhase",
+ "TurnEndPhase",
+] as const;
+
/**
* PhaseManager is responsible for managing the phases in the battle scene
*/
export class PhaseManager {
/** PhaseQueue: dequeue/remove the first element to get the next phase */
- public phaseQueue: Phase[] = [];
- public conditionalQueue: Array<[() => boolean, Phase]> = [];
- /** PhaseQueuePrepend: is a temp storage of what will be added to PhaseQueue */
- private phaseQueuePrepend: Phase[] = [];
+ private readonly phaseQueue: PhaseTree = new PhaseTree();
- /** overrides default of inserting phases to end of phaseQueuePrepend array. Useful for inserting Phases "out of order" */
- private phaseQueuePrependSpliceIndex = -1;
-
- /** 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[];
+ /** Holds priority queues for dynamically ordered phases */
+ public dynamicQueueManager = new DynamicQueueManager();
+ /** The currently-running phase */
private currentPhase: Phase;
+ /** The phase put on standby if {@linkcode overridePhase} is called */
private standbyPhase: Phase | null = null;
- constructor() {
- this.dynamicPhaseQueues = [new PostSummonPhasePriorityQueue()];
- this.dynamicPhaseTypes = [PostSummonPhase];
- }
-
/**
* Clear all previously set phases, then add a new {@linkcode TitlePhase} to transition to the title screen.
* @param addLogin - Whether to add a new {@linkcode LoginPhase} before the {@linkcode TitlePhase}
@@ -274,122 +274,76 @@ export class PhaseManager {
}
/**
- * 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.
- *
+ * Adds a phase to the end of the queue
+ * @param phase - The {@linkcode Phase} to add
*/
- pushConditionalPhase(phase: Phase, condition: () => boolean): void {
- this.conditionalQueue.push([condition, phase]);
+ public pushPhase(phase: Phase): void {
+ this.phaseQueue.pushPhase(this.checkDynamic(phase));
}
/**
- * Adds a phase to nextCommandPhaseQueue, as long as boolean passed in is false
- * @param phase {@linkcode Phase} the phase to add
+ * Queue a phase to be run immediately after the current phase finishes. \
+ * Unshifted phases are run in FIFO order if multiple are queued during a single phase's execution.
+ * @param phase - The {@linkcode Phase} to add
*/
- pushPhase(phase: Phase): void {
- if (this.getDynamicPhaseType(phase) !== undefined) {
- this.pushDynamicPhase(phase);
- } else {
- this.phaseQueue.push(phase);
- }
+ public unshiftPhase(phase: Phase): void {
+ const toAdd = this.checkDynamic(phase);
+ phase.is("MovePhase") ? this.phaseQueue.addAfter(toAdd, "MoveEndPhase") : this.phaseQueue.addPhase(toAdd);
}
/**
- * Adds Phase(s) to the end of phaseQueuePrepend, or at phaseQueuePrependSpliceIndex
- * @param phases {@linkcode Phase} the phase(s) to add
+ * Helper method to queue a phase as dynamic if necessary
+ * @param phase - The phase to check
+ * @returns The {@linkcode Phase} or a {@linkcode DynamicPhaseMarker} to be used in its place
*/
- unshiftPhase(...phases: Phase[]): void {
- if (this.phaseQueuePrependSpliceIndex === -1) {
- this.phaseQueuePrepend.push(...phases);
- } else {
- this.phaseQueuePrepend.splice(this.phaseQueuePrependSpliceIndex, 0, ...phases);
+ private checkDynamic(phase: Phase): Phase {
+ if (this.dynamicQueueManager.queueDynamicPhase(phase)) {
+ return new DynamicPhaseMarker(phase.phaseName);
}
+ return phase;
}
/**
* Clears the phaseQueue
+ * @param leaveUnshifted - If `true`, leaves the top level of the tree intact; default `false`
*/
- clearPhaseQueue(): void {
- this.phaseQueue.splice(0, this.phaseQueue.length);
+ public clearPhaseQueue(leaveUnshifted = false): void {
+ this.phaseQueue.clear(leaveUnshifted);
}
- /**
- * 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]) {
- queue.splice(0, queue.length);
- }
- this.dynamicPhaseQueues.forEach(queue => queue.clear());
+ /** Clears all phase queues and the standby phase */
+ public clearAllPhases(): void {
+ this.clearPhaseQueue();
+ this.dynamicQueueManager.clearQueues();
this.standbyPhase = null;
- this.clearPhaseQueueSplice();
}
/**
- * Used by function unshiftPhase(), sets index to start inserting at current length instead of the end of the array, useful if phaseQueuePrepend gets longer with Phases
+ * Determines the next phase to run and starts it.
+ * @privateRemarks
+ * This is called by {@linkcode Phase.end} by default, and should not be called by other methods.
*/
- setPhaseQueueSplice(): void {
- this.phaseQueuePrependSpliceIndex = this.phaseQueuePrepend.length;
- }
-
- /**
- * Resets phaseQueuePrependSpliceIndex to -1, implies that calls to unshiftPhase will insert at end of phaseQueuePrepend
- */
- clearPhaseQueueSplice(): void {
- this.phaseQueuePrependSpliceIndex = -1;
- }
-
- /**
- * Is called by each Phase implementations "end()" by default
- * We dump everything from phaseQueuePrepend to the start of of phaseQueue
- * then removes first Phase and starts it
- */
- shiftPhase(): void {
+ public shiftPhase(): void {
if (this.standbyPhase) {
this.currentPhase = this.standbyPhase;
this.standbyPhase = null;
return;
}
- if (this.phaseQueuePrependSpliceIndex > -1) {
- this.clearPhaseQueueSplice();
- }
- this.phaseQueue.unshift(...this.phaseQueuePrepend);
- this.phaseQueuePrepend.splice(0);
+ let nextPhase = this.phaseQueue.getNextPhase();
- const unactivatedConditionalPhases: [() => boolean, Phase][] = [];
- // Check if there are any conditional phases queued
- for (const [condition, phase] of this.conditionalQueue) {
- // Evaluate the condition associated with the phase
- if (condition()) {
- // If the condition is met, add the phase to the phase queue
- this.pushPhase(phase);
- } else {
- // If the condition is not met, re-add the phase back to the end of the conditional queue
- unactivatedConditionalPhases.push([condition, phase]);
- }
+ if (nextPhase?.is("DynamicPhaseMarker")) {
+ nextPhase = this.dynamicQueueManager.popNextPhase(nextPhase.phaseType);
}
- this.conditionalQueue = unactivatedConditionalPhases;
-
- // If no phases are left, unshift phases to start a new turn.
- if (this.phaseQueue.length === 0) {
- this.populatePhaseQueue();
- // Clear the conditionalQueue if there are no phases left in the phaseQueue
- this.conditionalQueue = [];
+ if (nextPhase == null) {
+ this.turnStart();
+ } else {
+ this.currentPhase = nextPhase;
}
- // Bang is justified as `populatePhaseQueue` ensures we always have _something_ in the queue at all times
- this.currentPhase = this.phaseQueue.shift()!;
-
this.startCurrentPhase();
}
-
/**
* Helper method to start and log the current phase.
*/
@@ -398,7 +352,14 @@ export class PhaseManager {
this.currentPhase.start();
}
- overridePhase(phase: Phase): boolean {
+ /**
+ * Overrides the currently running phase with another
+ * @param phase - The {@linkcode Phase} to override the current one with
+ * @returns If the override succeeded
+ *
+ * @todo This is antithetical to the phase structure and used a single time. Remove it.
+ */
+ public overridePhase(phase: Phase): boolean {
if (this.standbyPhase) {
return false;
}
@@ -411,173 +372,47 @@ export class PhaseManager {
}
/**
- * Find a specific {@linkcode Phase} in the phase queue.
+ * Determine if there is a queued {@linkcode Phase} meeting the specified conditions.
+ * @param type - The {@linkcode PhaseString | type} of phase to search for
+ * @param condition - An optional {@linkcode PhaseConditionFunc} to add conditions to the search
+ * @returns Whether a matching phase exists
+ */
+ public hasPhaseOfType(type: T, condition?: PhaseConditionFunc): boolean {
+ return this.dynamicQueueManager.exists(type, condition) || this.phaseQueue.exists(type, condition);
+ }
+
+ /**
+ * Attempt to find and remove the first queued {@linkcode Phase} matching the given conditions.
+ * @param type - The {@linkcode PhaseString | type} of phase to search for
+ * @param phaseFilter - An optional {@linkcode PhaseConditionFunc} to add conditions to the search
+ * @returns Whether a phase was successfully removed
+ */
+ public tryRemovePhase(type: T, phaseFilter?: PhaseConditionFunc): boolean {
+ if (this.dynamicQueueManager.removePhase(type, phaseFilter)) {
+ return true;
+ }
+ return this.phaseQueue.remove(type, phaseFilter);
+ }
+
+ /**
+ * Removes all {@linkcode Phase}s of the given type from the queue
+ * @param phaseType - The {@linkcode PhaseString | type} of phase to search for
*
- * @param phaseFilter filter function to use to find the wanted phase
- * @returns the found phase or undefined if none found
+ * @remarks
+ * This is not intended to be used with dynamically ordered phases, and does not operate on the dynamic queue. \
+ * However, it does remove {@linkcode DynamicPhaseMarker}s and so would prevent such phases from activating.
*/
- findPhase(phaseFilter: (phase: P) => boolean): P | undefined {
- return this.phaseQueue.find(phaseFilter) as P | undefined;
- }
-
- tryReplacePhase(phaseFilter: (phase: Phase) => boolean, phase: Phase): boolean {
- const phaseIndex = this.phaseQueue.findIndex(phaseFilter);
- if (phaseIndex > -1) {
- this.phaseQueue[phaseIndex] = phase;
- return true;
- }
- return false;
- }
-
- tryRemovePhase(phaseFilter: (phase: Phase) => boolean): boolean {
- const phaseIndex = this.phaseQueue.findIndex(phaseFilter);
- if (phaseIndex > -1) {
- this.phaseQueue.splice(phaseIndex, 1);
- return true;
- }
- return false;
+ public removeAllPhasesOfType(type: PhaseString): void {
+ this.phaseQueue.removeAll(type);
}
/**
- * 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 {
- const phaseIndex = this.phaseQueuePrepend.findIndex(phaseFilter);
- if (phaseIndex > -1) {
- this.phaseQueuePrepend.splice(phaseIndex, 1);
- return true;
- }
- return false;
- }
-
- /**
- * Tries to add the input phase to index before target phase in the phaseQueue, else simply calls unshiftPhase()
- * @param phase - The phase to be added
- * @param 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);
- const target = PHASES[targetPhase];
- const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof target);
-
- if (targetIndex !== -1) {
- this.phaseQueue.splice(targetIndex, 0, ...phase);
- return true;
- }
- this.unshiftPhase(...phase);
- return false;
- }
-
- /**
- * Tries to add the input phase(s) to index after target phase in the {@linkcode phaseQueue}, else simply calls {@linkcode unshiftPhase()}
- * @param phase {@linkcode Phase} the phase(s) to be added
- * @param targetPhase {@linkcode Phase} the type of phase to search for in {@linkcode phaseQueue}
- * @param condition Condition the target phase must meet to be appended to
- * @returns `true` if a `targetPhase` was found to append to
- */
- appendToPhase(phase: Phase | Phase[], targetPhase: PhaseString, condition?: (p: Phase) => boolean): boolean {
- phase = coerceArray(phase);
- const target = PHASES[targetPhase];
- const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof target && (!condition || condition(ph)));
-
- if (targetIndex !== -1 && this.phaseQueue.length > targetIndex) {
- this.phaseQueue.splice(targetIndex + 1, 0, ...phase);
- return true;
- }
- this.unshiftPhase(...phase);
- 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}
- *
- * The {@linkcode ActivatePriorityQueuePhase} will run the top phase in the dynamic queue (not necessarily {@linkcode phase})
- * @param phase The phase to push
- */
- public pushDynamicPhase(phase: Phase): void {
- const type = this.getDynamicPhaseType(phase);
- if (type === undefined) {
- return;
- }
-
- this.pushPhase(new ActivatePriorityQueuePhase(type));
- this.dynamicPhaseQueues[type].push(phase);
- }
-
- /**
- * Attempt to remove one or more Phases from the given DynamicPhaseQueue, removing the equivalent amount of {@linkcode ActivatePriorityQueuePhase}s from the queue.
- * @param type - The {@linkcode DynamicPhaseType} to check
- * @param phaseFilter - The function to select phases for removal
- * @param removeCount - The maximum number of phases to remove, or `all` to remove all matching phases;
- * default `1`
- * @todo Remove this eventually once the patchwork bug this is used for is fixed
- */
- public tryRemoveDynamicPhase(
- type: DynamicPhaseType,
- phaseFilter: (phase: Phase) => boolean,
- removeCount: number | "all" = 1,
- ): void {
- const numRemoved = this.dynamicPhaseQueues[type].tryRemovePhase(phaseFilter, removeCount);
- for (let x = 0; x < numRemoved; x++) {
- this.tryRemovePhase(p => p.is("ActivatePriorityQueuePhase"));
- }
- }
-
- /**
- * Unshifts the top phase from the corresponding dynamic queue onto {@linkcode phaseQueue}
- * @param type {@linkcode DynamicPhaseType} The type of dynamic phase to start
- */
- public startDynamicPhaseType(type: DynamicPhaseType): void {
- const phase = this.dynamicPhaseQueues[type].pop();
- if (phase) {
- this.unshiftPhase(phase);
- }
- }
-
- /**
- * Unshifts an {@linkcode ActivatePriorityQueuePhase} for {@linkcode phase}, then pushes {@linkcode phase} to its dynamic queue
- *
- * This is the same as {@linkcode pushDynamicPhase}, except the activation phase is unshifted
- *
- * {@linkcode phase} is not guaranteed to be the next phase from the queue to run (if the queue is not empty)
- * @param phase The phase to add
- * @returns
- */
- public startDynamicPhase(phase: Phase): void {
- const type = this.getDynamicPhaseType(phase);
- if (type === undefined) {
- return;
- }
-
- this.unshiftPhase(new ActivatePriorityQueuePhase(type));
- this.dynamicPhaseQueues[type].push(phase);
- }
-
- /**
- * Adds a MessagePhase, either to PhaseQueuePrepend or nextCommandPhaseQueue
+ * Adds a `MessagePhase` to the queue
* @param message - string for MessagePhase
* @param callbackDelay - optional param for MessagePhase constructor
* @param prompt - optional param for MessagePhase constructor
* @param promptDelay - optional param for MessagePhase constructor
- * @param defer - Whether to allow the phase to be deferred
+ * @param defer - If `true`, push the phase instead of unshifting; default `false`
*
* @see {@linkcode MessagePhase} for more details on the parameters
*/
@@ -589,20 +424,18 @@ export class PhaseManager {
defer?: boolean | null,
) {
const phase = new MessagePhase(message, callbackDelay, prompt, promptDelay);
- if (!defer) {
- // adds to the end of PhaseQueuePrepend
- this.unshiftPhase(phase);
- } else {
- //remember that pushPhase adds it to nextCommandPhaseQueue
+ if (defer) {
this.pushPhase(phase);
+ } else {
+ this.unshiftPhase(phase);
}
}
/**
- * Queue a phase to show or hide the ability flyout bar.
+ * Queues an ability bar flyout phase via {@linkcode unshiftPhase}
* @param pokemon - The {@linkcode Pokemon} whose ability is being activated
* @param passive - Whether the ability is a passive
- * @param show - Whether to show or hide the bar
+ * @param show - If `true`, show the bar. Otherwise, hide it
*/
public queueAbilityDisplay(pokemon: Pokemon, passive: boolean, show: boolean): void {
this.unshiftPhase(show ? new ShowAbilityPhase(pokemon.getBattlerIndex(), passive) : new HideAbilityPhase());
@@ -618,10 +451,12 @@ export class PhaseManager {
}
/**
- * Moves everything from nextCommandPhaseQueue to phaseQueue (keeping order)
+ * Clear all dynamic queues and begin a new {@linkcode TurnInitPhase} for the new turn.
+ * Called whenever the current phase queue is empty.
*/
- private populatePhaseQueue(): void {
- this.phaseQueue.push(new TurnInitPhase());
+ private turnStart(): void {
+ this.dynamicQueueManager.clearQueues();
+ this.currentPhase = new TurnInitPhase();
}
/**
@@ -663,50 +498,119 @@ export class PhaseManager {
}
/**
- * Create a new phase and immediately prepend it to an existing phase in the phase queue.
- * Equivalent to calling {@linkcode create} followed by {@linkcode prependToPhase}.
- * @param targetPhase - The phase to search for in phaseQueue
- * @param phase - The name of the phase to create
+ * Add a {@linkcode FaintPhase} to the queue
* @param args - The arguments to pass to the phase constructor
- * @returns `true` if a `targetPhase` was found to prepend to
+ *
+ * @remarks
+ *
+ * Faint phases are ordered in a special way to allow battle effects to settle before the pokemon faints.
+ * @see {@linkcode PhaseTree.addPhase}
*/
- public prependNewToPhase(
- targetPhase: PhaseString,
- phase: T,
- ...args: ConstructorParameters
- ): boolean {
- return this.prependToPhase(this.create(phase, ...args), targetPhase);
+ public queueFaintPhase(...args: ConstructorParameters): void {
+ this.phaseQueue.addPhase(this.create("FaintPhase", ...args), true);
}
/**
- * Create a new phase and immediately append it to an existing phase the phase queue.
- * Equivalent to calling {@linkcode create} followed by {@linkcode appendToPhase}.
- * @param targetPhase - The phase to search for in phaseQueue
- * @param phase - The name of the phase to create
- * @param args - The arguments to pass to the phase constructor
- * @returns `true` if a `targetPhase` was found to append to
+ * Attempts to add {@linkcode PostSummonPhase}s for the enemy pokemon
+ *
+ * This is used to ensure that wild pokemon (which have no {@linkcode SummonPhase}) do not queue a {@linkcode PostSummonPhase}
+ * until all pokemon are on the field.
*/
- public appendNewToPhase(
- targetPhase: PhaseString,
- phase: T,
- ...args: ConstructorParameters
- ): boolean {
- return this.appendToPhase(this.create(phase, ...args), targetPhase);
+ public tryAddEnemyPostSummonPhases(): void {
+ if (
+ ![BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(globalScene.currentBattle.battleType)
+ && !this.phaseQueue.exists("SummonPhase")
+ ) {
+ globalScene.getEnemyField().forEach(p => {
+ this.pushPhase(new PostSummonPhase(p.getBattlerIndex(), "SummonPhase"));
+ });
+ }
}
- public startNewDynamicPhase(
+ /**
+ * Create a new phase and queue it to run after all others queued by the currently running phase.
+ * @param phase - The name of the phase to create
+ * @param args - The arguments to pass to the phase constructor
+ *
+ * @deprecated Only used for switches and should be phased out eventually.
+ */
+ public queueDeferred(
phase: T,
...args: ConstructorParameters
): void {
- this.startDynamicPhase(this.create(phase, ...args));
+ this.phaseQueue.unshiftToCurrent(this.create(phase, ...args));
+ }
+
+ /**
+ * Finds the first {@linkcode MovePhase} meeting the condition
+ * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function
+ * @returns The MovePhase, or `undefined` if it does not exist
+ */
+ public getMovePhase(phaseCondition: PhaseConditionFunc<"MovePhase">): MovePhase | undefined {
+ return this.dynamicQueueManager.getMovePhase(phaseCondition);
+ }
+
+ /**
+ * Finds and cancels the first {@linkcode MovePhase} meeting the condition
+ * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function
+ */
+ public cancelMove(phaseCondition: PhaseConditionFunc<"MovePhase">): void {
+ this.dynamicQueueManager.cancelMovePhase(phaseCondition);
+ }
+
+ /**
+ * Finds the first {@linkcode MovePhase} meeting the condition and forces it next
+ * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function
+ */
+ public forceMoveNext(phaseCondition: PhaseConditionFunc<"MovePhase">): void {
+ this.dynamicQueueManager.setMoveTimingModifier(phaseCondition, MovePhaseTimingModifier.FIRST);
+ }
+
+ /**
+ * Finds the first {@linkcode MovePhase} meeting the condition and forces it last
+ * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function
+ */
+ public forceMoveLast(phaseCondition: PhaseConditionFunc<"MovePhase">): void {
+ this.dynamicQueueManager.setMoveTimingModifier(phaseCondition, MovePhaseTimingModifier.LAST);
+ }
+
+ /**
+ * Finds the first {@linkcode MovePhase} meeting the condition and changes its move
+ * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function
+ * @param move - The {@linkcode PokemonMove | move} to use in replacement
+ */
+ public changePhaseMove(phaseCondition: PhaseConditionFunc<"MovePhase">, move: PokemonMove): void {
+ this.dynamicQueueManager.setMoveForPhase(phaseCondition, move);
+ }
+
+ /**
+ * Redirects moves which were targeted at a {@linkcode Pokemon} that has been removed
+ * @param removedPokemon - The removed {@linkcode Pokemon}
+ * @param allyPokemon - The ally of the removed pokemon
+ */
+ public redirectMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void {
+ this.dynamicQueueManager.redirectMoves(removedPokemon, allyPokemon);
+ }
+
+ /** Queues phases which run at the end of each turn */
+ public queueTurnEndPhases(): void {
+ turnEndPhases.forEach(p => {
+ this.pushNew(p);
+ });
}
/** Prevents end of turn effects from triggering when transitioning to a new biome on a X0 wave */
public onInterlude(): void {
- const phasesToRemove = ["WeatherEffectPhase", "BerryPhase", "CheckStatusEffectPhase"];
- this.phaseQueue = this.phaseQueue.filter(p => !phasesToRemove.includes(p.phaseName));
+ const phasesToRemove: readonly PhaseString[] = [
+ "WeatherEffectPhase",
+ "BerryPhase",
+ "CheckStatusEffectPhase",
+ ] as const;
+ for (const phaseType of phasesToRemove) {
+ this.phaseQueue.removeAll(phaseType);
+ }
- const turnEndPhase = this.findPhase(p => p.phaseName === "TurnEndPhase");
+ const turnEndPhase = this.phaseQueue.find("TurnEndPhase");
if (turnEndPhase) {
turnEndPhase.upcomingInterlude = true;
}
diff --git a/src/phase-tree.ts b/src/phase-tree.ts
new file mode 100644
index 00000000000..55476f38d65
--- /dev/null
+++ b/src/phase-tree.ts
@@ -0,0 +1,206 @@
+// biome-ignore-start lint/correctness/noUnusedImports: TSDoc imports
+import type { PhaseManager } from "#app/@types/phase-types";
+import type { DynamicPhaseMarker } from "#phases/dynamic-phase-marker";
+
+// biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports
+
+import type { PhaseMap, PhaseString } from "#app/@types/phase-types";
+import type { Phase } from "#app/phase";
+import type { PhaseConditionFunc } from "#types/phase-types";
+
+/**
+ * The PhaseTree is the central storage location for {@linkcode Phase}s by the {@linkcode PhaseManager}.
+ *
+ * It has a tiered structure, where unshifted phases are added one level above the currently running Phase. Phases are generally popped from the Tree in FIFO order.
+ *
+ * Dynamically ordered phases are queued into the Tree only as {@linkcode DynamicPhaseMarker | Marker}s and as such are not guaranteed to run FIFO (otherwise, they would not be dynamic)
+ */
+export class PhaseTree {
+ /** Storage for all levels in the tree. This is a simple array because only one Phase may have "children" at a time. */
+ private levels: Phase[][] = [[]];
+ /** The level of the currently running {@linkcode Phase} in the Tree (note that such phase is not actually in the Tree while it is running) */
+ private currentLevel = 0;
+ /**
+ * True if a "deferred" level exists
+ * @see {@linkcode addPhase}
+ */
+ private deferredActive = false;
+
+ /**
+ * Adds a {@linkcode Phase} to the specified level
+ * @param phase - The phase to add
+ * @param level - The numeric level to add the phase
+ * @throws Error if `level` is out of legal bounds
+ */
+ private add(phase: Phase, level: number): void {
+ const addLevel = this.levels[level];
+ if (addLevel == null) {
+ throw new Error("Attempted to add a phase to a nonexistent level of the PhaseTree!\nLevel: " + level.toString());
+ }
+ this.levels[level].push(phase);
+ }
+
+ /**
+ * Used by the {@linkcode PhaseManager} to add phases to the Tree
+ * @param phase - The {@linkcode Phase} to be added
+ * @param defer - Whether to defer the execution of this phase by allowing subsequently-added phases to run before it
+ *
+ * @privateRemarks
+ * Deferral is implemented by moving the queue at {@linkcode currentLevel} up one level and inserting the new phase below it.
+ * {@linkcode deferredActive} is set until the moved queue (and anything added to it) is exhausted.
+ *
+ * If {@linkcode deferredActive} is `true` when a deferred phase is added, the phase will be pushed to the second-highest level queue.
+ * That is, it will execute after the originally deferred phase, but there is no possibility for nesting with deferral.
+ *
+ * @todo `setPhaseQueueSplice` had strange behavior. This is simpler, but there are probably some remnant edge cases with the current implementation
+ */
+ public addPhase(phase: Phase, defer = false): void {
+ if (defer && !this.deferredActive) {
+ this.deferredActive = true;
+ this.levels.splice(-1, 0, []);
+ }
+ this.add(phase, this.levels.length - 1 - +defer);
+ }
+
+ /**
+ * Adds a {@linkcode Phase} after the first occurence of the given type, or to the top of the Tree if no such phase exists
+ * @param phase - The {@linkcode Phase} to be added
+ * @param type - A {@linkcode PhaseString} representing the type to search for
+ */
+ public addAfter(phase: Phase, type: PhaseString): void {
+ for (let i = this.levels.length - 1; i >= 0; i--) {
+ const insertIdx = this.levels[i].findIndex(p => p.is(type)) + 1;
+ if (insertIdx !== 0) {
+ this.levels[i].splice(insertIdx, 0, phase);
+ return;
+ }
+ }
+
+ this.addPhase(phase);
+ }
+
+ /**
+ * Unshifts a {@linkcode Phase} to the current level.
+ * This is effectively the same as if the phase were added immediately after the currently-running phase, before it started.
+ * @param phase - The {@linkcode Phase} to be added
+ */
+ public unshiftToCurrent(phase: Phase): void {
+ this.levels[this.currentLevel].unshift(phase);
+ }
+
+ /**
+ * Pushes a {@linkcode Phase} to the last level of the queue. It will run only after all previously queued phases have been executed.
+ * @param phase - The {@linkcode Phase} to be added
+ */
+ public pushPhase(phase: Phase): void {
+ this.add(phase, 0);
+ }
+
+ /**
+ * Removes and returns the first {@linkcode Phase} from the topmost level of the tree
+ * @returns - The next {@linkcode Phase}, or `undefined` if the Tree is empty
+ */
+ public getNextPhase(): Phase | undefined {
+ this.currentLevel = this.levels.length - 1;
+ while (this.currentLevel > 0 && this.levels[this.currentLevel].length === 0) {
+ this.deferredActive = false;
+ this.levels.pop();
+ this.currentLevel--;
+ }
+
+ // TODO: right now, this is preventing properly marking when one set of unshifted phases ends
+ this.levels.push([]);
+ return this.levels[this.currentLevel].shift();
+ }
+
+ /**
+ * Finds a particular {@linkcode Phase} in the Tree by searching in pop order
+ * @param phaseType - The {@linkcode PhaseString | type} of phase to search for
+ * @param phaseFilter - A {@linkcode PhaseConditionFunc} to specify conditions for the phase
+ * @returns The matching {@linkcode Phase}, or `undefined` if none exists
+ */
+ public find(phaseType: P, phaseFilter?: PhaseConditionFunc
): PhaseMap[P] | undefined {
+ for (let i = this.levels.length - 1; i >= 0; i--) {
+ const level = this.levels[i];
+ const phase = level.find((p): p is PhaseMap[P] => p.is(phaseType) && (!phaseFilter || phaseFilter(p)));
+ if (phase) {
+ return phase;
+ }
+ }
+ }
+
+ /**
+ * Finds a particular {@linkcode Phase} in the Tree by searching in pop order
+ * @param phaseType - The {@linkcode PhaseString | type} of phase to search for
+ * @param phaseFilter - A {@linkcode PhaseConditionFunc} to specify conditions for the phase
+ * @returns The matching {@linkcode Phase}, or `undefined` if none exists
+ */
+ public findAll
(phaseType: P, phaseFilter?: PhaseConditionFunc
): PhaseMap[P][] {
+ const phases: PhaseMap[P][] = [];
+ for (let i = this.levels.length - 1; i >= 0; i--) {
+ const level = this.levels[i];
+ const levelPhases = level.filter((p): p is PhaseMap[P] => p.is(phaseType) && (!phaseFilter || phaseFilter(p)));
+ phases.push(...levelPhases);
+ }
+ return phases;
+ }
+
+ /**
+ * Clears the Tree
+ * @param leaveFirstLevel - If `true`, leaves the top level of the tree intact
+ *
+ * @privateremarks
+ * The parameter on this method exists because {@linkcode PhaseManager.clearPhaseQueue} previously (probably by mistake) ignored `phaseQueuePrepend`.
+ *
+ * This is (probably by mistake) relied upon by certain ME functions.
+ */
+ public clear(leaveFirstLevel = false) {
+ this.levels = [leaveFirstLevel ? (this.levels.at(-1) ?? []) : []];
+ }
+
+ /**
+ * Finds and removes a single {@linkcode Phase} from the Tree
+ * @param phaseType - The {@linkcode PhaseString | type} of phase to search for
+ * @param phaseFilter - A {@linkcode PhaseConditionFunc} to specify conditions for the phase
+ * @returns Whether a removal occurred
+ */
+ public remove
(phaseType: P, phaseFilter?: PhaseConditionFunc
): boolean {
+ for (let i = this.levels.length - 1; i >= 0; i--) {
+ const level = this.levels[i];
+ const phaseIndex = level.findIndex(p => p.is(phaseType) && (!phaseFilter || phaseFilter(p)));
+ if (phaseIndex !== -1) {
+ level.splice(phaseIndex, 1);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Removes all occurrences of {@linkcode Phase}s of the given type
+ * @param phaseType - The {@linkcode PhaseString | type} of phase to search for
+ */
+ public removeAll(phaseType: PhaseString): void {
+ for (let i = 0; i < this.levels.length; i++) {
+ const level = this.levels[i].filter(phase => !phase.is(phaseType));
+ this.levels[i] = level;
+ }
+ }
+
+ /**
+ * Determines if a particular phase exists in the Tree
+ * @param phaseType - The {@linkcode PhaseString | type} of phase to search for
+ * @param phaseFilter - A {@linkcode PhaseConditionFunc} to specify conditions for the phase
+ * @returns Whether a matching phase exists
+ */
+ public exists
(phaseType: P, phaseFilter?: PhaseConditionFunc
): boolean {
+ for (const level of this.levels) {
+ for (const phase of level) {
+ if (phase.is(phaseType) && (!phaseFilter || phaseFilter(phase))) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/phases/activate-priority-queue-phase.ts b/src/phases/activate-priority-queue-phase.ts
deleted file mode 100644
index a31d3291a60..00000000000
--- a/src/phases/activate-priority-queue-phase.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { globalScene } from "#app/global-scene";
-import { Phase } from "#app/phase";
-import type { DynamicPhaseType } from "#enums/dynamic-phase-type";
-
-export class ActivatePriorityQueuePhase extends Phase {
- public readonly phaseName = "ActivatePriorityQueuePhase";
- private type: DynamicPhaseType;
-
- constructor(type: DynamicPhaseType) {
- super();
- this.type = type;
- }
-
- override start() {
- super.start();
- globalScene.phaseManager.startDynamicPhaseType(this.type);
- this.end();
- }
-
- public getType(): DynamicPhaseType {
- return this.type;
- }
-}
diff --git a/src/phases/battle-end-phase.ts b/src/phases/battle-end-phase.ts
index 8a798d67554..45b0db76ced 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/check-status-effect-phase.ts b/src/phases/check-status-effect-phase.ts
index bdaa536986a..5955cd42c55 100644
--- a/src/phases/check-status-effect-phase.ts
+++ b/src/phases/check-status-effect-phase.ts
@@ -1,20 +1,14 @@
import { globalScene } from "#app/global-scene";
import { Phase } from "#app/phase";
-import type { BattlerIndex } from "#enums/battler-index";
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/check-switch-phase.ts b/src/phases/check-switch-phase.ts
index 504bb6eb4bd..a55db4203bc 100644
--- a/src/phases/check-switch-phase.ts
+++ b/src/phases/check-switch-phase.ts
@@ -28,7 +28,8 @@ export class CheckSwitchPhase extends BattlePhase {
// ...if the user is playing in Set Mode
if (globalScene.battleStyle === BattleStyle.SET) {
- return super.end();
+ this.end(true);
+ return;
}
// ...if the checked Pokemon is somehow not on the field
@@ -44,7 +45,8 @@ export class CheckSwitchPhase extends BattlePhase {
.slice(1)
.filter(p => p.isActive()).length === 0
) {
- return super.end();
+ this.end(true);
+ return;
}
// ...or if any player Pokemon has an effect that prevents the checked Pokemon from switching
@@ -53,7 +55,8 @@ export class CheckSwitchPhase extends BattlePhase {
|| pokemon.isTrapped()
|| globalScene.getPlayerField().some(p => p.getTag(BattlerTagType.COMMANDED))
) {
- return super.end();
+ this.end(true);
+ return;
}
globalScene.ui.showText(
@@ -71,10 +74,17 @@ export class CheckSwitchPhase extends BattlePhase {
},
() => {
globalScene.ui.setMode(UiMode.MESSAGE);
- this.end();
+ this.end(true);
},
);
},
);
}
+
+ public override end(queuePostSummon = false): void {
+ if (queuePostSummon) {
+ globalScene.phaseManager.unshiftNew("PostSummonPhase", this.fieldIndex);
+ }
+ super.end();
+ }
}
diff --git a/src/phases/dynamic-phase-marker.ts b/src/phases/dynamic-phase-marker.ts
new file mode 100644
index 00000000000..e2b241f29de
--- /dev/null
+++ b/src/phases/dynamic-phase-marker.ts
@@ -0,0 +1,17 @@
+import type { PhaseString } from "#app/@types/phase-types";
+import { Phase } from "#app/phase";
+
+/**
+ * This phase exists for the sole purpose of marking the location and type of a dynamic phase for the phase manager
+ */
+export class DynamicPhaseMarker extends Phase {
+ public override readonly phaseName = "DynamicPhaseMarker";
+
+ /** The type of phase which this phase is a marker for */
+ public phaseType: PhaseString;
+
+ constructor(type: PhaseString) {
+ super();
+ this.phaseType = type;
+ }
+}
diff --git a/src/phases/egg-hatch-phase.ts b/src/phases/egg-hatch-phase.ts
index 946288c4fd8..3f9b999e0c1 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.hasPhaseOfType("EggHatchPhase")) {
this.eggHatchHandler.clear();
} else {
globalScene.time.delayedCall(250, () => globalScene.setModifiersVisible(true));
diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts
index 0918ced65e5..9345170e718 100644
--- a/src/phases/encounter-phase.ts
+++ b/src/phases/encounter-phase.ts
@@ -565,29 +565,6 @@ 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 === 0) {
- 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;
- },
- ),
- );
const ivScannerModifier = globalScene.findModifier(m => m instanceof IvScannerModifier);
if (ivScannerModifier) {
enemyField.map(p => globalScene.phaseManager.pushNew("ScanIvsPhase", p.getBattlerIndex()));
@@ -596,36 +573,30 @@ export class EncounterPhase extends BattlePhase {
if (!this.loaded) {
const availablePartyMembers = globalScene.getPokemonAllowedInBattle();
+ const minPartySize = globalScene.currentBattle.double ? 2 : 1;
+ const currentBattle = globalScene.currentBattle;
+ const checkSwitch =
+ currentBattle.battleType !== BattleType.TRAINER
+ && (currentBattle.waveIndex > 1 || !globalScene.gameMode.isDaily)
+ && availablePartyMembers.length > minPartySize;
+ const phaseManager = globalScene.phaseManager;
if (!availablePartyMembers[0].isOnField()) {
- globalScene.phaseManager.pushNew("SummonPhase", 0);
+ phaseManager.pushNew("SummonPhase", 0, true, false, checkSwitch);
}
- if (globalScene.currentBattle.double) {
+ if (currentBattle.double) {
if (availablePartyMembers.length > 1) {
- globalScene.phaseManager.pushNew("ToggleDoublePositionPhase", true);
+ phaseManager.pushNew("ToggleDoublePositionPhase", true);
if (!availablePartyMembers[1].isOnField()) {
- globalScene.phaseManager.pushNew("SummonPhase", 1);
+ phaseManager.pushNew("SummonPhase", 1, true, false, checkSwitch);
}
}
} else {
if (availablePartyMembers.length > 1 && availablePartyMembers[1].isOnField()) {
globalScene.phaseManager.pushNew("ReturnPhase", 1);
}
- globalScene.phaseManager.pushNew("ToggleDoublePositionPhase", false);
- }
-
- if (
- globalScene.currentBattle.battleType !== BattleType.TRAINER
- && (globalScene.currentBattle.waveIndex > 1 || !globalScene.gameMode.isDaily)
- ) {
- const minPartySize = globalScene.currentBattle.double ? 2 : 1;
- if (availablePartyMembers.length > minPartySize) {
- globalScene.phaseManager.pushNew("CheckSwitchPhase", 0, globalScene.currentBattle.double);
- if (globalScene.currentBattle.double) {
- globalScene.phaseManager.pushNew("CheckSwitchPhase", 1, globalScene.currentBattle.double);
- }
- }
+ phaseManager.pushNew("ToggleDoublePositionPhase", false);
}
}
handleTutorial(Tutorial.Access_Menu).then(() => super.end());
diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts
index dd29b97d590..f229f872958 100644
--- a/src/phases/game-over-phase.ts
+++ b/src/phases/game-over-phase.ts
@@ -84,19 +84,12 @@ export class GameOverPhase extends BattlePhase {
globalScene.phaseManager.pushNew("EncounterPhase", true);
const availablePartyMembers = globalScene.getPokemonAllowedInBattle().length;
-
- globalScene.phaseManager.pushNew("SummonPhase", 0);
- if (globalScene.currentBattle.double && availablePartyMembers > 1) {
- globalScene.phaseManager.pushNew("SummonPhase", 1);
- }
- if (
+ const checkSwitch =
globalScene.currentBattle.waveIndex > 1
- && globalScene.currentBattle.battleType !== BattleType.TRAINER
- ) {
- globalScene.phaseManager.pushNew("CheckSwitchPhase", 0, globalScene.currentBattle.double);
- if (globalScene.currentBattle.double && availablePartyMembers > 1) {
- globalScene.phaseManager.pushNew("CheckSwitchPhase", 1, globalScene.currentBattle.double);
- }
+ && globalScene.currentBattle.battleType !== BattleType.TRAINER;
+ globalScene.phaseManager.pushNew("SummonPhase", 0, true, false, checkSwitch);
+ if (globalScene.currentBattle.double && availablePartyMembers > 1) {
+ globalScene.phaseManager.pushNew("SummonPhase", 1, true, false, checkSwitch);
}
globalScene.ui.fadeIn(1250);
@@ -267,7 +260,6 @@ export class GameOverPhase extends BattlePhase {
.then(success => doGameOver(!globalScene.gameMode.isDaily || !!success))
.catch(_err => {
globalScene.phaseManager.clearPhaseQueue();
- globalScene.phaseManager.clearPhaseQueueSplice();
globalScene.phaseManager.unshiftNew("MessagePhase", i18next.t("menu:serverCommunicationFailed"), 2500);
// force the game to reload after 2 seconds.
setTimeout(() => {
diff --git a/src/phases/learn-move-phase.ts b/src/phases/learn-move-phase.ts
index 4fc38b08d16..bbd1d0f5a2e 100644
--- a/src/phases/learn-move-phase.ts
+++ b/src/phases/learn-move-phase.ts
@@ -187,7 +187,7 @@ export class LearnMovePhase extends PlayerPartyMemberPokemonPhase {
pokemon.usedTMs = [];
}
pokemon.usedTMs.push(this.moveId);
- globalScene.phaseManager.tryRemovePhase(phase => phase.is("SelectModifierPhase"));
+ globalScene.phaseManager.tryRemovePhase("SelectModifierPhase");
} else if (this.learnMoveType === LearnMoveType.MEMORY) {
if (this.cost !== -1) {
if (!Overrides.WAIVE_ROLL_FEE_OVERRIDE) {
@@ -197,7 +197,7 @@ export class LearnMovePhase extends PlayerPartyMemberPokemonPhase {
}
globalScene.playSound("se/buy");
} else {
- globalScene.phaseManager.tryRemovePhase(phase => phase.is("SelectModifierPhase"));
+ globalScene.phaseManager.tryRemovePhase("SelectModifierPhase");
}
}
pokemon.setMove(index, this.moveId);
diff --git a/src/phases/move-charge-phase.ts b/src/phases/move-charge-phase.ts
index 0c83db10511..5dd75f4bab8 100644
--- a/src/phases/move-charge-phase.ts
+++ b/src/phases/move-charge-phase.ts
@@ -75,7 +75,7 @@ export class MoveChargePhase extends PokemonPhase {
// Otherwise, add the attack portion to the user's move queue to execute next turn.
// TODO: This checks status twice for a single-turn usage...
if (instantCharge.value) {
- globalScene.phaseManager.tryRemovePhase(phase => phase.is("MoveEndPhase") && phase.getPokemon() === user);
+ globalScene.phaseManager.tryRemovePhase("MoveEndPhase", phase => phase.getPokemon() === user);
globalScene.phaseManager.unshiftNew("MovePhase", user, [this.targetIndex], this.move, this.useMode);
} else {
user.pushMoveQueue({ move: move.id, targets: [this.targetIndex], useMode: this.useMode });
diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts
index 18e25b328f8..be6d0164698 100644
--- a/src/phases/move-effect-phase.ts
+++ b/src/phases/move-effect-phase.ts
@@ -1,7 +1,6 @@
import { applyAbAttrs } from "#abilities/apply-ab-attrs";
import { globalScene } from "#app/global-scene";
import { getPokemonNameWithAffix } from "#app/messages";
-import type { Phase } from "#app/phase";
import { ConditionalProtectTag } from "#data/arena-tag";
import { MoveAnim } from "#data/battle-anims";
import { DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag, TypeBoostTag } from "#data/battler-tags";
@@ -17,6 +16,7 @@ import { MoveCategory } from "#enums/move-category";
import { MoveEffectTrigger } from "#enums/move-effect-trigger";
import { MoveFlags } from "#enums/move-flags";
import { MoveId } from "#enums/move-id";
+import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
import { MoveResult } from "#enums/move-result";
import { MoveTarget } from "#enums/move-target";
import { isReflected, MoveUseMode } from "#enums/move-use-mode";
@@ -67,12 +67,6 @@ export class MoveEffectPhase extends PokemonPhase {
/** Is this the last strike of a move? */
private lastHit: boolean;
- /**
- * Phases queued during moves; used to add a new MovePhase for reflected moves after triggering.
- * TODO: Remove this and move the reflection logic to ability-side
- */
- private queuedPhases: Phase[] = [];
-
/**
* @param useMode - The {@linkcode MoveUseMode} corresponding to how this move was used.
*/
@@ -148,7 +142,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
@@ -159,24 +153,21 @@ 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),
- MoveUseMode.REFLECTED,
- ),
+ globalScene.phaseManager.unshiftNew(
+ "MovePhase",
+ target,
+ newTargets,
+ new PokemonMove(this.move.id),
+ MoveUseMode.REFLECTED,
+ MovePhaseTimingModifier.FIRST,
);
}
@@ -344,9 +335,6 @@ export class MoveEffectPhase extends PokemonPhase {
return;
}
- if (this.queuedPhases.length > 0) {
- 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);
@@ -905,10 +893,7 @@ export class MoveEffectPhase extends PokemonPhase {
* @param target - The {@linkcode Pokemon} that fainted
*/
protected onFaintTarget(user: Pokemon, target: Pokemon): void {
- // set splice index here, so future scene queues happen before FaintedPhase
- globalScene.phaseManager.setPhaseQueueSplice();
-
- globalScene.phaseManager.unshiftNew("FaintPhase", target.getBattlerIndex(), false, user);
+ globalScene.phaseManager.queueFaintPhase(target.getBattlerIndex(), false, user);
target.destroySubstitute();
target.lapseTag(BattlerTagType.COMMANDED);
diff --git a/src/phases/move-header-phase.ts b/src/phases/move-header-phase.ts
index 5c69dcd1217..5b8a6f998a1 100644
--- a/src/phases/move-header-phase.ts
+++ b/src/phases/move-header-phase.ts
@@ -5,8 +5,8 @@ import { BattlePhase } from "#phases/battle-phase";
export class MoveHeaderPhase extends BattlePhase {
public readonly phaseName = "MoveHeaderPhase";
- public pokemon: Pokemon;
public move: PokemonMove;
+ public pokemon: Pokemon;
constructor(pokemon: Pokemon, move: PokemonMove) {
super();
@@ -15,6 +15,10 @@ export class MoveHeaderPhase extends BattlePhase {
this.move = move;
}
+ public getPokemon(): Pokemon {
+ return this.pokemon;
+ }
+
canMove(): boolean {
return this.pokemon.isActive(true) && this.move.isUsable(this.pokemon);
}
diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts
index 96943065ff0..5e85401db77 100644
--- a/src/phases/move-phase.ts
+++ b/src/phases/move-phase.ts
@@ -3,6 +3,7 @@ import { MOVE_COLOR } from "#app/constants/colors";
import { globalScene } from "#app/global-scene";
import { getPokemonNameWithAffix } from "#app/messages";
import Overrides from "#app/overrides";
+import { PokemonPhase } from "#app/phases/pokemon-phase";
import { CenterOfAttentionTag } from "#data/battler-tags";
import { SpeciesFormChangePreMoveTrigger } from "#data/form-change-triggers";
import { getStatusEffectActivationText, getStatusEffectHealText } from "#data/status-effect";
@@ -15,6 +16,7 @@ import { BattlerTagType } from "#enums/battler-tag-type";
import { CommonAnim } from "#enums/move-anims-common";
import { MoveFlags } from "#enums/move-flags";
import { MoveId } from "#enums/move-id";
+import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
import { MoveResult } from "#enums/move-result";
import { isIgnorePP, isIgnoreStatus, isReflected, isVirtual, MoveUseMode } from "#enums/move-use-mode";
import { PokemonType } from "#enums/pokemon-type";
@@ -24,20 +26,19 @@ import type { Pokemon } from "#field/pokemon";
import { applyMoveAttrs } from "#moves/apply-attrs";
import { frenzyMissFunc } from "#moves/move-utils";
import type { PokemonMove } from "#moves/pokemon-move";
-import { BattlePhase } from "#phases/battle-phase";
import type { TurnMove } from "#types/turn-move";
import { NumberHolder } from "#utils/common";
import { enumValueToKey } from "#utils/enums";
import i18next from "i18next";
-export class MovePhase extends BattlePhase {
+export class MovePhase extends PokemonPhase {
public readonly phaseName = "MovePhase";
protected _pokemon: Pokemon;
- protected _move: PokemonMove;
+ public move: PokemonMove;
protected _targets: BattlerIndex[];
public readonly useMode: MoveUseMode; // Made public for quash
- /** Whether the current move is forced last (used for Quash). */
- protected forcedLast: boolean;
+ /** The timing modifier of the move (used by Quash and to force called moves to the front of their queue) */
+ public timingModifier: MovePhaseTimingModifier;
/** Whether the current move should fail but still use PP. */
protected failed = false;
/** Whether the current move should fail and retain PP. */
@@ -59,14 +60,6 @@ export class MovePhase extends BattlePhase {
this._pokemon = pokemon;
}
- public get move(): PokemonMove {
- return this._move;
- }
-
- protected set move(move: PokemonMove) {
- this._move = move;
- }
-
public get targets(): BattlerIndex[] {
return this._targets;
}
@@ -81,16 +74,22 @@ export class MovePhase extends BattlePhase {
* @param move - The {@linkcode PokemonMove} to use
* @param useMode - The {@linkcode MoveUseMode} corresponding to this move's means of execution (usually `MoveUseMode.NORMAL`).
* Not marked optional to ensure callers correctly pass on `useModes`.
- * @param forcedLast - Whether to force this phase to occur last in order (for {@linkcode MoveId.QUASH}); default `false`
+ * @param timingModifier - The {@linkcode MovePhaseTimingModifier} for the move; Default {@linkcode MovePhaseTimingModifier.NORMAL}
*/
- constructor(pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, useMode: MoveUseMode, forcedLast = false) {
- super();
+ constructor(
+ pokemon: Pokemon,
+ targets: BattlerIndex[],
+ move: PokemonMove,
+ useMode: MoveUseMode,
+ timingModifier: MovePhaseTimingModifier = MovePhaseTimingModifier.NORMAL,
+ ) {
+ super(pokemon.getBattlerIndex());
this.pokemon = pokemon;
this.targets = targets;
this.move = move;
this.useMode = useMode;
- this.forcedLast = forcedLast;
+ this.timingModifier = timingModifier;
this.moveHistoryEntry = {
move: MoveId.NONE,
targets,
@@ -121,14 +120,6 @@ export class MovePhase extends BattlePhase {
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/mystery-encounter-phases.ts b/src/phases/mystery-encounter-phases.ts
index 4f50b40c965..bb3f4a92033 100644
--- a/src/phases/mystery-encounter-phases.ts
+++ b/src/phases/mystery-encounter-phases.ts
@@ -48,7 +48,6 @@ export class MysteryEncounterPhase extends Phase {
// Clears out queued phases that are part of standard battle
globalScene.phaseManager.clearPhaseQueue();
- globalScene.phaseManager.clearPhaseQueueSplice();
const encounter = globalScene.currentBattle.mysteryEncounter!;
encounter.updateSeedOffset();
@@ -233,9 +232,7 @@ export class MysteryEncounterBattleStartCleanupPhase extends Phase {
});
// Remove any status tick phases
- while (globalScene.phaseManager.findPhase(p => p.is("PostTurnStatusEffectPhase"))) {
- globalScene.phaseManager.tryRemovePhase(p => p.is("PostTurnStatusEffectPhase"));
- }
+ globalScene.phaseManager.removeAllPhasesOfType("PostTurnStatusEffectPhase");
// The total number of Pokemon in the player's party that can legally fight
const legalPlayerPokemon = globalScene.getPokemonAllowedInBattle();
@@ -412,16 +409,21 @@ export class MysteryEncounterBattlePhase extends Phase {
}
const availablePartyMembers = globalScene.getPlayerParty().filter(p => p.isAllowedInBattle());
+ const minPartySize = globalScene.currentBattle.double ? 2 : 1;
+ const checkSwitch =
+ encounterMode !== MysteryEncounterMode.TRAINER_BATTLE
+ && !this.disableSwitch
+ && availablePartyMembers.length > minPartySize;
if (!availablePartyMembers[0].isOnField()) {
- globalScene.phaseManager.pushNew("SummonPhase", 0);
+ globalScene.phaseManager.pushNew("SummonPhase", 0, true, false, checkSwitch);
}
if (globalScene.currentBattle.double) {
if (availablePartyMembers.length > 1) {
globalScene.phaseManager.pushNew("ToggleDoublePositionPhase", true);
if (!availablePartyMembers[1].isOnField()) {
- globalScene.phaseManager.pushNew("SummonPhase", 1);
+ globalScene.phaseManager.pushNew("SummonPhase", 1, true, false, checkSwitch);
}
}
} else {
@@ -432,16 +434,6 @@ export class MysteryEncounterBattlePhase extends Phase {
globalScene.phaseManager.pushNew("ToggleDoublePositionPhase", false);
}
- if (encounterMode !== MysteryEncounterMode.TRAINER_BATTLE && !this.disableSwitch) {
- const minPartySize = globalScene.currentBattle.double ? 2 : 1;
- if (availablePartyMembers.length > minPartySize) {
- globalScene.phaseManager.pushNew("CheckSwitchPhase", 0, globalScene.currentBattle.double);
- if (globalScene.currentBattle.double) {
- globalScene.phaseManager.pushNew("CheckSwitchPhase", 1, globalScene.currentBattle.double);
- }
- }
- }
-
this.end();
}
@@ -540,7 +532,7 @@ export class MysteryEncounterRewardsPhase extends Phase {
if (encounter.doEncounterRewards) {
encounter.doEncounterRewards();
} else if (this.addHealPhase) {
- globalScene.phaseManager.tryRemovePhase(p => p.is("SelectModifierPhase"));
+ globalScene.phaseManager.removeAllPhasesOfType("SelectModifierPhase");
globalScene.phaseManager.unshiftNew("SelectModifierPhase", 0, undefined, {
fillRemaining: false,
rerollMultiplier: -1,
diff --git a/src/phases/new-battle-phase.ts b/src/phases/new-battle-phase.ts
index b9a57161bd0..7b5d132ccd2 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/phases/party-member-pokemon-phase.ts b/src/phases/party-member-pokemon-phase.ts
index 9536dafda60..545799cf36a 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/post-summon-activate-ability-phase.ts b/src/phases/post-summon-activate-ability-phase.ts
index 5f790c01ad1..a2b6c059bee 100644
--- a/src/phases/post-summon-activate-ability-phase.ts
+++ b/src/phases/post-summon-activate-ability-phase.ts
@@ -6,8 +6,8 @@ import { PostSummonPhase } from "#phases/post-summon-phase";
* Helper to {@linkcode PostSummonPhase} which applies abilities
*/
export class PostSummonActivateAbilityPhase extends PostSummonPhase {
- private priority: number;
- private passive: boolean;
+ private readonly priority: number;
+ private readonly passive: boolean;
constructor(battlerIndex: BattlerIndex, priority: number, passive: boolean) {
super(battlerIndex);
diff --git a/src/phases/post-summon-phase.ts b/src/phases/post-summon-phase.ts
index 5f66cf91eca..136f2fbd601 100644
--- a/src/phases/post-summon-phase.ts
+++ b/src/phases/post-summon-phase.ts
@@ -1,19 +1,29 @@
import { applyAbAttrs } from "#abilities/apply-ab-attrs";
+import type { PhaseString } from "#app/@types/phase-types";
import { globalScene } from "#app/global-scene";
import { EntryHazardTag } from "#data/arena-tag";
import { MysteryEncounterPostSummonTag } from "#data/battler-tags";
import { ArenaTagType } from "#enums/arena-tag-type";
+import type { BattlerIndex } from "#enums/battler-index";
import { BattlerTagType } from "#enums/battler-tag-type";
import { StatusEffect } from "#enums/status-effect";
import { PokemonPhase } from "#phases/pokemon-phase";
export class PostSummonPhase extends PokemonPhase {
public readonly phaseName = "PostSummonPhase";
+ /** Used to determine whether to push or unshift {@linkcode PostSummonActivateAbilityPhase}s */
+ public readonly source: PhaseString;
+
+ constructor(battlerIndex?: BattlerIndex | number, source: PhaseString = "SwitchSummonPhase") {
+ super(battlerIndex);
+ this.source = source;
+ }
+
start() {
super.start();
const pokemon = this.getPokemon();
-
+ console.log("Ran PSP for:", pokemon.name);
if (pokemon.status?.effect === StatusEffect.TOXIC) {
pokemon.status.toxicTurnCount = 0;
}
@@ -29,8 +39,7 @@ export class PostSummonPhase extends PokemonPhase {
) {
pokemon.lapseTag(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON);
}
-
- const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField();
+ const field = pokemon.isPlayer() ? globalScene.getPlayerField(true) : globalScene.getEnemyField(true);
for (const p of field) {
applyAbAttrs("CommanderAbAttr", { pokemon: p });
}
diff --git a/src/phases/quiet-form-change-phase.ts b/src/phases/quiet-form-change-phase.ts
index ef53b16cc56..920ff2252b8 100644
--- a/src/phases/quiet-form-change-phase.ts
+++ b/src/phases/quiet-form-change-phase.ts
@@ -9,7 +9,6 @@ import { BattleSpec } from "#enums/battle-spec";
import { BattlerTagType } from "#enums/battler-tag-type";
import type { Pokemon } from "#field/pokemon";
import { BattlePhase } from "#phases/battle-phase";
-import type { MovePhase } from "#phases/move-phase";
export class QuietFormChangePhase extends BattlePhase {
public readonly phaseName = "QuietFormChangePhase";
@@ -170,12 +169,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;
- if (movePhase) {
- movePhase.cancel();
- }
+ globalScene.phaseManager.cancelMove(p => p.pokemon === this.pokemon);
}
if (this.formChange.trigger instanceof SpeciesFormChangeTeraTrigger) {
const params = { pokemon: this.pokemon };
diff --git a/src/phases/stat-stage-change-phase.ts b/src/phases/stat-stage-change-phase.ts
index 2731c037d5f..3c2d1cb5fad 100644
--- a/src/phases/stat-stage-change-phase.ts
+++ b/src/phases/stat-stage-change-phase.ts
@@ -223,10 +223,7 @@ 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,
- );
- if (!existingPhase?.is("StatStageChangePhase")) {
+ if (!globalScene.phaseManager.hasPhaseOfType("StatStageChangePhase", p => p.battlerIndex === this.battlerIndex)) {
// Apply White Herb if needed
const whiteHerb = globalScene.applyModifier(
ResetNegativeStatStageModifier,
@@ -297,49 +294,6 @@ export class StatStageChangePhase extends PokemonPhase {
}
}
- aggregateStatStageChanges(): void {
- const accEva: BattleStat[] = [Stat.ACC, Stat.EVA];
- const isAccEva = accEva.some(s => this.stats.includes(s));
- let existingPhase: StatStageChangePhase;
- if (this.stats.length === 1) {
- while (
- (existingPhase = globalScene.phaseManager.findPhase(
- p =>
- p.is("StatStageChangePhase")
- && p.battlerIndex === this.battlerIndex
- && p.stats.length === 1
- && p.stats[0] === this.stats[0]
- && p.selfTarget === this.selfTarget
- && p.showMessage === this.showMessage
- && p.ignoreAbilities === this.ignoreAbilities,
- ) as StatStageChangePhase)
- ) {
- this.stages += existingPhase.stages;
-
- if (!globalScene.phaseManager.tryRemovePhase(p => p === existingPhase)) {
- break;
- }
- }
- }
- while (
- (existingPhase = globalScene.phaseManager.findPhase(
- p =>
- p.is("StatStageChangePhase")
- && p.battlerIndex === this.battlerIndex
- && p.selfTarget === this.selfTarget
- && accEva.some(s => p.stats.includes(s)) === isAccEva
- && p.stages === this.stages
- && p.showMessage === this.showMessage
- && p.ignoreAbilities === this.ignoreAbilities,
- ) as StatStageChangePhase)
- ) {
- this.stats.push(...existingPhase.stats);
- if (!globalScene.phaseManager.tryRemovePhase(p => p === existingPhase)) {
- break;
- }
- }
- }
-
getStatStageChangeMessages(stats: BattleStat[], stages: number, relStages: number[]): string[] {
const messages: string[] = [];
diff --git a/src/phases/summon-phase.ts b/src/phases/summon-phase.ts
index dda70f46ec9..26a8ba40ffc 100644
--- a/src/phases/summon-phase.ts
+++ b/src/phases/summon-phase.ts
@@ -16,12 +16,14 @@ import i18next from "i18next";
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";
- private loaded: boolean;
+ private readonly loaded: boolean;
+ private readonly checkSwitch: boolean;
- constructor(fieldIndex: number, player = true, loaded = false) {
+ constructor(fieldIndex: number, player = true, loaded = false, checkSwitch = false) {
super(fieldIndex, player);
this.loaded = loaded;
+ this.checkSwitch = checkSwitch;
}
start() {
@@ -288,7 +290,17 @@ export class SummonPhase extends PartyMemberPokemonPhase {
}
queuePostSummon(): void {
- globalScene.phaseManager.pushNew("PostSummonPhase", this.getPokemon().getBattlerIndex());
+ if (this.checkSwitch) {
+ globalScene.phaseManager.pushNew(
+ "CheckSwitchPhase",
+ this.getPokemon().getFieldIndex(),
+ globalScene.currentBattle.double,
+ );
+ } else {
+ globalScene.phaseManager.pushNew("PostSummonPhase", this.getPokemon().getBattlerIndex(), this.phaseName);
+ }
+
+ globalScene.phaseManager.tryAddEnemyPostSummonPhases();
}
end() {
@@ -296,4 +308,8 @@ export class SummonPhase extends PartyMemberPokemonPhase {
super.end();
}
+
+ public getFieldIndex(): number {
+ return this.fieldIndex;
+ }
}
diff --git a/src/phases/switch-phase.ts b/src/phases/switch-phase.ts
index 83a699b6b08..9ab06ec827c 100644
--- a/src/phases/switch-phase.ts
+++ b/src/phases/switch-phase.ts
@@ -1,5 +1,4 @@
import { globalScene } from "#app/global-scene";
-import { DynamicPhaseType } from "#enums/dynamic-phase-type";
import { SwitchType } from "#enums/switch-type";
import { UiMode } from "#enums/ui-mode";
import { BattlePhase } from "#phases/battle-phase";
@@ -77,14 +76,6 @@ export class SwitchPhase extends BattlePhase {
fieldIndex,
(slotIndex: number, option: PartyOption) => {
if (slotIndex >= globalScene.currentBattle.getBattlerCount() && slotIndex < 6) {
- // Remove any pre-existing PostSummonPhase under the same field index.
- // Pre-existing PostSummonPhases may occur when this phase is invoked during a prompt to switch at the start of a wave.
- // TODO: Separate the animations from `SwitchSummonPhase` and co. into another phase and use that on initial switch - this is a band-aid fix
- globalScene.phaseManager.tryRemoveDynamicPhase(
- DynamicPhaseType.POST_SUMMON,
- p => p.is("PostSummonPhase") && p.player && p.fieldIndex === this.fieldIndex,
- "all",
- );
const switchType = option === PartyOption.PASS_BATON ? SwitchType.BATON_PASS : this.switchType;
globalScene.phaseManager.unshiftNew("SwitchSummonPhase", switchType, fieldIndex, slotIndex, this.doReturn);
}
diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts
index ac47068c619..8cc7843b55f 100644
--- a/src/phases/switch-summon-phase.ts
+++ b/src/phases/switch-summon-phase.ts
@@ -241,11 +241,11 @@ 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 {
- globalScene.phaseManager.startNewDynamicPhase("PostSummonPhase", this.getPokemon().getBattlerIndex());
+ globalScene.phaseManager.unshiftNew("PostSummonPhase", this.getPokemon().getBattlerIndex());
}
/**
diff --git a/src/phases/title-phase.ts b/src/phases/title-phase.ts
index 414be4c820c..1920db8d20e 100644
--- a/src/phases/title-phase.ts
+++ b/src/phases/title-phase.ts
@@ -315,23 +315,15 @@ export class TitlePhase extends Phase {
if (this.loaded) {
const availablePartyMembers = globalScene.getPokemonAllowedInBattle().length;
-
- globalScene.phaseManager.pushNew("SummonPhase", 0, true, true);
- if (globalScene.currentBattle.double && availablePartyMembers > 1) {
- globalScene.phaseManager.pushNew("SummonPhase", 1, true, true);
- }
-
- if (
+ const minPartySize = globalScene.currentBattle.double ? 2 : 1;
+ const checkSwitch =
globalScene.currentBattle.battleType !== BattleType.TRAINER
&& (globalScene.currentBattle.waveIndex > 1 || !globalScene.gameMode.isDaily)
- ) {
- const minPartySize = globalScene.currentBattle.double ? 2 : 1;
- if (availablePartyMembers > minPartySize) {
- globalScene.phaseManager.pushNew("CheckSwitchPhase", 0, globalScene.currentBattle.double);
- if (globalScene.currentBattle.double) {
- globalScene.phaseManager.pushNew("CheckSwitchPhase", 1, globalScene.currentBattle.double);
- }
- }
+ && availablePartyMembers > minPartySize;
+
+ globalScene.phaseManager.pushNew("SummonPhase", 0, true, true, checkSwitch);
+ if (globalScene.currentBattle.double && availablePartyMembers > 1) {
+ globalScene.phaseManager.pushNew("SummonPhase", 1, true, true, checkSwitch);
}
}
diff --git a/src/phases/turn-end-phase.ts b/src/phases/turn-end-phase.ts
index 463f26e73a2..22ebbd2607b 100644
--- a/src/phases/turn-end-phase.ts
+++ b/src/phases/turn-end-phase.ts
@@ -25,6 +25,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/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts
index 1733901d527..cd45a73c813 100644
--- a/src/phases/turn-start-phase.ts
+++ b/src/phases/turn-start-phase.ts
@@ -1,89 +1,31 @@
import { applyAbAttrs } from "#abilities/apply-ab-attrs";
import type { TurnCommand } from "#app/battle";
import { globalScene } from "#app/global-scene";
-import { TrickRoomTag } from "#data/arena-tag";
-import { allMoves } from "#data/data-lists";
-import { BattlerIndex } from "#enums/battler-index";
+import { ArenaTagSide } from "#enums/arena-tag-side";
+import type { BattlerIndex } from "#enums/battler-index";
import { Command } from "#enums/command";
-import { Stat } from "#enums/stat";
import { SwitchType } from "#enums/switch-type";
import type { Pokemon } from "#field/pokemon";
import { BypassSpeedChanceModifier } from "#modifiers/modifier";
import { PokemonMove } from "#moves/pokemon-move";
import { FieldPhase } from "#phases/field-phase";
-import { BooleanHolder, randSeedShuffle } from "#utils/common";
+import { inSpeedOrder } from "#utils/speed-order-generator";
export class TurnStartPhase extends FieldPhase {
public readonly phaseName = "TurnStartPhase";
/**
- * Helper method to retrieve the current speed order of the combattants.
- * It also checks for Trick Room and reverses the array if it is present.
- * @returns The {@linkcode BattlerIndex}es of all on-field Pokemon, sorted in speed order.
- * @todo Make this private
- */
- getSpeedOrder(): BattlerIndex[] {
- const playerField = globalScene.getPlayerField().filter(p => p.isActive());
- const enemyField = globalScene.getEnemyField().filter(p => p.isActive());
-
- // Shuffle the list before sorting so speed ties produce random results
- // This is seeded with the current turn to prevent turn order varying
- // based on how long since you last reloaded.
- let orderedTargets = (playerField as Pokemon[]).concat(enemyField);
- globalScene.executeWithSeedOffset(
- () => {
- orderedTargets = randSeedShuffle(orderedTargets);
- },
- globalScene.currentBattle.turn,
- globalScene.waveSeed,
- );
-
- // Check for Trick Room and reverse sort order if active.
- // Notably, Pokerogue does NOT have the "outspeed trick room" glitch at >1809 spd.
- const speedReversed = new BooleanHolder(false);
- globalScene.arena.applyTags(TrickRoomTag, false, speedReversed);
-
- orderedTargets.sort((a: Pokemon, b: Pokemon) => {
- const aSpeed = a.getEffectiveStat(Stat.SPD);
- const bSpeed = b.getEffectiveStat(Stat.SPD);
-
- return speedReversed.value ? aSpeed - bSpeed : bSpeed - aSpeed;
- });
-
- return orderedTargets.map(t => t.getFieldIndex() + (t.isEnemy() ? BattlerIndex.ENEMY : BattlerIndex.PLAYER));
- }
-
- /**
- * This takes the result of {@linkcode 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 The `BattlerIndex`es of all on-field Pokemon sorted in action order.
+ * Returns an ordering of the current field based on command priority
+ * @returns 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).forEach(p => {
- const bypassSpeed = new BooleanHolder(false);
- const canCheckHeldItems = new BooleanHolder(true);
- applyAbAttrs("BypassSpeedChanceAbAttr", { pokemon: p, bypass: bypassSpeed });
- applyAbAttrs("PreventBypassSpeedChanceAbAttr", {
- pokemon: p,
- bypass: bypassSpeed,
- canCheckHeldItems,
- });
- if (canCheckHeldItems.value) {
- globalScene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p, bypassSpeed);
- }
- battlerBypassSpeed[p.getBattlerIndex()] = bypassSpeed;
- });
+ const playerField = globalScene.getPlayerField(true).map(p => p.getBattlerIndex());
+ const enemyField = globalScene.getEnemyField(true).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];
@@ -94,41 +36,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,
- // check 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;
}
// TODO: Refactor this alongside `CommandPhase.handleCommand` to use SEPARATE METHODS
@@ -139,9 +54,8 @@ export class TurnStartPhase extends FieldPhase {
const field = globalScene.getField();
const moveOrder = this.getCommandOrder();
- for (const o of this.getSpeedOrder()) {
- const pokemon = field[o];
- const preTurnCommand = globalScene.currentBattle.preTurnCommands[o];
+ for (const pokemon of inSpeedOrder(ArenaTagSide.BOTH)) {
+ const preTurnCommand = globalScene.currentBattle.preTurnCommands[pokemon.getBattlerIndex()];
if (preTurnCommand?.skip) {
continue;
@@ -154,6 +68,10 @@ export class TurnStartPhase extends FieldPhase {
}
const phaseManager = globalScene.phaseManager;
+ for (const pokemon of inSpeedOrder(ArenaTagSide.BOTH)) {
+ applyAbAttrs("BypassSpeedChanceAbAttr", { pokemon });
+ globalScene.applyModifiers(BypassSpeedChanceModifier, pokemon.isPlayer(), pokemon);
+ }
moveOrder.forEach((o, index) => {
const pokemon = field[o];
@@ -178,13 +96,8 @@ export class TurnStartPhase extends FieldPhase {
// TODO: Re-order these phases to be consistent with mainline turn order:
// https://www.smogon.com/forums/threads/sword-shield-battle-mechanics-research.3655528/page-64#post-9244179
- phaseManager.pushNew("WeatherEffectPhase");
- phaseManager.pushNew("PositionalTagPhase");
- phaseManager.pushNew("BerryPhase");
-
- phaseManager.pushNew("CheckStatusEffectPhase", moveOrder);
-
- phaseManager.pushNew("TurnEndPhase");
+ // TODO: In an ideal world, this is handled by the phase manager. The change is nontrivial due to the ordering of post-turn phases like those queued by VictoryPhase
+ globalScene.phaseManager.queueTurnEndPhases();
/*
* `this.end()` will call `PhaseManager#shiftPhase()`, which dumps everything from `phaseQueuePrepend`
diff --git a/src/queues/move-phase-priority-queue.ts b/src/queues/move-phase-priority-queue.ts
new file mode 100644
index 00000000000..5f0b20c3c2e
--- /dev/null
+++ b/src/queues/move-phase-priority-queue.ts
@@ -0,0 +1,103 @@
+import type { PokemonMove } from "#app/data/moves/pokemon-move";
+import type { Pokemon } from "#app/field/pokemon";
+import { globalScene } from "#app/global-scene";
+import type { MovePhase } from "#app/phases/move-phase";
+import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue";
+import type { BattlerIndex } from "#enums/battler-index";
+import type { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
+import type { PhaseConditionFunc } from "#types/phase-types";
+
+/** A priority queue responsible for the ordering of {@linkcode MovePhase}s */
+export class MovePhasePriorityQueue extends PokemonPhasePriorityQueue {
+ private lastTurnOrder: Pokemon[] = [];
+
+ protected override reorder(): void {
+ super.reorder();
+ this.sortPostSpeed();
+ }
+
+ public cancelMove(condition: PhaseConditionFunc<"MovePhase">): void {
+ this.queue.find(p => condition(p))?.cancel();
+ }
+
+ public setTimingModifier(condition: PhaseConditionFunc<"MovePhase">, modifier: MovePhaseTimingModifier): void {
+ const phase = this.queue.find(p => condition(p));
+ if (phase != null) {
+ phase.timingModifier = modifier;
+ }
+ }
+
+ public setMoveForPhase(condition: PhaseConditionFunc<"MovePhase">, move: PokemonMove) {
+ const phase = this.queue.find(p => condition(p));
+ if (phase != null) {
+ phase.move = move;
+ }
+ }
+
+ public redirectMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void {
+ // failsafe: if not a double battle just return
+ if (!globalScene.currentBattle.double) {
+ return;
+ }
+
+ // TODO: simplify later
+ if (allyPokemon?.isActive(true)) {
+ this.queue
+ .filter(
+ mp =>
+ mp.targets.length === 1
+ && mp.targets[0] === removedPokemon.getBattlerIndex()
+ && mp.pokemon.isPlayer() !== allyPokemon.isPlayer(),
+ )
+ .forEach(targetingMovePhase => {
+ if (targetingMovePhase && targetingMovePhase.targets[0] !== allyPokemon.getBattlerIndex()) {
+ targetingMovePhase.targets[0] = allyPokemon.getBattlerIndex();
+ }
+ });
+ }
+ }
+
+ public setMoveOrder(order: BattlerIndex[]) {
+ 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();
+ }
+
+ 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 timingModifiers = [a, b].map(movePhase => movePhase.timingModifier);
+
+ if (timingModifiers[0] !== timingModifiers[1]) {
+ return timingModifiers[1] - timingModifiers[0];
+ }
+
+ return priority[1] - priority[0];
+ });
+ }
+}
diff --git a/src/queues/pokemon-phase-priority-queue.ts b/src/queues/pokemon-phase-priority-queue.ts
new file mode 100644
index 00000000000..3098c5be435
--- /dev/null
+++ b/src/queues/pokemon-phase-priority-queue.ts
@@ -0,0 +1,20 @@
+import type { DynamicPhase } from "#app/@types/phase-types";
+import { PriorityQueue } from "#app/queues/priority-queue";
+import { sortInSpeedOrder } from "#app/utils/speed-order";
+import type { BattlerIndex } from "#enums/battler-index";
+
+/** A generic speed-based priority queue of {@linkcode DynamicPhase}s */
+export class PokemonPhasePriorityQueue extends PriorityQueue {
+ protected setOrder: BattlerIndex[] | undefined;
+ protected override reorder(): void {
+ const setOrder = this.setOrder;
+ if (setOrder) {
+ this.queue.sort(
+ (a, b) =>
+ setOrder.indexOf(a.getPokemon().getBattlerIndex()) - setOrder.indexOf(b.getPokemon().getBattlerIndex()),
+ );
+ } else {
+ this.queue = sortInSpeedOrder(this.queue);
+ }
+ }
+}
diff --git a/src/queues/pokemon-priority-queue.ts b/src/queues/pokemon-priority-queue.ts
new file mode 100644
index 00000000000..597bfb32c0d
--- /dev/null
+++ b/src/queues/pokemon-priority-queue.ts
@@ -0,0 +1,10 @@
+import type { Pokemon } from "#app/field/pokemon";
+import { PriorityQueue } from "#app/queues/priority-queue";
+import { sortInSpeedOrder } from "#app/utils/speed-order";
+
+/** A priority queue of {@linkcode Pokemon}s */
+export class PokemonPriorityQueue extends PriorityQueue {
+ protected 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..37da90a1427
--- /dev/null
+++ b/src/queues/post-summon-phase-priority-queue.ts
@@ -0,0 +1,45 @@
+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";
+import { sortInSpeedOrder } from "#app/utils/speed-order";
+
+/**
+ * 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 {
+ protected override reorder(): void {
+ this.queue = sortInSpeedOrder(this.queue, false);
+ this.queue.sort((phaseA, phaseB) => 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 {
+ if (phase instanceof PostSummonActivateAbilityPhase) {
+ return;
+ }
+
+ const phasePokemon = phase.getPokemon();
+
+ phasePokemon.getAbilityPriorities().forEach((priority, idx) => {
+ const activateAbilityPhase = new PostSummonActivateAbilityPhase(
+ phasePokemon.getBattlerIndex(),
+ priority,
+ idx !== 0,
+ );
+ phase.source === "SummonPhase"
+ ? globalScene.phaseManager.pushPhase(activateAbilityPhase)
+ : globalScene.phaseManager.unshiftPhase(activateAbilityPhase);
+ });
+ }
+}
diff --git a/src/queues/priority-queue.ts b/src/queues/priority-queue.ts
new file mode 100644
index 00000000000..b53cfec3f4d
--- /dev/null
+++ b/src/queues/priority-queue.ts
@@ -0,0 +1,78 @@
+/**
+ * Stores a list of elements.
+ *
+ * Dynamically updates ordering to always pop the highest "priority", based on implementation of {@linkcode reorder}.
+ */
+export abstract class PriorityQueue {
+ protected queue: T[] = [];
+
+ /**
+ * Sorts the elements in the queue
+ */
+ protected abstract reorder(): void;
+
+ /**
+ * Calls {@linkcode reorder} and shifts the queue
+ * @returns The front element of the queue after sorting, or `undefined` if the queue is empty
+ * @sealed
+ */
+ public pop(): T | undefined {
+ if (this.isEmpty()) {
+ return;
+ }
+
+ this.reorder();
+ return this.queue.shift();
+ }
+
+ /**
+ * Adds an element to the queue
+ * @param element The element to add
+ */
+ public push(element: T): void {
+ this.queue.push(element);
+ }
+
+ /**
+ * Removes all elements from the queue
+ * @sealed
+ */
+ public clear(): void {
+ this.queue.splice(0, this.queue.length);
+ }
+
+ /**
+ * @returns Whether the queue is empty
+ * @sealed
+ */
+ public isEmpty(): boolean {
+ return this.queue.length === 0;
+ }
+
+ /**
+ * Removes the first element matching the condition
+ * @param condition - An optional condition function (defaults to a function that always returns `true`)
+ * @returns Whether a removal occurred
+ */
+ public remove(condition: (t: T) => boolean = () => true): boolean {
+ // Reorder to remove the first element
+ this.reorder();
+ const index = this.queue.findIndex(condition);
+ if (index === -1) {
+ return false;
+ }
+
+ this.queue.splice(index, 1);
+ return true;
+ }
+
+ /** @returns An element matching the condition function */
+ public find(condition?: (t: T) => boolean): T | undefined {
+ return this.queue.find(e => !condition || condition(e));
+ }
+
+ /** @returns Whether an element matching the condition function exists */
+ public has(condition?: (t: T) => boolean): boolean {
+ return this.queue.some(e => !condition || condition(e));
+ }
+}
diff --git a/src/utils/speed-order-generator.ts b/src/utils/speed-order-generator.ts
new file mode 100644
index 00000000000..24f95de665f
--- /dev/null
+++ b/src/utils/speed-order-generator.ts
@@ -0,0 +1,39 @@
+import { globalScene } from "#app/global-scene";
+import { PokemonPriorityQueue } from "#app/queues/pokemon-priority-queue";
+import { ArenaTagSide } from "#enums/arena-tag-side";
+import type { Pokemon } from "#field/pokemon";
+
+/**
+ * A generator function which uses a priority queue to yield each pokemon from a given side of the field in speed order.
+ * @param side - The {@linkcode ArenaTagSide | side} of the field to use
+ * @returns A {@linkcode Generator} of {@linkcode Pokemon}
+ *
+ * @remarks
+ * This should almost always be used by iteration in a `for...of` loop
+ */
+export function* inSpeedOrder(side: ArenaTagSide = ArenaTagSide.BOTH): Generator {
+ let pokemonList: Pokemon[];
+ switch (side) {
+ case ArenaTagSide.PLAYER:
+ pokemonList = globalScene.getPlayerField(true);
+ break;
+ case ArenaTagSide.ENEMY:
+ pokemonList = globalScene.getEnemyField(true);
+ break;
+ default:
+ pokemonList = globalScene.getField(true);
+ }
+
+ const queue = new PokemonPriorityQueue();
+ let i = 0;
+ pokemonList.forEach(p => {
+ queue.push(p);
+ });
+ while (!queue.isEmpty()) {
+ // If the queue is not empty, this can never be undefined
+ i++;
+ yield queue.pop()!;
+ }
+
+ return i;
+}
diff --git a/src/utils/speed-order.ts b/src/utils/speed-order.ts
new file mode 100644
index 00000000000..1d894369bb3
--- /dev/null
+++ b/src/utils/speed-order.ts
@@ -0,0 +1,57 @@
+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";
+
+/** Interface representing an object associated with a specific Pokemon */
+interface hasPokemon {
+ getPokemon(): Pokemon;
+}
+
+/**
+ * Sorts an array of {@linkcode Pokemon} by speed, taking Trick Room into account.
+ * @param pokemonList - The list of Pokemon or objects containing Pokemon
+ * @param shuffleFirst - Whether to shuffle the list before sorting (to handle speed ties). Default `true`.
+ * @returns The sorted array of {@linkcode Pokemon}
+ */
+export function sortInSpeedOrder(pokemonList: T[], shuffleFirst = true): T[] {
+ pokemonList = shuffleFirst ? shufflePokemonList(pokemonList) : pokemonList;
+ sortBySpeed(pokemonList);
+ return pokemonList;
+}
+
+/**
+ * @param pokemonList - The array of Pokemon or objects containing Pokemon
+ * @returns The shuffled array
+ */
+function shufflePokemonList(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;
+}
+
+/** Sorts an array of {@linkcode Pokemon} by speed (without shuffling) */
+function sortBySpeed(pokemonList: T[]): void {
+ pokemonList.sort((a, b) => {
+ const aSpeed = (a instanceof Pokemon ? a : a.getPokemon()).getEffectiveStat(Stat.SPD);
+ const bSpeed = (b instanceof Pokemon ? b : b.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();
+ }
+}
diff --git a/test/abilities/dancer.test.ts b/test/abilities/dancer.test.ts
index e640e326d58..e206152715e 100644
--- a/test/abilities/dancer.test.ts
+++ b/test/abilities/dancer.test.ts
@@ -34,7 +34,7 @@ describe("Abilities - Dancer", () => {
game.override.enemyAbility(AbilityId.DANCER).enemySpecies(SpeciesId.MAGIKARP).enemyMoveset(MoveId.VICTORY_DANCE);
await game.classicMode.startBattle([SpeciesId.ORICORIO, SpeciesId.FEEBAS]);
- const [oricorio, feebas] = game.scene.getPlayerField();
+ const [oricorio, feebas, magikarp1] = game.scene.getField();
game.move.changeMoveset(oricorio, [MoveId.SWORDS_DANCE, MoveId.VICTORY_DANCE, MoveId.SPLASH]);
game.move.changeMoveset(feebas, [MoveId.SWORDS_DANCE, MoveId.SPLASH]);
@@ -44,8 +44,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/abilities/mycelium-might.test.ts b/test/abilities/mycelium-might.test.ts
index c3b7b4753b6..21b856d341e 100644
--- a/test/abilities/mycelium-might.test.ts
+++ b/test/abilities/mycelium-might.test.ts
@@ -2,8 +2,6 @@ import { AbilityId } from "#enums/ability-id";
import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id";
import { Stat } from "#enums/stat";
-import { TurnEndPhase } from "#phases/turn-end-phase";
-import { TurnStartPhase } from "#phases/turn-start-phase";
import { GameManager } from "#test/test-utils/game-manager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
@@ -45,65 +43,50 @@ describe("Abilities - Mycelium Might", () => {
it("should move last in its priority bracket and ignore protective abilities", async () => {
await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
- const enemyPokemon = game.field.getEnemyPokemon();
- const playerIndex = game.field.getPlayerPokemon().getBattlerIndex();
- const enemyIndex = enemyPokemon.getBattlerIndex();
+ const enemy = game.field.getEnemyPokemon();
+ const player = game.field.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);
+ expect(enemy.getStatStage(Stat.ATK)).toBe(-1);
});
it("should 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.field.getEnemyPokemon();
- const playerIndex = game.field.getPlayerPokemon().getBattlerIndex();
- const enemyIndex = enemyPokemon.getBattlerIndex();
+ const enemy = game.field.getEnemyPokemon();
+ const player = game.field.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);
+ expect(enemy.getStatStage(Stat.ATK)).toBe(-1);
});
it("should not affect non-status moves", async () => {
await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
- const playerIndex = game.field.getPlayerPokemon().getBattlerIndex();
- const enemyIndex = game.field.getEnemyPokemon().getBattlerIndex();
+ const player = game.field.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]);
+ expect(player.hp).toEqual(player.getMaxHp());
});
});
diff --git a/test/abilities/neutralizing-gas.test.ts b/test/abilities/neutralizing-gas.test.ts
index 555e5f8a19c..fd9138e4174 100644
--- a/test/abilities/neutralizing-gas.test.ts
+++ b/test/abilities/neutralizing-gas.test.ts
@@ -59,7 +59,7 @@ describe("Abilities - Neutralizing Gas", () => {
expect(game.field.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/abilities/quick-draw.test.ts b/test/abilities/quick-draw.test.ts
index ce5873af3a8..257892145e5 100644
--- a/test/abilities/quick-draw.test.ts
+++ b/test/abilities/quick-draw.test.ts
@@ -5,7 +5,7 @@ import { SpeciesId } from "#enums/species-id";
import { FaintPhase } from "#phases/faint-phase";
import { GameManager } from "#test/test-utils/game-manager";
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")
- .starterSpecies(SpeciesId.MAGIKARP)
.ability(AbilityId.QUICK_DRAW)
.moveset([MoveId.TACKLE, MoveId.TAIL_WHIP])
.enemyLevel(100)
@@ -40,8 +39,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.field.getPlayerPokemon();
const enemy = game.field.getEnemyPokemon();
@@ -57,33 +56,27 @@ describe("Abilities - Quick Draw", () => {
expect(pokemon.waveData.abilitiesApplied).toContain(AbilityId.QUICK_DRAW);
});
- 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.field.getPlayerPokemon();
- const enemy = game.field.getEnemyPokemon();
+ const pokemon = game.field.getPlayerPokemon();
+ const enemy = game.field.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.field.getPlayerPokemon();
const enemy = game.field.getEnemyPokemon();
diff --git a/test/abilities/stall.test.ts b/test/abilities/stall.test.ts
index 5b4e38f7099..b6a88964e09 100644
--- a/test/abilities/stall.test.ts
+++ b/test/abilities/stall.test.ts
@@ -1,7 +1,6 @@
import { AbilityId } from "#enums/ability-id";
import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id";
-import { TurnStartPhase } from "#phases/turn-start-phase";
import { GameManager } from "#test/test-utils/game-manager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
@@ -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.field.getPlayerPokemon().getBattlerIndex();
- const enemyIndex = game.field.getEnemyPokemon().getBattlerIndex();
+ const player = game.field.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]);
+ expect(player).toHaveFullHp();
});
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.field.getPlayerPokemon().getBattlerIndex();
- const enemyIndex = game.field.getEnemyPokemon().getBattlerIndex();
+ const player = game.field.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]);
+ expect(player).not.toHaveFullHp();
});
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.field.getPlayerPokemon().getBattlerIndex();
- const enemyIndex = game.field.getEnemyPokemon().getBattlerIndex();
+ const player = game.field.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]);
+ expect(player).not.toHaveFullHp();
});
});
diff --git a/test/battle/battle-order.test.ts b/test/battle/battle-order.test.ts
index 0b24fcbfa7d..de13b22df79 100644
--- a/test/battle/battle-order.test.ts
+++ b/test/battle/battle-order.test.ts
@@ -1,7 +1,8 @@
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 type { TurnStartPhase } from "#phases/turn-start-phase";
+import type { MovePhase } from "#phases/move-phase";
import { GameManager } from "#test/test-utils/game-manager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
@@ -34,38 +35,34 @@ describe("Battle order", () => {
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
const playerPokemon = game.field.getPlayerPokemon();
+ const playerStartHp = playerPokemon.hp;
const enemyPokemon = game.field.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.to("TurnStartPhase", false);
- 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);
+ 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.field.getPlayerPokemon();
+ const playerStartHp = playerPokemon.hp;
const enemyPokemon = game.field.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.to("TurnStartPhase", false);
- 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);
+ 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 () => {
@@ -73,23 +70,24 @@ describe("Battle order", () => {
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.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);
+ 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 () => {
@@ -101,18 +99,13 @@ 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.to("TurnStartPhase", false);
+ await game.phaseInterceptor.to("MovePhase", false);
- const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
- const order = phase.getCommandOrder();
- // enemy 2 should be first, followed by some other assortment of the other 3 pokemon
- expect(order[0]).toBe(enemyIndices[1]);
- expect(order.slice(1, 4)).toEqual(expect.arrayContaining([enemyIndices[0], ...playerIndices]));
+ const phase = game.scene.phaseManager.getCurrentPhase() as MovePhase;
+ expect(phase.pokemon).toEqual(enemyPokemon[1]);
});
it("double - speed tie 100/150 vs 100/150", async () => {
@@ -125,17 +118,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.to("TurnStartPhase", false);
- const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
- const order = phase.getCommandOrder();
- // P2/E2 should be randomly first/second, then P1/E1 randomly 3rd/4th
- expect(order.slice(0, 2)).toStrictEqual(expect.arrayContaining([playerIndices[1], enemyIndices[1]]));
- expect(order.slice(2, 4)).toStrictEqual(expect.arrayContaining([playerIndices[0], enemyIndices[0]]));
+ 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/moves/baton-pass.test.ts b/test/moves/baton-pass.test.ts
index f9bd92a63cd..caabcfa7158 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.field.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.field.getEnemyPokemon().summonData.moveHistory).toHaveLength(0);
});
it("doesn't transfer effects that aren't transferrable", async () => {
diff --git a/test/moves/delayed-attack.test.ts b/test/moves/delayed-attack.test.ts
index 6817c7fd17a..e31c7f28e48 100644
--- a/test/moves/delayed-attack.test.ts
+++ b/test/moves/delayed-attack.test.ts
@@ -193,7 +193,7 @@ describe("Moves - Delayed Attacks", () => {
// All attacks have concluded at this point, unshifting new `MoveEffectPhase`s to the queue.
expectFutureSightActive(0);
- const MEPs = game.scene.phaseManager.phaseQueue.filter(p => p.is("MoveEffectPhase"));
+ const MEPs = game.scene.phaseManager["phaseQueue"].findAll("MoveEffectPhase");
expect(MEPs).toHaveLength(4);
expect(MEPs.map(mep => mep.getPokemon())).toEqual(oldOrder);
});
diff --git a/test/moves/focus-punch.test.ts b/test/moves/focus-punch.test.ts
index d7b40569aaa..06594e85e27 100644
--- a/test/moves/focus-punch.test.ts
+++ b/test/moves/focus-punch.test.ts
@@ -3,7 +3,6 @@ import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id";
import { BerryPhase } from "#phases/berry-phase";
import { MessagePhase } from "#phases/message-phase";
-import { MoveHeaderPhase } from "#phases/move-header-phase";
import { SwitchSummonPhase } from "#phases/switch-summon-phase";
import { TurnStartPhase } from "#phases/turn-start-phase";
import { GameManager } from "#test/test-utils/game-manager";
@@ -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")).toBe(true);
});
it("should replace the 'but it failed' text when the user gets hit", async () => {
game.override.enemyMoveset([MoveId.TACKLE]);
diff --git a/test/moves/rage-fist.test.ts b/test/moves/rage-fist.test.ts
index 61164b5710c..c58d1296ac5 100644
--- a/test/moves/rage-fist.test.ts
+++ b/test/moves/rage-fist.test.ts
@@ -166,7 +166,6 @@ describe("Moves - Rage Fist", () => {
// Charizard hit
game.move.select(MoveId.SPLASH);
- await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.toNextTurn();
expect(getPartyHitCount()).toEqual([1, 0]);
diff --git a/test/moves/revival-blessing.test.ts b/test/moves/revival-blessing.test.ts
index 4dc7cb97f2d..8c751458ff7 100644
--- a/test/moves/revival-blessing.test.ts
+++ b/test/moves/revival-blessing.test.ts
@@ -119,17 +119,16 @@ describe("Moves - Revival Blessing", () => {
game.override
.battleStyle("double")
.enemyMoveset([MoveId.REVIVAL_BLESSING])
- .moveset([MoveId.SPLASH])
+ .moveset([MoveId.SPLASH, MoveId.JUDGMENT])
+ .startingLevel(100)
.startingWave(25); // 2nd rival battle - must have 3+ pokemon
await game.classicMode.startBattle([SpeciesId.ARCEUS, SpeciesId.GIRATINA]);
const enemyFainting = game.scene.getEnemyField()[0];
- game.move.select(MoveId.SPLASH, 0);
+ game.move.use(MoveId.JUDGMENT, 0, BattlerIndex.ENEMY);
game.move.select(MoveId.SPLASH, 1);
- await game.killPokemon(enemyFainting);
- await game.phaseInterceptor.to("BerryPhase");
await game.toNextTurn();
// If there are incorrectly two switch phases into this slot, the fainted pokemon will end up in slot 3
// Make sure it's still in slot 1
diff --git a/test/moves/shell-trap.test.ts b/test/moves/shell-trap.test.ts
index 5ecad3116af..2a83f2c3266 100644
--- a/test/moves/shell-trap.test.ts
+++ b/test/moves/shell-trap.test.ts
@@ -48,7 +48,7 @@ describe("Moves - Shell Trap", () => {
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER, BattlerIndex.PLAYER_2]);
- await game.phaseInterceptor.to(MoveEndPhase);
+ await game.phaseInterceptor.to("MoveEndPhase");
const movePhase = game.scene.phaseManager.getCurrentPhase();
expect(movePhase instanceof MovePhase).toBeTruthy();
diff --git a/test/moves/trick-room.test.ts b/test/moves/trick-room.test.ts
index a1d81efb17e..d970dc9762d 100644
--- a/test/moves/trick-room.test.ts
+++ b/test/moves/trick-room.test.ts
@@ -5,10 +5,10 @@ import { BattlerIndex } from "#enums/battler-index";
import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id";
import { Stat } from "#enums/stat";
-import { TurnStartPhase } from "#phases/turn-start-phase";
+import { WeatherType } from "#enums/weather-type";
import { GameManager } from "#test/test-utils/game-manager";
import Phaser from "phaser";
-import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
+import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Move - Trick Room", () => {
let phaserGame: Phaser.Game;
@@ -56,13 +56,11 @@ describe("Move - Trick Room", () => {
turnCount: 4, // The 5 turn limit _includes_ the current turn!
});
- // Now, check that speed was indeed reduced
- const turnOrderSpy = vi.spyOn(TurnStartPhase.prototype, "getSpeedOrder");
-
- game.move.use(MoveId.SPLASH);
+ game.move.use(MoveId.SUNNY_DAY);
+ await game.move.forceEnemyMove(MoveId.RAIN_DANCE);
await game.toEndOfTurn();
- expect(turnOrderSpy).toHaveLastReturnedWith([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
+ expect(game.scene.arena.getWeatherType()).toBe(WeatherType.SUNNY);
});
it("should be removed when overlapped", async () => {
diff --git a/test/moves/wish.test.ts b/test/moves/wish.test.ts
index 1c1f3f3b8ba..b64a15ac654 100644
--- a/test/moves/wish.test.ts
+++ b/test/moves/wish.test.ts
@@ -135,7 +135,7 @@ describe("Move - Wish", () => {
// all wishes have activated and added healing phases
expect(game).toHavePositionalTag(PositionalTagType.WISH, 0);
- const healPhases = game.scene.phaseManager.phaseQueue.filter(p => p.is("PokemonHealPhase"));
+ const healPhases = game.scene.phaseManager["phaseQueue"].findAll("PokemonHealPhase");
expect(healPhases).toHaveLength(4);
expect.soft(healPhases.map(php => php.getPokemon())).toEqual(oldOrder);
diff --git a/test/mystery-encounter/encounter-test-utils.ts b/test/mystery-encounter/encounter-test-utils.ts
index 4aad0e000d9..165678a88da 100644
--- a/test/mystery-encounter/encounter-test-utils.ts
+++ b/test/mystery-encounter/encounter-test-utils.ts
@@ -70,7 +70,6 @@ export async function runMysteryEncounterToEnd(
// If a battle is started, fast forward to end of the battle
game.onNextPrompt("CommandPhase", UiMode.COMMAND, () => {
game.scene.phaseManager.clearPhaseQueue();
- game.scene.phaseManager.clearPhaseQueueSplice();
game.scene.phaseManager.unshiftPhase(new VictoryPhase(0));
game.endPhase();
});
@@ -196,7 +195,6 @@ async function handleSecondaryOptionSelect(game: GameManager, pokemonNo: number,
*/
export async function skipBattleRunMysteryEncounterRewardsPhase(game: GameManager, runRewardsPhase = true) {
game.scene.phaseManager.clearPhaseQueue();
- game.scene.phaseManager.clearPhaseQueueSplice();
game.scene.getEnemyParty().forEach(p => {
p.hp = 0;
p.status = new Status(StatusEffect.FAINT);
diff --git a/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts b/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts
index 814e2ee07fb..3bbb858a15d 100644
--- a/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts
+++ b/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts
@@ -355,7 +355,6 @@ describe("The Winstrate Challenge - Mystery Encounter", () => {
*/
async function skipBattleToNextBattle(game: GameManager, isFinalBattle = false) {
game.scene.phaseManager.clearPhaseQueue();
- game.scene.phaseManager.clearPhaseQueueSplice();
const commandUiHandler = game.scene.ui.handlers[UiMode.COMMAND];
commandUiHandler.clear();
game.scene.getEnemyParty().forEach(p => {
diff --git a/test/test-utils/game-manager.ts b/test/test-utils/game-manager.ts
index f9db964ad26..f681846d935 100644
--- a/test/test-utils/game-manager.ts
+++ b/test/test-utils/game-manager.ts
@@ -464,6 +464,9 @@ export class GameManager {
* Faint a player or enemy pokemon instantly by setting their HP to 0.
* @param pokemon - The player/enemy pokemon being fainted
* @returns A Promise that resolves once the fainted pokemon's FaintPhase finishes running.
+ * @remarks
+ * This method *pushes* a FaintPhase and runs until it's finished. This may cause a turn to play out unexpectedly
+ * @todo Consider whether running the faint phase immediately can be done
*/
async killPokemon(pokemon: PlayerPokemon | EnemyPokemon) {
pokemon.hp = 0;
@@ -533,7 +536,7 @@ export 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.
@@ -545,7 +548,7 @@ export 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 25416ebf47bd1e55e5fbdfa35c2f8ee0093e0624 Mon Sep 17 00:00:00 2001
From: Dean <69436131+emdeann@users.noreply.github.com>
Date: Sat, 20 Sep 2025 20:24:50 -0700
Subject: [PATCH 35/42] [UI] Avoid prematurely updating HP bar when applying
damage (#6582)
Avoid prematurely updating HP bar when applying damage
---
src/field/pokemon.ts | 5 -----
1 file changed, 5 deletions(-)
diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts
index ec813e52e56..ea7c74904d8 100644
--- a/src/field/pokemon.ts
+++ b/src/field/pokemon.ts
@@ -3942,11 +3942,6 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
damage = 0;
}
damage = this.damage(damage, ignoreSegments, isIndirectDamage, ignoreFaintPhase);
- // Ensure the battle-info bar's HP is updated, though only if the battle info is visible
- // TODO: When battle-info UI is refactored, make this only update the HP bar
- if (this.battleInfo.visible) {
- this.updateInfo();
- }
// Damage amount may have changed, but needed to be queued before calling damage function
damagePhase.updateAmount(damage);
/**
From 9f851591cb64fe499aa63502936d0e5cb9f59754 Mon Sep 17 00:00:00 2001
From: NightKev <34855794+DayKev@users.noreply.github.com>
Date: Sun, 21 Sep 2025 00:21:27 -0700
Subject: [PATCH 36/42] [Dev] Update `pnpm`
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 1fb25c5d4ba..ac8bca50f76 100644
--- a/package.json
+++ b/package.json
@@ -74,5 +74,5 @@
"engines": {
"node": ">=22.0.0"
},
- "packageManager": "pnpm@10.16.1"
+ "packageManager": "pnpm@10.17.0"
}
From be61996044f748294e00f9186008c46c9c71443c Mon Sep 17 00:00:00 2001
From: NightKev <34855794+DayKev@users.noreply.github.com>
Date: Sun, 21 Sep 2025 00:22:53 -0700
Subject: [PATCH 37/42] [Docs] Fix/update some comments and spacing
---
src/@types/phase-types.ts | 22 +++++-----------------
src/phase-tree.ts | 1 -
2 files changed, 5 insertions(+), 18 deletions(-)
diff --git a/src/@types/phase-types.ts b/src/@types/phase-types.ts
index d396375c5fa..2324c927e3a 100644
--- a/src/@types/phase-types.ts
+++ b/src/@types/phase-types.ts
@@ -3,37 +3,25 @@ import type { Phase } from "#app/phase";
import type { PhaseConstructorMap } from "#app/phase-manager";
import type { ObjectValues } from "#types/type-helpers";
-// Intentionally export the types of everything in phase-manager, as this file is meant to be
+// Intentionally [re-]export the types of everything in phase-manager, as this file is meant to be
// the centralized place for type definitions for the phase system.
export type * from "#app/phase-manager";
-// This file includes helpful types for the phase system.
-// It intentionally imports the phase constructor map from the phase manager (and re-exports it)
-
-/**
- * Map of phase names to constructors for said phase
- */
+/** Map of phase names to constructors for said phase */
export type PhaseMap = {
[K in keyof PhaseConstructorMap]: InstanceType;
};
-/**
- * Union type of all phase constructors.
- */
+/** Union type of all phase constructors. */
export type PhaseClass = ObjectValues;
-/**
- * Union type of all phase names as strings.
- */
+/** Union type of all phase names as strings. */
export type PhaseString = keyof PhaseMap;
/** Type for predicate functions operating on a specific type of {@linkcode Phase}. */
-
export type PhaseConditionFunc = (phase: PhaseMap[T]) => boolean;
-/**
- * Interface type representing the assumption that all phases with pokemon associated are dynamic
- */
+/** Interface type representing the assumption that all phases with pokemon associated are dynamic */
export interface DynamicPhase extends Phase {
getPokemon(): Pokemon;
}
diff --git a/src/phase-tree.ts b/src/phase-tree.ts
index 55476f38d65..69bb72ca4f0 100644
--- a/src/phase-tree.ts
+++ b/src/phase-tree.ts
@@ -1,7 +1,6 @@
// biome-ignore-start lint/correctness/noUnusedImports: TSDoc imports
import type { PhaseManager } from "#app/@types/phase-types";
import type { DynamicPhaseMarker } from "#phases/dynamic-phase-marker";
-
// biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports
import type { PhaseMap, PhaseString } from "#app/@types/phase-types";
From cf5e7fd981504bcb41ff7b9ba1148b8e2b7c02f6 Mon Sep 17 00:00:00 2001
From: Lugiad
Date: Sun, 21 Sep 2025 15:55:54 +0200
Subject: [PATCH 38/42] [UI/UX] Optimize text PNGs (#6584)
---
.../ca/summary/summary_dexnb_label_ca.png | Bin 1618 -> 131 bytes
.../summary_dexnb_label_overlay_shiny_ca.png | Bin 1753 -> 202 bytes
.../summary/summary_stats_expbar_title_ca.png | Bin 1600 -> 119 bytes
.../en/battle_ui/overlay_exp_label.png | Bin 1401 -> 116 bytes
.../en/summary/summary_dexnb_label.png | Bin 1612 -> 124 bytes
.../summary_moves_descriptions_title.png | Bin 2173 -> 235 bytes
.../en/summary/summary_moves_effect_title.png | Bin 1870 -> 186 bytes
.../en/summary/summary_moves_moves_title.png | Bin 2035 -> 180 bytes
.../en/summary/summary_profile_ability.png | Bin 1675 -> 181 bytes
.../en/summary/summary_profile_memo_title.png | Bin 1965 -> 242 bytes
.../en/summary/summary_profile_passive.png | Bin 1503 -> 198 bytes
.../summary/summary_profile_profile_title.png | Bin 2050 -> 196 bytes
.../en/summary/summary_stats_expbar_title.png | Bin 1600 -> 119 bytes
.../en/summary/summary_stats_item_title.png | Bin 1892 -> 183 bytes
.../en/summary/summary_stats_stats_title.png | Bin 1869 -> 169 bytes
.../summary/summary_dexnb_label_es-ES.png | Bin 1618 -> 131 bytes
...summary_dexnb_label_overlay_shiny_es-ES.png | Bin 1753 -> 202 bytes
.../summary_moves_descriptions_title_es-ES.png | Bin 2193 -> 249 bytes
.../summary_moves_effect_title_es-ES.png | Bin 2033 -> 190 bytes
.../summary_moves_moves_title_es-ES.png | Bin 2231 -> 244 bytes
.../summary/summary_profile_ability_es-ES.png | Bin 2059 -> 195 bytes
.../summary_profile_memo_title_es-ES.png | Bin 1976 -> 235 bytes
.../summary/summary_profile_passive_es-ES.png | Bin 1834 -> 193 bytes
.../summary_profile_profile_title_es-ES.png | Bin 2030 -> 180 bytes
.../summary_stats_expbar_title_es-ES.png | Bin 1600 -> 119 bytes
.../summary/summary_stats_item_title_es-ES.png | Bin 2172 -> 197 bytes
.../summary_stats_stats_title_es-ES.png | Bin 2532 -> 307 bytes
.../summary/summary_dexnb_label_es-MX.png | Bin 1618 -> 131 bytes
...summary_dexnb_label_overlay_shiny_es-MX.png | Bin 1753 -> 202 bytes
.../summary_moves_descriptions_title_es-MX.png | Bin 2193 -> 249 bytes
.../summary_moves_effect_title_es-MX.png | Bin 2033 -> 190 bytes
.../summary_moves_moves_title_es-MX.png | Bin 2231 -> 244 bytes
.../summary/summary_profile_ability_es-MX.png | Bin 2059 -> 195 bytes
.../summary_profile_memo_title_es-MX.png | Bin 1976 -> 235 bytes
.../summary/summary_profile_passive_es-MX.png | Bin 1834 -> 193 bytes
.../summary_profile_profile_title_es-MX.png | Bin 2030 -> 180 bytes
.../summary_stats_expbar_title_es-MX.png | Bin 1600 -> 119 bytes
.../summary/summary_stats_item_title_es-MX.png | Bin 2172 -> 197 bytes
.../summary_stats_stats_title_es-MX.png | Bin 2532 -> 307 bytes
.../fr/battle_ui/overlay_exp_label_fr.png | Bin 1401 -> 116 bytes
.../ja/battle_ui/overlay_exp_label_ja.png | Bin 1401 -> 116 bytes
.../summary/summary_stats_expbar_title_ja.png | Bin 1600 -> 119 bytes
.../ko/battle_ui/overlay_exp_label_ko.png | Bin 1401 -> 116 bytes
.../summary/summary_stats_expbar_title_ko.png | Bin 1600 -> 119 bytes
...summary_dexnb_label_overlay_shiny_pt-BR.png | Bin 1753 -> 202 bytes
.../summary_stats_expbar_title_pt-BR.png | Bin 1600 -> 119 bytes
.../ro/battle_ui/overlay_exp_label_ro.png | Bin 1401 -> 116 bytes
.../summary/summary_stats_expbar_title_ro.png | Bin 1600 -> 119 bytes
.../tl/battle_ui/overlay_exp_label_tl.png | Bin 1401 -> 116 bytes
.../tl/summary/summary_stats_expbar_title.png | Bin 1600 -> 119 bytes
.../summary/summary_stats_expbar_title_tr.png | Bin 1600 -> 119 bytes
.../battle_ui/overlay_exp_label_zh-CN.png | Bin 1401 -> 116 bytes
.../summary/summary_dexnb_label_zh-CN.png | Bin 1612 -> 124 bytes
.../summary_moves_descriptions_title_zh-CN.png | Bin 2173 -> 235 bytes
.../summary_moves_effect_title_zh-CN.png | Bin 1870 -> 187 bytes
.../summary_moves_moves_title_zh-CN.png | Bin 2035 -> 180 bytes
.../summary/summary_profile_ability_zh-CN.png | Bin 1675 -> 181 bytes
.../summary_profile_memo_title_zh-CN.png | Bin 1965 -> 242 bytes
.../summary/summary_profile_passive_zh-CN.png | Bin 1503 -> 198 bytes
.../summary_profile_profile_title_zh-CN.png | Bin 2050 -> 196 bytes
.../summary_stats_expbar_title_zh-CN.png | Bin 1600 -> 119 bytes
.../summary/summary_stats_item_title_zh-CN.png | Bin 1892 -> 183 bytes
.../summary_stats_stats_title_zh-CN.png | Bin 1869 -> 169 bytes
.../battle_ui/overlay_exp_label_zh-TW.png | Bin 1401 -> 116 bytes
.../summary/summary_dexnb_label_zh-TW.png | Bin 1612 -> 124 bytes
.../summary_moves_descriptions_title_zh-TW.png | Bin 2173 -> 235 bytes
.../summary_moves_effect_title_zh-TW.png | Bin 1870 -> 187 bytes
.../summary_moves_moves_title_zh-TW.png | Bin 2035 -> 180 bytes
.../summary/summary_profile_ability_zh-TW.png | Bin 1675 -> 181 bytes
.../summary_profile_memo_title_zh-TW.png | Bin 1965 -> 242 bytes
.../summary/summary_profile_passive_zh-TW.png | Bin 1503 -> 198 bytes
.../summary_profile_profile_title_zh-TW.png | Bin 2050 -> 196 bytes
.../summary_stats_expbar_title_zh-TW.png | Bin 1600 -> 119 bytes
.../summary/summary_stats_item_title_zh-TW.png | Bin 1892 -> 183 bytes
.../summary_stats_stats_title_zh-TW.png | Bin 1869 -> 169 bytes
75 files changed, 0 insertions(+), 0 deletions(-)
diff --git a/public/images/ui/legacy/text_images/ca/summary/summary_dexnb_label_ca.png b/public/images/ui/legacy/text_images/ca/summary/summary_dexnb_label_ca.png
index a457468d8d0630acdec6da6a17d0174454a9f198..5477e3385a8c5fe552c6e601a21330829e7e4f13 100644
GIT binary patch
delta 114
zcmcb_)66(QBAbbsfq|jyL}&w$;tcQ!asB`Qe?mgSj~_o6Qbl(G1sO|%{DK)Ap4~_T
zax^?$978JNL=SJ|WKa+|df|%SclYk5Ly`9vBz;WiQ2iUbQnqhHi>8XreFp9_!`mm1
Q0QE35hjuH9vadPoEx>Q3p{k&hWt1hH)|2`D>)9Lx@~*wR)aVUG(SGhDfj@0a@J
zl1MwqbQ}j67P2fI5PG&6OES>o?C2zd$1|FSi3~-o<`GFCmZhr!(@_agQiP4OahsrF
zXh0HVmW;flA&}7mmn^5fVRA+h@9~JoG6SqxV3V$pBI}ARLKmMmB!JfXevxBSE>TpZ
z$mGlllre6|rs%A-nsBthGqIdfKC=QekLJ}RxRG)qMcNXgHyPCYgtFRbsK+jZF%$jl
zlb7J!Ba*wCWmpy6sOgs7GEHoo*s6VISlBQ|P+zdHy}B8+0u#13Kw)7Rk>rhFMsZuD
z5rJ~Uh;%qg;*O>kO~eh+6DcGHcI6=p-^a~3lO(3R>A4zITna;mExT4_HY`f5Vz>2x
z*fl)}NLjZ7+qT;dEe8Re_2y20$KP6)SjR%6wm;sjY<@LnhZgg7{->`h|PJG-_+9h#^gGeGR
zF9qdtG8zbabymY~FG0E23GxeO
zU?`h>)&j`O@N{tusfgQh?jR?Ff&lZu1)@usEv;|7-fymZkU{T>rNGU+Kl21!7KHO1
z@(vJf?45kX=OBmUN~41B;&Z+|o598)(7qXt17rY|XWt~;~4bI3L(yRlj5#wFR3RK@1(nX|iNcIHfH
zW^X|uu0AMu0R>Y;O8a1?FX~HMq1a-fQXeD_)+YF3r4RK*qAiHNh-daPwh~(`4$RCs
zv)}o?|NGB>{(U1qvwtZ1Sd?Lyq3m>KmcAG1{o;dz^qJmXc!9olIMatbhS~X0a7UOA
zJ{)0~ftFcV@E39i03wzE4N}BH({d=9VaCRr4n#}XXN$OO+9_`H+y#y`jTASp<)oaG
z#uamV*~N3qGX=D~gmi-&-^Y$M0VS}o580+wwLQ>GaUEVj$H8ZjV>=LkDaB0$g6u*r
z&!&lsSxt!ZNLCd#p$l?c(iL?tt4MM}loFyG<0TnLaS+ql-o;Th*C>J6%+ozBI!kdC
z-*mGRKBV1Zh$wE)}NH#X3>sav*DaKC}_eW>Oq=ESRPN
za8XW{Ak=uQV~vlgD&~twQg~e}Ce^sEp;#RE*JlW-1^e1xZ~Vvg&$uQ%SFn2DJhcA*
zcGzc`ppHDcabE9qOu}++R5kYnHiIw?T~#Cvm3U2;3?9m=#wQacorgsQ%7%gCs8k#i
zgQGwfvE5OZNW8mZr$E898AQCWLJVFYZaCILjjSxa#!C^Bqtz0c--??{)nQNfh-
zF6GySd8Fhwpo=HUbocI2++yE;*M;~`gPkPZ)<61i+LVG}*bV65EhKP1L2`^2+$T
zm|-G=*~~CK)6}pIdov$Nt3P@cgx}2DP_NeVMUudd*`GaKHZc
z-Pub=XQLMme0%-YrR3WutWEis#_HHl@biyG(B=7-@M`(`z~!?i&B-4cUnh4xyP~z=
z#?{kRZ8Lgt{ruX@&d;8I?c#}Zw;s7t`{I=+ULM@2@7id;pI&=-?^@){qw9aZex$vg
zua6#o>FAA*hwFudr&o{74PRS*XWN^Rft77fMUFFhW|SFbqRjs={rzy{kCSawIQaPC
Y#qBq4*3RttH2Cw$PR(RKnOtoD1sggu#{d8T
diff --git a/public/images/ui/legacy/text_images/ca/summary/summary_stats_expbar_title_ca.png b/public/images/ui/legacy/text_images/ca/summary/summary_stats_expbar_title_ca.png
index e9dfb10e5d62cbd9e2c24bef5d40012719e0a292..da999975932c33cd88790ae41711c3d9f5d39f5d 100644
GIT binary patch
delta 101
zcmX@WQ$9f=o{5=(f#KbWe@B57XMj(L>;M1%U)+$mu){*}=Oq`QAY)08UoeBivm0qZ
zj*O>^V@O3@GDjh2VnIP+B8Ol>V#bSzgBuntW@OOc!MayliTNy09fPN<
zkdPP{KnO7~FfcG6CKwo4;2*HUA7E$Vbv`T#<nL%kbKr%wNr5e}8^&Qyx7&1b|mzYgsOP=Lr*0i3Y;wr9qSc
zT2W5T3=+y#xKw@K31e6L^5(6khJmYHsd(5+>bx7yZ>0R<#zKp2tS~3gW=^Z81_THq
zE~z?*dT~Yut~TT)u+F!UrVb%;#nonWhw8GosMbZw)k?|MnNc>?s#7v-?3m?~s)>y%
z!c}BgIyMNliRGx{O9O9d&?ZZbvtwWIy4QPUl63#;!0s~gylGU^~
zbRICodpzQ?%m8Z?*rY3@$hzVe)Wz>j4j{FjSNPacOB59@GC8*fVT=>9B|2-ZCmb#D
zOsuAii&Xqj@w5si&Muk+y{BO$xO*k*wD1>airP|01*#AZduHQlsY
zhD|CJV%1JylVCi8dLjth>pwutEyE&Km6)5M&@cg&^pD_xk+w)93gLzk?QoRD9ZfBg
zNa~^|Qg96Ftf30eBh5IIG-kZna5ady6ovsYOoLXcc2%d)h`K?`w(bO&=@r9oQ>TXg
zT9r=h8-lIob)DD;U{BkeGMkq5nqxS+<>&U;ujqko2ORshNoj3je<2OwT+!Zt^W>*3
zZwN9UW>Bj2@p-t&Z;V&HP#yLFp)BuhS7Uimd7zEA!!0uKcM*(e`&|yCzbW?!mWj6P
z)0EG4AX|TECiE-m8D0Cm@V_hFmcoB-zJC`!PATisxWi#cA}tRE<>O>j5cFfMhX1}B
zJrg+0vwT?$QuruFF^_>dg+cp#U=YEPbi-Fpe9zoInf78)-+V2pYQMQqou{`Bg5mv9LJ3m
z7OZ79kFjxbaDe@HhX#^qC|tN5bKI%XWb|>*o}cBo6JNdJ8eOw*LWlSga!CnGO+RF4
zj+>rohR9jRlrQ0m7ns7g*KY*gb4_7Yvwca7O<^_>$gkN;e4a#@*QA{2C|Q-)4N1uXL(N|1RiNlH
z&}Ahf0tJE`%ou$46PUEfEyHDNzAKCEOrc6?2xYm^Xh@BWM52nU7=|GORaRAzA;h>9
zP}CHIc%sK(;n<11ka{HG6Gl`bb!rMs(~b##n9>Gu*GZzL))lgM*5F%cVICo(*}z0Y=wF2h-8wP>T*(p9V_HN4Q$`-tJfZ!HGyQA
zb|1D$`xg#a(~4Mk+Ieu}KI?3`f;C&59CLo|?alOWD?5Yw_m7{y?19r~-p)_F8{K`s
sdv$#4>G>zWK#OYDx#166k1zD~b1&{}|M;ZSS4nd$%q?0kuibt48<8ltA^-pY
diff --git a/public/images/ui/legacy/text_images/en/summary/summary_dexnb_label.png b/public/images/ui/legacy/text_images/en/summary/summary_dexnb_label.png
index eab90a91c7fc8dd67b9cf24a7e8c37dffd62b2d8..bf568c486aac5e8af4e95a237819caf922a9e50c 100644
GIT binary patch
delta 106
zcmX@ZQ!_y#g^8Jgfno2IHdP?S8Q>G*`v3p`A3uI1BqZFKcTEW>$XF8O7tG-B>_!@p
zqv+}47*Y|J?BS7+(82KV3QOCTEi7$53Vtk)R21qDu3}}VtK-z%ysM-MsF=ai)z4*}
HQ$iB}`|BmX
literal 1612
zcmbVMJB-{!7&Zl`a8XbYLOM~690`G7c4q8-oJaA6WA|WJ!N+k~DTkoQcxJq7-P&X9
zx!b)HH0T;iB4`i=Qc4h_KnRI~K!cEIXsJMHsEFtgO&IUP%Hu8xSh79#eEDonv;6e3Wm#u>-Ojdom+Mofmdx||Kk3)z^;p(@qp+;gPnP#1*2kZ$Sk|Lo#r++<
z6TC{fN@`4~P}U|%X3&;({=y_j)PLb!#93o)U%NnH=jFWW@RMMSqdFVAC{FW(+&ol%6w<8f{5)>J+~
z#Pd9a9ppIBAYgHjYBqssac<$EBMY9#nT}NoN{=j5d)l`R(^(2hR=G`!d6`VX(1c}(
z)Nq;76exIw%l7ioG`QeMj$|TJT^KB>u-Q;)RSeY$uB+`k0+`YUK^5azE=f{_DD=j@
zNn>7+W6?$bAd_fY7HTi&a%11nJXdCu(RMDGR(W5k(V|gX3(BC~2G3rOQ=!JibGN}|
zhiU2C4t5$4uR^EZCxq4sbylCp4#ju|4U~wZgL|P4bqFQS9Z)kcf@$_Zu;4UOdBRM(
zal!@?W$C~MRS{`hjZ|&|8+PuLMG(+lT41}Nj@Z2LAZn<@>1+3)z63Orl}GyCyS
z8mISZ_Y793NRL@A*9WFr4;UtN67_=Z-=FxyweBS1cMacvCq6GJAF^~HjYuN9Oa+y4
zGHVFByI0e_+oWd!H+ohsDl@!a=xXz8x{`&5RZyAwZ>vh)qDe)!{^m8FZ|;?>X3KJ)mEYu(?j{l5G`@cn0JBl!CAFHe2?%@Yy)d5qWSlJ(`A
WfBw`TT)k52eQ#s4^TkV7-uoA*z5f^h
diff --git a/public/images/ui/legacy/text_images/en/summary/summary_moves_descriptions_title.png b/public/images/ui/legacy/text_images/en/summary/summary_moves_descriptions_title.png
index 3d2b4d083760bfed09c6fae182fb0a997420f282..e83e8cafbfcc09a3761b9d915f3c0f0205109579 100644
GIT binary patch
delta 219
zcmew>@S1UgL_G^L0|Udt0+u8o#UJ1k;+l|<@Z-mi|Ns9389;JX#u*);6mv@JZ!+jay6~7t*qfntizYh4ms&
z?VDfRyXn1dOPhm}$?=zuzWYlXm|asivL|;gD`Uu@lRJ(3&GNQSdOPmq
zObx5dI(>ife9ZpI@-s@b8fx~JIF2VHs#uB=gC2r7U_u05*FON(mZ}fv*moA
TSG>1@j$-h1^>bP0l+XkK`;cKC
literal 2173
zcmb_eU2NM_6iy{zTeraj0(DeqmisWK;n==$?AU5r+N510HC@*Rtz%o)>+9>pYGS*#
z)1-YWAW=YTi%_5FC=!)GeE?|%A*O-WZ5k6oJOE+{_JV{E51UqvfrJ=;*ZCuZt{W4P
z9A965=bZ2S-QySf`}S^ce55fD2y9RE#0SW|mt31S-$(ujhj;8Jw=H(hK{pWC`k;R`
z1YSAb6bNie8mS>~DA}tZD;I>CrQ%>QXA^WF(Ar+KAsWRVrQ)<@pWfyXGO|9ux!bAeb7NO1ZLC$xn1k_LsXXl-4$+(7CoW(h8dM?58HLRVnJj=~k
zw@_VseUkwa+GMh3V_hz}T+M{*b&V4@Dgjv+?WQJd%no4J$~y>mjT4$JzBjfKb1?KQ
zCuLdLYNGn9l&M&ZdhCE&RYCN0g-
zC+eXQ1_&u$Qo=wHA~!)v!D!Hfw*_lR(JdzjiMvJ)rZH=qX_~49QHfbu%OQpdyHJTm
zGN~j?*Mla)iFlMI9tRCWQxH%EK@}s6$V(Cv!2)Jf4G0WUMJa+rBuY@O%Ev8~_w!nn
z*N8k3gAgKt(IKxhnk0l6Rg@uvHIWN*9H%3WugdRp4ANIHd)InM{r~M@RZ9o)$R&w$
zE8Wov)0I`$X!G6lX#xWcBDg$|lK^O>70%DzXNl%#uyD{HVc`H4J&wrv~cMy-&7*M5(Rv_{_
zHA*ADM6pI!wvBc2d1v`3t1VrZCZo96?GSmZ%iCZ5iX*V
z$^)7j_G1hfHADifM?hEr2=F)ztB%(bFSkNN=Q)#nrTdqe3rg@SMIE*
z{+)w$Plf->l~PfbYm5D7*_Gba%>nQNi5_3N{KQ91FVBDV#ZwDEo?wrz9eKC-
z$?&JAn|FShSepwzw0iZPeGTugXy3Km*!|(Jk3RkTPOd9`YGhj9^k#2!;p)SC=9bZ7
z=LZYZm*2VAGG)x`uXX3=8v~=q#v9JH77ugR?FTM2b*~J4KeI^P*!Jy_)s<@tQy-5#
zcd+GT;IqaPR}N>@`QaI7YV3OC=il0YnwgzFHuv=#=g)SZy)@H&@6xNPmij>)`DW(X
p>G98Z%|>6m_Qz5}Ji5HNb?=qecdQ;dzsvtXNObkZ-|ifF<}ZAE%%=bV
diff --git a/public/images/ui/legacy/text_images/en/summary/summary_moves_effect_title.png b/public/images/ui/legacy/text_images/en/summary/summary_moves_effect_title.png
index 55fb0efd832da789da351d76a5e3c7eea57b224e..55c4b545d9886576fbe0deef5921f1989e23d844 100644
GIT binary patch
delta 170
zcmX@dw~KLtL_HHT0|SG|1HX+xiYLG)#P$FG{|N~R5Hi32$W5R)Q%R6tFatx`TZTbv(57wEbEikz3u(E
z`Y^+zmJL=6f)g00{hZF=;=_Jm&T3vBo_giv^-ZY~&a4uWE`Ob*BpKxA%XdX|T>J~P
Ohr!d;&t;ucLK6TGCqAwK
literal 1870
zcmb_dU5MON6pjyFcU!8UAf+ON)CWZ~H@W$nTsE%jWON72y6z5i`{tdSJ2PP?xoMKw
z$?l?}PgQGC=#vQc$p^*0iLDA+@j(%!4W&Q^Io}QlK&)~@05`WFa3kOm`*mZYt-zL2A<{m+q
zdOm2bW~<(lh_JANeb%CtJd8P75N7A{7?U-ci7na*B1gLL(I=7^_>Q#S@Dwku(QdG?
znb4KZ#U|NYBepNiJtEHLh#LrLhQ&PWMJdW1X=oSmaWM@faR|xQ9BICAD6V=-VvQwK
zG%BV{R2_--Xu{=6Ut((CO)t~
zhSIpZ>knDAkx*#puAvw
zoZ%aSeS+F72{D%>$XN9CakI3?izr9!t1m&Q2S$@I2Ir(hNt0He5&IiYr0jBjOWE
z+nhcnRFMr|*X62ZSh8iPW=l7HwPhQV^)4g*BCnJ6K3|VDLNIJ$8JdPB`<7mnahqV7
z=o+>yXz0}zoUDH;3HZLky<4_Z?6#sH&_a;%RBep+!wNk(9`yooSOW-?qP8806iKDN
zG@cH&iNMV%7?~Y(DK~nZbdO*uYi9$T(D@FJ)(wgYT#I^&*RM}pgSbjyOP4iGC9-Df
zP_FV^$ZcEqb<;4Y-m)jcuO@CFMMIh~DZc+sd>m5J#ZiayLlQ{0F6HK`aN8VQy^sDc
zDWEtRN3{j6EW6>qOHNUq#NiY_nu}rC6Zl8jRcOScl<=PM*4gh)@qW{9xbw~GSMMKv
z`i@V<>DtyeKmK#q?`t!)J;%DA-G1l&ubPj#rw{$|+R^zVhtKc*@LZ#E@RiQr$MauL
zd@*VAxx1)=2^v32@d6!;b-nJ);%G$21~PVuE6
zqP&*N10t)C*NvQq4a|kS2WIJfO{A4EoKLJ(OuoA~#Ijz~@ts*a{Ysg20%zC3s=x
z){i@BVhIzQI27faluBv=ZIa=_g{a;NZWEE#sYm@JW>`7HhP5CG;##nU>+JJ20a$8H
zGmEh)7thN=#L37alhH}Yrs%k`6jE`T#=$~F=*S|Yxj$7CqEbY05=50CsCNrB)uqgr
zO8kKt*SCX4{ODD1TE+=22vC9=kOlx$E3$+X1!>yj5=4^JhMIxxI!phC>PUk~QLllr
zhOu#iZwR&ta)QXiOs?zUDiuS&D)3nnQ7Nbg5ev-NDJ>SJiN^gn!9Jnm<$}Nz=UmrD
zwxT%DQV38Tf`Nfm1z6CrK;FT`G6rlNYEIX_9FT>yu3dYZ*+Z!43bAbf9YY61O6_%-
z0?DA%&}GA+7j49NhiglPD64Oy&MtpnKb|h{evDKhx%uN|Ab>beO&t(kwR!<^K
z(pf7AByBQk3!T^QCKb84jJDl2YLq$Mrv2KmIB=2%j%cyUs&|9t7Pq1v)WpN?W(_ew};tQkuI}6ialhz9Q9e2BDRNlqp#%I{`b;&xmX!^
z`K>cMdY{gT7xrKhbyQ*s-5hR{z{{>6@m@8{0m^Y9z7w0im7ed)G5
NJ~CN;@6hq*{|0a5ki7r^
diff --git a/public/images/ui/legacy/text_images/en/summary/summary_profile_ability.png b/public/images/ui/legacy/text_images/en/summary/summary_profile_ability.png
index 6600db26802ed202ae0536dd122605998b551fdf..a05c22b7d47b527d91b0406ae0ffcbfcb21882aa 100644
GIT binary patch
delta 165
zcmeC?-O4yYqMnJFfq~)E$=0txiYLG)#1%;Y|NlQBApuAxwcg_e@|jA4{DK)6$|j$+
z0P?~-T^vIy;*vMK|NrrS?1}&1|I67VRD8|5yQ@_3b=Y2}P4oA4cYo&YEUw*D&3O3o
zcYB4Ly=-m&-(PdQQN!c#wa~yU;H>@v#sBl0Zg7Xmt&L!1;T9_}WMEjGV45Ff*((gR
Og2B_(&t;ucLK6U0ibQJw
literal 1675
zcmbVNU5MON6iyX&tyH1)rwZydb=&%r|Ln}>vT<2wcHJpvyX=nL?Thx#&CN{MNp4KC
zJF`pM2a6BBs1G8t_*j${>{FFiS@b~!6?_mB3Pn`#MPWtggNjm5<|lMz`lBJ4+$7&Q
z=ljk%_slD^GtcbU{=jxY5O!3i%X4sl9Im0ETjBS$cLNV@w@1^56G6CRS9Wa^*5BSO
z2)CT|tBdKP^DL%3kcrD{Ol}7epao%Uyd4p`#8Omabw9MlUqAX-M80c_3nPx|L?zbn
zr`KXOzcy2)YfIE}#qp=mSQ~?bfTaYrgJziEwk>x2VpwOd6%lnH>5?r@WC78lGmA<*
zW@tn%NK`X*RJ3HRpjx{5B+^x_sHjCnGbB~RY5^M->K!6Pi(L=Tm8W{K;K>#nX&PZg
zX|-B%%aD0oS2WAA6jfJrT>^xZtcEFROJTC7@1e{R8v9Y|^AKeoNsTY3wg^nS5`rjq
z8z#LpLBf!-5ac0}baDk`^b#@SoJ3m1~zs&z}dEZ60`_VUEJz+d@V6w6QCgISc_ksTH56ACYId|%J`L6HY
zKQ?~ur`zsdIKKCbAD?^ZfO+?kmrp&s>)r?1Z^K`$f3tJ=>lcRCKi}Lueb_&_F?H$n
zv74Jym*0GT=ilPSpYN59PF;HI#Ps-T(jqk4Z#9RCr!(kAVrpKnw#v`5z$z6gLBymp731pQ-kzw1n0+
zLW0BC&8j3ye#j+q=M}fZ&yd1T7uyAb)k~mBqTF^+0Yqu|1zQ>)H0G4avWy8v#I%v>
z;~yB5e1V7%2kxK*X@Q-DOISAoBlWg1oLJyaTEzuU`vIS%@$+@Y@7ECDU_*5czS_F?
ca^5}T3!11op!@JzlmGw#07*qoM6N<$g4_0GJpcdz
literal 1965
zcmb_dU1%It6y64mvC@DQv5F7l6!EXKbLY;?PUf;}NOocq?WQ4JO@ayb?#!LtF*`q*
znPhhZBD7*vsI(6SE72AOt5vJg5*3L)DAg)egy@6HYO(s1l2R3vLiNsOcS$w`X@>!W*B
z?75kBu~_S4R(3cX&TK-uYsazS=19D3dlVguB|FO=*2hW6^nHnS>4F3qMU4vyph9T_1A(CMK;Z?E0|EjGB&tmPVyQRZFwvlPZ`~LD
zO0k77^bpUNN~L&7jJtlG7gSZ{0pua%D1r+rPKe8#6RdAo&`6;BmKR#C!$cNw&YcWX
zEM>YR!SE`eA9MI5aGEE;Var=;+omgX$$)G+b
z2(hD+J}t#k#c|6rkRS+z2ugyJbHw08Q|jP4L~L*%Az(vM1Vsc=W4-3;lTlt9>kYbI
zRIq7Cipb?Uu*jJ?DZ#0xkmIm`36!CP6_{wO-|Sm-U*X~v+lh8tR1m1w3TUb-_5HAg
zOw>n3i>cKB!g^HODOQh?N({C>ZLJW2%Tusqwp1Y0=ta`K1Pfdmk((
z*vS)mNb>BJOS!x%{C5sk+(-YH6h1l`muiduXW7;MEjvYN5^GcRXpV->o-7EQ
z*^SyO{n}}@9X$HDUi|SyenOr-x|oA!f3F^VZS9VI+b7$9KKT5eoqTeprK(l$8-8Ns
z?V+1?e)iDetB*hV=ZRAX_CCDrr#YJ)OFme+9n5t~o)V`j6?NImDZGQuSe~S_T
diff --git a/public/images/ui/legacy/text_images/en/summary/summary_profile_passive.png b/public/images/ui/legacy/text_images/en/summary/summary_profile_passive.png
index 66f56ff435e5148fc3c48b19c28e38351538635b..c026e87a2157734bad03093215bae165aa01e8c0 100644
GIT binary patch
delta 182
zcmcc5eT;E}L_G^L0|P^CXF({C;t%i%aRt%|2?_uI|A&wpnLmU9g_ui%{DOh>ud*Ze
zfK;-li(^PdoM}%t=TQTmBRuQ5XXsr>Yhp=lEV@^}to$W=H#2uY$k(@vy}T-&Up(G7
zMd8Yp69&G|H@&TSyCzTW^|IX-TQ+Z)D|}JknP;K)*-f`)vf8;T#SKD#WmYu%Oq!d>
c@c)4Pi6x4b2J3}#fc7zXy85}Sb4q9e087bAdjJ3c
literal 1503
zcmbVMU5MO798a-muX+V(5v?F&`rv6VnI!v>%XqPzo88_*bDnn#JrzG@lgVy+n@pO_
z-tHbEa`dGqQbnpDMNpK!w8aO%K(Q4pKKLM(`k+`UJ{0YX;FB+>PWB^QrCMxYGGD*>
z{lEW5=jZm1Z{NLL5QOpSthK=BhxpjG?NFg0S^h
zzqZ7d>;uT5fs9>RCvrE4I9d><_jM!eEE5Lmq~V9A_}iyvMc})pc(7nAc2puwe|9w{
zi>q@rXLZ>zTyftcV7iMqK|mM=-JlgFsB4OSUc}ewF%&@`!j?^OCKU*l?0Hb4F#!d+
zC^>3g1G*urMa9talR#5c9V$9hbCRMWrHFC{7=9v`7P}r=uqs1Yd}oSH#v%k^r_+%;
zIhn={s2YX=6%A^d#1T@`3mNW8VX|k$V3EX${fPNA1Sun~(>60juIa#pAj)XNWauXD
zFxbTrRAnVKsSk9W3>UTIR$sX5K++-s30cCiYKD!Pl+mO~|AjiczD59dt!-yAuB%HB
zWFit)Ug2g89dccCQtL$oE|7$_V~3PixSD%XYaN31&2|QQ8^>YJQ|LD3!p(K|ge9
zCwXuaoLHCwCS;&?!mF*VH0XrchqL2Fer0
z#SGsN>^jJ!ae%qGet;VUMqxt)nG;cowrI=+bM10{Eo>WA!-V0`Ayvy1xy7>YyC|nR
zxg7Ca$uo#2>BW3qs@IFURKR&daf=?&mBQ$~MV)pU*U@|Tf4+Y#_IX|5)=l%I)s`j%
zn)MSNs@||37Rlk^s^x=z4j}BLxowJ08dTzn!(;!ZJusl{GzlmDk8lrQ3H4Y9$7H6#
zy>)|Rf?Rc&*5JSj_)i1-cbn>U1m{g4U1r^fZ?gVHLf*7u-kr)H9nyJcJ5jY}YExgI
zu8r+D2kyAKxbfEmKa!p9Pwd+xlciG=m)^NB@!rLzbKA4$Ke_UIPm}>PzgRnSPJZF%
z4YI2}w#&NZ-dCOvU;NnbK6Ck=q=GLk((fj|dhO1K|2+2i_fHuHJd(fq
zL#6!9tM`AtsvmII->4j>)2G(oI=|)Q%g2A&|Jla++fVbpK6l-(Q
zUsv>$DwqV6rmCeM+tOa#5mL8gv#Zu~T@9s0ow^x854%+6%cst|^P?d5%6%^hKmp#RqK>g%q?{j3_>c0rf$Rh`xvq1)&x_vwyaAo7UpM%-osz
z&N<(Ae+sGkwpFq1%Qyj@vOcnV*Hb2It`5b#Og?^4kpDuJG7tI$pVY`6010FM)>W6HGy|%MmVS!q=KJg
zRcCS~UE$j=vM
z_?#CoUQZf`$eO~NR#G-3OVP%7MUqWXGDTTMl59(cty+BN5FlDW%l2&k?oKRt$_Ujc
z@@-LUG#bfdcKoz{)+WU-cp{ta_Jl-TQn&
z07z}2(2LQROTFF;5k?bBAfuCzzUZ*H>@#teh2CO7*u)ah+#9R$?OeccT<5I5?-gzFJ<#EI^f+8t8l2S-fi;8ONnk`$Sl446z8(Q$FQ(nFZsug9^
z))iZkFMvYBP#odQf+?}fUQov%*Qw(Q6MeTL@VzA3Ij`mg5E$60Ef$4>J>`ZGb_tux
zX9Q53bR24{hGi02DI?8-3sW=$l`JYFlcrLpDQS{XB3=8uM;7C{cI_$HYZ@R8lOoN;
zB_x3X!n&>@RW&q1Bur%(lQ_Q+|Ng*%xx%%p_5=O@kHW5(EGP=0aA9XU_Tx%tRde>m
zGGm$s)fK}~3@DM7Lb|0jDW#OI@V>-#=*I3a{CDD=l#(iTD-3?x
zqHy(6u52p$*5KcB{eMYmwP(Ax#EaXm_3gaBdbfvRtB9AqJqjLryBu>NO9AX*FFb*6
zg#GWOseHDWT0d3H?mWp4{&@7y~Ztf9pG8!7JA&)*z+_uMZdpKc$JzYte<
zzqPS@`1jM_-{3r1+4(^2u8-5-sGCovZ~D@)M&B7re01XBWTLon__OzaJ@(b9_0;M1%U)+$mu){*}=Oq`QAY)08UoeBivm0qZ
zj*O>^V@O3@GDjh2VnIP+B8Ol>V#bSzgBuntW@OOc!MayliTNy09fPN<
zkdPP{KnO7~FfcG6CKwo4;2*HUA7E$Vbv`T#<nL%kbKr%wNr5e}8^&Qyx7&1b|mzYgsOP=Lr*0i3Y;wr9qSc
zT2W5T3=+y#xKw@K31e6L^5(6khJmYHsd(5+>bx7yZ>0R<#zKp2tS~3gW=^Z81_THq
zE~z?*dT~Yut~TT)u+F!UrVb%;#nonWhw8GosMbZw)k?|MnNc>?s#7v-?3m?~s)>y%
z!c}BgIyMNliRGx{O9O9d&?ZZbvtwWIy4QPUl63#;!0s~gylGU^~
zbRICodpzQ?%m8Z?*rY3@$hzVe)Wz>j4j{FjSNPacOB59@GC8*fVT=>9B|2-ZCmb#D
zOsuAii&Xqj@w5si&Muk+y{BO$xO*k*wD1>airP|01*#AZduHQlsY
zhD|CJV%1JylVCi8dLjth>pwutEyE&Km6)5M&@cg&^pD_xk+w)93gLzk?QoRD9ZfBg
zNa~^|Qg96Ftf30eBh5IIG-kZna5ady6ovsYOoLXcc2%d)h`K?`w(bO&=@r9oQ>TXg
zT9r=h8-lIob)DD;U{BkeGMkq5nqxS+<>&U;ujqko2ORshNoj3je<2OwT+!Zt^W>*3
zZwN9UW>Bj2@p-t&Z;V&HP#yLFp)BuhS7Uimd7zEA!!0uKcM*(e`&|yCzbW?!mWj6P
z)0EG4AX|TECiE-m8D0Cm@V_hFmcoB-zJC`!PATisxWi#cA}tRE<>O>j5cFfMhX1}B
zJrg+0vwT?$QuruFF^_>dg+cp#U=YEPbi-Fpe9zoInf78)q5E3)-o`thv>2-tjsh5
PTEgJz>gTe~DWM4foEpm6y6GGLQxP<5QttZ=Riv{>;JVqWLv^IZ5H7qBr7$Lf;u}h-nDk^acw8N
zNrfut0jeCi1uh72Ktdcql@JI465_y*5G?{E)C&g?0x1Y_K|;WIf2{tbKU7$C{ZjEN1ND779c4zmF!sg5O
z6$(2~`n9EW$$boB78DU-bz1BO5l0salLxyI!Y!H#b=vU5vbgo$2cqDUvUtRBWjCtO
zra!YD)5Z1K8eVTgD~-cNnI34N>R=v^?(HDxM(GA_nZ?f(KZcem?j*n$%wZ7q$tF+>Xb#Uxaj)?
znx<(HPceX9(g{FB_W)8Y9hizvWMr2-OU>6i46kH)9j+&QJ+7;UrV|a6bRC1bt((A7
zEDUT*r<$xO9#)m1|GC)b`wF#h*iN?FvVwp!e!^3=HrNk~^!Q-Z_Jv*zK!mf}E{ixz
zDkb7z+TSJu*QcOww$r5C=vC6)hb7EQJ1C}84IZs)6q9r%>IquCI&nfurloiu(6FZg
z?CA!m*O3RXZP_ZtNH&l$6n;5z6RIYZjbWYscjAMP;wB0klpm6kc;iy8uL`%#!R7nt
z|B@nQCu6_1q)W@L_wRyJlqaz_#gFD}nD+$!k#`ju@+if;XDm;Se8v0CM%9_BS)aYO
z`0Ut+!ssur|NYO+FP+wwcF+m`^Ub&JdGL-qkJe8-+4}wYt)DjjJpa+DGfyS0+h2Y7
zUg_9@v&VNo{>&%Bp>N-QXzWK{mAhL<|JZj5-ldN0hvQE>FBYfY*b?1OjpoODX5YPA
z+4IVG(K~;^Tjn14=H&eOAHIKKZuwVd;%89$%{_bK%ddmYk#k?P&pmq6;da&stJAa2
Jn~xlO?qB$|S|0!a
diff --git a/public/images/ui/legacy/text_images/en/summary/summary_stats_stats_title.png b/public/images/ui/legacy/text_images/en/summary/summary_stats_stats_title.png
index 5531819ef665bd054b5eb15d84500a1a4fe9dff9..f602a43c39d509720f1cfb19ab25d4aeaf676ed3 100644
GIT binary patch
delta 153
zcmX@hw~}#!L_HHT0|P^i@R?2^#S`EY;+l|<@c;jR2zmO6f-g{vTY
z&&$)rF{C0cxugD1y`6wgTe~DWM4f(nL8j
literal 1869
zcmb_dO^Dq@9M3AO>&o^}L8TXCsIZrf^eAYlllKwYy4%)bAjxF%o8SNc
z`+v;5yt=Y9H@j
z!j5jPcw}>(I8-@!K;Az(|j^n7BuIf4v2#{_>86JQr-92Hbv6Li1oCQ3Rb4J|c-K?SrPe&5MxS);F
zahOEF)B%oFSkiJyLm(vuF776sp>ax7)?p!wvQ%JUfsNZd<7u03LtXs7!hi^^>lQY)
z^d$@n6KS^C6EemD*%F<48!=NGEalyVu*IHmb9b&LM%9Gjj3*xFok^lrCoap?s=W7D
z5K-Pw_gw~OHJq`EqHDSZw0WQ#9<-5VB4|IP=}6N?P?yud-?$2Dda#Tvh%Dy{s3;hU
zGkjezCCKMVh=trB#4V=AQA?4FAfhVo@I)9E-WfwLTo=`&G{X^L^;$&{ic3L2k=-o&
zI(9H%^A-Vi)AWI3YYgZ%(V*?y#4*TZeT|cDp4Z8GD%KOKW4&x?z@fwdCNUvs+FBX-
z79%F6Hq>=ES$`x6L|@^~P4nd4mKOwC4pNb-jd4F5WzUR9oj@Me0Kz1%?TSM3q%x|E
zr-LmbaAOKawEZ>{q}NFI2$pg`>*Itiv_!P7Q%vf1uBW(nZO0k4Ox=cMP@Xp&VA-Y#
znoY|B%=Ue1*u-a06yP?F>l%V4GKxC=?~ad|NgGEkCN@b`xp^r!R)t$i`F9`vUsBY3
zXB^d*dUe?i|6R6=iX;xF#MYb-i!(ud6sHP{M3fS7X1sgqj3Le&(MJp3-fzw~>N}+S
zW`2JA;-x+4ZnLwq`yl!B`Ooi~J=FO2*IOTzN`89$yCcoHSARNwV(EY<{qR-quP8XreFp9_!`mm1
Q0QE35hjuH9vadPoEx>Q3p{k&hWt1hH)|2`D>)9Lx@~*wR)aVUG(SGhDfj@0a@J
zl1MwqbQ}j67P2fI5PG&6OES>o?C2zd$1|FSi3~-o<`GFCmZhr!(@_agQiP4OahsrF
zXh0HVmW;flA&}7mmn^5fVRA+h@9~JoG6SqxV3V$pBI}ARLKmMmB!JfXevxBSE>TpZ
z$mGlllre6|rs%A-nsBthGqIdfKC=QekLJ}RxRG)qMcNXgHyPCYgtFRbsK+jZF%$jl
zlb7J!Ba*wCWmpy6sOgs7GEHoo*s6VISlBQ|P+zdHy}B8+0u#13Kw)7Rk>rhFMsZuD
z5rJ~Uh;%qg;*O>kO~eh+6DcGHcI6=p-^a~3lO(3R>A4zITna;mExT4_HY`f5Vz>2x
z*fl)}NLjZ7+qT;dEe8Re_2y20$KP6)SjR%6wm;sjY<@LnhZgg7{->`h|PJG-_+9h#^gGeGR
zF9qdtG8zbabymY~FG0E23GxeO
zU?`h>)&j`O@N{tusfgQh?jR?Ff&lZu1)@usEv;|7-fymZkU{T>rNGU+Kl21!7KHO1
z@(vJf?45kX=OBmUN~41B;&Z+|o598)(7qXt17rY|XWt~;~4bI3L(yRlj5#wFR3RK@1(nX|iNcIHfH
zW^X|uu0AMu0R>Y;O8a1?FX~HMq1a-fQXeD_)+YF3r4RK*qAiHNh-daPwh~(`4$RCs
zv)}o?|NGB>{(U1qvwtZ1Sd?Lyq3m>KmcAG1{o;dz^qJmXc!9olIMatbhS~X0a7UOA
zJ{)0~ftFcV@E39i03wzE4N}BH({d=9VaCRr4n#}XXN$OO+9_`H+y#y`jTASp<)oaG
z#uamV*~N3qGX=D~gmi-&-^Y$M0VS}o580+wwLQ>GaUEVj$H8ZjV>=LkDaB0$g6u*r
z&!&lsSxt!ZNLCd#p$l?c(iL?tt4MM}loFyG<0TnLaS+ql-o;Th*C>J6%+ozBI!kdC
z-*mGRKBV1Zh$wE)}NH#X3>sav*DaKC}_eW>Oq=ESRPN
za8XW{Ak=uQV~vlgD&~twQg~e}Ce^sEp;#RE*JlW-1^e1xZ~Vvg&$uQ%SFn2DJhcA*
zcGzc`ppHDcabE9qOu}++R5kYnHiIw?T~#Cvm3U2;3?9m=#wQacorgsQ%7%gCs8k#i
zgQGwfvE5OZNW8mZr$E898AQCWLJVFYZaCILjjSxa#!C^Bqtz0c--??{)nQNfh-
zF6GySd8Fhwpo=HUbocI2++yE;*M;~`gPkPZ)<61i+LVG}*bV65EhKP1L2`^2+$T
zm|-G=*~~CK)6}pIdov$Nt3P@cgx}2DP_NeVMUudd*`GaKHZc
z-Pub=XQLMme0%-YrR3WutWEis#_HHl@biyG(B=7-@M`(`z~!?i&B-4cUnh4xyP~z=
z#?{kRZ8Lgt{ruX@&d;8I?c#}Zw;s7t`{I=+ULM@2@7id;pI&=-?^@){qw9aZex$vg
zua6#o>FAA*hwFudr&o{74PRS*XWN^Rft77fMUFFhW|SFbqRjs={rzy{kCSawIQaPC
Y#qBq4*3RttH2Cw$PR(RKnOtoD1sggu#{d8T
diff --git a/public/images/ui/legacy/text_images/es-ES/summary/summary_moves_descriptions_title_es-ES.png b/public/images/ui/legacy/text_images/es-ES/summary/summary_moves_descriptions_title_es-ES.png
index ffcae31894d0e1ed67909908144200a707e7b342..3a4e3c7c375b17ab3a0154262c6a3b7e7668f453 100644
GIT binary patch
delta 233
zcmbOz_>*yhL_G^L0|SHAL$?wj#UJ1k;+l|<@Z-mi|Ns9389;JX#u*);6mvkbc|KAbOW`^<;{D!h7%akDAP?T-;
z12Th&UM6Nagb4>F4hte=WF;s_K!M^m1_D9mfy@gb4hSlc)R4kdek_gVXu8@Lf2XwGgh?HbP}o60$7XO%`p!_Yv0`b}(rhp)?zPHMSaa2=Xi^
zX<6B7p?a&7nOKZ@c)&0$t4G;wK0~O}6`HpUb1mJ=BZsu6Y3*+?8vOOByJ%!(;zW*sND7J^
z)KXzT$B;{fXrlE942uv$frL?2_)_BKQ7WyJ09L2&zZ0*d6lai`CUnW?*;|)#b2Iht
z8Z5gi{9jT^O