From 0dbb68c3ba6da59e0dab7bc65094f61ea46c21fd Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sun, 21 Sep 2025 14:13:04 -0400 Subject: [PATCH 1/9] Doc fixes & test changes --- src/dynamic-queue-manager.ts | 21 +++------- src/field/pokemon.ts | 4 +- src/queues/move-phase-priority-queue.ts | 38 +++++++----------- src/queues/pokemon-phase-priority-queue.ts | 14 +------ .../post-summon-phase-priority-queue.ts | 2 +- src/queues/priority-queue.ts | 29 ++++++++------ src/utils/common.ts | 4 +- src/utils/speed-order-generator.ts | 3 +- src/utils/speed-order.ts | 23 ++++++----- test/test-utils/game-manager.ts | 39 +++++++++++++++---- 10 files changed, 91 insertions(+), 86 deletions(-) diff --git a/src/dynamic-queue-manager.ts b/src/dynamic-queue-manager.ts index 7356f67bc1d..599d4f061c4 100644 --- a/src/dynamic-queue-manager.ts +++ b/src/dynamic-queue-manager.ts @@ -7,7 +7,6 @@ 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 @@ -71,8 +70,8 @@ export class DynamicQueueManager { } /** - * Returns the highest-priority (generally by speed) {@linkcode Phase} of the specified type - * @param type - The {@linkcode PhaseString | type} to pop + * Returns the highest-priority (generally by speed) {@linkcode Phase} of the specified type. + * @param type - The {@linkcode PhaseString | type} of phase to access * @returns The popped {@linkcode Phase}, or `undefined` if none of the specified type exist */ public popNextPhase(type: PhaseString): Phase | undefined { @@ -85,7 +84,7 @@ export class DynamicQueueManager { * @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 { + public exists(type: T, condition: PhaseConditionFunc = () => true): boolean { return !!this.dynamicPhaseMap.get(type)?.has(condition); } @@ -136,21 +135,13 @@ export class DynamicQueueManager { } /** - * Finds and cancels a {@linkcode MovePhase} meeting the condition - * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function + * Find and cancel a {@linkcode MovePhase} meeting the condition + * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function to filter phases by */ 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 */ @@ -171,7 +162,7 @@ export class DynamicQueueManager { /** * Internal helper to determine if a phase is dynamic. * @param phase - The {@linkcode Phase} to check - * @returns Whether `phase` is dynamic + * @returns Whether `phase` is dynamic. * @privateRemarks * Currently, this checks that `phase` has a `getPokemon` method * and is not blacklisted in `nonDynamicPokemonPhases`. diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index ea7c74904d8..c40a9445031 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3216,9 +3216,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Check whether the specified Pokémon is an opponent + * Check whether 2 Pokémon oppose one another during battle. * @param target - The {@linkcode Pokemon} to compare against - * @returns `true` if the two pokemon are allies, `false` otherwise + * @returns Whether this Pokemon is an opponent of `target` (one is player and the other enemy). */ public isOpponent(target: Pokemon): boolean { return this.isPlayer() !== target.isPlayer(); diff --git a/src/queues/move-phase-priority-queue.ts b/src/queues/move-phase-priority-queue.ts index 5f0b20c3c2e..4a8304293ad 100644 --- a/src/queues/move-phase-priority-queue.ts +++ b/src/queues/move-phase-priority-queue.ts @@ -3,7 +3,6 @@ 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"; @@ -17,18 +16,18 @@ export class MovePhasePriorityQueue extends PokemonPhasePriorityQueue } public cancelMove(condition: PhaseConditionFunc<"MovePhase">): void { - this.queue.find(p => condition(p))?.cancel(); + this.queue.find(condition)?.cancel(); } public setTimingModifier(condition: PhaseConditionFunc<"MovePhase">, modifier: MovePhaseTimingModifier): void { - const phase = this.queue.find(p => condition(p)); + const phase = this.queue.find(condition); if (phase != null) { phase.timingModifier = modifier; } } public setMoveForPhase(condition: PhaseConditionFunc<"MovePhase">, move: PokemonMove) { - const phase = this.queue.find(p => condition(p)); + const phase = this.queue.find(condition); if (phase != null) { phase.move = move; } @@ -47,7 +46,7 @@ export class MovePhasePriorityQueue extends PokemonPhasePriorityQueue mp => mp.targets.length === 1 && mp.targets[0] === removedPokemon.getBattlerIndex() - && mp.pokemon.isPlayer() !== allyPokemon.isPlayer(), + && mp.pokemon.isOpponent(allyPokemon), ) .forEach(targetingMovePhase => { if (targetingMovePhase && targetingMovePhase.targets[0] !== allyPokemon.getBattlerIndex()) { @@ -57,10 +56,6 @@ export class MovePhasePriorityQueue extends PokemonPhasePriorityQueue } } - public setMoveOrder(order: BattlerIndex[]) { - this.setOrder = order; - } - public override pop(): MovePhase | undefined { this.reorder(); const phase = this.queue.shift(); @@ -79,25 +74,20 @@ export class MovePhasePriorityQueue extends PokemonPhasePriorityQueue } 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]; - }); + this.queue.sort( + (a: MovePhase, b: MovePhase) => + // formatting + b.timingModifier - a.timingModifier || getPriorityForMP(b) - getPriorityForMP(a), + ); } } + +function getPriorityForMP(mp: MovePhase): number { + const move = mp.move.getMove(); + return move.getPriority(mp.pokemon, true); +} diff --git a/src/queues/pokemon-phase-priority-queue.ts b/src/queues/pokemon-phase-priority-queue.ts index 3098c5be435..ab93a04ff21 100644 --- a/src/queues/pokemon-phase-priority-queue.ts +++ b/src/queues/pokemon-phase-priority-queue.ts @@ -1,20 +1,10 @@ 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 */ +/** 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); - } + sortInSpeedOrder(this.queue); } } diff --git a/src/queues/post-summon-phase-priority-queue.ts b/src/queues/post-summon-phase-priority-queue.ts index 37da90a1427..da86ca48643 100644 --- a/src/queues/post-summon-phase-priority-queue.ts +++ b/src/queues/post-summon-phase-priority-queue.ts @@ -11,7 +11,7 @@ import { sortInSpeedOrder } from "#app/utils/speed-order"; */ export class PostSummonPhasePriorityQueue extends PokemonPhasePriorityQueue { protected override reorder(): void { - this.queue = sortInSpeedOrder(this.queue, false); + sortInSpeedOrder(this.queue, false); this.queue.sort((phaseA, phaseB) => phaseB.getPriority() - phaseA.getPriority()); } diff --git a/src/queues/priority-queue.ts b/src/queues/priority-queue.ts index b53cfec3f4d..e1e8f25ae68 100644 --- a/src/queues/priority-queue.ts +++ b/src/queues/priority-queue.ts @@ -1,19 +1,24 @@ /** - * Stores a list of elements. + * Abstract class representing a {@link https://en.wikipedia.org/wiki/Priority_queue#Min-priority_queue | Min-priority queue}. * - * Dynamically updates ordering to always pop the highest "priority", based on implementation of {@linkcode reorder}. + * Dynamically updates ordering to always return the highest "priority" item, + * based on the implementation of {@linkcode reorder}. */ export abstract class PriorityQueue { + /** The items in the queue. */ protected queue: T[] = []; /** - * Sorts the elements in the queue + * Sort the elements in the queue. + * @remarks + * When sorting, earlier elements will be accessed before later ones. */ 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 + * Reorder the queue before removing and returning the highest priority element. + * @returns The front-most element of the queue after sorting, + * or `undefined` if the queue is empty. * @sealed */ public pop(): T | undefined { @@ -34,7 +39,7 @@ export abstract class PriorityQueue { } /** - * Removes all elements from the queue + * Remove all elements from the queue. * @sealed */ public clear(): void { @@ -50,8 +55,8 @@ export abstract class PriorityQueue { } /** - * Removes the first element matching the condition - * @param condition - An optional condition function (defaults to a function that always returns `true`) + * Remove the first element matching the condition + * @param condition - If provided, will restrict the removal to only phases matching the condition * @returns Whether a removal occurred */ public remove(condition: (t: T) => boolean = () => true): boolean { @@ -67,12 +72,12 @@ export abstract class PriorityQueue { } /** @returns An element matching the condition function */ - public find(condition?: (t: T) => boolean): T | undefined { - return this.queue.find(e => !condition || condition(e)); + public find(condition: (t: T) => boolean): T | undefined { + return this.queue.find(condition); } /** @returns Whether an element matching the condition function exists */ - public has(condition?: (t: T) => boolean): boolean { - return this.queue.some(e => !condition || condition(e)); + public has(condition: (t: T) => boolean): boolean { + return this.queue.some(condition); } } diff --git a/src/utils/common.ts b/src/utils/common.ts index f0166b1e74c..b227da948c4 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -135,8 +135,10 @@ export function randSeedItem(items: T[]): T { /** * Shuffle a list using the seeded rng. Utilises the Fisher-Yates algorithm. - * @param items An array of items. + * @param items - The array of items to shuffle. * @returns A new shuffled array of items. + * @remarks + * This does _not_ mutate the array (unlike {@linkcode Array.sort}). */ export function randSeedShuffle(items: T[]): T[] { if (items.length <= 1) { diff --git a/src/utils/speed-order-generator.ts b/src/utils/speed-order-generator.ts index 24f95de665f..44b83b394b6 100644 --- a/src/utils/speed-order-generator.ts +++ b/src/utils/speed-order-generator.ts @@ -5,7 +5,8 @@ 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 + * @param side - The {@linkcode ArenaTagSide | side} of the field to use; + * default `ArenaTagSide.BOTH` * @returns A {@linkcode Generator} of {@linkcode Pokemon} * * @remarks diff --git a/src/utils/speed-order.ts b/src/utils/speed-order.ts index 1d894369bb3..5d29c7dfe89 100644 --- a/src/utils/speed-order.ts +++ b/src/utils/speed-order.ts @@ -10,20 +10,23 @@ interface hasPokemon { } /** - * 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} + * Sort an array of {@linkcode Pokemon} in _ascending_ speed order, taking Trick Room into account. + * @param pokemonList - An array of `Pokemon` or objects containing `Pokemon` to sort; + * will be mutated and sorted in place. + * @param shuffleFirst - Whether to shuffle the list before sorting (to handle speed ties); default `true`. + * If `false`, will sort speed ties in ascending order of `BattlerIndex`es. */ -export function sortInSpeedOrder(pokemonList: T[], shuffleFirst = true): T[] { - pokemonList = shuffleFirst ? shufflePokemonList(pokemonList) : pokemonList; +export function sortInSpeedOrder(pokemonList: T[], shuffleFirst = true): void { + if (shuffleFirst) { + pokemonList = shufflePokemonList(pokemonList); + } sortBySpeed(pokemonList); - return pokemonList; } /** - * @param pokemonList - The array of Pokemon or objects containing Pokemon - * @returns The shuffled array + * Helper function to randomly shuffle an array of Pokemon. + * @param pokemonList - The array of Pokemon or objects containing Pokemon to shuffle + * @returns The shuffled array. */ function shufflePokemonList(pokemonList: T[]): T[] { // This is seeded with the current turn to prevent an inconsistency where it @@ -38,7 +41,7 @@ function shufflePokemonList(pokemonList: T[]): T return pokemonList; } -/** Sorts an array of {@linkcode Pokemon} by speed (without shuffling) */ +/** Sort an array of {@linkcode Pokemon} in descending speed order (without shuffling) */ function sortBySpeed(pokemonList: T[]): void { pokemonList.sort((a, b) => { const aSpeed = (a instanceof Pokemon ? a : a.getPokemon()).getEffectiveStat(Stat.SPD); diff --git a/test/test-utils/game-manager.ts b/test/test-utils/game-manager.ts index f681846d935..ad2b1961073 100644 --- a/test/test-utils/game-manager.ts +++ b/test/test-utils/game-manager.ts @@ -14,7 +14,7 @@ import { PlayerGender } from "#enums/player-gender"; import type { PokeballType } from "#enums/pokeball"; import type { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; -import type { EnemyPokemon, PlayerPokemon } from "#field/pokemon"; +import { type EnemyPokemon, type PlayerPokemon, Pokemon } from "#field/pokemon"; import { Trainer } from "#field/trainer"; import { ModifierTypeOption } from "#modifiers/modifier-type"; import { CheckSwitchPhase } from "#phases/check-switch-phase"; @@ -52,10 +52,13 @@ import type { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import type { PartyUiHandler } from "#ui/party-ui-handler"; import type { StarterSelectUiHandler } from "#ui/starter-select-ui-handler"; import type { TargetSelectUiHandler } from "#ui/target-select-ui-handler"; +import { sortInSpeedOrder } from "#utils/speed-order"; import fs from "node:fs"; import { AES, enc } from "crypto-js"; import { expect, vi } from "vitest"; +vi.mock(import("#utils/speed-order"), { spy: true }); + /** * Class to manage the game state and transitions between phases. */ @@ -536,19 +539,39 @@ export class GameManager { } /** - * Modifies the queue manager to return move phases in a particular order + * Override the speed order of the battle's current combatants. * 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. + * @param order - The turn order to set as an array of {@linkcode BattlerIndex}es * @example * ```ts - * await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2]); + * game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2]); * ``` + * @throws Fails test immediately if `order` does not contain all non-fainted combatants' `BattlerIndex`es. + * @remarks + * This does not account for priority, the battlers' relative speed stats. + * @todo What should happen if the number of active battlers changes mid-test? */ - async setTurnOrder(order: BattlerIndex[]): Promise { - await this.phaseInterceptor.to("TurnStartPhase", false); + public setTurnOrder(order: Exclude[]): void { + // TODO: Remove type assertions once `BattlerIndex.ATTACKER` ceases to exist + expect(order).toEqualUnsorted( + this.scene.getField(true).map(p => p.getBattlerIndex() as Exclude), + ); - this.scene.phaseManager.dynamicQueueManager.setMoveOrder(order); + expect(vi.isMockFunction(sortInSpeedOrder)).toBe(true); + vi.mocked(sortInSpeedOrder).mockImplementation(list => { + list.sort((a, b) => { + const aBattlerIndex = (a instanceof Pokemon ? a : a.getPokemon()).getBattlerIndex() as Exclude< + BattlerIndex, + BattlerIndex.ATTACKER + >; + const bBattlerIndex = (b instanceof Pokemon ? b : b.getPokemon()).getBattlerIndex() as Exclude< + BattlerIndex, + BattlerIndex.ATTACKER + >; + + return order.indexOf(bBattlerIndex) - order.indexOf(aBattlerIndex); + }); + }); } /** From c4e2022e18b6f38a7cea7f0c5359d3cc92ac1415 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sun, 21 Sep 2025 15:21:50 -0400 Subject: [PATCH 2/9] Changed `setTurnOrder` to forbid `BattlerIndex.ATTACKER` --- src/utils/speed-order.ts | 4 ++-- test/moves/delayed-attack.test.ts | 11 +++++++---- test/moves/destiny-bond.test.ts | 4 ++-- test/moves/encore.test.ts | 10 ++++------ test/moves/wish.test.ts | 8 +++++--- test/test-utils/helpers/field-helper.ts | 2 +- 6 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/utils/speed-order.ts b/src/utils/speed-order.ts index 5d29c7dfe89..4ed472cdea5 100644 --- a/src/utils/speed-order.ts +++ b/src/utils/speed-order.ts @@ -10,7 +10,7 @@ interface hasPokemon { } /** - * Sort an array of {@linkcode Pokemon} in _ascending_ speed order, taking Trick Room into account. + * Sort an array of {@linkcode Pokemon} in speed order, taking Trick Room into account. * @param pokemonList - An array of `Pokemon` or objects containing `Pokemon` to sort; * will be mutated and sorted in place. * @param shuffleFirst - Whether to shuffle the list before sorting (to handle speed ties); default `true`. @@ -41,7 +41,7 @@ function shufflePokemonList(pokemonList: T[]): T return pokemonList; } -/** Sort an array of {@linkcode Pokemon} in descending speed order (without shuffling) */ +/** Sort an array of {@linkcode Pokemon} in speed order (without shuffling) */ function sortBySpeed(pokemonList: T[]): void { pokemonList.sort((a, b) => { const aSpeed = (a instanceof Pokemon ? a : a.getPokemon()).getEffectiveStat(Stat.SPD); diff --git a/test/moves/delayed-attack.test.ts b/test/moves/delayed-attack.test.ts index e31c7f28e48..e9192b4c206 100644 --- a/test/moves/delayed-attack.test.ts +++ b/test/moves/delayed-attack.test.ts @@ -167,16 +167,16 @@ describe("Moves - Delayed Attacks", () => { game.override.battleStyle("double"); await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS]); - const [alomomola, blissey] = game.scene.getField(); + const [alomomola, blissey] = game.scene.getPlayerField(); - const oldOrder = game.field.getSpeedOrder(); + const oldOrder = game.field.getSpeedOrder(true); game.move.use(MoveId.FUTURE_SIGHT, BattlerIndex.PLAYER, BattlerIndex.ENEMY); game.move.use(MoveId.FUTURE_SIGHT, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2); await game.move.forceEnemyMove(MoveId.FUTURE_SIGHT, BattlerIndex.PLAYER); await game.move.forceEnemyMove(MoveId.FUTURE_SIGHT, BattlerIndex.PLAYER_2); // Ensure that the moves are used deterministically in speed order (for speed ties) - await game.setTurnOrder(oldOrder.map(p => p.getBattlerIndex())); + await game.setTurnOrder(oldOrder); await game.toNextTurn(); expectFutureSightActive(4); @@ -195,7 +195,10 @@ describe("Moves - Delayed Attacks", () => { const MEPs = game.scene.phaseManager["phaseQueue"].findAll("MoveEffectPhase"); expect(MEPs).toHaveLength(4); - expect(MEPs.map(mep => mep.getPokemon())).toEqual(oldOrder); + expect( + MEPs.map(mep => mep.getPokemon().getBattlerIndex()), + "Delayed Attacks were not queued in correct order!", + ).toEqual(oldOrder); }); it("should vanish silently if it would otherwise hit the user", async () => { diff --git a/test/moves/destiny-bond.test.ts b/test/moves/destiny-bond.test.ts index a5020b83944..c96c4e18f36 100644 --- a/test/moves/destiny-bond.test.ts +++ b/test/moves/destiny-bond.test.ts @@ -17,8 +17,8 @@ describe("Moves - Destiny Bond", () => { let game: GameManager; const defaultParty = [SpeciesId.BULBASAUR, SpeciesId.SQUIRTLE]; - const enemyFirst = [BattlerIndex.ENEMY, BattlerIndex.PLAYER]; - const playerFirst = [BattlerIndex.PLAYER, BattlerIndex.ENEMY]; + const enemyFirst = [BattlerIndex.ENEMY, BattlerIndex.PLAYER] as const; + const playerFirst = [BattlerIndex.PLAYER, BattlerIndex.ENEMY] as const; beforeAll(() => { phaserGame = new Phaser.Game({ diff --git a/test/moves/encore.test.ts b/test/moves/encore.test.ts index 0840346c3b1..8e8853abf4c 100644 --- a/test/moves/encore.test.ts +++ b/test/moves/encore.test.ts @@ -78,8 +78,9 @@ describe("Moves - Encore", () => { game.move.select(MoveId.ENCORE); - const turnOrder = delay ? [BattlerIndex.PLAYER, BattlerIndex.ENEMY] : [BattlerIndex.ENEMY, BattlerIndex.PLAYER]; - await game.setTurnOrder(turnOrder); + await game.setTurnOrder( + delay ? [BattlerIndex.PLAYER, BattlerIndex.ENEMY] : [BattlerIndex.ENEMY, BattlerIndex.PLAYER], + ); await game.phaseInterceptor.to("BerryPhase", false); expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL); @@ -88,25 +89,22 @@ describe("Moves - Encore", () => { }); it("Pokemon under both Encore and Torment should alternate between Struggle and restricted move", async () => { - const turnOrder = [BattlerIndex.ENEMY, BattlerIndex.PLAYER]; game.override.moveset([MoveId.ENCORE, MoveId.TORMENT, MoveId.SPLASH]); await game.classicMode.startBattle([SpeciesId.FEEBAS]); const enemyPokemon = game.field.getEnemyPokemon(); game.move.select(MoveId.ENCORE); - await game.setTurnOrder(turnOrder); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.phaseInterceptor.to("BerryPhase"); expect(enemyPokemon.getTag(BattlerTagType.ENCORE)).toBeDefined(); await game.toNextTurn(); game.move.select(MoveId.TORMENT); - await game.setTurnOrder(turnOrder); await game.phaseInterceptor.to("BerryPhase"); expect(enemyPokemon.getTag(BattlerTagType.TORMENT)).toBeDefined(); await game.toNextTurn(); game.move.select(MoveId.SPLASH); - await game.setTurnOrder(turnOrder); await game.phaseInterceptor.to("BerryPhase"); const lastMove = enemyPokemon.getLastXMoves()[0]; expect(lastMove?.move).toBe(MoveId.STRUGGLE); diff --git a/test/moves/wish.test.ts b/test/moves/wish.test.ts index b64a15ac654..ef46c85eb73 100644 --- a/test/moves/wish.test.ts +++ b/test/moves/wish.test.ts @@ -109,14 +109,14 @@ describe("Move - Wish", () => { vi.spyOn(karp1, "getNameToRender").mockReturnValue("Karp 1"); vi.spyOn(karp2, "getNameToRender").mockReturnValue("Karp 2"); - const oldOrder = game.field.getSpeedOrder(); + const oldOrder = game.field.getSpeedOrder(true); game.move.use(MoveId.WISH, BattlerIndex.PLAYER); game.move.use(MoveId.WISH, BattlerIndex.PLAYER_2); await game.move.forceEnemyMove(MoveId.WISH); await game.move.forceEnemyMove(MoveId.WISH); // Ensure that the wishes are used deterministically in speed order (for speed ties) - await game.setTurnOrder(oldOrder.map(p => p.getBattlerIndex())); + await game.setTurnOrder(oldOrder); await game.toNextTurn(); expect(game).toHavePositionalTag(PositionalTagType.WISH, 4); @@ -137,7 +137,9 @@ describe("Move - Wish", () => { const healPhases = game.scene.phaseManager["phaseQueue"].findAll("PokemonHealPhase"); expect(healPhases).toHaveLength(4); - expect.soft(healPhases.map(php => php.getPokemon())).toEqual(oldOrder); + expect + .soft(healPhases.map(php => php.getPokemon().getBattlerIndex(), "Wishes were not queued in correct order!")) + .toEqual(oldOrder); await game.toEndOfTurn(); diff --git a/test/test-utils/helpers/field-helper.ts b/test/test-utils/helpers/field-helper.ts index 29eb70ae20c..db48e411d5e 100644 --- a/test/test-utils/helpers/field-helper.ts +++ b/test/test-utils/helpers/field-helper.ts @@ -68,7 +68,7 @@ export class FieldHelper extends GameManagerHelper { * This does not account for Trick Room as it does not modify the _speed_ of Pokemon on the field, * only their turn order. */ - public getSpeedOrder(indices: true): BattlerIndex[]; + public getSpeedOrder(indices: true): Exclude[]; public getSpeedOrder(indices = false): BattlerIndex[] | Pokemon[] { const ret = this.game.scene .getField(true) From 7da255fe57614a29c1840e975fbb2201ec9c8b87 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sun, 21 Sep 2025 15:22:12 -0400 Subject: [PATCH 3/9] Fixed type error oopsie --- biome.jsonc | 12 ++++++++++-- src/queues/pokemon-priority-queue.ts | 2 +- test/test-utils/game-manager.ts | 8 ++++---- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/biome.jsonc b/biome.jsonc index 2433ba52010..186cd9f3a74 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -236,7 +236,7 @@ }, "overrides": [ { - "includes": ["**/test/**/*.test.ts"], + "includes": ["**/test/**/*.ts"], "linter": { "rules": { "performance": { @@ -245,8 +245,16 @@ }, "style": { "noNonNullAssertion": "off" // tedious in some tests - }, + } + } + } + }, + { + "includes": ["**/test/**/*.test.ts"], + "linter": { + "rules": { "nursery": { + // TODO: Enable for normal test folder files as well "noFloatingPromises": "error" } } diff --git a/src/queues/pokemon-priority-queue.ts b/src/queues/pokemon-priority-queue.ts index 597bfb32c0d..b4f1c852555 100644 --- a/src/queues/pokemon-priority-queue.ts +++ b/src/queues/pokemon-priority-queue.ts @@ -5,6 +5,6 @@ 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); + sortInSpeedOrder(this.queue); } } diff --git a/test/test-utils/game-manager.ts b/test/test-utils/game-manager.ts index ad2b1961073..55cb74e461c 100644 --- a/test/test-utils/game-manager.ts +++ b/test/test-utils/game-manager.ts @@ -539,16 +539,16 @@ export class GameManager { } /** - * Override the speed order of the battle's current combatants. - * Used to manually modify Pokemon turn order. - * @param order - The turn order to set as an array of {@linkcode BattlerIndex}es + * Override the turn order of the battle's current combatants. + * @param order - The turn order to set, as an array of {@linkcode BattlerIndex}es * @example * ```ts * game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2]); * ``` * @throws Fails test immediately if `order` does not contain all non-fainted combatants' `BattlerIndex`es. * @remarks - * This does not account for priority, the battlers' relative speed stats. + * This does not account for priority, nor does it change the battlers' speed stats + * (for the purposes of Electro Ball, etc). * @todo What should happen if the number of active battlers changes mid-test? */ public setTurnOrder(order: Exclude[]): void { From 76beb6c4a1e6ce8bc041737bc4653fbb98d97808 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sun, 21 Sep 2025 16:19:53 -0400 Subject: [PATCH 4/9] Fixed test stuff --- test/moves/delayed-attack.test.ts | 7 +++---- test/moves/destiny-bond.test.ts | 19 +++++++------------ 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/test/moves/delayed-attack.test.ts b/test/moves/delayed-attack.test.ts index e9192b4c206..db3c8bb17a3 100644 --- a/test/moves/delayed-attack.test.ts +++ b/test/moves/delayed-attack.test.ts @@ -195,10 +195,9 @@ describe("Moves - Delayed Attacks", () => { const MEPs = game.scene.phaseManager["phaseQueue"].findAll("MoveEffectPhase"); expect(MEPs).toHaveLength(4); - expect( - MEPs.map(mep => mep.getPokemon().getBattlerIndex()), - "Delayed Attacks were not queued in correct order!", - ).toEqual(oldOrder); + console.log(`Expected: ${oldOrder.map(o => game.scene.getField()[o].getNameToRender())} +Actual: ${MEPs.map(mep => mep.getPokemon().getNameToRender())}`); + expect(MEPs.map(mep => mep.getPokemon().getBattlerIndex())).toEqual(oldOrder); }); it("should vanish silently if it would otherwise hit the user", async () => { diff --git a/test/moves/destiny-bond.test.ts b/test/moves/destiny-bond.test.ts index c96c4e18f36..bbe0ae2bc37 100644 --- a/test/moves/destiny-bond.test.ts +++ b/test/moves/destiny-bond.test.ts @@ -17,8 +17,6 @@ describe("Moves - Destiny Bond", () => { let game: GameManager; const defaultParty = [SpeciesId.BULBASAUR, SpeciesId.SQUIRTLE]; - const enemyFirst = [BattlerIndex.ENEMY, BattlerIndex.PLAYER] as const; - const playerFirst = [BattlerIndex.PLAYER, BattlerIndex.ENEMY] as const; beforeAll(() => { phaserGame = new Phaser.Game({ @@ -52,7 +50,7 @@ describe("Moves - Destiny Bond", () => { const playerPokemon = game.field.getPlayerPokemon(); game.move.select(moveToUse); - await game.setTurnOrder(enemyFirst); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.phaseInterceptor.to("BerryPhase"); expect(enemyPokemon.isFainted()).toBe(true); @@ -70,7 +68,7 @@ describe("Moves - Destiny Bond", () => { // Turn 1: Enemy uses Destiny Bond and doesn't faint game.move.select(MoveId.SPLASH); - await game.setTurnOrder(playerFirst); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.toNextTurn(); expect(enemyPokemon.isFainted()).toBe(false); @@ -78,7 +76,6 @@ describe("Moves - Destiny Bond", () => { // Turn 2: Player KO's the enemy before the enemy's turn game.move.select(moveToUse); - await game.setTurnOrder(playerFirst); await game.phaseInterceptor.to("BerryPhase"); expect(enemyPokemon.isFainted()).toBe(true); @@ -96,7 +93,7 @@ describe("Moves - Destiny Bond", () => { // Turn 1: Enemy uses Destiny Bond and doesn't faint game.move.select(MoveId.SPLASH); - await game.setTurnOrder(enemyFirst); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.toNextTurn(); expect(enemyPokemon.isFainted()).toBe(false); @@ -104,7 +101,6 @@ describe("Moves - Destiny Bond", () => { // Turn 2: Enemy should fail Destiny Bond then get KO'd game.move.select(moveToUse); - await game.setTurnOrder(enemyFirst); await game.phaseInterceptor.to("BerryPhase"); expect(enemyPokemon.isFainted()).toBe(true); @@ -122,7 +118,7 @@ describe("Moves - Destiny Bond", () => { const playerPokemon = game.field.getPlayerPokemon(); game.move.select(moveToUse); - await game.setTurnOrder(enemyFirst); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.phaseInterceptor.to("BerryPhase"); expect(enemyPokemon.isFainted()).toBe(true); @@ -140,7 +136,7 @@ describe("Moves - Destiny Bond", () => { // Turn 1: Enemy uses Destiny Bond and doesn't faint game.move.select(MoveId.SPORE); - await game.setTurnOrder(enemyFirst); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.toNextTurn(); expect(enemyPokemon.isFainted()).toBe(false); @@ -149,7 +145,6 @@ describe("Moves - Destiny Bond", () => { // Turn 2: Enemy should skip a turn due to sleep, then get KO'd game.move.select(moveToUse); - await game.setTurnOrder(enemyFirst); await game.phaseInterceptor.to("BerryPhase"); expect(enemyPokemon.isFainted()).toBe(true); @@ -184,7 +179,7 @@ describe("Moves - Destiny Bond", () => { const playerPokemon = game.field.getPlayerPokemon(); game.move.select(moveToUse); - await game.setTurnOrder(enemyFirst); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.phaseInterceptor.to("BerryPhase"); expect(enemyPokemon.isFainted()).toBe(true); @@ -238,7 +233,7 @@ describe("Moves - Destiny Bond", () => { const playerPokemon = game.field.getPlayerPokemon(); game.move.select(moveToUse); - await game.setTurnOrder(enemyFirst); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.phaseInterceptor.to("BerryPhase"); expect(enemyPokemon.isFainted()).toBe(true); From bf37a8ca51e735e63960fd688c86a0e76c62446e Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sun, 21 Sep 2025 16:21:45 -0400 Subject: [PATCH 5/9] Fixed duplicate calls to `game.field.setTurnOrder` --- test/abilities/neutralizing-gas.test.ts | 1 - test/abilities/protosynthesis.test.ts | 1 - test/abilities/supreme-overlord.test.ts | 8 -------- test/items/multi-lens.test.ts | 1 - test/moves/baton-pass.test.ts | 1 - test/moves/grudge.test.ts | 1 - test/moves/instruct.test.ts | 1 - test/moves/last-respects.test.ts | 7 ------- test/moves/metronome.test.ts | 1 - test/moves/pledge-moves.test.ts | 1 - test/moves/rage-fist.test.ts | 3 --- test/moves/roost.test.ts | 2 -- test/moves/sketch.test.ts | 1 - test/moves/spite.test.ts | 1 - test/phases/frenzy-move-reset.test.ts | 1 - 15 files changed, 31 deletions(-) diff --git a/test/abilities/neutralizing-gas.test.ts b/test/abilities/neutralizing-gas.test.ts index fd9138e4174..140b7eae75b 100644 --- a/test/abilities/neutralizing-gas.test.ts +++ b/test/abilities/neutralizing-gas.test.ts @@ -120,7 +120,6 @@ describe("Abilities - Neutralizing Gas", () => { game.move.select(MoveId.SPLASH, 1); await game.move.selectEnemyMove(MoveId.ENTRAINMENT, BattlerIndex.PLAYER_2); await game.move.selectEnemyMove(MoveId.SPLASH); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]); await game.phaseInterceptor.to("BerryPhase"); expect(game.scene.arena.getTag(ArenaTagType.NEUTRALIZING_GAS)).toBeUndefined(); // No neut gas users are left }); diff --git a/test/abilities/protosynthesis.test.ts b/test/abilities/protosynthesis.test.ts index ea2e1e20c17..1d4a9845f8f 100644 --- a/test/abilities/protosynthesis.test.ts +++ b/test/abilities/protosynthesis.test.ts @@ -97,7 +97,6 @@ describe("Abilities - Protosynthesis", () => { true, ); game.move.select(MoveId.TACKLE); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.toNextTurn(); const boosted_dmg = initialHp - enemy.hp; expect(boosted_dmg).toBeGreaterThan(unboosted_dmg); diff --git a/test/abilities/supreme-overlord.test.ts b/test/abilities/supreme-overlord.test.ts index d5470b70476..5ce04f0d0f4 100644 --- a/test/abilities/supreme-overlord.test.ts +++ b/test/abilities/supreme-overlord.test.ts @@ -53,12 +53,10 @@ describe("Abilities - Supreme Overlord", () => { await game.toNextTurn(); game.move.select(MoveId.EXPLOSION); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); game.doSelectPartyPokemon(2); await game.toNextTurn(); game.move.select(MoveId.TACKLE); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.phaseInterceptor.to(MoveEffectPhase); expect(move.calculateBattlePower).toHaveReturnedWith(basePower * 1.2); @@ -80,7 +78,6 @@ describe("Abilities - Supreme Overlord", () => { */ game.doRevivePokemon(1); game.move.select(MoveId.EXPLOSION); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); game.doSelectPartyPokemon(1); await game.toNextTurn(); @@ -88,12 +85,10 @@ describe("Abilities - Supreme Overlord", () => { * Bulbasur faints twice */ game.move.select(MoveId.EXPLOSION); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); game.doSelectPartyPokemon(2); await game.toNextTurn(); game.move.select(MoveId.TACKLE); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.phaseInterceptor.to(MoveEffectPhase); expect(move.calculateBattlePower).toHaveReturnedWith(basePower * 1.3); @@ -116,7 +111,6 @@ describe("Abilities - Supreme Overlord", () => { * Enemy Pokemon faints and new wave is entered. */ game.move.select(MoveId.TACKLE); - await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.toNextWave(); game.move.select(MoveId.TACKLE); @@ -137,7 +131,6 @@ describe("Abilities - Supreme Overlord", () => { await game.toNextTurn(); game.move.select(MoveId.TACKLE); - await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.toNextWave(); game.move.select(MoveId.TACKLE); @@ -158,7 +151,6 @@ describe("Abilities - Supreme Overlord", () => { await game.toNextTurn(); game.move.select(MoveId.TACKLE); - await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.toNextWave(); game.move.select(MoveId.TACKLE); diff --git a/test/items/multi-lens.test.ts b/test/items/multi-lens.test.ts index 3686aff0fcf..ac04bcb3b3d 100644 --- a/test/items/multi-lens.test.ts +++ b/test/items/multi-lens.test.ts @@ -70,7 +70,6 @@ describe("Items - Multi Lens", () => { const playerPokemon = game.field.getPlayerPokemon(); game.move.select(MoveId.TACKLE); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.phaseInterceptor.to("MoveEndPhase"); expect(playerPokemon.turnData.hitCount).toBe(3); diff --git a/test/moves/baton-pass.test.ts b/test/moves/baton-pass.test.ts index caabcfa7158..11133a42a95 100644 --- a/test/moves/baton-pass.test.ts +++ b/test/moves/baton-pass.test.ts @@ -111,7 +111,6 @@ describe("Moves - Baton Pass", () => { expect(enemy.getTag(BattlerTagType.FIRE_SPIN)).toBeDefined(); game.move.select(MoveId.BATON_PASS); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); game.doSelectPartyPokemon(1); await game.toNextTurn(); diff --git a/test/moves/grudge.test.ts b/test/moves/grudge.test.ts index cc75024bfbf..4fd66066a22 100644 --- a/test/moves/grudge.test.ts +++ b/test/moves/grudge.test.ts @@ -64,7 +64,6 @@ describe("Moves - Grudge", () => { game.move.use(MoveId.GUILLOTINE); await game.move.forceEnemyMove(MoveId.SPLASH); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.toEndOfTurn(); expect(ratatta).toHaveFainted(); diff --git a/test/moves/instruct.test.ts b/test/moves/instruct.test.ts index eb3eccff400..d0053acc447 100644 --- a/test/moves/instruct.test.ts +++ b/test/moves/instruct.test.ts @@ -343,7 +343,6 @@ describe("Moves - Instruct", () => { expect(player.getLastXMoves()[0].result).toBe(MoveResult.FAIL); game.move.select(MoveId.INSTRUCT); - await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.phaseInterceptor.to("TurnEndPhase", false); expect(player.getLastXMoves()[0].result).toBe(MoveResult.FAIL); diff --git a/test/moves/last-respects.test.ts b/test/moves/last-respects.test.ts index 9dadb316144..ea6f3f2f854 100644 --- a/test/moves/last-respects.test.ts +++ b/test/moves/last-respects.test.ts @@ -59,7 +59,6 @@ describe("Moves - Last Respects", () => { * Charmander faints once */ game.move.select(MoveId.EXPLOSION); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); game.doSelectPartyPokemon(2); await game.toNextTurn(); @@ -86,7 +85,6 @@ describe("Moves - Last Respects", () => { */ game.doRevivePokemon(1); game.move.select(MoveId.EXPLOSION); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); game.doSelectPartyPokemon(1); await game.toNextTurn(); @@ -99,7 +97,6 @@ describe("Moves - Last Respects", () => { await game.toNextTurn(); game.move.select(MoveId.LAST_RESPECTS); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.phaseInterceptor.to(MoveEffectPhase); expect(move.calculateBattlePower).toHaveReturnedWith(basePower + 3 * 50); @@ -127,7 +124,6 @@ describe("Moves - Last Respects", () => { * Enemy Pokemon faints and new wave is entered. */ game.move.select(MoveId.LAST_RESPECTS); - await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.toNextWave(); expect(game.scene.arena.playerFaints).toBe(1); @@ -160,7 +156,6 @@ describe("Moves - Last Respects", () => { * Enemy Pokemon faints and new wave is entered. */ game.move.select(MoveId.LAST_RESPECTS); - await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.toNextWave(); expect(game.scene.currentBattle.enemyFaints).toBe(0); @@ -184,7 +179,6 @@ describe("Moves - Last Respects", () => { await game.toNextTurn(); game.move.select(MoveId.LAST_RESPECTS); - await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.toNextWave(); game.move.select(MoveId.LAST_RESPECTS); @@ -205,7 +199,6 @@ describe("Moves - Last Respects", () => { await game.toNextTurn(); game.move.select(MoveId.LAST_RESPECTS); - await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.toNextWave(); game.move.select(MoveId.LAST_RESPECTS); diff --git a/test/moves/metronome.test.ts b/test/moves/metronome.test.ts index 2215c18f451..63400ad8303 100644 --- a/test/moves/metronome.test.ts +++ b/test/moves/metronome.test.ts @@ -102,7 +102,6 @@ describe("Moves - Metronome", () => { expect.soft(turn1PpUsed).toBeGreaterThan(1); expect(solarBeamMove.ppUsed).toBe(0); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.toNextTurn(); expect(player.getTag(BattlerTagType.CHARGING)).toBeFalsy(); diff --git a/test/moves/pledge-moves.test.ts b/test/moves/pledge-moves.test.ts index 34058829d07..3cba2cf6e18 100644 --- a/test/moves/pledge-moves.test.ts +++ b/test/moves/pledge-moves.test.ts @@ -166,7 +166,6 @@ describe("Moves - Pledge Moves", () => { game.move.select(MoveId.FIERY_DANCE, 0, BattlerIndex.ENEMY_2); game.move.select(MoveId.SPLASH, 1); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]); await game.phaseInterceptor.to("MoveEndPhase"); // Rainbow effect should increase Fiery Dance's chance of raising Sp. Atk to 100% diff --git a/test/moves/rage-fist.test.ts b/test/moves/rage-fist.test.ts index c58d1296ac5..f883bef1242 100644 --- a/test/moves/rage-fist.test.ts +++ b/test/moves/rage-fist.test.ts @@ -85,7 +85,6 @@ describe("Moves - Rage Fist", () => { // remove substitute and get confused game.move.select(MoveId.TIDY_UP); await game.move.selectEnemyMove(MoveId.CONFUSE_RAY); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.toNextTurn(); game.move.select(MoveId.RAGE_FIST); @@ -108,7 +107,6 @@ describe("Moves - Rage Fist", () => { expect(game.field.getPlayerPokemon().battleData.hitCount).toBe(2); game.move.select(MoveId.RAGE_FIST); - await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.phaseInterceptor.to("TurnEndPhase"); expect(game.field.getPlayerPokemon().battleData.hitCount).toBe(4); @@ -147,7 +145,6 @@ describe("Moves - Rage Fist", () => { await game.toNextTurn(); game.move.select(MoveId.RAGE_FIST); - await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.phaseInterceptor.to("BerryPhase", false); expect(move.calculateBattlePower).toHaveLastReturnedWith(150); diff --git a/test/moves/roost.test.ts b/test/moves/roost.test.ts index bb567a41cd0..c47b3e4b72f 100644 --- a/test/moves/roost.test.ts +++ b/test/moves/roost.test.ts @@ -159,7 +159,6 @@ describe("Moves - Roost", () => { await game.phaseInterceptor.to(TurnEndPhase); game.move.select(MoveId.ROOST); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.phaseInterceptor.to(MoveEffectPhase); // Should only be typeless type after roost and is grounded @@ -195,7 +194,6 @@ describe("Moves - Roost", () => { await game.phaseInterceptor.to(TurnEndPhase); game.move.select(MoveId.ROOST); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.phaseInterceptor.to(MoveEffectPhase); // Should only be typeless type after roost and is grounded diff --git a/test/moves/sketch.test.ts b/test/moves/sketch.test.ts index 0c2527bc09c..62f2aa33bc6 100644 --- a/test/moves/sketch.test.ts +++ b/test/moves/sketch.test.ts @@ -71,7 +71,6 @@ describe("Moves - Sketch", () => { await game.toNextTurn(); game.move.select(MoveId.SKETCH); - await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.move.forceStatusActivation(true); await game.phaseInterceptor.to("TurnEndPhase"); expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); diff --git a/test/moves/spite.test.ts b/test/moves/spite.test.ts index 56c1be76198..af0f802819e 100644 --- a/test/moves/spite.test.ts +++ b/test/moves/spite.test.ts @@ -49,7 +49,6 @@ describe("Moves - Spite", () => { game.move.use(MoveId.SPITE); await game.move.selectEnemyMove(MoveId.SPLASH); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.toEndOfTurn(); expect(karp).toHaveUsedPP(MoveId.TACKLE, 4 + 1); diff --git a/test/phases/frenzy-move-reset.test.ts b/test/phases/frenzy-move-reset.test.ts index feedd1b5865..7872a56d716 100644 --- a/test/phases/frenzy-move-reset.test.ts +++ b/test/phases/frenzy-move-reset.test.ts @@ -62,7 +62,6 @@ describe("Frenzy Move Reset", () => { expect(playerPokemon.summonData.moveQueue.length).toBe(2); expect(playerPokemon.summonData.tags.some(tag => tag.tagType === BattlerTagType.FRENZY)).toBe(true); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.move.forceStatusActivation(true); await game.toNextTurn(); From dbaa70203521587507a348b5747181f89dc23b49 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sun, 21 Sep 2025 17:23:08 -0400 Subject: [PATCH 6/9] foo --- test/abilities/stall.test.ts | 7 +++++-- test/moves/delayed-attack.test.ts | 6 ++++-- test/test-utils/game-manager.ts | 7 ++----- test/test-utils/helpers/field-helper.ts | 2 +- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/test/abilities/stall.test.ts b/test/abilities/stall.test.ts index b6a88964e09..df522867722 100644 --- a/test/abilities/stall.test.ts +++ b/test/abilities/stall.test.ts @@ -1,4 +1,5 @@ import { AbilityId } from "#enums/ability-id"; +import { BattlerIndex } from "#enums/battler-index"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { GameManager } from "#test/test-utils/game-manager"; @@ -24,7 +25,7 @@ describe("Abilities - Stall", () => { game.override .battleStyle("single") .criticalHits(false) - .enemySpecies(SpeciesId.REGIELEKI) + .enemySpecies(SpeciesId.SHUCKLE) .enemyAbility(AbilityId.STALL) .enemyMoveset(MoveId.QUICK_ATTACK) .moveset([MoveId.QUICK_ATTACK, MoveId.TACKLE]); @@ -42,7 +43,7 @@ describe("Abilities - Stall", () => { const player = game.field.getPlayerPokemon(); game.move.select(MoveId.QUICK_ATTACK); - + game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); 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. @@ -55,6 +56,7 @@ describe("Abilities - Stall", () => { const player = game.field.getPlayerPokemon(); game.move.select(MoveId.TACKLE); + game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); 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. @@ -69,6 +71,7 @@ describe("Abilities - Stall", () => { const player = game.field.getPlayerPokemon(); game.move.select(MoveId.TACKLE); + game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.phaseInterceptor.to("MoveEndPhase", false); diff --git a/test/moves/delayed-attack.test.ts b/test/moves/delayed-attack.test.ts index db3c8bb17a3..5079e471dda 100644 --- a/test/moves/delayed-attack.test.ts +++ b/test/moves/delayed-attack.test.ts @@ -165,9 +165,11 @@ describe("Moves - Delayed Attacks", () => { it("should trigger multiple pending attacks in order of creation, even if that order changes later on", async () => { game.override.battleStyle("double"); - await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS]); + await game.classicMode.startBattle([SpeciesId.ALOMOMOLA, SpeciesId.BLISSEY]); - const [alomomola, blissey] = game.scene.getPlayerField(); + const [alomomola, blissey, karp1, karp2] = game.scene.getField(); + vi.spyOn(karp1, "getNameToRender").mockReturnValue("Karp 1"); + vi.spyOn(karp2, "getNameToRender").mockReturnValue("Karp 2"); const oldOrder = game.field.getSpeedOrder(true); diff --git a/test/test-utils/game-manager.ts b/test/test-utils/game-manager.ts index 55cb74e461c..be7fb1579ad 100644 --- a/test/test-utils/game-manager.ts +++ b/test/test-utils/game-manager.ts @@ -52,13 +52,11 @@ import type { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import type { PartyUiHandler } from "#ui/party-ui-handler"; import type { StarterSelectUiHandler } from "#ui/starter-select-ui-handler"; import type { TargetSelectUiHandler } from "#ui/target-select-ui-handler"; -import { sortInSpeedOrder } from "#utils/speed-order"; +import * as speedOrderUtils from "#utils/speed-order"; import fs from "node:fs"; import { AES, enc } from "crypto-js"; import { expect, vi } from "vitest"; -vi.mock(import("#utils/speed-order"), { spy: true }); - /** * Class to manage the game state and transitions between phases. */ @@ -557,8 +555,7 @@ export class GameManager { this.scene.getField(true).map(p => p.getBattlerIndex() as Exclude), ); - expect(vi.isMockFunction(sortInSpeedOrder)).toBe(true); - vi.mocked(sortInSpeedOrder).mockImplementation(list => { + vi.spyOn(speedOrderUtils, "sortInSpeedOrder").mockImplementation(list => { list.sort((a, b) => { const aBattlerIndex = (a instanceof Pokemon ? a : a.getPokemon()).getBattlerIndex() as Exclude< BattlerIndex, diff --git a/test/test-utils/helpers/field-helper.ts b/test/test-utils/helpers/field-helper.ts index db48e411d5e..6e52a9e3dde 100644 --- a/test/test-utils/helpers/field-helper.ts +++ b/test/test-utils/helpers/field-helper.ts @@ -61,7 +61,7 @@ export class FieldHelper extends GameManagerHelper { * Helper function to return all on-field {@linkcode Pokemon} in speed order (fastest first). * @param indices - Whether to only return {@linkcode BattlerIndex}es instead of full Pokemon objects * (such as for comparison with other speed order-related mechanisms); default `false` - * @returns An array containing the {@linkcode BattlerIndex}es of all on-field {@linkcode Pokemon} on the field in order of descending Speed. \ + * @returns An array containing the {@linkcode BattlerIndex}es of all on-field `Pokemon` on the field in order of **descending** Speed. \ * Speed ties are returned in increasing order of index. * * @remarks From 74675471cc27f6d5f83b495e854537785c568bfa Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sun, 21 Sep 2025 18:31:55 -0400 Subject: [PATCH 7/9] Fixed speed being reversed --- src/phase-tree.ts | 41 ++++++++++++------------------- src/utils/common.ts | 11 +++++---- src/utils/speed-order.ts | 8 +++--- test/moves/delayed-attack.test.ts | 8 +++--- test/test-utils/game-manager.ts | 3 ++- 5 files changed, 32 insertions(+), 39 deletions(-) diff --git a/src/phase-tree.ts b/src/phase-tree.ts index 69bb72ca4f0..2ea14c32a6b 100644 --- a/src/phase-tree.ts +++ b/src/phase-tree.ts @@ -96,10 +96,11 @@ export class PhaseTree { } /** - * Removes and returns the first {@linkcode Phase} from the topmost level of the tree + * Remove and return 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 { + // Clear out all empty levels from the tree this.currentLevel = this.levels.length - 1; while (this.currentLevel > 0 && this.levels[this.currentLevel].length === 0) { this.deferredActive = false; @@ -113,10 +114,10 @@ export class PhaseTree { } /** - * Finds a particular {@linkcode Phase} in the Tree by searching in pop order + * Find the first {@linkcode Phase} in the Tree matching the given conditions. * @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 + * @param phaseFilter - An optional {@linkcode PhaseConditionFunc} to add conditions to the search + * @returns The first `Phase` that matches the criteria, or `undefined` if none exists */ public find

(phaseType: P, phaseFilter?: PhaseConditionFunc

): PhaseMap[P] | undefined { for (let i = this.levels.length - 1; i >= 0; i--) { @@ -129,26 +130,24 @@ export class PhaseTree { } /** - * Finds a particular {@linkcode Phase} in the Tree by searching in pop order + * Find all {@linkcode Phase}s in the Tree matching the given conditions. * @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 + * @param phaseFilter - An optional {@linkcode PhaseConditionFunc} to add conditions to the search + * @returns An array containing all `Phase`s matching the criteria. */ 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); + phases.push(...level.filter((p): p is PhaseMap[P] => p.is(phaseType) && (!phaseFilter || phaseFilter(p)))); } return phases; } /** - * Clears the Tree - * @param leaveFirstLevel - If `true`, leaves the top level of the tree intact - * - * @privateremarks + * Clear all Phases from the Tree. + * @param leaveFirstLevel - Whether to leave the top level of the tree intact; default `false` + * @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. @@ -181,25 +180,17 @@ export class PhaseTree { */ 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; + this.levels[i] = this.levels[i].filter(phase => !phase.is(phaseType)); } } /** - * Determines if a particular phase exists in the Tree + * Check whether 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 + * @param phaseFilter - An optional {@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; + return this.levels.some(level => level.some(phase => phase.is(phaseType) && (!phaseFilter || phaseFilter(phase)))); } } diff --git a/src/utils/common.ts b/src/utils/common.ts index b227da948c4..99206fd46d8 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -135,16 +135,17 @@ export function randSeedItem(items: T[]): T { /** * Shuffle a list using the seeded rng. Utilises the Fisher-Yates algorithm. - * @param items - The array of items to shuffle. + * @param items - The array of items to shuffle + * @param mutate - Whether to mutate the array in place (`true`) or create a new one + * (`false`); default `false` * @returns A new shuffled array of items. - * @remarks - * This does _not_ mutate the array (unlike {@linkcode Array.sort}). */ -export function randSeedShuffle(items: T[]): T[] { +export function randSeedShuffle(items: T[], mutate = false): T[] { if (items.length <= 1) { return items; } - const newArray = items.slice(0); + + const newArray = mutate ? items.slice(0) : items; for (let i = items.length - 1; i > 0; i--) { const j = Phaser.Math.RND.integerInRange(0, i); [newArray[i], newArray[j]] = [newArray[j], newArray[i]]; diff --git a/src/utils/speed-order.ts b/src/utils/speed-order.ts index 4ed472cdea5..86ecbb8a515 100644 --- a/src/utils/speed-order.ts +++ b/src/utils/speed-order.ts @@ -18,7 +18,7 @@ interface hasPokemon { */ export function sortInSpeedOrder(pokemonList: T[], shuffleFirst = true): void { if (shuffleFirst) { - pokemonList = shufflePokemonList(pokemonList); + shufflePokemonList(pokemonList); } sortBySpeed(pokemonList); } @@ -26,19 +26,17 @@ export function sortInSpeedOrder(pokemonList: T[ /** * Helper function to randomly shuffle an array of Pokemon. * @param pokemonList - The array of Pokemon or objects containing Pokemon to shuffle - * @returns The shuffled array. */ -function shufflePokemonList(pokemonList: T[]): T[] { +function shufflePokemonList(pokemonList: T[]): void { // 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); + randSeedShuffle(pokemonList, true); }, globalScene.currentBattle.turn * 1000 + pokemonList.length, globalScene.waveSeed, ); - return pokemonList; } /** Sort an array of {@linkcode Pokemon} in speed order (without shuffling) */ diff --git a/test/moves/delayed-attack.test.ts b/test/moves/delayed-attack.test.ts index 5079e471dda..8d2379d5835 100644 --- a/test/moves/delayed-attack.test.ts +++ b/test/moves/delayed-attack.test.ts @@ -197,9 +197,11 @@ describe("Moves - Delayed Attacks", () => { const MEPs = game.scene.phaseManager["phaseQueue"].findAll("MoveEffectPhase"); expect(MEPs).toHaveLength(4); - console.log(`Expected: ${oldOrder.map(o => game.scene.getField()[o].getNameToRender())} -Actual: ${MEPs.map(mep => mep.getPokemon().getNameToRender())}`); - expect(MEPs.map(mep => mep.getPokemon().getBattlerIndex())).toEqual(oldOrder); + expect( + MEPs.map(mep => mep.getPokemon().getBattlerIndex()), + `Expected: ${oldOrder.map(o => game.scene.getField()[o].getNameToRender())} +Actual: ${MEPs.map(mep => mep.getPokemon().getNameToRender())}`, + ).toEqual(oldOrder); }); it("should vanish silently if it would otherwise hit the user", async () => { diff --git a/test/test-utils/game-manager.ts b/test/test-utils/game-manager.ts index be7fb1579ad..0f3b0e93e76 100644 --- a/test/test-utils/game-manager.ts +++ b/test/test-utils/game-manager.ts @@ -555,6 +555,7 @@ export class GameManager { this.scene.getField(true).map(p => p.getBattlerIndex() as Exclude), ); + // NB: This will need to be changed if `sortInSpeedOrder`'s order is ever changed vi.spyOn(speedOrderUtils, "sortInSpeedOrder").mockImplementation(list => { list.sort((a, b) => { const aBattlerIndex = (a instanceof Pokemon ? a : a.getPokemon()).getBattlerIndex() as Exclude< @@ -566,7 +567,7 @@ export class GameManager { BattlerIndex.ATTACKER >; - return order.indexOf(bBattlerIndex) - order.indexOf(aBattlerIndex); + return order.indexOf(aBattlerIndex) - order.indexOf(bBattlerIndex); }); }); } From 0a3ab00c56b26708063a4b8a797066bcceb9e78d Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sun, 21 Sep 2025 20:40:53 -0400 Subject: [PATCH 8/9] Fixed failing tests --- test/moves/destiny-bond.test.ts | 3 ++- test/moves/metronome.test.ts | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/test/moves/destiny-bond.test.ts b/test/moves/destiny-bond.test.ts index bbe0ae2bc37..ba51a0a53c2 100644 --- a/test/moves/destiny-bond.test.ts +++ b/test/moves/destiny-bond.test.ts @@ -76,7 +76,8 @@ describe("Moves - Destiny Bond", () => { // Turn 2: Player KO's the enemy before the enemy's turn game.move.select(moveToUse); - await game.phaseInterceptor.to("BerryPhase"); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.toEndOfTurn(); expect(enemyPokemon.isFainted()).toBe(true); expect(playerPokemon.isFainted()).toBe(true); diff --git a/test/moves/metronome.test.ts b/test/moves/metronome.test.ts index 63400ad8303..df51394ad00 100644 --- a/test/moves/metronome.test.ts +++ b/test/moves/metronome.test.ts @@ -95,16 +95,16 @@ describe("Moves - Metronome", () => { game.move.select(MoveId.METRONOME); await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); - await game.phaseInterceptor.to("TurnEndPhase"); + await game.toNextTurn(); - expect(player.getTag(BattlerTagType.CHARGING)).toBeTruthy(); + expect(player).toHaveBattlerTag(BattlerTagType.CHARGING); const turn1PpUsed = metronomeMove.ppUsed; expect.soft(turn1PpUsed).toBeGreaterThan(1); expect(solarBeamMove.ppUsed).toBe(0); await game.toNextTurn(); - expect(player.getTag(BattlerTagType.CHARGING)).toBeFalsy(); + expect(player).not.toHaveBattlerTag(BattlerTagType.CHARGING); const turn2PpUsed = metronomeMove.ppUsed - turn1PpUsed; expect(turn2PpUsed).toBeGreaterThan(1); expect(solarBeamMove.ppUsed).toBe(0); From ba83f18e2acd90dadb675431786360ac902f2102 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sun, 21 Sep 2025 20:42:21 -0400 Subject: [PATCH 9/9] Comment fix --- test/test-utils/game-manager.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test-utils/game-manager.ts b/test/test-utils/game-manager.ts index 0f3b0e93e76..99f6a61f68c 100644 --- a/test/test-utils/game-manager.ts +++ b/test/test-utils/game-manager.ts @@ -548,6 +548,7 @@ export class GameManager { * This does not account for priority, nor does it change the battlers' speed stats * (for the purposes of Electro Ball, etc). * @todo What should happen if the number of active battlers changes mid-test? + * @todo Remove `await`s from existing test files in a follow-up PR */ public setTurnOrder(order: Exclude[]): void { // TODO: Remove type assertions once `BattlerIndex.ATTACKER` ceases to exist