diff --git a/src/queues/post-summon-phase-priority-queue.ts b/src/queues/post-summon-phase-priority-queue.ts index 28e727de01b..fe08713e501 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); + this.queue = sortInSpeedOrder(this.queue); this.queue.sort((phaseA, phaseB) => phaseB.getPriority() - phaseA.getPriority()); } diff --git a/src/utils/speed-order.ts b/src/utils/speed-order.ts index f2733a74998..aaf3a4526c7 100644 --- a/src/utils/speed-order.ts +++ b/src/utils/speed-order.ts @@ -12,15 +12,13 @@ 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} */ -export function sortInSpeedOrder(pokemonList: T[], shuffleFirst = true): T[] { - if (shuffleFirst) { - shufflePokemonList(pokemonList); - } - sortBySpeed(pokemonList); - return pokemonList; +export function sortInSpeedOrder(pokemonList: T[]): T[] { + const grouped = groupPokemon(pokemonList); + shufflePokemonList(grouped); + sortBySpeed(grouped); + return grouped.flat(); } /** @@ -28,7 +26,7 @@ export function sortInSpeedOrder(pokemonList: T[ * @param pokemonList - The array of Pokemon or objects containing Pokemon * @returns The same array instance that was passed in, shuffled. */ -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( @@ -36,7 +34,6 @@ function shufflePokemonList(pokemonList: T[]): T globalScene.currentBattle.turn * 1000 + pokemonList.length, globalScene.waveSeed, ); - return pokemonList; } /** Type guard for {@linkcode sortBySpeed} to avoid importing {@linkcode Pokemon} */ @@ -44,11 +41,15 @@ function isPokemon(p: Pokemon | hasPokemon): p is Pokemon { return typeof (p as hasPokemon).getPokemon !== "function"; } +function getPokemon(p: Pokemon | hasPokemon): Pokemon { + return isPokemon(p) ? p : p.getPokemon(); +} + /** Sorts an array of {@linkcode Pokemon} by speed (without shuffling) */ -function sortBySpeed(pokemonList: T[]): void { - pokemonList.sort((a, b) => { - const aSpeed = (isPokemon(a) ? a : a.getPokemon()).getEffectiveStat(Stat.SPD); - const bSpeed = (isPokemon(b) ? b : b.getPokemon()).getEffectiveStat(Stat.SPD); +function sortBySpeed(groupedPokemonList: T[][]): void { + groupedPokemonList.sort((a, b) => { + const aSpeed = getPokemon(a[0]).getEffectiveStat(Stat.SPD); + const bSpeed = getPokemon(b[0]).getEffectiveStat(Stat.SPD); return bSpeed - aSpeed; }); @@ -57,6 +58,21 @@ function sortBySpeed(pokemonList: T[]): void { const speedReversed = new BooleanHolder(false); globalScene.arena.applyTags(ArenaTagType.TRICK_ROOM, speedReversed); if (speedReversed.value) { - pokemonList.reverse(); + groupedPokemonList.reverse(); } } + +function groupPokemon(pokemonList: T[]): T[][] { + const runs: T[][] = []; + for (const pkmn of pokemonList) { + const pokemon = getPokemon(pkmn); + const lastGroup = runs.at(-1); + if (lastGroup != null && lastGroup.length > 0 && getPokemon(lastGroup[0]) === pokemon) { + lastGroup.push(pkmn); + } else { + runs.push([pkmn]); + } + } + + return runs; +} diff --git a/test/utils/speed-order.test.ts b/test/utils/speed-order.test.ts new file mode 100644 index 00000000000..e408c5823c4 --- /dev/null +++ b/test/utils/speed-order.test.ts @@ -0,0 +1,58 @@ +import { AbilityId } from "#enums/ability-id"; +import { MoveId } from "#enums/move-id"; +import { SpeciesId } from "#enums/species-id"; +import { Stat } from "#enums/stat"; +import { GameManager } from "#test/test-utils/game-manager"; +import { sortInSpeedOrder } from "#utils/speed-order"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Utils - Speed Order", () => { + 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("single") + .startingLevel(100) + .enemyLevel(100) + .enemyMoveset(MoveId.SPLASH) + .enemyAbility(AbilityId.BALL_FETCH) + .ability(AbilityId.BALL_FETCH) + .enemySpecies(SpeciesId.REGIELEKI); + }); + + it("Sorts correctly in the basic case", async () => { + await game.classicMode.startBattle([SpeciesId.SLOWPOKE, SpeciesId.MEW]); + const [slowpoke, mew] = game.field.getPlayerParty(); + const regieleki = game.field.getEnemyPokemon(); + const pkmnList = [slowpoke, regieleki, mew]; + + expect(sortInSpeedOrder(pkmnList)).toEqual([regieleki, mew, slowpoke]); + }); + + it("Correctly sorts grouped pokemon", async () => { + await game.classicMode.startBattle([SpeciesId.SLOWPOKE, SpeciesId.MEW, SpeciesId.DITTO]); + const [slowpoke, mew, ditto] = game.field.getPlayerParty(); + const regieleki = game.field.getEnemyPokemon(); + ditto.stats[Stat.SPD] = slowpoke.getStat(Stat.SPD); + + const pkmnList = [slowpoke, slowpoke, ditto, ditto, mew, regieleki, regieleki]; + const sorted = sortInSpeedOrder(pkmnList); + + expect([ + [regieleki, regieleki, mew, slowpoke, slowpoke, ditto, ditto], + [regieleki, regieleki, mew, ditto, ditto, slowpoke, slowpoke], + ]).toContainEqual(sorted); + }); +});