Doc fixes & test changes

This commit is contained in:
Bertie690 2025-09-21 14:13:04 -04:00
parent cf5e7fd981
commit 0dbb68c3ba
10 changed files with 91 additions and 86 deletions

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 { 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<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);
}
@ -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`.

View File

@ -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();

View File

@ -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<MovePhase>
}
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<MovePhase>
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<MovePhase>
}
}
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<MovePhase>
}
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);
}

View File

@ -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<T extends DynamicPhase> extends PriorityQueue<T> {
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);
}
}

View File

@ -11,7 +11,7 @@ import { sortInSpeedOrder } from "#app/utils/speed-order";
*/
export class PostSummonPhasePriorityQueue extends PokemonPhasePriorityQueue<PostSummonPhase> {
protected override reorder(): void {
this.queue = sortInSpeedOrder(this.queue, false);
sortInSpeedOrder(this.queue, false);
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> {
/** 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<T> {
}
/**
* Removes all elements from the queue
* Remove all elements from the queue.
* @sealed
*/
public clear(): void {
@ -50,8 +55,8 @@ export abstract class PriorityQueue<T> {
}
/**
* 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<T> {
}
/** @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);
}
}

View File

@ -135,8 +135,10 @@ export function randSeedItem<T>(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<T>(items: T[]): T[] {
if (items.length <= 1) {

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.
* @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

View File

@ -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<T extends Pokemon | hasPokemon>(pokemonList: T[], shuffleFirst = true): T[] {
pokemonList = shuffleFirst ? shufflePokemonList(pokemonList) : pokemonList;
export function sortInSpeedOrder<T extends Pokemon | hasPokemon>(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<T extends Pokemon | hasPokemon>(pokemonList: T[]): T[] {
// This is seeded with the current turn to prevent an inconsistency where it
@ -38,7 +41,7 @@ function shufflePokemonList<T extends Pokemon | hasPokemon>(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<T extends Pokemon | hasPokemon>(pokemonList: T[]): void {
pokemonList.sort((a, b) => {
const aSpeed = (a instanceof Pokemon ? a : a.getPokemon()).getEffectiveStat(Stat.SPD);

View File

@ -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<void> {
await this.phaseInterceptor.to("TurnStartPhase", false);
public setTurnOrder(order: Exclude<BattlerIndex, BattlerIndex.ATTACKER>[]): void {
// 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);
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);
});
});
}
/**