This commit is contained in:
Bertie690 2025-09-22 21:10:29 -04:00 committed by GitHub
commit 6b96e9160a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 161 additions and 185 deletions

View File

@ -236,7 +236,7 @@
}, },
"overrides": [ "overrides": [
{ {
"includes": ["**/test/**/*.test.ts"], "includes": ["**/test/**/*.ts"],
"linter": { "linter": {
"rules": { "rules": {
"performance": { "performance": {
@ -245,8 +245,16 @@
}, },
"style": { "style": {
"noNonNullAssertion": "off" // tedious in some tests "noNonNullAssertion": "off" // tedious in some tests
}, }
}
}
},
{
"includes": ["**/test/**/*.test.ts"],
"linter": {
"rules": {
"nursery": { "nursery": {
// TODO: Enable for normal test folder files as well
"noFloatingPromises": "error" "noFloatingPromises": "error"
} }
} }

View File

@ -7,7 +7,6 @@ import { MovePhasePriorityQueue } from "#app/queues/move-phase-priority-queue";
import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue"; import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue";
import { PostSummonPhasePriorityQueue } from "#app/queues/post-summon-phase-priority-queue"; import { PostSummonPhasePriorityQueue } from "#app/queues/post-summon-phase-priority-queue";
import type { PriorityQueue } from "#app/queues/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"; import type { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
// TODO: might be easier to define which phases should be dynamic instead // TODO: might be easier to define which phases should be dynamic instead
@ -76,8 +75,8 @@ export class DynamicQueueManager {
} }
/** /**
* Returns the highest-priority (generally by speed) {@linkcode Phase} of the specified type * Returns the highest-priority (generally by speed) {@linkcode Phase} of the specified type.
* @param type - The {@linkcode PhaseString | type} to pop * @param type - The {@linkcode PhaseString | type} of phase to access
* @returns The popped {@linkcode Phase}, or `undefined` if none of the specified type exist * @returns The popped {@linkcode Phase}, or `undefined` if none of the specified type exist
*/ */
public popNextPhase(type: PhaseString): Phase | undefined { public popNextPhase(type: PhaseString): Phase | undefined {
@ -90,7 +89,7 @@ export class DynamicQueueManager {
* @param condition - An optional {@linkcode PhaseConditionFunc} to add conditions to the search * @param condition - An optional {@linkcode PhaseConditionFunc} to add conditions to the search
* @returns Whether a matching phase exists * @returns Whether a matching phase exists
*/ */
public exists<T extends PhaseString>(type: T, condition?: PhaseConditionFunc<T>): boolean { public exists<T extends PhaseString>(type: T, condition: PhaseConditionFunc<T> = () => true): boolean {
return !!this.dynamicPhaseMap.get(type)?.has(condition); return !!this.dynamicPhaseMap.get(type)?.has(condition);
} }
@ -141,21 +140,13 @@ export class DynamicQueueManager {
} }
/** /**
* Finds and cancels a {@linkcode MovePhase} meeting the condition * Find and cancel a {@linkcode MovePhase} meeting the condition
* @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function to filter phases by
*/ */
public cancelMovePhase(condition: PhaseConditionFunc<"MovePhase">): void { public cancelMovePhase(condition: PhaseConditionFunc<"MovePhase">): void {
this.getMovePhaseQueue().cancelMove(condition); 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 * @returns An in-order array of {@linkcode Pokemon}, representing the turn order as played out in the most recent turn
*/ */
@ -176,7 +167,7 @@ export class DynamicQueueManager {
/** /**
* Internal helper to determine if a phase is dynamic. * Internal helper to determine if a phase is dynamic.
* @param phase - The {@linkcode Phase} to check * @param phase - The {@linkcode Phase} to check
* @returns Whether `phase` is dynamic * @returns Whether `phase` is dynamic.
* @privateRemarks * @privateRemarks
* Currently, this checks that `phase` has a `getPokemon` method * Currently, this checks that `phase` has a `getPokemon` method
* and is not blacklisted in `nonDynamicPokemonPhases`. * and is not blacklisted in `nonDynamicPokemonPhases`.

View File

@ -3218,9 +3218,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 * @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 { public isOpponent(target: Pokemon): boolean {
return this.isPlayer() !== target.isPlayer(); return this.isPlayer() !== target.isPlayer();

View File

@ -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 * @returns - The next {@linkcode Phase}, or `undefined` if the Tree is empty
*/ */
public getNextPhase(): Phase | undefined { public getNextPhase(): Phase | undefined {
// Clear out all empty levels from the tree
this.currentLevel = this.levels.length - 1; this.currentLevel = this.levels.length - 1;
while (this.currentLevel > 0 && this.levels[this.currentLevel].length === 0) { while (this.currentLevel > 0 && this.levels[this.currentLevel].length === 0) {
this.deferredActive = false; 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 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 add conditions to the search
* @returns The matching {@linkcode Phase}, or `undefined` if none exists * @returns The first `Phase` that matches the criteria, or `undefined` if none exists
*/ */
public find<P extends PhaseString>(phaseType: P, phaseFilter?: PhaseConditionFunc<P>): PhaseMap[P] | undefined { public find<P extends PhaseString>(phaseType: P, phaseFilter?: PhaseConditionFunc<P>): PhaseMap[P] | undefined {
for (let i = this.levels.length - 1; i >= 0; i--) { 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 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 add conditions to the search
* @returns The matching {@linkcode Phase}, or `undefined` if none exists * @returns An array containing all `Phase`s matching the criteria.
*/ */
public findAll<P extends PhaseString>(phaseType: P, phaseFilter?: PhaseConditionFunc<P>): PhaseMap[P][] { public findAll<P extends PhaseString>(phaseType: P, phaseFilter?: PhaseConditionFunc<P>): PhaseMap[P][] {
const phases: PhaseMap[P][] = []; const phases: PhaseMap[P][] = [];
for (let i = this.levels.length - 1; i >= 0; i--) { for (let i = this.levels.length - 1; i >= 0; i--) {
const level = this.levels[i]; const level = this.levels[i];
const levelPhases = level.filter((p): p is PhaseMap[P] => p.is(phaseType) && (!phaseFilter || phaseFilter(p))); phases.push(...level.filter((p): p is PhaseMap[P] => p.is(phaseType) && (!phaseFilter || phaseFilter(p))));
phases.push(...levelPhases);
} }
return phases; return phases;
} }
/** /**
* Clears the Tree * Clear all Phases from the Tree.
* @param leaveFirstLevel - If `true`, leaves the top level of the tree intact * @param leaveFirstLevel - Whether to leave the top level of the tree intact; default `false`
* * @privateRemarks
* @privateremarks
* The parameter on this method exists because {@linkcode PhaseManager.clearPhaseQueue} previously (probably by mistake) ignored `phaseQueuePrepend`. * 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. * This is (probably by mistake) relied upon by certain ME functions.
@ -181,25 +180,17 @@ export class PhaseTree {
*/ */
public removeAll(phaseType: PhaseString): void { public removeAll(phaseType: PhaseString): void {
for (let i = 0; i < this.levels.length; i++) { for (let i = 0; i < this.levels.length; i++) {
const level = this.levels[i].filter(phase => !phase.is(phaseType)); this.levels[i] = this.levels[i].filter(phase => !phase.is(phaseType));
this.levels[i] = level;
} }
} }
/** /**
* 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 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 * @returns Whether a matching phase exists
*/ */
public exists<P extends PhaseString>(phaseType: P, phaseFilter?: PhaseConditionFunc<P>): boolean { public exists<P extends PhaseString>(phaseType: P, phaseFilter?: PhaseConditionFunc<P>): boolean {
for (const level of this.levels) { return this.levels.some(level => level.some(phase => phase.is(phaseType) && (!phaseFilter || phaseFilter(phase))));
for (const phase of level) {
if (phase.is(phaseType) && (!phaseFilter || phaseFilter(phase))) {
return true;
}
}
}
return false;
} }
} }

View File

@ -3,7 +3,6 @@ import type { Pokemon } from "#app/field/pokemon";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import type { MovePhase } from "#app/phases/move-phase"; import type { MovePhase } from "#app/phases/move-phase";
import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue"; 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 { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
import type { PhaseConditionFunc } from "#types/phase-types"; import type { PhaseConditionFunc } from "#types/phase-types";
@ -17,18 +16,18 @@ export class MovePhasePriorityQueue extends PokemonPhasePriorityQueue<MovePhase>
} }
public cancelMove(condition: PhaseConditionFunc<"MovePhase">): void { 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 { 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) { if (phase != null) {
phase.timingModifier = modifier; phase.timingModifier = modifier;
} }
} }
public setMoveForPhase(condition: PhaseConditionFunc<"MovePhase">, move: PokemonMove) { public setMoveForPhase(condition: PhaseConditionFunc<"MovePhase">, move: PokemonMove) {
const phase = this.queue.find(p => condition(p)); const phase = this.queue.find(condition);
if (phase != null) { if (phase != null) {
phase.move = move; phase.move = move;
} }
@ -47,7 +46,7 @@ export class MovePhasePriorityQueue extends PokemonPhasePriorityQueue<MovePhase>
mp => mp =>
mp.targets.length === 1 mp.targets.length === 1
&& mp.targets[0] === removedPokemon.getBattlerIndex() && mp.targets[0] === removedPokemon.getBattlerIndex()
&& mp.pokemon.isPlayer() !== allyPokemon.isPlayer(), && mp.pokemon.isOpponent(allyPokemon),
) )
.forEach(targetingMovePhase => { .forEach(targetingMovePhase => {
if (targetingMovePhase && targetingMovePhase.targets[0] !== allyPokemon.getBattlerIndex()) { if (targetingMovePhase && targetingMovePhase.targets[0] !== allyPokemon.getBattlerIndex()) {
@ -57,10 +56,6 @@ export class MovePhasePriorityQueue extends PokemonPhasePriorityQueue<MovePhase>
} }
} }
public setMoveOrder(order: BattlerIndex[]) {
this.setOrder = order;
}
public override pop(): MovePhase | undefined { public override pop(): MovePhase | undefined {
this.reorder(); this.reorder();
const phase = this.queue.shift(); const phase = this.queue.shift();
@ -79,25 +74,20 @@ export class MovePhasePriorityQueue extends PokemonPhasePriorityQueue<MovePhase>
} }
public override clear(): void { public override clear(): void {
this.setOrder = undefined;
this.lastTurnOrder = []; this.lastTurnOrder = [];
super.clear(); super.clear();
} }
private sortPostSpeed(): void { private sortPostSpeed(): void {
this.queue.sort((a: MovePhase, b: MovePhase) => { this.queue.sort(
const priority = [a, b].map(movePhase => { (a: MovePhase, b: MovePhase) =>
const move = movePhase.move.getMove(); // formatting
return move.getPriority(movePhase.pokemon, true); b.timingModifier - a.timingModifier || getPriorityForMP(b) - getPriorityForMP(a),
}); );
const timingModifiers = [a, b].map(movePhase => movePhase.timingModifier);
if (timingModifiers[0] !== timingModifiers[1]) {
return timingModifiers[1] - timingModifiers[0];
}
return priority[1] - priority[0];
});
} }
} }
function getPriorityForMP(mp: MovePhase): number {
const move = mp.move.getMove();
return move.getPriority(mp.pokemon, true);
}

View File

@ -1,20 +1,10 @@
import type { DynamicPhase } from "#app/@types/phase-types"; import type { DynamicPhase } from "#app/@types/phase-types";
import { PriorityQueue } from "#app/queues/priority-queue"; import { PriorityQueue } from "#app/queues/priority-queue";
import { sortInSpeedOrder } from "#app/utils/speed-order"; 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<T extends DynamicPhase> extends PriorityQueue<T> { export class PokemonPhasePriorityQueue<T extends DynamicPhase> extends PriorityQueue<T> {
protected setOrder: BattlerIndex[] | undefined;
protected override reorder(): void { protected override reorder(): void {
const setOrder = this.setOrder; sortInSpeedOrder(this.queue);
if (setOrder) {
this.queue.sort(
(a, b) =>
setOrder.indexOf(a.getPokemon().getBattlerIndex()) - setOrder.indexOf(b.getPokemon().getBattlerIndex()),
);
} else {
this.queue = sortInSpeedOrder(this.queue);
}
} }
} }

View File

@ -5,6 +5,6 @@ import { sortInSpeedOrder } from "#app/utils/speed-order";
/** A priority queue of {@linkcode Pokemon}s */ /** A priority queue of {@linkcode Pokemon}s */
export class PokemonPriorityQueue extends PriorityQueue<Pokemon> { export class PokemonPriorityQueue extends PriorityQueue<Pokemon> {
protected override reorder(): void { protected override reorder(): void {
this.queue = sortInSpeedOrder(this.queue); sortInSpeedOrder(this.queue);
} }
} }

View File

@ -11,7 +11,7 @@ import { sortInSpeedOrder } from "#app/utils/speed-order";
*/ */
export class PostSummonPhasePriorityQueue extends PokemonPhasePriorityQueue<PostSummonPhase> { export class PostSummonPhasePriorityQueue extends PokemonPhasePriorityQueue<PostSummonPhase> {
protected override reorder(): void { protected override reorder(): void {
this.queue = sortInSpeedOrder(this.queue, false); sortInSpeedOrder(this.queue, false);
this.queue.sort((phaseA, phaseB) => phaseB.getPriority() - phaseA.getPriority()); this.queue.sort((phaseA, phaseB) => phaseB.getPriority() - phaseA.getPriority());
} }

View File

@ -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<T> { export abstract class PriorityQueue<T> {
/** The items in the queue. */
protected queue: T[] = []; 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; protected abstract reorder(): void;
/** /**
* Calls {@linkcode reorder} and shifts the queue * Reorder the queue before removing and returning the highest priority element.
* @returns The front element of the queue after sorting, or `undefined` if the queue is empty * @returns The front-most element of the queue after sorting,
* or `undefined` if the queue is empty.
* @sealed * @sealed
*/ */
public pop(): T | undefined { public pop(): T | undefined {
@ -34,7 +39,7 @@ export abstract class PriorityQueue<T> {
} }
/** /**
* Removes all elements from the queue * Remove all elements from the queue.
* @sealed * @sealed
*/ */
public clear(): void { public clear(): void {
@ -50,8 +55,8 @@ export abstract class PriorityQueue<T> {
} }
/** /**
* Removes the first element matching the condition * Remove the first element matching the condition
* @param condition - An optional condition function (defaults to a function that always returns `true`) * @param condition - If provided, will restrict the removal to only phases matching the condition
* @returns Whether a removal occurred * @returns Whether a removal occurred
*/ */
public remove(condition: (t: T) => boolean = () => true): boolean { public remove(condition: (t: T) => boolean = () => true): boolean {
@ -67,12 +72,12 @@ export abstract class PriorityQueue<T> {
} }
/** @returns An element matching the condition function */ /** @returns An element matching the condition function */
public find(condition?: (t: T) => boolean): T | undefined { public find(condition: (t: T) => boolean): T | undefined {
return this.queue.find(e => !condition || condition(e)); return this.queue.find(condition);
} }
/** @returns Whether an element matching the condition function exists */ /** @returns Whether an element matching the condition function exists */
public has(condition?: (t: T) => boolean): boolean { public has(condition: (t: T) => boolean): boolean {
return this.queue.some(e => !condition || condition(e)); return this.queue.some(condition);
} }
} }

View File

@ -135,14 +135,17 @@ export function randSeedItem<T>(items: T[]): T {
/** /**
* Shuffle a list using the seeded rng. Utilises the Fisher-Yates algorithm. * 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
* @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. * @returns A new shuffled array of items.
*/ */
export function randSeedShuffle<T>(items: T[]): T[] { export function randSeedShuffle<T>(items: T[], mutate = false): T[] {
if (items.length <= 1) { if (items.length <= 1) {
return items; return items;
} }
const newArray = items.slice(0);
const newArray = mutate ? items.slice(0) : items;
for (let i = items.length - 1; i > 0; i--) { for (let i = items.length - 1; i > 0; i--) {
const j = Phaser.Math.RND.integerInRange(0, i); const j = Phaser.Math.RND.integerInRange(0, i);
[newArray[i], newArray[j]] = [newArray[j], newArray[i]]; [newArray[i], newArray[j]] = [newArray[j], newArray[i]];

View File

@ -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. * 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} * @returns A {@linkcode Generator} of {@linkcode Pokemon}
* *
* @remarks * @remarks

View File

@ -10,35 +10,36 @@ interface hasPokemon {
} }
/** /**
* Sorts an array of {@linkcode Pokemon} by speed, taking Trick Room into account. * Sort an array of {@linkcode Pokemon} in speed order, taking Trick Room into account.
* @param pokemonList - The list of Pokemon or objects containing Pokemon * @param pokemonList - An array of `Pokemon` or objects containing `Pokemon` to sort;
* @param shuffleFirst - Whether to shuffle the list before sorting (to handle speed ties). Default `true`. * will be mutated and sorted in place.
* @returns The sorted array of {@linkcode Pokemon} * @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<T extends Pokemon | hasPokemon>(pokemonList: T[], shuffleFirst = true): T[] { export function sortInSpeedOrder<T extends Pokemon | hasPokemon>(pokemonList: T[], shuffleFirst = true): void {
pokemonList = shuffleFirst ? shufflePokemonList(pokemonList) : pokemonList; if (shuffleFirst) {
shufflePokemonList(pokemonList);
}
sortBySpeed(pokemonList); sortBySpeed(pokemonList);
return pokemonList;
} }
/** /**
* @param pokemonList - The array of Pokemon or objects containing Pokemon * Helper function to randomly shuffle an array of Pokemon.
* @returns The shuffled array * @param pokemonList - The array of Pokemon or objects containing Pokemon to shuffle
*/ */
function shufflePokemonList<T extends Pokemon | hasPokemon>(pokemonList: T[]): T[] { function shufflePokemonList<T extends Pokemon | hasPokemon>(pokemonList: T[]): void {
// This is seeded with the current turn to prevent an inconsistency where it // This is seeded with the current turn to prevent an inconsistency where it
// was varying based on how long since you last reloaded // was varying based on how long since you last reloaded
globalScene.executeWithSeedOffset( globalScene.executeWithSeedOffset(
() => { () => {
pokemonList = randSeedShuffle(pokemonList); randSeedShuffle(pokemonList, true);
}, },
globalScene.currentBattle.turn * 1000 + pokemonList.length, globalScene.currentBattle.turn * 1000 + pokemonList.length,
globalScene.waveSeed, globalScene.waveSeed,
); );
return pokemonList;
} }
/** Sorts an array of {@linkcode Pokemon} by speed (without shuffling) */ /** Sort an array of {@linkcode Pokemon} in speed order (without shuffling) */
function sortBySpeed<T extends Pokemon | hasPokemon>(pokemonList: T[]): void { function sortBySpeed<T extends Pokemon | hasPokemon>(pokemonList: T[]): void {
pokemonList.sort((a, b) => { pokemonList.sort((a, b) => {
const aSpeed = (a instanceof Pokemon ? a : a.getPokemon()).getEffectiveStat(Stat.SPD); const aSpeed = (a instanceof Pokemon ? a : a.getPokemon()).getEffectiveStat(Stat.SPD);

View File

@ -120,7 +120,6 @@ describe("Abilities - Neutralizing Gas", () => {
game.move.select(MoveId.SPLASH, 1); game.move.select(MoveId.SPLASH, 1);
await game.move.selectEnemyMove(MoveId.ENTRAINMENT, BattlerIndex.PLAYER_2); await game.move.selectEnemyMove(MoveId.ENTRAINMENT, BattlerIndex.PLAYER_2);
await game.move.selectEnemyMove(MoveId.SPLASH); await game.move.selectEnemyMove(MoveId.SPLASH);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);
await game.phaseInterceptor.to("BerryPhase"); await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.arena.getTag(ArenaTagType.NEUTRALIZING_GAS)).toBeUndefined(); // No neut gas users are left expect(game.scene.arena.getTag(ArenaTagType.NEUTRALIZING_GAS)).toBeUndefined(); // No neut gas users are left
}); });

View File

@ -97,7 +97,6 @@ describe("Abilities - Protosynthesis", () => {
true, true,
); );
game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.toNextTurn(); await game.toNextTurn();
const boosted_dmg = initialHp - enemy.hp; const boosted_dmg = initialHp - enemy.hp;
expect(boosted_dmg).toBeGreaterThan(unboosted_dmg); expect(boosted_dmg).toBeGreaterThan(unboosted_dmg);

View File

@ -1,4 +1,5 @@
import { AbilityId } from "#enums/ability-id"; import { AbilityId } from "#enums/ability-id";
import { BattlerIndex } from "#enums/battler-index";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { GameManager } from "#test/test-utils/game-manager"; import { GameManager } from "#test/test-utils/game-manager";
@ -24,7 +25,7 @@ describe("Abilities - Stall", () => {
game.override game.override
.battleStyle("single") .battleStyle("single")
.criticalHits(false) .criticalHits(false)
.enemySpecies(SpeciesId.REGIELEKI) .enemySpecies(SpeciesId.SHUCKLE)
.enemyAbility(AbilityId.STALL) .enemyAbility(AbilityId.STALL)
.enemyMoveset(MoveId.QUICK_ATTACK) .enemyMoveset(MoveId.QUICK_ATTACK)
.moveset([MoveId.QUICK_ATTACK, MoveId.TACKLE]); .moveset([MoveId.QUICK_ATTACK, MoveId.TACKLE]);
@ -42,7 +43,7 @@ describe("Abilities - Stall", () => {
const player = game.field.getPlayerPokemon(); const player = game.field.getPlayerPokemon();
game.move.select(MoveId.QUICK_ATTACK); game.move.select(MoveId.QUICK_ATTACK);
game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("MoveEndPhase", false); await game.phaseInterceptor.to("MoveEndPhase", false);
// The player Pokemon (without Stall) goes first despite having lower speed than the opponent. // 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. // 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(); const player = game.field.getPlayerPokemon();
game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE);
game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("MoveEndPhase", false); 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 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(); const player = game.field.getPlayerPokemon();
game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE);
game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("MoveEndPhase", false); await game.phaseInterceptor.to("MoveEndPhase", false);

View File

@ -53,12 +53,10 @@ describe("Abilities - Supreme Overlord", () => {
await game.toNextTurn(); await game.toNextTurn();
game.move.select(MoveId.EXPLOSION); game.move.select(MoveId.EXPLOSION);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
game.doSelectPartyPokemon(2); game.doSelectPartyPokemon(2);
await game.toNextTurn(); await game.toNextTurn();
game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to(MoveEffectPhase); await game.phaseInterceptor.to(MoveEffectPhase);
expect(move.calculateBattlePower).toHaveReturnedWith(basePower * 1.2); expect(move.calculateBattlePower).toHaveReturnedWith(basePower * 1.2);
@ -80,7 +78,6 @@ describe("Abilities - Supreme Overlord", () => {
*/ */
game.doRevivePokemon(1); game.doRevivePokemon(1);
game.move.select(MoveId.EXPLOSION); game.move.select(MoveId.EXPLOSION);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
game.doSelectPartyPokemon(1); game.doSelectPartyPokemon(1);
await game.toNextTurn(); await game.toNextTurn();
@ -88,12 +85,10 @@ describe("Abilities - Supreme Overlord", () => {
* Bulbasur faints twice * Bulbasur faints twice
*/ */
game.move.select(MoveId.EXPLOSION); game.move.select(MoveId.EXPLOSION);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
game.doSelectPartyPokemon(2); game.doSelectPartyPokemon(2);
await game.toNextTurn(); await game.toNextTurn();
game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to(MoveEffectPhase); await game.phaseInterceptor.to(MoveEffectPhase);
expect(move.calculateBattlePower).toHaveReturnedWith(basePower * 1.3); expect(move.calculateBattlePower).toHaveReturnedWith(basePower * 1.3);
@ -116,7 +111,6 @@ describe("Abilities - Supreme Overlord", () => {
* Enemy Pokemon faints and new wave is entered. * Enemy Pokemon faints and new wave is entered.
*/ */
game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.toNextWave(); await game.toNextWave();
game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE);
@ -137,7 +131,6 @@ describe("Abilities - Supreme Overlord", () => {
await game.toNextTurn(); await game.toNextTurn();
game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.toNextWave(); await game.toNextWave();
game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE);
@ -158,7 +151,6 @@ describe("Abilities - Supreme Overlord", () => {
await game.toNextTurn(); await game.toNextTurn();
game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.toNextWave(); await game.toNextWave();
game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE);

View File

@ -70,7 +70,6 @@ describe("Items - Multi Lens", () => {
const playerPokemon = game.field.getPlayerPokemon(); const playerPokemon = game.field.getPlayerPokemon();
game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to("MoveEndPhase"); await game.phaseInterceptor.to("MoveEndPhase");
expect(playerPokemon.turnData.hitCount).toBe(3); expect(playerPokemon.turnData.hitCount).toBe(3);

View File

@ -111,7 +111,6 @@ describe("Moves - Baton Pass", () => {
expect(enemy.getTag(BattlerTagType.FIRE_SPIN)).toBeDefined(); expect(enemy.getTag(BattlerTagType.FIRE_SPIN)).toBeDefined();
game.move.select(MoveId.BATON_PASS); game.move.select(MoveId.BATON_PASS);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
game.doSelectPartyPokemon(1); game.doSelectPartyPokemon(1);
await game.toNextTurn(); await game.toNextTurn();

View File

@ -165,18 +165,20 @@ describe("Moves - Delayed Attacks", () => {
it("should trigger multiple pending attacks in order of creation, even if that order changes later on", async () => { it("should trigger multiple pending attacks in order of creation, even if that order changes later on", async () => {
game.override.battleStyle("double"); 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.getField(); 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(); const oldOrder = game.field.getSpeedOrder(true);
game.move.use(MoveId.FUTURE_SIGHT, BattlerIndex.PLAYER, BattlerIndex.ENEMY); game.move.use(MoveId.FUTURE_SIGHT, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
game.move.use(MoveId.FUTURE_SIGHT, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2); 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);
await game.move.forceEnemyMove(MoveId.FUTURE_SIGHT, BattlerIndex.PLAYER_2); await game.move.forceEnemyMove(MoveId.FUTURE_SIGHT, BattlerIndex.PLAYER_2);
// Ensure that the moves are used deterministically in speed order (for speed ties) // 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(); await game.toNextTurn();
expectFutureSightActive(4); expectFutureSightActive(4);
@ -195,7 +197,11 @@ describe("Moves - Delayed Attacks", () => {
const MEPs = game.scene.phaseManager["phaseQueue"].findAll("MoveEffectPhase"); const MEPs = game.scene.phaseManager["phaseQueue"].findAll("MoveEffectPhase");
expect(MEPs).toHaveLength(4); expect(MEPs).toHaveLength(4);
expect(MEPs.map(mep => mep.getPokemon())).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 () => { it("should vanish silently if it would otherwise hit the user", async () => {

View File

@ -17,8 +17,6 @@ describe("Moves - Destiny Bond", () => {
let game: GameManager; let game: GameManager;
const defaultParty = [SpeciesId.BULBASAUR, SpeciesId.SQUIRTLE]; const defaultParty = [SpeciesId.BULBASAUR, SpeciesId.SQUIRTLE];
const enemyFirst = [BattlerIndex.ENEMY, BattlerIndex.PLAYER];
const playerFirst = [BattlerIndex.PLAYER, BattlerIndex.ENEMY];
beforeAll(() => { beforeAll(() => {
phaserGame = new Phaser.Game({ phaserGame = new Phaser.Game({
@ -52,7 +50,7 @@ describe("Moves - Destiny Bond", () => {
const playerPokemon = game.field.getPlayerPokemon(); const playerPokemon = game.field.getPlayerPokemon();
game.move.select(moveToUse); game.move.select(moveToUse);
await game.setTurnOrder(enemyFirst); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("BerryPhase"); await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon.isFainted()).toBe(true); expect(enemyPokemon.isFainted()).toBe(true);
@ -70,7 +68,7 @@ describe("Moves - Destiny Bond", () => {
// Turn 1: Enemy uses Destiny Bond and doesn't faint // Turn 1: Enemy uses Destiny Bond and doesn't faint
game.move.select(MoveId.SPLASH); game.move.select(MoveId.SPLASH);
await game.setTurnOrder(playerFirst); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.toNextTurn(); await game.toNextTurn();
expect(enemyPokemon.isFainted()).toBe(false); expect(enemyPokemon.isFainted()).toBe(false);
@ -78,8 +76,8 @@ describe("Moves - Destiny Bond", () => {
// Turn 2: Player KO's the enemy before the enemy's turn // Turn 2: Player KO's the enemy before the enemy's turn
game.move.select(moveToUse); game.move.select(moveToUse);
await game.setTurnOrder(playerFirst); await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to("BerryPhase"); await game.toEndOfTurn();
expect(enemyPokemon.isFainted()).toBe(true); expect(enemyPokemon.isFainted()).toBe(true);
expect(playerPokemon.isFainted()).toBe(true); expect(playerPokemon.isFainted()).toBe(true);
@ -96,7 +94,7 @@ describe("Moves - Destiny Bond", () => {
// Turn 1: Enemy uses Destiny Bond and doesn't faint // Turn 1: Enemy uses Destiny Bond and doesn't faint
game.move.select(MoveId.SPLASH); game.move.select(MoveId.SPLASH);
await game.setTurnOrder(enemyFirst); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.toNextTurn(); await game.toNextTurn();
expect(enemyPokemon.isFainted()).toBe(false); expect(enemyPokemon.isFainted()).toBe(false);
@ -104,7 +102,6 @@ describe("Moves - Destiny Bond", () => {
// Turn 2: Enemy should fail Destiny Bond then get KO'd // Turn 2: Enemy should fail Destiny Bond then get KO'd
game.move.select(moveToUse); game.move.select(moveToUse);
await game.setTurnOrder(enemyFirst);
await game.phaseInterceptor.to("BerryPhase"); await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon.isFainted()).toBe(true); expect(enemyPokemon.isFainted()).toBe(true);
@ -122,7 +119,7 @@ describe("Moves - Destiny Bond", () => {
const playerPokemon = game.field.getPlayerPokemon(); const playerPokemon = game.field.getPlayerPokemon();
game.move.select(moveToUse); game.move.select(moveToUse);
await game.setTurnOrder(enemyFirst); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("BerryPhase"); await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon.isFainted()).toBe(true); expect(enemyPokemon.isFainted()).toBe(true);
@ -140,7 +137,7 @@ describe("Moves - Destiny Bond", () => {
// Turn 1: Enemy uses Destiny Bond and doesn't faint // Turn 1: Enemy uses Destiny Bond and doesn't faint
game.move.select(MoveId.SPORE); game.move.select(MoveId.SPORE);
await game.setTurnOrder(enemyFirst); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.toNextTurn(); await game.toNextTurn();
expect(enemyPokemon.isFainted()).toBe(false); expect(enemyPokemon.isFainted()).toBe(false);
@ -149,7 +146,6 @@ describe("Moves - Destiny Bond", () => {
// Turn 2: Enemy should skip a turn due to sleep, then get KO'd // Turn 2: Enemy should skip a turn due to sleep, then get KO'd
game.move.select(moveToUse); game.move.select(moveToUse);
await game.setTurnOrder(enemyFirst);
await game.phaseInterceptor.to("BerryPhase"); await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon.isFainted()).toBe(true); expect(enemyPokemon.isFainted()).toBe(true);
@ -184,7 +180,7 @@ describe("Moves - Destiny Bond", () => {
const playerPokemon = game.field.getPlayerPokemon(); const playerPokemon = game.field.getPlayerPokemon();
game.move.select(moveToUse); game.move.select(moveToUse);
await game.setTurnOrder(enemyFirst); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("BerryPhase"); await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon.isFainted()).toBe(true); expect(enemyPokemon.isFainted()).toBe(true);
@ -238,7 +234,7 @@ describe("Moves - Destiny Bond", () => {
const playerPokemon = game.field.getPlayerPokemon(); const playerPokemon = game.field.getPlayerPokemon();
game.move.select(moveToUse); game.move.select(moveToUse);
await game.setTurnOrder(enemyFirst); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("BerryPhase"); await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon.isFainted()).toBe(true); expect(enemyPokemon.isFainted()).toBe(true);

View File

@ -78,8 +78,9 @@ describe("Moves - Encore", () => {
game.move.select(MoveId.ENCORE); game.move.select(MoveId.ENCORE);
const turnOrder = delay ? [BattlerIndex.PLAYER, BattlerIndex.ENEMY] : [BattlerIndex.ENEMY, BattlerIndex.PLAYER]; await game.setTurnOrder(
await game.setTurnOrder(turnOrder); delay ? [BattlerIndex.PLAYER, BattlerIndex.ENEMY] : [BattlerIndex.ENEMY, BattlerIndex.PLAYER],
);
await game.phaseInterceptor.to("BerryPhase", false); await game.phaseInterceptor.to("BerryPhase", false);
expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL); 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 () => { 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]); game.override.moveset([MoveId.ENCORE, MoveId.TORMENT, MoveId.SPLASH]);
await game.classicMode.startBattle([SpeciesId.FEEBAS]); await game.classicMode.startBattle([SpeciesId.FEEBAS]);
const enemyPokemon = game.field.getEnemyPokemon(); const enemyPokemon = game.field.getEnemyPokemon();
game.move.select(MoveId.ENCORE); game.move.select(MoveId.ENCORE);
await game.setTurnOrder(turnOrder); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("BerryPhase"); await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon.getTag(BattlerTagType.ENCORE)).toBeDefined(); expect(enemyPokemon.getTag(BattlerTagType.ENCORE)).toBeDefined();
await game.toNextTurn(); await game.toNextTurn();
game.move.select(MoveId.TORMENT); game.move.select(MoveId.TORMENT);
await game.setTurnOrder(turnOrder);
await game.phaseInterceptor.to("BerryPhase"); await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon.getTag(BattlerTagType.TORMENT)).toBeDefined(); expect(enemyPokemon.getTag(BattlerTagType.TORMENT)).toBeDefined();
await game.toNextTurn(); await game.toNextTurn();
game.move.select(MoveId.SPLASH); game.move.select(MoveId.SPLASH);
await game.setTurnOrder(turnOrder);
await game.phaseInterceptor.to("BerryPhase"); await game.phaseInterceptor.to("BerryPhase");
const lastMove = enemyPokemon.getLastXMoves()[0]; const lastMove = enemyPokemon.getLastXMoves()[0];
expect(lastMove?.move).toBe(MoveId.STRUGGLE); expect(lastMove?.move).toBe(MoveId.STRUGGLE);

View File

@ -64,7 +64,6 @@ describe("Moves - Grudge", () => {
game.move.use(MoveId.GUILLOTINE); game.move.use(MoveId.GUILLOTINE);
await game.move.forceEnemyMove(MoveId.SPLASH); await game.move.forceEnemyMove(MoveId.SPLASH);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.toEndOfTurn(); await game.toEndOfTurn();
expect(ratatta).toHaveFainted(); expect(ratatta).toHaveFainted();

View File

@ -343,7 +343,6 @@ describe("Moves - Instruct", () => {
expect(player.getLastXMoves()[0].result).toBe(MoveResult.FAIL); expect(player.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
game.move.select(MoveId.INSTRUCT); game.move.select(MoveId.INSTRUCT);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("TurnEndPhase", false); await game.phaseInterceptor.to("TurnEndPhase", false);
expect(player.getLastXMoves()[0].result).toBe(MoveResult.FAIL); expect(player.getLastXMoves()[0].result).toBe(MoveResult.FAIL);

View File

@ -59,7 +59,6 @@ describe("Moves - Last Respects", () => {
* Charmander faints once * Charmander faints once
*/ */
game.move.select(MoveId.EXPLOSION); game.move.select(MoveId.EXPLOSION);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
game.doSelectPartyPokemon(2); game.doSelectPartyPokemon(2);
await game.toNextTurn(); await game.toNextTurn();
@ -86,7 +85,6 @@ describe("Moves - Last Respects", () => {
*/ */
game.doRevivePokemon(1); game.doRevivePokemon(1);
game.move.select(MoveId.EXPLOSION); game.move.select(MoveId.EXPLOSION);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
game.doSelectPartyPokemon(1); game.doSelectPartyPokemon(1);
await game.toNextTurn(); await game.toNextTurn();
@ -99,7 +97,6 @@ describe("Moves - Last Respects", () => {
await game.toNextTurn(); await game.toNextTurn();
game.move.select(MoveId.LAST_RESPECTS); game.move.select(MoveId.LAST_RESPECTS);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to(MoveEffectPhase); await game.phaseInterceptor.to(MoveEffectPhase);
expect(move.calculateBattlePower).toHaveReturnedWith(basePower + 3 * 50); expect(move.calculateBattlePower).toHaveReturnedWith(basePower + 3 * 50);
@ -127,7 +124,6 @@ describe("Moves - Last Respects", () => {
* Enemy Pokemon faints and new wave is entered. * Enemy Pokemon faints and new wave is entered.
*/ */
game.move.select(MoveId.LAST_RESPECTS); game.move.select(MoveId.LAST_RESPECTS);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.toNextWave(); await game.toNextWave();
expect(game.scene.arena.playerFaints).toBe(1); expect(game.scene.arena.playerFaints).toBe(1);
@ -160,7 +156,6 @@ describe("Moves - Last Respects", () => {
* Enemy Pokemon faints and new wave is entered. * Enemy Pokemon faints and new wave is entered.
*/ */
game.move.select(MoveId.LAST_RESPECTS); game.move.select(MoveId.LAST_RESPECTS);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.toNextWave(); await game.toNextWave();
expect(game.scene.currentBattle.enemyFaints).toBe(0); expect(game.scene.currentBattle.enemyFaints).toBe(0);
@ -184,7 +179,6 @@ describe("Moves - Last Respects", () => {
await game.toNextTurn(); await game.toNextTurn();
game.move.select(MoveId.LAST_RESPECTS); game.move.select(MoveId.LAST_RESPECTS);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.toNextWave(); await game.toNextWave();
game.move.select(MoveId.LAST_RESPECTS); game.move.select(MoveId.LAST_RESPECTS);
@ -205,7 +199,6 @@ describe("Moves - Last Respects", () => {
await game.toNextTurn(); await game.toNextTurn();
game.move.select(MoveId.LAST_RESPECTS); game.move.select(MoveId.LAST_RESPECTS);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.toNextWave(); await game.toNextWave();
game.move.select(MoveId.LAST_RESPECTS); game.move.select(MoveId.LAST_RESPECTS);

View File

@ -95,17 +95,16 @@ describe("Moves - Metronome", () => {
game.move.select(MoveId.METRONOME); game.move.select(MoveId.METRONOME);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); 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; const turn1PpUsed = metronomeMove.ppUsed;
expect.soft(turn1PpUsed).toBeGreaterThan(1); expect.soft(turn1PpUsed).toBeGreaterThan(1);
expect(solarBeamMove.ppUsed).toBe(0); expect(solarBeamMove.ppUsed).toBe(0);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.toNextTurn(); await game.toNextTurn();
expect(player.getTag(BattlerTagType.CHARGING)).toBeFalsy(); expect(player).not.toHaveBattlerTag(BattlerTagType.CHARGING);
const turn2PpUsed = metronomeMove.ppUsed - turn1PpUsed; const turn2PpUsed = metronomeMove.ppUsed - turn1PpUsed;
expect(turn2PpUsed).toBeGreaterThan(1); expect(turn2PpUsed).toBeGreaterThan(1);
expect(solarBeamMove.ppUsed).toBe(0); expect(solarBeamMove.ppUsed).toBe(0);

View File

@ -166,7 +166,6 @@ describe("Moves - Pledge Moves", () => {
game.move.select(MoveId.FIERY_DANCE, 0, BattlerIndex.ENEMY_2); game.move.select(MoveId.FIERY_DANCE, 0, BattlerIndex.ENEMY_2);
game.move.select(MoveId.SPLASH, 1); game.move.select(MoveId.SPLASH, 1);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);
await game.phaseInterceptor.to("MoveEndPhase"); await game.phaseInterceptor.to("MoveEndPhase");
// Rainbow effect should increase Fiery Dance's chance of raising Sp. Atk to 100% // Rainbow effect should increase Fiery Dance's chance of raising Sp. Atk to 100%

View File

@ -85,7 +85,6 @@ describe("Moves - Rage Fist", () => {
// remove substitute and get confused // remove substitute and get confused
game.move.select(MoveId.TIDY_UP); game.move.select(MoveId.TIDY_UP);
await game.move.selectEnemyMove(MoveId.CONFUSE_RAY); await game.move.selectEnemyMove(MoveId.CONFUSE_RAY);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.toNextTurn(); await game.toNextTurn();
game.move.select(MoveId.RAGE_FIST); game.move.select(MoveId.RAGE_FIST);
@ -108,7 +107,6 @@ describe("Moves - Rage Fist", () => {
expect(game.field.getPlayerPokemon().battleData.hitCount).toBe(2); expect(game.field.getPlayerPokemon().battleData.hitCount).toBe(2);
game.move.select(MoveId.RAGE_FIST); game.move.select(MoveId.RAGE_FIST);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("TurnEndPhase"); await game.phaseInterceptor.to("TurnEndPhase");
expect(game.field.getPlayerPokemon().battleData.hitCount).toBe(4); expect(game.field.getPlayerPokemon().battleData.hitCount).toBe(4);
@ -147,7 +145,6 @@ describe("Moves - Rage Fist", () => {
await game.toNextTurn(); await game.toNextTurn();
game.move.select(MoveId.RAGE_FIST); game.move.select(MoveId.RAGE_FIST);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("BerryPhase", false); await game.phaseInterceptor.to("BerryPhase", false);
expect(move.calculateBattlePower).toHaveLastReturnedWith(150); expect(move.calculateBattlePower).toHaveLastReturnedWith(150);

View File

@ -159,7 +159,6 @@ describe("Moves - Roost", () => {
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to(TurnEndPhase);
game.move.select(MoveId.ROOST); game.move.select(MoveId.ROOST);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to(MoveEffectPhase); await game.phaseInterceptor.to(MoveEffectPhase);
// Should only be typeless type after roost and is grounded // Should only be typeless type after roost and is grounded
@ -195,7 +194,6 @@ describe("Moves - Roost", () => {
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to(TurnEndPhase);
game.move.select(MoveId.ROOST); game.move.select(MoveId.ROOST);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to(MoveEffectPhase); await game.phaseInterceptor.to(MoveEffectPhase);
// Should only be typeless type after roost and is grounded // Should only be typeless type after roost and is grounded

View File

@ -71,7 +71,6 @@ describe("Moves - Sketch", () => {
await game.toNextTurn(); await game.toNextTurn();
game.move.select(MoveId.SKETCH); game.move.select(MoveId.SKETCH);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.move.forceStatusActivation(true); await game.move.forceStatusActivation(true);
await game.phaseInterceptor.to("TurnEndPhase"); await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);

View File

@ -49,7 +49,6 @@ describe("Moves - Spite", () => {
game.move.use(MoveId.SPITE); game.move.use(MoveId.SPITE);
await game.move.selectEnemyMove(MoveId.SPLASH); await game.move.selectEnemyMove(MoveId.SPLASH);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.toEndOfTurn(); await game.toEndOfTurn();
expect(karp).toHaveUsedPP(MoveId.TACKLE, 4 + 1); expect(karp).toHaveUsedPP(MoveId.TACKLE, 4 + 1);

View File

@ -109,14 +109,14 @@ describe("Move - Wish", () => {
vi.spyOn(karp1, "getNameToRender").mockReturnValue("Karp 1"); vi.spyOn(karp1, "getNameToRender").mockReturnValue("Karp 1");
vi.spyOn(karp2, "getNameToRender").mockReturnValue("Karp 2"); 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);
game.move.use(MoveId.WISH, BattlerIndex.PLAYER_2); game.move.use(MoveId.WISH, BattlerIndex.PLAYER_2);
await game.move.forceEnemyMove(MoveId.WISH); await game.move.forceEnemyMove(MoveId.WISH);
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) // 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(); await game.toNextTurn();
expect(game).toHavePositionalTag(PositionalTagType.WISH, 4); expect(game).toHavePositionalTag(PositionalTagType.WISH, 4);
@ -137,7 +137,9 @@ describe("Move - Wish", () => {
const healPhases = game.scene.phaseManager["phaseQueue"].findAll("PokemonHealPhase"); const healPhases = game.scene.phaseManager["phaseQueue"].findAll("PokemonHealPhase");
expect(healPhases).toHaveLength(4); 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(); await game.toEndOfTurn();

View File

@ -62,7 +62,6 @@ describe("Frenzy Move Reset", () => {
expect(playerPokemon.summonData.moveQueue.length).toBe(2); expect(playerPokemon.summonData.moveQueue.length).toBe(2);
expect(playerPokemon.summonData.tags.some(tag => tag.tagType === BattlerTagType.FRENZY)).toBe(true); 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.move.forceStatusActivation(true);
await game.toNextTurn(); await game.toNextTurn();

View File

@ -14,7 +14,7 @@ import { PlayerGender } from "#enums/player-gender";
import type { PokeballType } from "#enums/pokeball"; import type { PokeballType } from "#enums/pokeball";
import type { SpeciesId } from "#enums/species-id"; import type { SpeciesId } from "#enums/species-id";
import { UiMode } from "#enums/ui-mode"; 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 { Trainer } from "#field/trainer";
import { ModifierTypeOption } from "#modifiers/modifier-type"; import { ModifierTypeOption } from "#modifiers/modifier-type";
import { CheckSwitchPhase } from "#phases/check-switch-phase"; import { CheckSwitchPhase } from "#phases/check-switch-phase";
@ -52,6 +52,7 @@ import type { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler";
import type { PartyUiHandler } from "#ui/party-ui-handler"; import type { PartyUiHandler } from "#ui/party-ui-handler";
import type { StarterSelectUiHandler } from "#ui/starter-select-ui-handler"; import type { StarterSelectUiHandler } from "#ui/starter-select-ui-handler";
import type { TargetSelectUiHandler } from "#ui/target-select-ui-handler"; import type { TargetSelectUiHandler } from "#ui/target-select-ui-handler";
import * as speedOrderUtils from "#utils/speed-order";
import fs from "node:fs"; import fs from "node:fs";
import { AES, enc } from "crypto-js"; import { AES, enc } from "crypto-js";
import { expect, vi } from "vitest"; import { expect, vi } from "vitest";
@ -536,19 +537,40 @@ export class GameManager {
} }
/** /**
* Modifies the queue manager to return move phases in a particular order * Override the turn 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
* Note: This *DOES NOT* account for priority.
* @param order - The turn order to set as an array of {@linkcode BattlerIndex}es.
* @example * @example
* ```ts * ```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, 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
*/ */
async setTurnOrder(order: BattlerIndex[]): Promise<void> { public setTurnOrder(order: Exclude<BattlerIndex, BattlerIndex.ATTACKER>[]): void {
await this.phaseInterceptor.to("TurnStartPhase", false); // TODO: Remove type assertions once `BattlerIndex.ATTACKER` ceases to exist
expect(order).toEqualUnsorted(
this.scene.getField(true).map(p => p.getBattlerIndex() as Exclude<BattlerIndex, BattlerIndex.ATTACKER>),
);
this.scene.phaseManager.dynamicQueueManager.setMoveOrder(order); // 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<
BattlerIndex,
BattlerIndex.ATTACKER
>;
const bBattlerIndex = (b instanceof Pokemon ? b : b.getPokemon()).getBattlerIndex() as Exclude<
BattlerIndex,
BattlerIndex.ATTACKER
>;
return order.indexOf(aBattlerIndex) - order.indexOf(bBattlerIndex);
});
});
} }
/** /**

View File

@ -61,14 +61,14 @@ export class FieldHelper extends GameManagerHelper {
* Helper function to return all on-field {@linkcode Pokemon} in speed order (fastest first). * 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 * @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` * (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. * Speed ties are returned in increasing order of index.
* *
* @remarks * @remarks
* This does not account for Trick Room as it does not modify the _speed_ of Pokemon on the field, * This does not account for Trick Room as it does not modify the _speed_ of Pokemon on the field,
* only their turn order. * only their turn order.
*/ */
public getSpeedOrder(indices: true): BattlerIndex[]; public getSpeedOrder(indices: true): Exclude<BattlerIndex, BattlerIndex.ATTACKER>[];
public getSpeedOrder(indices = false): BattlerIndex[] | Pokemon[] { public getSpeedOrder(indices = false): BattlerIndex[] | Pokemon[] {
const ret = this.game.scene const ret = this.game.scene
.getField(true) .getField(true)