Move array utils to its own file

This commit is contained in:
Sirz Benjie 2025-09-11 21:51:30 -05:00
parent 284df1ac1a
commit 5991907fbc
No known key found for this signature in database
GPG Key ID: 4A524B4D196C759E
28 changed files with 149 additions and 130 deletions

View File

@ -169,9 +169,13 @@ export type ReadonlyTypedArray =
*/
export type BigIntArray = BigInt64Array | BigUint64Array;
export type ReadonlyBigIntArray = ReadonlyBigInt64Array | ReadonlyBigUint64Array;
/** Any {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray | TypedArray} whose elements are not `bigint`s */
export type NumberCompatibleTypedArray = Exclude<TypedArray, BigIntArray>;
export type ReadonlyNumberCompatibleTypedArray = Exclude<ReadonlyTypedArray, ReadonlyBigIntArray>;
/**
* A partial interface of `Uint8Array` where methods that return the array type have been modified to return a more specific type.
*

View File

@ -120,6 +120,7 @@ import { vouchers } from "#system/voucher";
import { trainerConfigs } from "#trainers/trainer-config";
import type { HeldModifierConfig } from "#types/held-modifier-config";
import type { Localizable } from "#types/locales";
import type { ReadonlyUint8Array } from "#types/typed-arrays";
import { AbilityBar } from "#ui/ability-bar";
import { ArenaFlyout } from "#ui/arena-flyout";
import { CandyBar } from "#ui/candy-bar";
@ -866,7 +867,7 @@ export class BattleScene extends SceneBase {
gender?: Gender,
shiny?: boolean,
variant?: Variant,
ivs?: number[],
ivs?: ReadonlyUint8Array | number[],
nature?: Nature,
dataSource?: Pokemon | PokemonData,
postProcess?: (playerPokemon: PlayerPokemon) => void,

View File

@ -64,6 +64,7 @@ import type {
} from "#types/ability-types";
import type { Localizable } from "#types/locales";
import type { Closed, Exact } from "#types/type-helpers";
import { coerceArray } from "#utils/array";
import type { Constructor } from "#utils/common";
import {
BooleanHolder,

View File

@ -14,7 +14,8 @@ import { TimeOfDay } from "#enums/time-of-day";
import { WeatherType } from "#enums/weather-type";
import type { Pokemon } from "#field/pokemon";
import type { SpeciesStatBoosterItem, SpeciesStatBoosterModifierType } from "#modifiers/modifier-type";
import { coerceArray, randSeedInt } from "#utils/common";
import { coerceArray } from "#utils/array";
import { randSeedInt } from "#utils/common";
import { getPokemonSpecies } from "#utils/pokemon-utils";
import { toCamelCase } from "#utils/strings";
import i18next from "i18next";

View File

@ -7,7 +7,8 @@ import { AnimBlendType, AnimFocus, AnimFrameTarget, ChargeAnim, CommonAnim } fro
import { MoveFlags } from "#enums/move-flags";
import { MoveId } from "#enums/move-id";
import type { Pokemon } from "#field/pokemon";
import { coerceArray, getFrameMs, type nil } from "#utils/common";
import { coerceArray } from "#utils/array";
import { getFrameMs, type nil } from "#utils/common";
import { getEnumKeys, getEnumValues } from "#utils/enums";
import { toKebabCase } from "#utils/strings";
import Phaser from "phaser";

View File

@ -88,7 +88,8 @@ import type {
TypeBoostTagType,
} from "#types/battler-tags";
import type { Mutable } from "#types/type-helpers";
import { BooleanHolder, coerceArray, getFrameMs, NumberHolder, toDmgValue } from "#utils/common";
import { coerceArray } from "#utils/array";
import { BooleanHolder, getFrameMs, NumberHolder, toDmgValue } from "#utils/common";
import { toCamelCase } from "#utils/strings";
/** Interface containing the serializable fields of BattlerTag */

View File

@ -87,7 +87,8 @@ import type { AttackMoveResult } from "#types/attack-move-result";
import type { Localizable } from "#types/locales";
import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString, MoveMessageFunc } from "#types/move-types";
import type { TurnMove } from "#types/turn-move";
import { BooleanHolder, coerceArray, type Constructor, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common";
import { BooleanHolder, type Constructor, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common";
import { coerceArray } from "#utils/array";
import { getEnumValues } from "#utils/enums";
import { toCamelCase, toTitleCase } from "#utils/strings";
import i18next from "i18next";

View File

@ -15,7 +15,7 @@ import { WeatherType } from "#enums/weather-type";
import type { PlayerPokemon } from "#field/pokemon";
import { AttackTypeBoosterModifier } from "#modifiers/modifier";
import type { AttackTypeBoosterModifierType } from "#modifiers/modifier-type";
import { coerceArray } from "#utils/common";
import { coerceArray } from "#utils/array";
export interface EncounterRequirement {
meetsRequirement(): boolean; // Boolean to see if a requirement is met

View File

@ -25,7 +25,8 @@ import {
StatusEffectRequirement,
WaveRangeRequirement,
} from "#mystery-encounters/mystery-encounter-requirements";
import { coerceArray, randSeedInt } from "#utils/common";
import { coerceArray } from "#utils/array";
import { randSeedInt } from "#utils/common";
import { capitalizeFirstLetter } from "#utils/strings";
export interface EncounterStartOfBattleEffect {

View File

@ -3,7 +3,7 @@ import type { MoveId } from "#enums/move-id";
import type { PlayerPokemon } from "#field/pokemon";
import { PokemonMove } from "#moves/pokemon-move";
import { EncounterPokemonRequirement } from "#mystery-encounters/mystery-encounter-requirements";
import { coerceArray } from "#utils/common";
import { coerceArray } from "#utils/array";
/**
* {@linkcode CanLearnMoveRequirement} options

View File

@ -49,7 +49,8 @@ import type { HeldModifierConfig } from "#types/held-modifier-config";
import type { OptionSelectConfig, OptionSelectItem } from "#ui/abstract-option-select-ui-handler";
import type { PartyOption, PokemonSelectFilter } from "#ui/party-ui-handler";
import { PartyUiMode } from "#ui/party-ui-handler";
import { coerceArray, randomString, randSeedInt, randSeedItem } from "#utils/common";
import { coerceArray } from "#utils/array";
import { randomString, randSeedInt, randSeedItem } from "#utils/common";
import { getPokemonSpecies } from "#utils/pokemon-utils";
import i18next from "i18next";

View File

@ -11,7 +11,8 @@ import type { TimeOfDay } from "#enums/time-of-day";
import { WeatherType } from "#enums/weather-type";
import type { Pokemon } from "#field/pokemon";
import type { PokemonFormChangeItemModifier } from "#modifiers/modifier";
import { type Constructor, coerceArray } from "#utils/common";
import { coerceArray } from "#utils/array";
import type { Constructor } from "#utils/common";
import { toCamelCase } from "#utils/strings";
import i18next from "i18next";

View File

@ -44,7 +44,8 @@ import type {
TrainerTierPools,
} from "#types/trainer-funcs";
import type { Mutable } from "#types/type-helpers";
import { coerceArray, randSeedInt, randSeedIntRange, randSeedItem } from "#utils/common";
import { coerceArray } from "#utils/array";
import { randSeedInt, randSeedIntRange, randSeedItem } from "#utils/common";
import { getPokemonSpecies } from "#utils/pokemon-utils";
import { toCamelCase, toTitleCase } from "#utils/strings";
import i18next from "i18next";

View File

@ -1,6 +1,7 @@
import { globalScene } from "#app/global-scene";
import { Pokemon } from "#field/pokemon";
import { coerceArray, fixedInt, randInt } from "#utils/common";
import { coerceArray } from "#utils/array";
import { fixedInt, randInt } from "#utils/common";
export class PokemonSpriteSparkleHandler {
private sprites: Set<Phaser.GameObjects.Sprite>;

View File

@ -146,16 +146,17 @@ import type { DamageCalculationResult, DamageResult } from "#types/damage-result
import type { IllusionData } from "#types/illusion-data";
import type { StarterDataEntry, StarterMoveset } from "#types/save-data";
import type { TurnMove } from "#types/turn-move";
import type { ReadonlyUint8Array } from "#types/typed-arrays";
import { BattleInfo } from "#ui/battle-info";
import { EnemyBattleInfo } from "#ui/enemy-battle-info";
import type { PartyOption } from "#ui/party-ui-handler";
import { PartyUiHandler, PartyUiMode } from "#ui/party-ui-handler";
import { PlayerBattleInfo } from "#ui/player-battle-info";
import { coerceArray, setTypedArray } from "#utils/array";
import { applyChallenges } from "#utils/challenge-utils";
import {
BooleanHolder,
type Constructor,
coerceArray,
deltaRgb,
fixedInt,
getIvsFromId,
@ -168,7 +169,6 @@ import {
rgbaToInt,
rgbHexToRgba,
rgbToHsv,
subArray,
toDmgValue,
} from "#utils/common";
import { getEnumValues } from "#utils/enums";
@ -312,7 +312,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
gender?: Gender,
shiny?: boolean,
variant?: Variant,
ivs?: Uint8Array | number[],
ivs?: ReadonlyUint8Array | number[],
nature?: Nature,
dataSource?: Pokemon | PokemonData,
) {
@ -346,8 +346,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
if (dataSource) {
this.id = dataSource.id;
this.hp = dataSource.hp;
this.stats.set(subArray(dataSource.stats, 6));
this.ivs.set(subArray(dataSource.ivs, 6));
setTypedArray(this.stats, dataSource.stats);
setTypedArray(this.ivs, dataSource.ivs ?? getIvsFromId(dataSource.id));
this.passive = !!dataSource.passive;
if (this.variant === undefined) {
this.variant = 0;
@ -386,7 +386,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
this.stellarTypesBoosted = dataSource.stellarTypesBoosted ?? [];
} else {
this.id = randSeedInt(4294967296);
this.ivs.set(subArray(ivs ?? getIvsFromId(this.id), 6));
setTypedArray(this.ivs, ivs ?? getIvsFromId(this.id));
if (this.gender === undefined) {
this.gender = this.species.generateGender();
@ -5701,7 +5701,7 @@ export class PlayerPokemon extends Pokemon {
gender?: Gender,
shiny?: boolean,
variant?: Variant,
ivs?: Uint8Array | number[],
ivs?: ReadonlyUint8Array | number[],
nature?: Nature,
dataSource?: Pokemon | PokemonData,
) {

View File

@ -1,4 +1,4 @@
import { coerceArray } from "#utils/common";
import { coerceArray } from "#utils/array";
let manifest: object;

View File

@ -1,4 +1,4 @@
import { coerceArray } from "#utils/common";
import { coerceArray } from "#utils/array";
export const legacyCompatibleImages: string[] = [];

View File

@ -1,5 +1,6 @@
import { globalScene } from "#app/global-scene";
import { coerceArray, fixedInt } from "#utils/common";
import { coerceArray } from "#utils/array";
import { fixedInt } from "#utils/common";
export enum PokemonIconAnimMode {
NONE,

101
src/utils/array.ts Normal file
View File

@ -0,0 +1,101 @@
import type {
ReadonlyBigInt64Array,
ReadonlyBigIntArray,
ReadonlyBigUint64Array,
ReadonlyGenericArray,
ReadonlyNumberCompatibleTypedArray,
ReadonlyTypedArray,
TypedArray,
} from "#types/typed-arrays";
/**
* If the input isn't already an array, turns it into one.
* @returns An array with the same type as the type of the input
*/
export function coerceArray<T>(input: T): T extends any[] ? T : [T];
export function coerceArray<T>(input: T): T | [T] {
return Array.isArray(input) ? input : [input];
}
/**
* Type guard to check if an input is a typed array, defined as an `ArrayBufferView` that is not a `DataView`.
*/
export function isTypedArray(input: unknown): input is TypedArray {
return ArrayBuffer.isView(input) && !(input instanceof DataView);
}
/**
* Get a subarray view of the first `n` elements of an array-like object, to use
* with constructing a new one, with minimal allocaitons.
*
* @remarks
* This is primarily useful for setting elements of a `TypedArray` using its `set` method.
*
* @privateRemarks
* Note that if this is used with a tuple type, typescript will improperly set
* the return type to be a tuple of the same length as input.
*
* @param arr - The array-like object to take elements from
* @param n - The maximum number of elements to take. If
* @returns An array-like object whose `length` property is guaranteed to be <= `n`
*
* @typeParam T - The element type of the array-like object
* @typeParam A - The type of the array-like object
*
* @example
* ```
* const arr = new Uint8Array(3);
* const other = [1, 2, 3, 4, 5];
* // Using arr.set(other) would throw, as other.length > arr.length
* arr.set(subArray(other, arr.length));
*
* @throws {TypeError}
* If `arr` is not an array or typed array (though typescript should prevent this)
*/
export function subArray<const A extends ReadonlyTypedArray | readonly unknown[]>(arr: A, n: number): typeof arr {
if (arr.length <= n) {
return arr;
}
const len = Math.min(arr.length, n);
if (Array.isArray(arr)) {
// The only path with a new allocation
return arr.slice(0, len) as typeof arr;
}
if (isTypedArray(arr)) {
return arr.subarray(0, len) as typeof arr;
}
throw new TypeError("Expecting an array or typed array");
}
/**
* Store multiple values in the typed array, from input values from a specified array, without
* the possibility of a `RangeError` being thrown.
*
* @remarks
* Almost equivalent to calling `target.set(source, offset)`, though ensures that
* `RangeError` can never be thrown by clamping the number of elements taken from `source`
* to the available space in `target` starting at `offset`.
*
* @param target - The typed array to set values in
* @param source - The array-like object to take values from
* @param offset - The offset in `target` to start writing values at, default `0`
*
* @see {@linkcode https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/set | TypedArray#set}
*/
export function setTypedArray<const T extends ReadonlyTypedArray | ReadonlyGenericArray>(
target: T,
source: T extends ReadonlyBigInt64Array | ReadonlyBigUint64Array
? ReadonlyBigIntArray | readonly bigint[]
: ReadonlyNumberCompatibleTypedArray | readonly number[],
offset = 0,
): void {
if (offset < 0 || offset >= target.length) {
return;
}
// @ts-expect-error - TS can't link the conditional type of source to the conditional type of target
// in the body here, despite the fact that the function signature guarantees it.
target.set(subArray(source, target.length - offset), offset);
}

View File

@ -1,13 +1,6 @@
import { pokerogueApi } from "#api/pokerogue-api";
import { MoneyFormat } from "#enums/money-format";
import type { Variant } from "#sprites/variant";
import type {
BigIntArray,
NumberCompatibleTypedArray,
ReadonlyGenericArray,
ReadonlyTypedArray,
TypedArray,
} from "#types/typed-arrays";
import i18next from "i18next";
export type nil = null | undefined;
@ -513,95 +506,3 @@ export function getShinyDescriptor(variant: Variant): string {
return i18next.t("common:commonShiny");
}
}
/**
* If the input isn't already an array, turns it into one.
* @returns An array with the same type as the type of the input
*/
export function coerceArray<T>(input: T): T extends any[] ? T : [T];
export function coerceArray<T>(input: T): T | [T] {
return Array.isArray(input) ? input : [input];
}
/**
* Type guard to check if an input is a typed array, defined as an `ArrayBufferView` that is not a `DataView`.
*/
export function isTypedArray(input: unknown): input is TypedArray {
return ArrayBuffer.isView(input) && !(input instanceof DataView);
}
/**
* Get a subarray view of the first `n` elements of an array-like object, to use
* with constructing a new one, with minimal allocaitons.
*
* @remarks
* This is primarily useful for setting elements of a `TypedArray` using its `set` method.
*
* @privateRemarks
* Note that if this is used with a tuple type, typescript will improperly set
* the return type to be a tuple of the same length as input.
*
* @param arr - The array-like object to take elements from
* @param n - The maximum number of elements to take. If
* @returns An array-like object whose `length` property is guaranteed to be <= `n`
*
* @typeParam T - The element type of the array-like object
* @typeParam A - The type of the array-like object
*
* @example
* ```
* const arr = new Uint8Array(3);
* const other = [1, 2, 3, 4, 5];
* // Using arr.set(other) would throw, as other.length > arr.length
* arr.set(subArray(other, arr.length));
*
* @throws {TypeError}
* If `arr` is not an array or typed array (though typescript should prevent this)
*/
export function subArray<const A extends TypedArray | readonly unknown[]>(arr: A, n: number): typeof arr {
if (arr.length <= n) {
return arr;
}
const len = Math.min(arr.length, n);
if (Array.isArray(arr)) {
// The only path with a new allocation
return arr.slice(0, len) as typeof arr;
}
if (isTypedArray(arr)) {
return arr.subarray(0, len) as typeof arr;
}
throw new TypeError("Expecting an array or typed array");
}
/**
* Store multiple values in the typed array, from input values from a specified array, without
* the possibility of a `RangeError` being thrown.
*
* @remarks
* Almost equivalent to calling `target.set(source, offset)`, though ensures that
* `RangeError` can never be thrown by clamping the number of elements taken from `source`
* to the available space in `target` starting at `offset`.
*
* @param target - The typed array to set values in
* @param source - The array-like object to take values from
* @param offset - The offset in `target` to start writing values at. Defaults to `0`
*
* @see {@linkcode https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/set | TypedArray#set}
*/
export function setTypedArray<const T extends ReadonlyTypedArray | ReadonlyGenericArray>(
target: T,
source: T extends BigIntArray
? Readonly<BigIntArray> | readonly bigint[]
: Readonly<NumberCompatibleTypedArray> | readonly number[],
offset = 0,
): void {
if (offset < 0 || offset >= target.length) {
return;
}
// @ts-expect-error - TS can't link the conditional type of source to the conditional type of target
// in the body here, despite the fact that the function signature guarantees it.
target.set(subArray(source, target.length - offset), offset);
}

View File

@ -12,7 +12,7 @@ import type { CommandPhase } from "#phases/command-phase";
import type { EnemyCommandPhase } from "#phases/enemy-command-phase";
import { MoveEffectPhase } from "#phases/move-effect-phase";
import { GameManagerHelper } from "#test/test-utils/helpers/game-manager-helper";
import { coerceArray } from "#utils/common";
import { coerceArray } from "#utils/array";
import { toTitleCase } from "#utils/strings";
import type { MockInstance } from "vitest";
import { expect, vi } from "vitest";

View File

@ -20,7 +20,8 @@ import { WeatherType } from "#enums/weather-type";
import type { ModifierOverride } from "#modifiers/modifier-type";
import type { Variant } from "#sprites/variant";
import { GameManagerHelper } from "#test/test-utils/helpers/game-manager-helper";
import { coerceArray, shiftCharCodes } from "#utils/common";
import { coerceArray } from "#utils/array";
import { shiftCharCodes } from "#utils/common";
import chalk from "chalk";
import { vi } from "vitest";

View File

@ -7,7 +7,7 @@ import Overrides from "#app/overrides";
import { MoveId } from "#enums/move-id";
import { getEnumStr } from "#test/test-utils/string-utils";
import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils";
import { coerceArray } from "#utils/common";
import { coerceArray } from "#utils/array";
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
/**

View File

@ -1,6 +1,6 @@
import { DEBUG_COLOR, NEW_TURN_COLOR, TRACE_COLOR, UI_MSG_COLOR } from "#app/constants/colors";
import { inferColorFormat } from "#test/test-utils/mocks/mock-console/infer-color";
import { coerceArray } from "#utils/common";
import { coerceArray } from "#utils/array";
import { type InspectOptions, inspect } from "node:util";
import chalk, { type ChalkInstance } from "chalk";

View File

@ -1,6 +1,6 @@
import type { MockGameObject } from "#test/test-utils/mocks/mock-game-object";
import type { MockTextureManager } from "#test/test-utils/mocks/mock-texture-manager";
import { coerceArray } from "#utils/common";
import { coerceArray } from "#utils/array";
export class MockContainer implements MockGameObject {
protected x: number;

View File

@ -1,5 +1,5 @@
import type { MockGameObject } from "#test/test-utils/mocks/mock-game-object";
import { coerceArray } from "#utils/common";
import { coerceArray } from "#utils/array";
export class MockRectangle implements MockGameObject {
private fillColor;

View File

@ -1,5 +1,5 @@
import type { MockGameObject } from "#test/test-utils/mocks/mock-game-object";
import { coerceArray } from "#utils/common";
import { coerceArray } from "#utils/array";
import Phaser from "phaser";
type Frame = Phaser.Textures.Frame;

View File

@ -1,8 +1,8 @@
import { setTypedArray, subArray } from "#utils/common";
import { setTypedArray, subArray } from "#utils/array";
import { describe, expect, it } from "vitest";
/**
* Unit tests for the utility methods in `src/utils/common.ts`
* Unit tests for the utility methods in `src/utils/array-utils.ts`
* @module
*/