From 0ebdc1b0efbfc866149502b5cabe3143ea3996a8 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Thu, 11 Sep 2025 13:06:27 -0500 Subject: [PATCH 01/21] make IVs use Uint8Array --- src/battle-scene.ts | 10 +++++----- .../utils/encounter-phase-utils.ts | 2 +- src/field/pokemon.ts | 17 +++++++++-------- src/system/game-data.ts | 2 +- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 289c9a8f051..7352979daa0 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -866,7 +866,7 @@ export class BattleScene extends SceneBase { gender?: Gender, shiny?: boolean, variant?: Variant, - ivs?: number[], + ivs?: Uint8Array, nature?: Nature, dataSource?: Pokemon | PokemonData, postProcess?: (playerPokemon: PlayerPokemon) => void, @@ -897,12 +897,12 @@ export class BattleScene extends SceneBase { if (Overrides.IVS_OVERRIDE.some(value => !isBetween(value, 0, 31))) { throw new Error("All IVs in the player IV override must be between 0 and 31!"); } - pokemon.ivs = Overrides.IVS_OVERRIDE; + pokemon.ivs = new Uint8Array(Overrides.IVS_OVERRIDE); } else { if (!isBetween(Overrides.IVS_OVERRIDE, 0, 31)) { throw new Error("The Player IV override must be a value between 0 and 31!"); } - pokemon.ivs = new Array(6).fill(Overrides.IVS_OVERRIDE); + pokemon.ivs = new Uint8Array(6).fill(Overrides.IVS_OVERRIDE); } if (Overrides.NATURE_OVERRIDE !== null) { @@ -962,12 +962,12 @@ export class BattleScene extends SceneBase { if (Overrides.ENEMY_IVS_OVERRIDE.some(value => !isBetween(value, 0, 31))) { throw new Error("All IVs in the enemy IV override must be between 0 and 31!"); } - pokemon.ivs = Overrides.ENEMY_IVS_OVERRIDE; + pokemon.ivs = new Uint8Array(Overrides.ENEMY_IVS_OVERRIDE); } else { if (!isBetween(Overrides.ENEMY_IVS_OVERRIDE, 0, 31)) { throw new Error("The Enemy IV override must be a value between 0 and 31!"); } - pokemon.ivs = new Array(6).fill(Overrides.ENEMY_IVS_OVERRIDE); + pokemon.ivs = new Uint8Array(6).fill(Overrides.ENEMY_IVS_OVERRIDE); } if (Overrides.ENEMY_NATURE_OVERRIDE !== null) { diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 0ba0dec896a..16d388c2e96 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -318,7 +318,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): // Set IVs if (config.ivs) { - enemyPokemon.ivs = config.ivs; + enemyPokemon.ivs = new Uint8Array(config.ivs); } // Set Status diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 06e5e0d85aa..a2e352ddf5d 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -204,7 +204,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { public gender: Gender; public hp: number; public stats: number[]; - public ivs: number[]; + public ivs: Uint8Array; public nature: Nature; public moveset: PokemonMove[]; /** @@ -311,7 +311,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { gender?: Gender, shiny?: boolean, variant?: Variant, - ivs?: number[], + ivs?: Uint8Array, nature?: Nature, dataSource?: Pokemon | PokemonData, ) { @@ -346,7 +346,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { this.id = dataSource.id; this.hp = dataSource.hp; this.stats = dataSource.stats; - this.ivs = dataSource.ivs; + + this.ivs = new Uint8Array(dataSource.ivs); this.passive = !!dataSource.passive; if (this.variant === undefined) { this.variant = 0; @@ -385,7 +386,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { this.stellarTypesBoosted = dataSource.stellarTypesBoosted ?? []; } else { this.id = randSeedInt(4294967296); - this.ivs = ivs || getIvsFromId(this.id); + this.ivs = new Uint8Array(ivs || getIvsFromId(this.id)); if (this.gender === undefined) { this.gender = this.species.generateGender(); @@ -5704,7 +5705,7 @@ export class PlayerPokemon extends Pokemon { gender?: Gender, shiny?: boolean, variant?: Variant, - ivs?: number[], + ivs?: Uint8Array, nature?: Nature, dataSource?: Pokemon | PokemonData, ) { @@ -6324,9 +6325,9 @@ export class EnemyPokemon extends Pokemon { if (this.hasTrainer() && globalScene.currentBattle) { const { waveIndex } = globalScene.currentBattle; - const ivs: number[] = []; - while (ivs.length < 6) { - ivs.push(randSeedIntRange(Math.floor(waveIndex / 10), 31)); + const ivs = new Uint8Array(6); + for (let i = 0; i < 6; i++) { + ivs[i] = this.randBattleSeedIntRange(Math.floor(waveIndex / 10), 31); } this.ivs = ivs; } diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 3ffa7482706..5f89c0a6da8 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -1914,7 +1914,7 @@ export class GameData { _unlockSpeciesNature(species.speciesId); } - updateSpeciesDexIvs(speciesId: SpeciesId, ivs: number[]): void { + updateSpeciesDexIvs(speciesId: SpeciesId, ivs: Uint8Array): void { let dexEntry: DexEntry; do { dexEntry = globalScene.gameData.dexData[speciesId]; From 284df1ac1af2c9435fc53823867cc02f17d2d221 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Thu, 11 Sep 2025 19:56:33 -0500 Subject: [PATCH 02/21] Add many typed array helpers --- src/@types/typed-arrays.ts | 526 +++++++++++++++++++++++++++++++ src/battle-scene.ts | 2 +- src/data/pokemon/pokemon-data.ts | 12 +- src/field/pokemon.ts | 22 +- src/system/pokemon-data.ts | 6 +- src/utils/common.ts | 96 +++++- test/utils/common.test.ts | 122 +++++++ 7 files changed, 764 insertions(+), 22 deletions(-) create mode 100644 src/@types/typed-arrays.ts create mode 100644 test/utils/common.test.ts diff --git a/src/@types/typed-arrays.ts b/src/@types/typed-arrays.ts new file mode 100644 index 00000000000..eed7a6c37c8 --- /dev/null +++ b/src/@types/typed-arrays.ts @@ -0,0 +1,526 @@ +/* + * SPDX-FileCopyrightText: 2025 Pagefault Games + * SPDX-FileContributor: SirzBenjie + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/** + * Collection of utility types for working with + * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray | TypedArray} + * with enhanced type safety and usability. + * @module + */ + +/** + * Union type of all {@linkcode https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray | TypedArray}s + */ +export type TypedArray = + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | Float32Array + | Float64Array + | BigInt64Array + | BigUint64Array; + +/** + * A readonly version of {@linkcode https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int8Array | Int8Array} + * + * @remarks + * Is to `Int8Array` what `ReadonlyArray` is to `Array` + */ +export interface ReadonlyInt8Array + extends Omit { + subarray(begin?: number, end?: number): ReadonlyInt8Array; + readonly [index: number]: number; +} +/** + * A readonly version of {@linkcode https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array | Uint8Array} + * + * @remarks + * Is to `Uint8Array` what `ReadonlyArray` is to `Array` + */ +export interface ReadonlyUint8Array + extends Omit { + subarray(begin?: number, end?: number): ReadonlyUint8Array; + readonly [index: number]: number; +} +/** + * A readonly version of {@linkcode https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8ClampedArray | Uint8ClampedArray} + * + * @remarks + * Is to `Uint8ClampedArray` what `ReadonlyArray` is to `Array` + */ +export interface ReadonlyUint8ClampedArray + extends Omit, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> { + subarray(begin?: number, end?: number): ReadonlyUint8ClampedArray; + readonly [index: number]: number; +} +/** + * A readonly version of {@linkcode https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int16Array | Int16Array} + * + * @remarks + * Is to `Int16Array` what `ReadonlyArray` is to `Array` + */ +export interface ReadonlyInt16Array + extends Omit, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> { + subarray(begin?: number, end?: number): ReadonlyInt16Array; + readonly [index: number]: number; +} +/** + * A readonly version of {@linkcode https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint16Array | Uint16Array} + * + * @remarks + * Is to `Uint16Array` what `ReadonlyArray` is to `Array` + */ +export interface ReadonlyUint16Array + extends Omit, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> { + subarray(begin?: number, end?: number): ReadonlyUint16Array; + readonly [index: number]: number; +} +/** + * A readonly version of {@linkcode https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int32Array | Int32Array} + * + * @remarks + * Is to `Int32Array` what `ReadonlyArray` is to `Array` + */ +export interface ReadonlyInt32Array + extends Omit, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> { + subarray(begin?: number, end?: number): ReadonlyInt32Array; + readonly [index: number]: number; +} +/** + * A readonly version of {@linkcode https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint32Array | Uint32Array} + * + * @remarks + * Is to `Uint32Array` what `ReadonlyArray` is to `Array` + */ +export interface ReadonlyUint32Array + extends Omit, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> { + subarray(begin?: number, end?: number): ReadonlyUint32Array; + readonly [index: number]: number; +} +/** + * A readonly version of {@linkcode https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Float32Array | Float32Array} + * + * @remarks + * Is to `Float32Array` what `ReadonlyArray` is to `Array` + */ +export interface ReadonlyFloat32Array + extends Omit, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> { + subarray(begin?: number, end?: number): ReadonlyFloat32Array; + readonly [index: number]: number; +} +/** + * A readonly version of {@linkcode https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Float64Array | Float64Array} + * + * @remarks + * Is to `Float64Array` what `ReadonlyArray` is to `Array` + */ +export interface ReadonlyFloat64Array + extends Omit { + subarray(begin?: number, end?: number): ReadonlyFloat64Array; + readonly [index: number]: number; +} +/** + * A readonly version of {@linkcode https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt64Array | BigInt64Array} + * + * @remarks + * Is to `BigInt64Array` what `ReadonlyArray` is to `Array` + */ +export interface ReadonlyBigInt64Array + extends Omit { + subarray(begin?: number, end?: number): ReadonlyBigInt64Array; + readonly [index: number]: bigint; +} +/** + * A readonly version of {@linkcode https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigUint64Array | BigUint64Array} + * + * @remarks + * Is to `BigUint64Array` what `ReadonlyArray` is to `Array` + */ +export interface ReadonlyBigUint64Array + extends Omit { + subarray(begin?: number, end?: number): ReadonlyBigUint64Array; + readonly [index: number]: bigint; +} + +export type ReadonlyTypedArray = + | ReadonlyInt8Array + | ReadonlyUint8Array + | ReadonlyUint8ClampedArray + | ReadonlyInt16Array + | ReadonlyUint16Array + | ReadonlyInt32Array + | ReadonlyUint32Array + | ReadonlyFloat32Array + | ReadonlyFloat64Array + | ReadonlyBigInt64Array + | ReadonlyBigUint64Array; + +/** + * Either {@linkcode https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt64Array | BigInt64Array} + * or {@linkcode https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt64Array | BigUint64Array} + */ +export type BigIntArray = BigInt64Array | BigUint64Array; + +/** 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; + +/** + * A partial interface of `Uint8Array` where methods that return the array type have been modified to return a more specific type. + * + * @privateRemarks + * Excludes methods that return a new array (e.g. `subarray`, `slice`, `toReversed`, `toSorted`, `with`) as these cannot be resolved by typescript + * @internal + */ +interface PartialUint8Array extends Uint8Array { + at(index: number): T | undefined; // ES2022 + entries(): ArrayIterator<[number, T]>; + fill(value: T, start?: number, end?: number): this; + find(predicate: (value: T, index: number, array: this) => boolean, thisArg?: any): T | undefined; + findLast(predicate: (value: number, index: number, array: this) => boolean, thisArg?: any): T | undefined; + forEach(callbackfn: (value: T, index: number, array: this) => void, thisArg?: any): void; + includes(searchElement: T, fromIndex?: number): boolean; + indexOf(searchElement: T, fromIndex?: number): number; + lastIndexOf(searchElement: T, fromIndex?: number): number; + reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: this) => T): T; + reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: this) => T): T; + set(array: ArrayLike, offset?: number): void; + some(predicate: (value: T, index: number, obj: this) => boolean, thisArg?: any): boolean; + sort(compareFn?: (a: T, b: T) => number): this; + values(): ArrayIterator; + [Symbol.iterator](): ArrayIterator; + [index: number]: T; +} + +/** + * A `Uint8Array` whose elements typescript considers to be of type `T`, allowing for type-safe iteration and access. + * + * @remarks + * Useful to leverage the benefits of `TypedArrays` without losing type information. Typescript will consider the elements to be of type `T` instead of just `number`. + * @typeParam T - The specific numeric type of the elements in the array. + */ +// @ts-expect-error - These methods _will_ error, as we are overriding the return type to be more specific +// in a way that makes typescript unhappy +export interface GenericUint8Array extends PartialUint8Array { + // map(callbackfn: (value: T, index: number, array: this) => T, thisArg?: any): GenericUint8Array; + // this method does not trigger a typescript error on its own, but if we add in `toReversed` it causes issues.... + subarray(begin?: number, end?: number): GenericUint8Array; + toReversed(): GenericUint8Array; + toSorted(compareFn?: (a: T, b: T) => number): GenericUint8Array; + filter(predicate: (value: T, index: number, array: this) => any, thisArg?: any): GenericUint8Array; +} + +/** + * A partial interface of `Uint16Array` where methods that return the array type have been modified to return a more specific type. + * + * @privateRemarks + * Excludes methods that return a new array (e.g. `subarray`, `slice`, `toReversed`, `toSorted`, `with`) as these cannot be resolved by typescript + * @internal + */ +interface PartialUint16Array extends Uint16Array { + at(index: number): T | undefined; // ES2022 + entries(): ArrayIterator<[number, T]>; + fill(value: T, start?: number, end?: number): this; + find(predicate: (value: T, index: number, array: this) => boolean, thisArg?: any): T | undefined; + findLast(predicate: (value: number, index: number, array: this) => boolean, thisArg?: any): T | undefined; + forEach(callbackfn: (value: T, index: number, array: this) => void, thisArg?: any): void; + includes(searchElement: T, fromIndex?: number): boolean; + indexOf(searchElement: T, fromIndex?: number): number; + lastIndexOf(searchElement: T, fromIndex?: number): number; + reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: this) => T): T; + reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: this) => T): T; + set(array: ArrayLike, offset?: number): void; + some(predicate: (value: T, index: number, obj: this) => boolean, thisArg?: any): boolean; + sort(compareFn?: (a: T, b: T) => number): this; + values(): ArrayIterator; + [Symbol.iterator](): ArrayIterator; + [index: number]: T; +} + +/** + * A `Uint16Array` whose elements typescript considers to be of type `T`, allowing for type-safe iteration and access. + * + * @remarks + * Useful to leverage the benefits of `TypedArrays` without losing type information. Typescript will consider the elements to be of type `T` instead of just `number`. + * @typeParam T - The specific numeric type of the elements in the array. + */ +// @ts-expect-error - These methods _will_ error, as we are overriding the return type to be more specific +// in a way that makes typescript unhappy +export interface GenericUint16Array extends PartialUint16Array { + // map(callbackfn: (value: T, index: number, array: this) => T, thisArg?: any): GenericUint16Array; + // this method does not trigger a typescript error on its own, but if we add in `toReversed` it causes issues.... + subarray(begin?: number, end?: number): GenericUint16Array; + toReversed(): GenericUint16Array; + toSorted(compareFn?: (a: T, b: T) => number): GenericUint16Array; + filter(predicate: (value: T, index: number, array: this) => any, thisArg?: any): GenericUint16Array; +} + +/** + * A partial interface of `Uint32Array` where methods that return the array type have been modified to return a more specific type. + * + * @privateRemarks + * Excludes methods that return a new array (e.g. `subarray`, `slice`, `toReversed`, `toSorted`, `with`) as these cannot be resolved by typescript + * @internal + */ +interface PartialUint32Array extends Uint32Array { + at(index: number): T | undefined; // ES2022 + entries(): ArrayIterator<[number, T]>; + fill(value: T, start?: number, end?: number): this; + find(predicate: (value: T, index: number, array: this) => boolean, thisArg?: any): T | undefined; + findLast(predicate: (value: number, index: number, array: this) => boolean, thisArg?: any): T | undefined; + forEach(callbackfn: (value: T, index: number, array: this) => void, thisArg?: any): void; + includes(searchElement: T, fromIndex?: number): boolean; + indexOf(searchElement: T, fromIndex?: number): number; + lastIndexOf(searchElement: T, fromIndex?: number): number; + reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: this) => T): T; + reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: this) => T): T; + set(array: ArrayLike, offset?: number): void; + some(predicate: (value: T, index: number, obj: this) => boolean, thisArg?: any): boolean; + sort(compareFn?: (a: T, b: T) => number): this; + values(): ArrayIterator; + [Symbol.iterator](): ArrayIterator; + [index: number]: T; +} + +/** + * A `Uint32Array` whose elements typescript considers to be of type `T`, allowing for type-safe iteration and access. + * + * @remarks + * Useful to leverage the benefits of `TypedArrays` without losing type information. Typescript will consider the elements to be of type `T` instead of just `number`. + * @typeParam T - The specific numeric type of the elements in the array. + */ +// @ts-expect-error - These methods _will_ error, as we are overriding the return type to be more specific +// in a way that makes typescript unhappy +export interface GenericUint32Array extends PartialUint32Array { + // map(callbackfn: (value: T, index: number, array: this) => T, thisArg?: any): GenericUint32Array; + // this method does not trigger a typescript error on its own, but if we add in `toReversed` it causes issues.... + subarray(begin?: number, end?: number): GenericUint32Array; + toReversed(): GenericUint32Array; + toSorted(compareFn?: (a: T, b: T) => number): GenericUint32Array; + filter(predicate: (value: T, index: number, array: this) => any, thisArg?: any): GenericUint32Array; +} + +/** + * A partial interface of `Int8Array` where methods that return the array type have been modified to return a more specific type. + * + * @privateRemarks + * Excludes methods that return a new array (e.g. `subarray`, `slice`, `toReversed`, `toSorted`, `with`) as these cannot be resolved by typescript + * @internal + */ +interface PartialInt8Array extends Int8Array { + at(index: number): T | undefined; // ES2022 + entries(): ArrayIterator<[number, T]>; + fill(value: T, start?: number, end?: number): this; + find(predicate: (value: T, index: number, array: this) => boolean, thisArg?: any): T | undefined; + findLast(predicate: (value: number, index: number, array: this) => boolean, thisArg?: any): T | undefined; + forEach(callbackfn: (value: T, index: number, array: this) => void, thisArg?: any): void; + includes(searchElement: T, fromIndex?: number): boolean; + indexOf(searchElement: T, fromIndex?: number): number; + lastIndexOf(searchElement: T, fromIndex?: number): number; + reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: this) => T): T; + reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: this) => T): T; + set(array: ArrayLike, offset?: number): void; + some(predicate: (value: T, index: number, obj: this) => boolean, thisArg?: any): boolean; + sort(compareFn?: (a: T, b: T) => number): this; + values(): ArrayIterator; + [Symbol.iterator](): ArrayIterator; + [index: number]: T; +} + +/** + * A `Int8Array` whose elements typescript considers to be of type `T`, allowing for type-safe iteration and access. + * + * @remarks + * Useful to leverage the benefits of `TypedArrays` without losing type information. Typescript will consider the elements to be of type `T` instead of just `number`. + * @typeParam T - The specific numeric type of the elements in the array. + */ +// @ts-expect-error - These methods _will_ error, as we are overriding the return type to be more specific +// in a way that makes typescript unhappy +export interface GenericInt8Array extends PartialInt8Array { + // map(callbackfn: (value: T, index: number, array: this) => T, thisArg?: any): GenericInt8Array; + // this method does not trigger a typescript error on its own, but if we add in `toReversed` it causes issues.... + subarray(begin?: number, end?: number): GenericInt8Array; + toReversed(): GenericInt8Array; + toSorted(compareFn?: (a: T, b: T) => number): GenericInt8Array; + filter(predicate: (value: T, index: number, array: this) => any, thisArg?: any): GenericInt8Array; +} + +/** + * A partial interface of `Int16Array` where methods that return the array type have been modified to return a more specific type. + * + * @privateRemarks + * Excludes methods that return a new array (e.g. `subarray`, `slice`, `toReversed`, `toSorted`, `with`) as these cannot be resolved by typescript + * @internal + */ +interface PartialInt16Array extends Int16Array { + at(index: number): T | undefined; // ES2022 + entries(): ArrayIterator<[number, T]>; + fill(value: T, start?: number, end?: number): this; + find(predicate: (value: T, index: number, array: this) => boolean, thisArg?: any): T | undefined; + findLast(predicate: (value: number, index: number, array: this) => boolean, thisArg?: any): T | undefined; + forEach(callbackfn: (value: T, index: number, array: this) => void, thisArg?: any): void; + includes(searchElement: T, fromIndex?: number): boolean; + indexOf(searchElement: T, fromIndex?: number): number; + lastIndexOf(searchElement: T, fromIndex?: number): number; + reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: this) => T): T; + reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: this) => T): T; + set(array: ArrayLike, offset?: number): void; + some(predicate: (value: T, index: number, obj: this) => boolean, thisArg?: any): boolean; + sort(compareFn?: (a: T, b: T) => number): this; + values(): ArrayIterator; + [Symbol.iterator](): ArrayIterator; + [index: number]: T; +} + +/** + * A `Int16Array` whose elements typescript considers to be of type `T`, allowing for type-safe iteration and access. + * + * @remarks + * Useful to leverage the benefits of `TypedArrays` without losing type information. Typescript will consider the elements to be of type `T` instead of just `number`. + * @typeParam T - The specific numeric type of the elements in the array. + */ +// @ts-expect-error - These methods _will_ error, as we are overriding the return type to be more specific +// in a way that makes typescript unhappy +export interface GenericInt16Array extends PartialInt16Array { + // map(callbackfn: (value: T, index: number, array: this) => T, thisArg?: any): GenericInt16Array; + // this method does not trigger a typescript error on its own, but if we add in `toReversed` it causes issues.... + subarray(begin?: number, end?: number): GenericInt16Array; + toReversed(): GenericInt16Array; + toSorted(compareFn?: (a: T, b: T) => number): GenericInt16Array; + filter(predicate: (value: T, index: number, array: this) => any, thisArg?: any): GenericInt16Array; +} + +/** + * A partial interface of `Int32Array` where methods that return the array type have been modified to return a more specific type. + * + * @privateRemarks + * Excludes methods that return a new array (e.g. `subarray`, `slice`, `toReversed`, `toSorted`, `with`) as these cannot be resolved by typescript + * @internal + */ +interface PartialInt32Array extends Int32Array { + at(index: number): T | undefined; // ES2022 + entries(): ArrayIterator<[number, T]>; + fill(value: T, start?: number, end?: number): this; + find(predicate: (value: T, index: number, array: this) => boolean, thisArg?: any): T | undefined; + findLast(predicate: (value: number, index: number, array: this) => boolean, thisArg?: any): T | undefined; + forEach(callbackfn: (value: T, index: number, array: this) => void, thisArg?: any): void; + includes(searchElement: T, fromIndex?: number): boolean; + indexOf(searchElement: T, fromIndex?: number): number; + lastIndexOf(searchElement: T, fromIndex?: number): number; + reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: this) => T): T; + reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: this) => T): T; + set(array: ArrayLike, offset?: number): void; + some(predicate: (value: T, index: number, obj: this) => boolean, thisArg?: any): boolean; + sort(compareFn?: (a: T, b: T) => number): this; + values(): ArrayIterator; + [Symbol.iterator](): ArrayIterator; + [index: number]: T; +} + +/** + * A `Int32Array` whose elements typescript considers to be of type `T`, allowing for type-safe iteration and access. + * + * @remarks + * Useful to leverage the benefits of `TypedArrays` without losing type information. Typescript will consider the elements to be of type `T` instead of just `number`. + * @typeParam T - The specific numeric type of the elements in the array. + */ +// @ts-expect-error - These methods _will_ error, as we are overriding the return type to be more specific +// in a way that makes typescript unhappy +export interface GenericInt32Array extends PartialInt32Array { + // map(callbackfn: (value: T, index: number, array: this) => T, thisArg?: any): GenericInt32Array; + // this method does not trigger a typescript error on its own, but if we add in `toReversed` it causes issues.... + subarray(begin?: number, end?: number): GenericInt32Array; + toReversed(): GenericInt32Array; + toSorted(compareFn?: (a: T, b: T) => number): GenericInt32Array; + filter(predicate: (value: T, index: number, array: this) => any, thisArg?: any): GenericInt32Array; +} + +/** + * A readonly version of {@link GenericUint8Array} where elements are of type `T`. + * @typeParam T - The specific numeric type of the elements in the array. + * @remarks + * Is to `GenericUint8Array` what `ReadonlyArray` is to `Array` + */ +export interface ReadonlyGenericUint8Array + extends Omit, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> { + subarray(begin?: number, end?: number): ReadonlyGenericUint8Array; + readonly [index: number]: T; +} + +/** + * A readonly version of {@link GenericUint16Array} where elements are of type `T`. + * @typeParam T - The specific numeric type of the elements in the array. + * @remarks + * Is to `GenericUint16Array` what `ReadonlyArray` is to `Array` + */ +export interface ReadonlyGenericUint16Array + extends Omit, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> { + subarray(begin?: number, end?: number): ReadonlyGenericUint16Array; + readonly [index: number]: T; +} + +/** + * A readonly version of {@link GenericUint32Array} where elements are of type `T`. + * @typeParam T - The specific numeric type of the elements in the array. + * @remarks + * Is to `GenericUint32Array` what `ReadonlyArray` is to `Array` + */ +export interface ReadonlyGenericUint32Array + extends Omit, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> { + subarray(begin?: number, end?: number): ReadonlyGenericUint32Array; + readonly [index: number]: T; +} + +/** + * A readonly version of {@link GenericInt8Array} where elements are of type `T`. + * @typeParam T - The specific numeric type of the elements in the array. + * @remarks + * Is to `GenericInt8Array` what `ReadonlyArray` is to `Array` + */ +export interface ReadonlyGenericInt8Array + extends Omit, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> { + subarray(begin?: number, end?: number): ReadonlyGenericInt8Array; + readonly [index: number]: T; +} + +/** + * A readonly version of {@link GenericInt16Array} where elements are of type `T`. + * @typeParam T - The specific numeric type of the elements in the array. + * @remarks + * Is to `GenericInt16Array` what `ReadonlyArray` is to `Array` + */ +export interface ReadonlyGenericInt16Array + extends Omit, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> { + subarray(begin?: number, end?: number): ReadonlyGenericInt16Array; + readonly [index: number]: T; +} + +/** + * A readonly version of {@link GenericInt32Array} where elements are of type `T`. + * @typeParam T - The specific numeric type of the elements in the array. + * @remarks + * Is to `GenericInt32Array` what `ReadonlyArray` is to `Array` + */ +export interface ReadonlyGenericInt32Array + extends Omit, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> { + subarray(begin?: number, end?: number): ReadonlyGenericInt32Array; + readonly [index: number]: T; +} + +/** + * A union type of all `GenericTypedArray`s + */ +export type ReadonlyGenericArray = + | ReadonlyGenericUint8Array + | ReadonlyGenericUint16Array + | ReadonlyGenericUint32Array + | ReadonlyGenericInt8Array + | ReadonlyGenericInt16Array + | ReadonlyGenericInt32Array; diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 7352979daa0..70a3e1dcf56 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -866,7 +866,7 @@ export class BattleScene extends SceneBase { gender?: Gender, shiny?: boolean, variant?: Variant, - ivs?: Uint8Array, + ivs?: number[], nature?: Nature, dataSource?: Pokemon | PokemonData, postProcess?: (playerPokemon: PlayerPokemon) => void, diff --git a/src/data/pokemon/pokemon-data.ts b/src/data/pokemon/pokemon-data.ts index 4fbb70bccb2..e213d55abfd 100644 --- a/src/data/pokemon/pokemon-data.ts +++ b/src/data/pokemon/pokemon-data.ts @@ -16,6 +16,7 @@ import type { AttackMoveResult } from "#types/attack-move-result"; import type { IllusionData } from "#types/illusion-data"; import type { TurnMove } from "#types/turn-move"; import type { CoerceNullPropertiesToUndefined } from "#types/type-helpers"; +import { setTypedArray } from "#utils/common"; import { getPokemonSpeciesForm } from "#utils/pokemon-utils"; /** @@ -129,7 +130,7 @@ export class PokemonSummonData { public passiveAbility: AbilityId | undefined; public gender: Gender | undefined; public fusionGender: Gender | undefined; - public stats: number[] = [0, 0, 0, 0, 0, 0]; + public stats: Uint32Array = new Uint32Array(6); public moveset: PokemonMove[] | null; // If not initialized this value will not be populated from save data. @@ -165,6 +166,11 @@ export class PokemonSummonData { continue; } + if (key === "stats") { + setTypedArray(this.stats, source.stats); + continue; + } + if (key === "illusion" && typeof value === "object") { // Make a copy so as not to mutate provided value const illusionData = { @@ -221,8 +227,10 @@ export class PokemonSummonData { // We coerce null to undefined in the type, as the for loop below replaces `null` with `undefined` ...(this as Omit< CoerceNullPropertiesToUndefined, - "speciesForm" | "fusionSpeciesForm" | "illusion" + "speciesForm" | "fusionSpeciesForm" | "illusion" | "stats" >), + // TypedArrays do not serialize to JSON as an array. + stats: Array.from(this.stats), speciesForm: speciesForm == null ? undefined : { id: speciesForm.speciesId, formIdx: speciesForm.formIndex }, fusionSpeciesForm: fusionSpeciesForm == null diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index a2e352ddf5d..e6a993f9fb3 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -168,6 +168,7 @@ import { rgbaToInt, rgbHexToRgba, rgbToHsv, + subArray, toDmgValue, } from "#utils/common"; import { getEnumValues } from "#utils/enums"; @@ -203,7 +204,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { public levelExp: number; public gender: Gender; public hp: number; - public stats: number[]; + public stats = new Uint32Array(6).fill(1); public ivs: Uint8Array; public nature: Nature; public moveset: PokemonMove[]; @@ -311,7 +312,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { gender?: Gender, shiny?: boolean, variant?: Variant, - ivs?: Uint8Array, + ivs?: Uint8Array | number[], nature?: Nature, dataSource?: Pokemon | PokemonData, ) { @@ -345,9 +346,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { if (dataSource) { this.id = dataSource.id; this.hp = dataSource.hp; - this.stats = dataSource.stats; - - this.ivs = new Uint8Array(dataSource.ivs); + this.stats.set(subArray(dataSource.stats, 6)); + this.ivs.set(subArray(dataSource.ivs, 6)); 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 = new Uint8Array(ivs || getIvsFromId(this.id)); + this.ivs.set(subArray(ivs ?? getIvsFromId(this.id), 6)); if (this.gender === undefined) { this.gender = this.species.generateGender(); @@ -1320,7 +1320,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @param bypassSummonData - Whether to prefer actual stats (`true`) or in-battle overridden stats (`false`); default `true` * @returns The numeric values of this {@linkcode Pokemon}'s stats as an array. */ - getStats(bypassSummonData = true): number[] { + getStats(bypassSummonData = true): ArrayLike { if (!bypassSummonData) { // Only grab summon data stats if nonzero return this.summonData.stats.map((s, i) => s || this.stats[i]); @@ -1552,10 +1552,6 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } calculateStats(): void { - if (!this.stats) { - this.stats = [0, 0, 0, 0, 0, 0]; - } - // Get and manipulate base stats const baseStats = this.calculateBaseStats(); // Using base stats, calculate and store stats one by one @@ -1588,7 +1584,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { globalScene.applyModifier(PokemonIncrementingStatModifier, this.isPlayer(), this, s, statHolder); } - statHolder.value = Phaser.Math.Clamp(statHolder.value, 1, Number.MAX_SAFE_INTEGER); + statHolder.value = Phaser.Math.Clamp(statHolder.value, 1, 0xffffffff); this.setStat(s, statHolder.value); } @@ -5705,7 +5701,7 @@ export class PlayerPokemon extends Pokemon { gender?: Gender, shiny?: boolean, variant?: Variant, - ivs?: Uint8Array, + ivs?: Uint8Array | number[], nature?: Nature, dataSource?: Pokemon | PokemonData, ) { diff --git a/src/system/pokemon-data.ts b/src/system/pokemon-data.ts index 0ddfedeff84..4aa036f621c 100644 --- a/src/system/pokemon-data.ts +++ b/src/system/pokemon-data.ts @@ -99,8 +99,8 @@ export class PokemonData { this.levelExp = source.levelExp; this.gender = source.gender; this.hp = source.hp; - this.stats = source.stats; - this.ivs = source.ivs; + this.stats = Array.from(source.stats); + this.ivs = Array.from(source.ivs); // TODO: Can't we move some of this verification stuff to an upgrade script? this.nature = source.nature ?? Nature.HARDY; @@ -162,7 +162,7 @@ export class PokemonData { this.gender, this.shiny, this.variant, - this.ivs, + new Uint8Array(this.ivs.slice(0, 6)), this.nature, this, playerPokemon => { diff --git a/src/utils/common.ts b/src/utils/common.ts index 569333209bf..6759e498d63 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -1,6 +1,13 @@ 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; @@ -176,15 +183,15 @@ export function getPlayTimeString(totalSeconds: number): string { * @param id 32-bit number * @returns An array of six numbers corresponding to 5-bit chunks from {@linkcode id} */ -export function getIvsFromId(id: number): number[] { - return [ +export function getIvsFromId(id: number): Uint8Array { + return Uint8Array.of( (id & 0x3e000000) >>> 25, (id & 0x01f00000) >>> 20, (id & 0x000f8000) >>> 15, (id & 0x00007c00) >>> 10, (id & 0x000003e0) >>> 5, id & 0x0000001f, - ]; + ); } export function formatLargeNumber(count: number, threshold: number): string { @@ -515,3 +522,86 @@ export function coerceArray(input: T): T extends any[] ? T : [T]; export function coerceArray(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(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( + target: T, + source: T extends BigIntArray + ? Readonly | readonly bigint[] + : Readonly | 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); +} diff --git a/test/utils/common.test.ts b/test/utils/common.test.ts new file mode 100644 index 00000000000..9a37e770ada --- /dev/null +++ b/test/utils/common.test.ts @@ -0,0 +1,122 @@ +import { setTypedArray, subArray } from "#utils/common"; +import { describe, expect, it } from "vitest"; + +/** + * Unit tests for the utility methods in `src/utils/common.ts` + * @module + */ + +describe("subArray", () => { + it("returns the same array if length <= n (plain array)", () => { + const arr = [1, 2, 3]; + const result = subArray(arr, 5); + expect(result).toBe(arr); + expect(result).toEqual([1, 2, 3]); + }); + + it("returns a sliced array if length > n (plain array)", () => { + const arr = [1, 2, 3, 4, 5]; + const result = subArray(arr, 3); + expect(result).not.toBe(arr); + expect(result).toEqual([1, 2, 3]); + }); + + it("returns the same typed array if length <= n", () => { + const arr = new Uint8Array([1, 2, 3]); + const result = subArray(arr, 5); + expect(result).toBe(arr); + expect(Array.from(result)).toEqual([1, 2, 3]); + }); + + it("returns a subarray if length > n (typed array)", () => { + const arr = new Uint8Array([1, 2, 3, 4, 5]); + const result = subArray(arr, 2); + expect(result).not.toBe(arr); + expect(Array.from(result)).toEqual([1, 2]); + }); + + it("returns empty array if n is 0 (plain array)", () => { + const arr = [1, 2, 3]; + const result = subArray(arr, 0); + expect(result).toEqual([]); + }); + + it("returns empty typed array if n is 0", () => { + const arr = new Uint8Array([1, 2, 3]); + const result = subArray(arr, 0); + expect(Array.from(result)).toEqual([]); + }); + + it("throws TypeError for non-array-like input", () => { + // @ts-expect-error + expect(() => subArray({ length: 4 }, 2)).toThrow(TypeError); + }); +}); + +describe("setTypedArray", () => { + it("sets values from source to target with no offset (fits exactly)", () => { + const target = new Uint8Array(3); + const source = [1, 2, 3]; + setTypedArray(target, source); + expect(Array.from(target)).toEqual([1, 2, 3]); + }); + + it("sets values from source to target with offset", () => { + const target = new Uint8Array([0, 0, 0, 0, 0]); + const source = [9, 8]; + setTypedArray(target, source, 2); + expect(Array.from(target)).toEqual([0, 0, 9, 8, 0]); + }); + + it("clamps source if it would overflow target", () => { + const target = new Uint8Array(4); + const source = [1, 2, 3, 4, 5, 6]; + setTypedArray(target, source, 2); + expect(Array.from(target)).toEqual([0, 0, 1, 2]); + }); + + it("does nothing if offset < 0", () => { + const target = new Uint8Array([1, 2, 3]); + const source = [4, 5, 6]; + setTypedArray(target, source, -1); + expect(Array.from(target)).toEqual([1, 2, 3]); + }); + + it("does nothing if offset >= target.length", () => { + const target = new Uint8Array([1, 2, 3]); + const source = [4, 5, 6]; + setTypedArray(target, source, 3); + expect(Array.from(target)).toEqual([1, 2, 3]); + }); + + it("does nothing if source is empty", () => { + const target = new Uint8Array([1, 2, 3]); + const source: number[] = []; + setTypedArray(target, source, 1); + expect(Array.from(target)).toEqual([1, 2, 3]); + }); + + it("works with typed array as source", () => { + const target = new Uint8Array(4); + const source = new Uint8Array([7, 8, 9]); + setTypedArray(target, source, 1); + expect(Array.from(target)).toEqual([0, 7, 8, 9]); + }); + + it("clamps source typed array if it would overflow target", () => { + const target = new Uint8Array(3); + const source = new Uint8Array([1, 2, 3, 4, 5]); + setTypedArray(target, source, 1); + expect(Array.from(target)).toEqual([0, 1, 2]); + }); + + it("works with BigUint64Array and bigint[]", () => { + if (typeof BigUint64Array !== "undefined") { + const target = new BigUint64Array(3); + const source = [1n, 2n, 3n, 4n]; + + setTypedArray(target, source, 1); + expect(Array.from(target)).toEqual([0n, 1n, 2n]); + } + }); +}); From 5991907fbc2d6aa94737e230e1487d745566f608 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Thu, 11 Sep 2025 21:51:30 -0500 Subject: [PATCH 03/21] Move array utils to its own file --- src/@types/typed-arrays.ts | 4 + src/battle-scene.ts | 3 +- src/data/abilities/ability.ts | 1 + src/data/balance/pokemon-evolutions.ts | 3 +- src/data/battle-anims.ts | 3 +- src/data/battler-tags.ts | 3 +- src/data/moves/move.ts | 3 +- .../mystery-encounter-requirements.ts | 2 +- .../mystery-encounters/mystery-encounter.ts | 3 +- .../can-learn-move-requirement.ts | 2 +- .../utils/encounter-phase-utils.ts | 3 +- .../pokemon-forms/form-change-triggers.ts | 3 +- src/data/trainers/trainer-config.ts | 3 +- src/field/pokemon-sprite-sparkle-handler.ts | 3 +- src/field/pokemon.ts | 14 +-- src/plugins/cache-busted-loader-plugin.ts | 2 +- src/scene-base.ts | 2 +- src/ui/utils/pokemon-icon-anim-helper.ts | 3 +- src/utils/array.ts | 101 ++++++++++++++++++ src/utils/common.ts | 99 ----------------- test/test-utils/helpers/move-helper.ts | 2 +- test/test-utils/helpers/overrides-helper.ts | 3 +- test/test-utils/matchers/to-have-used-pp.ts | 2 +- .../mocks/mock-console/mock-console.ts | 2 +- .../mocks/mocks-container/mock-container.ts | 2 +- .../mocks/mocks-container/mock-rectangle.ts | 2 +- .../mocks/mocks-container/mock-sprite.ts | 2 +- test/utils/{common.test.ts => array.test.ts} | 4 +- 28 files changed, 149 insertions(+), 130 deletions(-) create mode 100644 src/utils/array.ts rename test/utils/{common.test.ts => array.test.ts} (96%) diff --git a/src/@types/typed-arrays.ts b/src/@types/typed-arrays.ts index eed7a6c37c8..8ed6fbe4a04 100644 --- a/src/@types/typed-arrays.ts +++ b/src/@types/typed-arrays.ts @@ -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; +export type ReadonlyNumberCompatibleTypedArray = Exclude; + /** * A partial interface of `Uint8Array` where methods that return the array type have been modified to return a more specific type. * diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 70a3e1dcf56..225f806d491 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -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, diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index f6494548b99..5e37833127e 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -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, diff --git a/src/data/balance/pokemon-evolutions.ts b/src/data/balance/pokemon-evolutions.ts index 0c2fa4e78fa..f7f53672c9f 100644 --- a/src/data/balance/pokemon-evolutions.ts +++ b/src/data/balance/pokemon-evolutions.ts @@ -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"; diff --git a/src/data/battle-anims.ts b/src/data/battle-anims.ts index 573a1730796..ac93a0cac1c 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -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"; diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 8abd98f4683..f9bd31beb17 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -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 */ diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 075876d8ddd..9e0942eb4c8 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -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"; diff --git a/src/data/mystery-encounters/mystery-encounter-requirements.ts b/src/data/mystery-encounters/mystery-encounter-requirements.ts index 85906044b77..e0deee3a4b9 100644 --- a/src/data/mystery-encounters/mystery-encounter-requirements.ts +++ b/src/data/mystery-encounters/mystery-encounter-requirements.ts @@ -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 diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts index f18660b5d71..caa5347ec97 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -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 { diff --git a/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts b/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts index a5810406ef9..4a567040a83 100644 --- a/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts +++ b/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts @@ -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 diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 16d388c2e96..0cf90b0fcb1 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -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"; diff --git a/src/data/pokemon-forms/form-change-triggers.ts b/src/data/pokemon-forms/form-change-triggers.ts index f51b090878f..3a6c84f80d2 100644 --- a/src/data/pokemon-forms/form-change-triggers.ts +++ b/src/data/pokemon-forms/form-change-triggers.ts @@ -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"; diff --git a/src/data/trainers/trainer-config.ts b/src/data/trainers/trainer-config.ts index b5786d1f0a2..982ef989971 100644 --- a/src/data/trainers/trainer-config.ts +++ b/src/data/trainers/trainer-config.ts @@ -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"; diff --git a/src/field/pokemon-sprite-sparkle-handler.ts b/src/field/pokemon-sprite-sparkle-handler.ts index bd44dc03330..2c8f42524c1 100644 --- a/src/field/pokemon-sprite-sparkle-handler.ts +++ b/src/field/pokemon-sprite-sparkle-handler.ts @@ -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; diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index e6a993f9fb3..4d47ae1b412 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -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, ) { diff --git a/src/plugins/cache-busted-loader-plugin.ts b/src/plugins/cache-busted-loader-plugin.ts index 4853265ec65..da0850c613c 100644 --- a/src/plugins/cache-busted-loader-plugin.ts +++ b/src/plugins/cache-busted-loader-plugin.ts @@ -1,4 +1,4 @@ -import { coerceArray } from "#utils/common"; +import { coerceArray } from "#utils/array"; let manifest: object; diff --git a/src/scene-base.ts b/src/scene-base.ts index 3f2e4409794..26a680f2162 100644 --- a/src/scene-base.ts +++ b/src/scene-base.ts @@ -1,4 +1,4 @@ -import { coerceArray } from "#utils/common"; +import { coerceArray } from "#utils/array"; export const legacyCompatibleImages: string[] = []; diff --git a/src/ui/utils/pokemon-icon-anim-helper.ts b/src/ui/utils/pokemon-icon-anim-helper.ts index 0d8de7ce1ca..fbe435c7845 100644 --- a/src/ui/utils/pokemon-icon-anim-helper.ts +++ b/src/ui/utils/pokemon-icon-anim-helper.ts @@ -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, diff --git a/src/utils/array.ts b/src/utils/array.ts new file mode 100644 index 00000000000..75807d2da21 --- /dev/null +++ b/src/utils/array.ts @@ -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(input: T): T extends any[] ? T : [T]; +export function coerceArray(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(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( + 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); +} diff --git a/src/utils/common.ts b/src/utils/common.ts index 6759e498d63..9c6e03714db 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -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(input: T): T extends any[] ? T : [T]; -export function coerceArray(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(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( - target: T, - source: T extends BigIntArray - ? Readonly | readonly bigint[] - : Readonly | 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); -} diff --git a/test/test-utils/helpers/move-helper.ts b/test/test-utils/helpers/move-helper.ts index 747a8ad576e..445aa3aa078 100644 --- a/test/test-utils/helpers/move-helper.ts +++ b/test/test-utils/helpers/move-helper.ts @@ -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"; diff --git a/test/test-utils/helpers/overrides-helper.ts b/test/test-utils/helpers/overrides-helper.ts index da0d75bf564..331d1c94acf 100644 --- a/test/test-utils/helpers/overrides-helper.ts +++ b/test/test-utils/helpers/overrides-helper.ts @@ -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"; diff --git a/test/test-utils/matchers/to-have-used-pp.ts b/test/test-utils/matchers/to-have-used-pp.ts index 4815cfcadab..bdbe4e140ff 100644 --- a/test/test-utils/matchers/to-have-used-pp.ts +++ b/test/test-utils/matchers/to-have-used-pp.ts @@ -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"; /** diff --git a/test/test-utils/mocks/mock-console/mock-console.ts b/test/test-utils/mocks/mock-console/mock-console.ts index 52ed0af6aa7..55de3f71556 100644 --- a/test/test-utils/mocks/mock-console/mock-console.ts +++ b/test/test-utils/mocks/mock-console/mock-console.ts @@ -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"; diff --git a/test/test-utils/mocks/mocks-container/mock-container.ts b/test/test-utils/mocks/mocks-container/mock-container.ts index dd19dc3259c..0c99545a109 100644 --- a/test/test-utils/mocks/mocks-container/mock-container.ts +++ b/test/test-utils/mocks/mocks-container/mock-container.ts @@ -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; diff --git a/test/test-utils/mocks/mocks-container/mock-rectangle.ts b/test/test-utils/mocks/mocks-container/mock-rectangle.ts index 96c49dec692..ca96daba44d 100644 --- a/test/test-utils/mocks/mocks-container/mock-rectangle.ts +++ b/test/test-utils/mocks/mocks-container/mock-rectangle.ts @@ -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; diff --git a/test/test-utils/mocks/mocks-container/mock-sprite.ts b/test/test-utils/mocks/mocks-container/mock-sprite.ts index d5e11f5c4f9..30548370c68 100644 --- a/test/test-utils/mocks/mocks-container/mock-sprite.ts +++ b/test/test-utils/mocks/mocks-container/mock-sprite.ts @@ -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; diff --git a/test/utils/common.test.ts b/test/utils/array.test.ts similarity index 96% rename from test/utils/common.test.ts rename to test/utils/array.test.ts index 9a37e770ada..d0ed471edec 100644 --- a/test/utils/common.test.ts +++ b/test/utils/array.test.ts @@ -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 */ From d95398097ff32f0114b49729043ca846f5c5a416 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Thu, 11 Sep 2025 21:55:18 -0500 Subject: [PATCH 04/21] Add suppression comment --- src/@types/typed-arrays.ts | 22 +++++++++++----------- src/utils/data.ts | 1 + 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/@types/typed-arrays.ts b/src/@types/typed-arrays.ts index 8ed6fbe4a04..226bed898cc 100644 --- a/src/@types/typed-arrays.ts +++ b/src/@types/typed-arrays.ts @@ -34,7 +34,7 @@ export type TypedArray = * @remarks * Is to `Int8Array` what `ReadonlyArray` is to `Array` */ -export interface ReadonlyInt8Array +export interface ReadonlyInt8Array extends Omit { subarray(begin?: number, end?: number): ReadonlyInt8Array; readonly [index: number]: number; @@ -45,7 +45,7 @@ export interface ReadonlyInt8Array +export interface ReadonlyUint8Array extends Omit { subarray(begin?: number, end?: number): ReadonlyUint8Array; readonly [index: number]: number; @@ -56,7 +56,7 @@ export interface ReadonlyUint8Array +export interface ReadonlyUint8ClampedArray extends Omit, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> { subarray(begin?: number, end?: number): ReadonlyUint8ClampedArray; readonly [index: number]: number; @@ -67,7 +67,7 @@ export interface ReadonlyUint8ClampedArray +export interface ReadonlyInt16Array extends Omit, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> { subarray(begin?: number, end?: number): ReadonlyInt16Array; readonly [index: number]: number; @@ -78,7 +78,7 @@ export interface ReadonlyInt16Array +export interface ReadonlyUint16Array extends Omit, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> { subarray(begin?: number, end?: number): ReadonlyUint16Array; readonly [index: number]: number; @@ -89,7 +89,7 @@ export interface ReadonlyUint16Array +export interface ReadonlyInt32Array extends Omit, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> { subarray(begin?: number, end?: number): ReadonlyInt32Array; readonly [index: number]: number; @@ -100,7 +100,7 @@ export interface ReadonlyInt32Array +export interface ReadonlyUint32Array extends Omit, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> { subarray(begin?: number, end?: number): ReadonlyUint32Array; readonly [index: number]: number; @@ -111,7 +111,7 @@ export interface ReadonlyUint32Array +export interface ReadonlyFloat32Array extends Omit, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> { subarray(begin?: number, end?: number): ReadonlyFloat32Array; readonly [index: number]: number; @@ -122,7 +122,7 @@ export interface ReadonlyFloat32Array +export interface ReadonlyFloat64Array extends Omit { subarray(begin?: number, end?: number): ReadonlyFloat64Array; readonly [index: number]: number; @@ -133,7 +133,7 @@ export interface ReadonlyFloat64Array +export interface ReadonlyBigInt64Array extends Omit { subarray(begin?: number, end?: number): ReadonlyBigInt64Array; readonly [index: number]: bigint; @@ -144,7 +144,7 @@ export interface ReadonlyBigInt64Array +export interface ReadonlyBigUint64Array extends Omit { subarray(begin?: number, end?: number): ReadonlyBigUint64Array; readonly [index: number]: bigint; diff --git a/src/utils/data.ts b/src/utils/data.ts index 1383d8e6ff2..046f7f855d9 100644 --- a/src/utils/data.ts +++ b/src/utils/data.ts @@ -67,6 +67,7 @@ export function isBareObject(obj: any): boolean { if (typeof obj !== "object") { return false; } + // biome-ignore lint/suspicious/useGuardForIn: Checking a bare object should include prototype chain for (const _ in obj) { return false; } From 1a179cc9cb33e6a347be7f71e9a46a09963a06da Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Thu, 11 Sep 2025 22:26:17 -0500 Subject: [PATCH 05/21] Adjust type of `getStats` --- src/field/pokemon.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 4d47ae1b412..ea0c026bf45 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -1320,7 +1320,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @param bypassSummonData - Whether to prefer actual stats (`true`) or in-battle overridden stats (`false`); default `true` * @returns The numeric values of this {@linkcode Pokemon}'s stats as an array. */ - getStats(bypassSummonData = true): ArrayLike { + getStats(bypassSummonData = true): Uint32Array { if (!bypassSummonData) { // Only grab summon data stats if nonzero return this.summonData.stats.map((s, i) => s || this.stats[i]); From 5ec0088ffce9fafe9cfe19cde0f2ea083c2b19c0 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Thu, 11 Sep 2025 22:35:46 -0500 Subject: [PATCH 06/21] Adjust test mocks to use typed arrays --- .../encounters/fun-and-games-encounter.ts | 2 +- test/abilities/beast-boost.test.ts | 6 +- test/battle/battle-order.test.ts | 26 +++--- test/escape-calculations.test.ts | 92 ++++++++----------- test/moves/fusion-flare-bolt.test.ts | 8 +- test/moves/rollout.test.ts | 4 +- .../encounters/part-timer-encounter.test.ts | 4 +- 7 files changed, 63 insertions(+), 79 deletions(-) diff --git a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts index f2363ade500..0327eb2908d 100644 --- a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts +++ b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts @@ -220,7 +220,7 @@ async function summonPlayerPokemon() { false, true, ); - wobbuffet.ivs = [0, 0, 0, 0, 0, 0]; + wobbuffet.ivs.set([0, 0, 0, 0, 0, 0]); wobbuffet.setNature(Nature.MILD); wobbuffet.setAlpha(0); wobbuffet.setVisible(false); diff --git a/test/abilities/beast-boost.test.ts b/test/abilities/beast-boost.test.ts index aeb4d854b1a..7ec3ac355ff 100644 --- a/test/abilities/beast-boost.test.ts +++ b/test/abilities/beast-boost.test.ts @@ -38,7 +38,7 @@ describe("Abilities - Beast Boost", () => { const playerPokemon = game.field.getPlayerPokemon(); // Set the pokemon's highest stat to DEF, so it should be picked by Beast Boost - vi.spyOn(playerPokemon, "stats", "get").mockReturnValue([10000, 100, 1000, 200, 100, 100]); + vi.spyOn(playerPokemon, "stats", "get").mockReturnValue(Uint32Array.of(10000, 100, 1000, 200, 100, 100)); console.log(playerPokemon.stats); expect(playerPokemon.getStatStage(Stat.DEF)).toBe(0); @@ -56,7 +56,7 @@ describe("Abilities - Beast Boost", () => { const playerPokemon = game.field.getPlayerPokemon(); // If the opponent uses Guard Split, the pokemon's second highest stat (SPATK) should be chosen - vi.spyOn(playerPokemon, "stats", "get").mockReturnValue([10000, 100, 201, 200, 100, 100]); + vi.spyOn(playerPokemon, "stats", "get").mockReturnValue(Uint32Array.of(10000, 100, 201, 200, 100, 100)); expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(0); @@ -75,7 +75,7 @@ describe("Abilities - Beast Boost", () => { const playerPokemon = game.field.getPlayerPokemon(); // Set up tie between SPATK, SPDEF, and SPD, where SPATK should win - vi.spyOn(playerPokemon, "stats", "get").mockReturnValue([10000, 1, 1, 100, 100, 100]); + vi.spyOn(playerPokemon, "stats", "get").mockReturnValue(Uint32Array.of(10000, 1, 1, 100, 100, 100)); expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(0); diff --git a/test/battle/battle-order.test.ts b/test/battle/battle-order.test.ts index de13b22df79..382604bffcd 100644 --- a/test/battle/battle-order.test.ts +++ b/test/battle/battle-order.test.ts @@ -39,8 +39,8 @@ describe("Battle order", () => { const enemyPokemon = game.field.getEnemyPokemon(); const enemyStartHp = enemyPokemon.hp; - vi.spyOn(playerPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 50]); // set playerPokemon's speed to 50 - vi.spyOn(enemyPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set enemyPokemon's speed to 150 + vi.spyOn(playerPokemon, "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, 50)); // set playerPokemon's speed to 50 + vi.spyOn(enemyPokemon, "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, 150)); // set enemyPokemon's speed to 150 game.move.select(MoveId.TACKLE); await game.phaseInterceptor.to("MoveEndPhase", false); @@ -55,8 +55,8 @@ describe("Battle order", () => { const playerStartHp = playerPokemon.hp; const enemyPokemon = game.field.getEnemyPokemon(); const enemyStartHp = enemyPokemon.hp; - vi.spyOn(playerPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set playerPokemon's speed to 150 - vi.spyOn(enemyPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 50]); // set enemyPokemon's speed to 50 + vi.spyOn(playerPokemon, "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, 150)); // set playerPokemon's speed to 150 + vi.spyOn(enemyPokemon, "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, 50)); // set enemyPokemon's speed to 50 game.move.select(MoveId.TACKLE); @@ -74,8 +74,8 @@ describe("Battle order", () => { const enemyPokemon = game.scene.getEnemyField(); const enemyHps = enemyPokemon.map(p => p.hp); - playerPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 50])); // set both playerPokemons' speed to 50 - enemyPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150])); // set both enemyPokemons' speed to 150 + playerPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, 50))); // set both playerPokemons' speed to 50 + enemyPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, 150))); // set both enemyPokemons' speed to 150 game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE, 1); @@ -96,9 +96,9 @@ describe("Battle order", () => { const playerPokemon = game.scene.getPlayerField(); const enemyPokemon = game.scene.getEnemyField(); - playerPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100])); //set both playerPokemons' speed to 100 - vi.spyOn(enemyPokemon[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100]); // set enemyPokemon's speed to 100 - vi.spyOn(enemyPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set enemyPokemon's speed to 150 + playerPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, 100))); //set both playerPokemons' speed to 100 + vi.spyOn(enemyPokemon[0], "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, 100)); // set enemyPokemon's speed to 100 + vi.spyOn(enemyPokemon[1], "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, 150)); // set enemyPokemon's speed to 150 game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE, 1); @@ -114,10 +114,10 @@ describe("Battle order", () => { const playerPokemon = game.scene.getPlayerField(); const enemyPokemon = game.scene.getEnemyField(); - vi.spyOn(playerPokemon[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100]); // set one playerPokemon's speed to 100 - vi.spyOn(playerPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set other playerPokemon's speed to 150 - vi.spyOn(enemyPokemon[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100]); // set one enemyPokemon's speed to 100 - vi.spyOn(enemyPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set other enemyPokemon's speed to 150 + vi.spyOn(playerPokemon[0], "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, 100)); // set one playerPokemon's speed to 100 + vi.spyOn(playerPokemon[1], "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, 150)); // set other playerPokemon's speed to 150 + vi.spyOn(enemyPokemon[0], "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, 100)); // set one enemyPokemon's speed to 100 + vi.spyOn(enemyPokemon[1], "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, 150)); // set other enemyPokemon's speed to 150 game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE, 1); diff --git a/test/escape-calculations.test.ts b/test/escape-calculations.test.ts index e1e521f4394..062a86c6fa2 100644 --- a/test/escape-calculations.test.ts +++ b/test/escape-calculations.test.ts @@ -38,7 +38,7 @@ describe("Escape chance calculations", () => { const enemyField = game.scene.getEnemyField(); const enemySpeed = 100; // set enemyPokemon's speed to 100 - vi.spyOn(enemyField[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, enemySpeed]); + vi.spyOn(enemyField[0], "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, enemySpeed)); const commandPhase = game.scene.phaseManager.getCurrentPhase() as CommandPhase; commandPhase.handleCommand(Command.RUN, 0); @@ -81,14 +81,9 @@ describe("Escape chance calculations", () => { // set the number of escape attempts to the required amount game.scene.currentBattle.escapeAttempts = check.escapeAttempts; // set playerPokemon's speed to a multiple of the enemySpeed - vi.spyOn(playerPokemon[0], "stats", "get").mockReturnValue([ - 20, - 20, - 20, - 20, - 20, - check.pokemonSpeedRatio * enemySpeed, - ]); + vi.spyOn(playerPokemon[0], "stats", "get").mockReturnValue( + Uint32Array.of(20, 20, 20, 20, 20, check.pokemonSpeedRatio * enemySpeed), + ); const chance = phase.calculateEscapeChance(game.scene.currentBattle.escapeAttempts); expect(chance).toBe(check.expectedEscapeChance); } @@ -107,9 +102,9 @@ describe("Escape chance calculations", () => { // this is used to find the ratio of the player's first pokemon const playerASpeedPercentage = 0.4; // set enemyAPokemon's speed to 70 - vi.spyOn(enemyField[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, enemyASpeed]); + vi.spyOn(enemyField[0], "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, enemyASpeed)); // set enemyBPokemon's speed to 30 - vi.spyOn(enemyField[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, enemyBSpeed]); + vi.spyOn(enemyField[1], "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, enemyBSpeed)); const commandPhase = game.scene.phaseManager.getCurrentPhase() as CommandPhase; commandPhase.handleCommand(Command.RUN, 0); @@ -151,23 +146,20 @@ describe("Escape chance calculations", () => { // sets the number of escape attempts to the required amount game.scene.currentBattle.escapeAttempts = check.escapeAttempts; // set the first playerPokemon's speed to a multiple of the enemySpeed - vi.spyOn(playerPokemon[0], "stats", "get").mockReturnValue([ - 20, - 20, - 20, - 20, - 20, - Math.floor(check.pokemonSpeedRatio * totalEnemySpeed * playerASpeedPercentage), - ]); + vi.spyOn(playerPokemon[0], "stats", "get").mockReturnValue( + Uint32Array.of( + 20, + 20, + 20, + 20, + 20, + Math.floor(check.pokemonSpeedRatio * totalEnemySpeed * playerASpeedPercentage), + ), + ); // set the second playerPokemon's speed to the remaining value of speed - vi.spyOn(playerPokemon[1], "stats", "get").mockReturnValue([ - 20, - 20, - 20, - 20, - 20, - check.pokemonSpeedRatio * totalEnemySpeed - playerPokemon[0].stats[5], - ]); + vi.spyOn(playerPokemon[1], "stats", "get").mockReturnValue( + Uint32Array.of(20, 20, 20, 20, 20, check.pokemonSpeedRatio * totalEnemySpeed - playerPokemon[0].stats[5]), + ); const chance = phase.calculateEscapeChance(game.scene.currentBattle.escapeAttempts); // checks to make sure the escape values are the same expect(chance).toBe(check.expectedEscapeChance); @@ -184,7 +176,7 @@ describe("Escape chance calculations", () => { const enemyField = game.scene.getEnemyField()!; const enemySpeed = 100; // set enemyPokemon's speed to 100 - vi.spyOn(enemyField[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, enemySpeed]); + vi.spyOn(enemyField[0], "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, enemySpeed)); const commandPhase = game.scene.phaseManager.getCurrentPhase() as CommandPhase; commandPhase.handleCommand(Command.RUN, 0); @@ -241,14 +233,9 @@ describe("Escape chance calculations", () => { // sets the number of escape attempts to the required amount game.scene.currentBattle.escapeAttempts = check.escapeAttempts; // set playerPokemon's speed to a multiple of the enemySpeed - vi.spyOn(playerPokemon[0], "stats", "get").mockReturnValue([ - 20, - 20, - 20, - 20, - 20, - check.pokemonSpeedRatio * enemySpeed, - ]); + vi.spyOn(playerPokemon[0], "stats", "get").mockReturnValue( + Uint32Array.of(20, 20, 20, 20, 20, check.pokemonSpeedRatio * enemySpeed), + ); const chance = phase.calculateEscapeChance(game.scene.currentBattle.escapeAttempts); expect(chance).toBe(check.expectedEscapeChance); } @@ -267,9 +254,9 @@ describe("Escape chance calculations", () => { // this is used to find the ratio of the player's first pokemon const playerASpeedPercentage = 0.8; // set enemyAPokemon's speed to 70 - vi.spyOn(enemyField[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, enemyASpeed]); + vi.spyOn(enemyField[0], "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, enemyASpeed)); // set enemyBPokemon's speed to 30 - vi.spyOn(enemyField[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, enemyBSpeed]); + vi.spyOn(enemyField[1], "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, enemyBSpeed)); const commandPhase = game.scene.phaseManager.getCurrentPhase() as CommandPhase; commandPhase.handleCommand(Command.RUN, 0); @@ -324,23 +311,20 @@ describe("Escape chance calculations", () => { // sets the number of escape attempts to the required amount game.scene.currentBattle.escapeAttempts = check.escapeAttempts; // set the first playerPokemon's speed to a multiple of the enemySpeed - vi.spyOn(playerPokemon[0], "stats", "get").mockReturnValue([ - 20, - 20, - 20, - 20, - 20, - Math.floor(check.pokemonSpeedRatio * totalEnemySpeed * playerASpeedPercentage), - ]); + vi.spyOn(playerPokemon[0], "stats", "get").mockReturnValue( + Uint32Array.of( + 20, + 20, + 20, + 20, + 20, + Math.floor(check.pokemonSpeedRatio * totalEnemySpeed * playerASpeedPercentage), + ), + ); // set the second playerPokemon's speed to the remaining value of speed - vi.spyOn(playerPokemon[1], "stats", "get").mockReturnValue([ - 20, - 20, - 20, - 20, - 20, - check.pokemonSpeedRatio * totalEnemySpeed - playerPokemon[0].stats[5], - ]); + vi.spyOn(playerPokemon[1], "stats", "get").mockReturnValue( + Uint32Array.of(20, 20, 20, 20, 20, check.pokemonSpeedRatio * totalEnemySpeed - playerPokemon[0].stats[5]), + ); const chance = phase.calculateEscapeChance(game.scene.currentBattle.escapeAttempts); // checks to make sure the escape values are the same expect(chance).toBe(check.expectedEscapeChance); diff --git a/test/moves/fusion-flare-bolt.test.ts b/test/moves/fusion-flare-bolt.test.ts index f5d556bde48..cce34441d38 100644 --- a/test/moves/fusion-flare-bolt.test.ts +++ b/test/moves/fusion-flare-bolt.test.ts @@ -166,8 +166,8 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { // Mock stats by replacing entries in copy with desired values for specific stats const stats = { - enemy: [[...enemyParty[0].stats], [...enemyParty[1].stats]], - player: [[...party[0].stats], [...party[1].stats]], + enemy: [enemyParty[0].stats.slice(), enemyParty[1].stats.slice()], + player: [party[0].stats.slice(), party[1].stats.slice()], }; // Ensure survival by reducing enemy Sp. Atk and boosting party Sp. Def @@ -220,8 +220,8 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { // Mock stats by replacing entries in copy with desired values for specific stats const stats = { - enemy: [[...enemyParty[0].stats], [...enemyParty[1].stats]], - player: [[...party[0].stats], [...party[1].stats]], + enemy: [enemyParty[0].stats.slice(), enemyParty[1].stats.slice()], + player: [party[0].stats.slice(), party[1].stats.slice()], }; // Ensure survival by reducing enemy Sp. Atk and boosting party Sp. Def diff --git a/test/moves/rollout.test.ts b/test/moves/rollout.test.ts index 0e01725a188..9e6d8b87e06 100644 --- a/test/moves/rollout.test.ts +++ b/test/moves/rollout.test.ts @@ -45,10 +45,10 @@ describe("Moves - Rollout", () => { await game.classicMode.startBattle(); const playerPkm = game.field.getPlayerPokemon(); - vi.spyOn(playerPkm, "stats", "get").mockReturnValue([500000, 1, 1, 1, 1, 1]); // HP, ATK, DEF, SPATK, SPDEF, SPD + vi.spyOn(playerPkm, "stats", "get").mockReturnValue(Uint32Array.of(500000, 1, 1, 1, 1, 1)); // HP, ATK, DEF, SPATK, SPDEF, SPD const enemyPkm = game.field.getEnemyPokemon(); - vi.spyOn(enemyPkm, "stats", "get").mockReturnValue([500000, 1, 1, 1, 1, 1]); // HP, ATK, DEF, SPATK, SPDEF, SPD + vi.spyOn(enemyPkm, "stats", "get").mockReturnValue(Uint32Array.of(500000, 1, 1, 1, 1, 1)); // HP, ATK, DEF, SPATK, SPDEF, SPD vi.spyOn(enemyPkm, "getHeldItems").mockReturnValue([]); //no berries enemyPkm.hp = enemyPkm.getMaxHp(); diff --git a/test/mystery-encounter/encounters/part-timer-encounter.test.ts b/test/mystery-encounter/encounters/part-timer-encounter.test.ts index 15d2664364c..78369570fa9 100644 --- a/test/mystery-encounter/encounters/part-timer-encounter.test.ts +++ b/test/mystery-encounter/encounters/part-timer-encounter.test.ts @@ -122,7 +122,7 @@ describe("Part-Timer - Mystery Encounter", () => { // Override party levels to 50 so stats can be fully reflective scene.getPlayerParty().forEach(p => { p.level = 50; - p.ivs = [20, 20, 20, 20, 20, 20]; + p.ivs = Uint8Array.of(20, 20, 20, 20, 20, 20); p.calculateStats(); }); await runMysteryEncounterToEnd(game, 1, { pokemonNo: 2 }); @@ -188,7 +188,7 @@ describe("Part-Timer - Mystery Encounter", () => { // Override party levels to 50 so stats can be fully reflective scene.getPlayerParty().forEach(p => { p.level = 50; - p.ivs = [20, 20, 20, 20, 20, 20]; + p.ivs = Uint8Array.of(20, 20, 20, 20, 20, 20); p.calculateStats(); }); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 4 }); From e558339445e0dbdeaa899f0bc4d49bdb75831438 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Thu, 11 Sep 2025 22:43:26 -0500 Subject: [PATCH 07/21] Adjust signatures of some phases to use ArrayLike --- src/phases/encounter-phase.ts | 2 +- src/ui/handlers/battle-message-ui-handler.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index 9345170e718..b53b151072f 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -125,7 +125,7 @@ export class EncounterPhase extends BattlePhase { !!globalScene.getEncounterBossSegments(battle.waveIndex, level, enemySpecies), ); if (globalScene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) { - battle.enemyParty[e].ivs = new Array(6).fill(31); + battle.enemyParty[e].ivs.fill(31); } globalScene .getPlayerParty() diff --git a/src/ui/handlers/battle-message-ui-handler.ts b/src/ui/handlers/battle-message-ui-handler.ts index f845f22a730..79e7e318915 100644 --- a/src/ui/handlers/battle-message-ui-handler.ts +++ b/src/ui/handlers/battle-message-ui-handler.ts @@ -195,7 +195,7 @@ export class BattleMessageUiHandler extends MessageUiHandler { super.showDialogue(text, name, delay, callback, callbackDelay, prompt, promptDelay); } - promptLevelUpStats(partyMemberIndex: number, prevStats: number[], showTotals: boolean): Promise { + promptLevelUpStats(partyMemberIndex: number, prevStats: ArrayLike, showTotals: boolean): Promise { return new Promise(resolve => { if (!globalScene.showLevelUpStats) { return resolve(); @@ -219,7 +219,7 @@ export class BattleMessageUiHandler extends MessageUiHandler { }); } - promptIvs(pokemonId: number, ivs: number[]): Promise { + promptIvs(pokemonId: number, ivs: ArrayLike): Promise { return new Promise(resolve => { globalScene.executeWithSeedOffset(() => { let levelUpStatsValuesText = ""; From 25f1f65c127b4fc6ee609d551b38f4992fdfb8ea Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Thu, 11 Sep 2025 22:44:42 -0500 Subject: [PATCH 08/21] Adjust signature of src/ui/containers/stats-container#updateIvs --- src/ui/containers/stats-container.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/containers/stats-container.ts b/src/ui/containers/stats-container.ts index fa53c8c8fd4..55b099b95c5 100644 --- a/src/ui/containers/stats-container.ts +++ b/src/ui/containers/stats-container.ts @@ -104,7 +104,7 @@ export class StatsContainer extends Phaser.GameObjects.Container { } } - updateIvs(ivs: number[], originalIvs?: number[]): void { + updateIvs(ivs: Uint8Array | number[], originalIvs?: number[]): void { if (ivs) { const ivChartData = new Array(6) .fill(null) From 7aecfc9863ea25aadb0573c95792b8629764ef80 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Thu, 11 Sep 2025 22:47:03 -0500 Subject: [PATCH 09/21] Remove comment gap to try to satisfy typedoc --- src/@types/typed-arrays.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/@types/typed-arrays.ts b/src/@types/typed-arrays.ts index 226bed898cc..860b8bf0557 100644 --- a/src/@types/typed-arrays.ts +++ b/src/@types/typed-arrays.ts @@ -210,8 +210,7 @@ interface PartialUint8Array extends Uint8Array { * Useful to leverage the benefits of `TypedArrays` without losing type information. Typescript will consider the elements to be of type `T` instead of just `number`. * @typeParam T - The specific numeric type of the elements in the array. */ -// @ts-expect-error - These methods _will_ error, as we are overriding the return type to be more specific -// in a way that makes typescript unhappy +// @ts-expect-error - These methods _will_ error, as we are overriding the return type to be more specific in a way that makes typescript unhappy export interface GenericUint8Array extends PartialUint8Array { // map(callbackfn: (value: T, index: number, array: this) => T, thisArg?: any): GenericUint8Array; // this method does not trigger a typescript error on its own, but if we add in `toReversed` it causes issues.... @@ -255,8 +254,7 @@ interface PartialUint16Array extends Uint16Array { * Useful to leverage the benefits of `TypedArrays` without losing type information. Typescript will consider the elements to be of type `T` instead of just `number`. * @typeParam T - The specific numeric type of the elements in the array. */ -// @ts-expect-error - These methods _will_ error, as we are overriding the return type to be more specific -// in a way that makes typescript unhappy +// @ts-expect-error - These methods _will_ error, as we are overriding the return type to be more specific in a way that makes typescript unhappy export interface GenericUint16Array extends PartialUint16Array { // map(callbackfn: (value: T, index: number, array: this) => T, thisArg?: any): GenericUint16Array; // this method does not trigger a typescript error on its own, but if we add in `toReversed` it causes issues.... @@ -300,8 +298,7 @@ interface PartialUint32Array extends Uint32Array { * Useful to leverage the benefits of `TypedArrays` without losing type information. Typescript will consider the elements to be of type `T` instead of just `number`. * @typeParam T - The specific numeric type of the elements in the array. */ -// @ts-expect-error - These methods _will_ error, as we are overriding the return type to be more specific -// in a way that makes typescript unhappy +// @ts-expect-error - These methods _will_ error, as we are overriding the return type to be more specific in a way that makes typescript unhappy export interface GenericUint32Array extends PartialUint32Array { // map(callbackfn: (value: T, index: number, array: this) => T, thisArg?: any): GenericUint32Array; // this method does not trigger a typescript error on its own, but if we add in `toReversed` it causes issues.... @@ -345,8 +342,7 @@ interface PartialInt8Array extends Int8Array { * Useful to leverage the benefits of `TypedArrays` without losing type information. Typescript will consider the elements to be of type `T` instead of just `number`. * @typeParam T - The specific numeric type of the elements in the array. */ -// @ts-expect-error - These methods _will_ error, as we are overriding the return type to be more specific -// in a way that makes typescript unhappy +// @ts-expect-error - These methods _will_ error, as we are overriding the return type to be more specific in a way that makes typescript unhappy export interface GenericInt8Array extends PartialInt8Array { // map(callbackfn: (value: T, index: number, array: this) => T, thisArg?: any): GenericInt8Array; // this method does not trigger a typescript error on its own, but if we add in `toReversed` it causes issues.... @@ -390,8 +386,7 @@ interface PartialInt16Array extends Int16Array { * Useful to leverage the benefits of `TypedArrays` without losing type information. Typescript will consider the elements to be of type `T` instead of just `number`. * @typeParam T - The specific numeric type of the elements in the array. */ -// @ts-expect-error - These methods _will_ error, as we are overriding the return type to be more specific -// in a way that makes typescript unhappy +// @ts-expect-error - These methods _will_ error, as we are overriding the return type to be more specific in a way that makes typescript unhappy export interface GenericInt16Array extends PartialInt16Array { // map(callbackfn: (value: T, index: number, array: this) => T, thisArg?: any): GenericInt16Array; // this method does not trigger a typescript error on its own, but if we add in `toReversed` it causes issues.... @@ -435,8 +430,7 @@ interface PartialInt32Array extends Int32Array { * Useful to leverage the benefits of `TypedArrays` without losing type information. Typescript will consider the elements to be of type `T` instead of just `number`. * @typeParam T - The specific numeric type of the elements in the array. */ -// @ts-expect-error - These methods _will_ error, as we are overriding the return type to be more specific -// in a way that makes typescript unhappy +// @ts-expect-error - These methods _will_ error, as we are overriding the return type to be more specific in a way that makes typescript unhappy export interface GenericInt32Array extends PartialInt32Array { // map(callbackfn: (value: T, index: number, array: this) => T, thisArg?: any): GenericInt32Array; // this method does not trigger a typescript error on its own, but if we add in `toReversed` it causes issues.... From 4e570b6d85b35e10634c126ff333ac1d8de128e1 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Fri, 12 Sep 2025 08:04:28 -0500 Subject: [PATCH 10/21] Ensure ivs are always set --- src/field/pokemon.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index ea0c026bf45..f223bbfd64f 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -204,8 +204,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { public levelExp: number; public gender: Gender; public hp: number; - public stats = new Uint32Array(6).fill(1); - public ivs: Uint8Array; + public stats = Uint32Array.of(1, 1, 1, 1, 1, 1); + public ivs = Uint8Array.of(0, 0, 0, 0, 0, 0); public nature: Nature; public moveset: PokemonMove[]; /** From 59afce9276112aecb01ff005d544f25fb7aa3ed5 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Fri, 12 Sep 2025 08:34:58 -0500 Subject: [PATCH 11/21] fix: fun-and-games me to use typed array --- .../mystery-encounters/encounters/fun-and-games-encounter.ts | 2 +- .../encounters/fun-and-games-encounter.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts index 0327eb2908d..70493c7c915 100644 --- a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts +++ b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts @@ -220,7 +220,7 @@ async function summonPlayerPokemon() { false, true, ); - wobbuffet.ivs.set([0, 0, 0, 0, 0, 0]); + wobbuffet.ivs.fill(0); wobbuffet.setNature(Nature.MILD); wobbuffet.setAlpha(0); wobbuffet.setVisible(false); diff --git a/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts b/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts index bc1a2893627..fb1e5034c9d 100644 --- a/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts +++ b/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts @@ -144,7 +144,7 @@ describe("Fun And Games! - Mystery Encounter", () => { expect(game).toBeAtPhase("CommandPhase"); expect(game.field.getEnemyPokemon().species.speciesId).toBe(SpeciesId.WOBBUFFET); - expect(game.field.getEnemyPokemon().ivs).toEqual([0, 0, 0, 0, 0, 0]); + expect(game.field.getEnemyPokemon().ivs).toEqual(Uint8Array.of(0, 0, 0, 0, 0, 0)); expect(game.field.getEnemyPokemon().nature).toBe(Nature.MILD); game.onNextPrompt("MessagePhase", UiMode.MESSAGE, () => { From d93bfab8312c8482bb693389768b1a799c082c91 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Fri, 12 Sep 2025 08:49:02 -0500 Subject: [PATCH 12/21] Add new tests for array utilities --- src/data/abilities/ability.ts | 10 +- src/data/pokemon/pokemon-data.ts | 2 +- test/utils/array.test.ts | 371 ++++++++++++++++++++++--------- 3 files changed, 266 insertions(+), 117 deletions(-) diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 5e37833127e..c4deeef3c1f 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -66,15 +66,7 @@ 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, - coerceArray, - NumberHolder, - randSeedFloat, - randSeedInt, - randSeedItem, - toDmgValue, -} from "#utils/common"; +import { BooleanHolder, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common"; import { toCamelCase } from "#utils/strings"; import i18next from "i18next"; diff --git a/src/data/pokemon/pokemon-data.ts b/src/data/pokemon/pokemon-data.ts index e213d55abfd..ed94a87f2af 100644 --- a/src/data/pokemon/pokemon-data.ts +++ b/src/data/pokemon/pokemon-data.ts @@ -16,7 +16,7 @@ import type { AttackMoveResult } from "#types/attack-move-result"; import type { IllusionData } from "#types/illusion-data"; import type { TurnMove } from "#types/turn-move"; import type { CoerceNullPropertiesToUndefined } from "#types/type-helpers"; -import { setTypedArray } from "#utils/common"; +import { setTypedArray } from "#utils/array"; import { getPokemonSpeciesForm } from "#utils/pokemon-utils"; /** diff --git a/test/utils/array.test.ts b/test/utils/array.test.ts index d0ed471edec..9265830ec86 100644 --- a/test/utils/array.test.ts +++ b/test/utils/array.test.ts @@ -1,122 +1,279 @@ -import { setTypedArray, subArray } from "#utils/array"; -import { describe, expect, it } from "vitest"; +import type { ReadonlyUint8Array } from "#types/typed-arrays"; +import { coerceArray, isTypedArray, setTypedArray, subArray } from "#utils/array"; +import { describe, expect, expectTypeOf, it } from "vitest"; /** - * Unit tests for the utility methods in `src/utils/array-utils.ts` + * Unit tests for the utility methods in `src/utils/array.ts` * @module */ -describe("subArray", () => { - it("returns the same array if length <= n (plain array)", () => { - const arr = [1, 2, 3]; - const result = subArray(arr, 5); - expect(result).toBe(arr); - expect(result).toEqual([1, 2, 3]); +describe("Utils - Array", () => { + describe("subArray", () => { + it("returns the same array if length <= n (plain array)", () => { + const arr = [1, 2, 3]; + const result = subArray(arr, 5); + expect(result).toBe(arr); + expect(result).toEqual([1, 2, 3]); + }); + + it("returns a sliced array if length > n (plain array)", () => { + const arr = [1, 2, 3, 4, 5]; + const result = subArray(arr, 3); + expect(result).not.toBe(arr); + expect(result).toEqual([1, 2, 3]); + }); + + it("returns the same typed array if length <= n", () => { + const arr = new Uint8Array([1, 2, 3]); + const result = subArray(arr, 5); + expect(result).toBe(arr); + expect(Array.from(result)).toEqual([1, 2, 3]); + }); + + it("returns a subarray if length > n (typed array)", () => { + const arr = new Uint8Array([1, 2, 3, 4, 5]); + const result = subArray(arr, 2); + expect(result).not.toBe(arr); + expect(Array.from(result)).toEqual([1, 2]); + }); + + it("returns empty array if n is 0 (plain array)", () => { + const arr = [1, 2, 3]; + const result = subArray(arr, 0); + expect(result).toEqual([]); + }); + + it("returns empty typed array if n is 0", () => { + const arr = new Uint8Array([1, 2, 3]); + const result = subArray(arr, 0); + expect(Array.from(result)).toEqual([]); + }); + + it("throws TypeError for non-array-like input", () => { + // @ts-expect-error + expect(() => subArray({ length: 4 }, 2)).toThrow(TypeError); + }); + + describe("output type inference", () => { + it("plain array input", () => { + const arr = [1, 2, 3, 4]; + const result = subArray(arr, 2); + expectTypeOf(result).toEqualTypeOf(); + }); + + it("typed array input", () => { + const arr = new Uint8Array([1, 2, 3, 4]); + const result = subArray(arr, 2); + // @ts-expect-error We get a questionable error about Uint8Array not being assignable to Uint8Array... + expectTypeOf(result).toEqualTypeOf(); + }); + + it("readonly array input", () => { + const arr: readonly number[] = [1, 2, 3, 4]; + const result = subArray(arr, 2); + expectTypeOf(result).toEqualTypeOf(); + }); + + it("readonly typed array input", () => { + const arr = new Uint8Array([1, 2, 3, 4]) as ReadonlyUint8Array; + const result = subArray(arr, 2); + expectTypeOf(result).toEqualTypeOf(); + }); + }); }); - it("returns a sliced array if length > n (plain array)", () => { - const arr = [1, 2, 3, 4, 5]; - const result = subArray(arr, 3); - expect(result).not.toBe(arr); - expect(result).toEqual([1, 2, 3]); - }); + describe("setTypedArray", () => { + it("sets values from source to target with no offset (fits exactly)", () => { + const target = new Uint8Array(3); + const source = [1, 2, 3]; + setTypedArray(target, source); + expect(Array.from(target)).toEqual([1, 2, 3]); + }); - it("returns the same typed array if length <= n", () => { - const arr = new Uint8Array([1, 2, 3]); - const result = subArray(arr, 5); - expect(result).toBe(arr); - expect(Array.from(result)).toEqual([1, 2, 3]); - }); + it("sets values from source to target with offset", () => { + const target = new Uint8Array([0, 0, 0, 0, 0]); + const source = [9, 8]; + setTypedArray(target, source, 2); + expect(Array.from(target)).toEqual([0, 0, 9, 8, 0]); + }); - it("returns a subarray if length > n (typed array)", () => { - const arr = new Uint8Array([1, 2, 3, 4, 5]); - const result = subArray(arr, 2); - expect(result).not.toBe(arr); - expect(Array.from(result)).toEqual([1, 2]); - }); + it("clamps source if it would overflow target", () => { + const target = new Uint8Array(4); + const source = [1, 2, 3, 4, 5, 6]; + setTypedArray(target, source, 2); + expect(Array.from(target)).toEqual([0, 0, 1, 2]); + }); - it("returns empty array if n is 0 (plain array)", () => { - const arr = [1, 2, 3]; - const result = subArray(arr, 0); - expect(result).toEqual([]); - }); + it("does nothing if offset < 0", () => { + const target = new Uint8Array([1, 2, 3]); + const source = [4, 5, 6]; + setTypedArray(target, source, -1); + expect(Array.from(target)).toEqual([1, 2, 3]); + }); - it("returns empty typed array if n is 0", () => { - const arr = new Uint8Array([1, 2, 3]); - const result = subArray(arr, 0); - expect(Array.from(result)).toEqual([]); - }); - - it("throws TypeError for non-array-like input", () => { - // @ts-expect-error - expect(() => subArray({ length: 4 }, 2)).toThrow(TypeError); - }); -}); - -describe("setTypedArray", () => { - it("sets values from source to target with no offset (fits exactly)", () => { - const target = new Uint8Array(3); - const source = [1, 2, 3]; - setTypedArray(target, source); - expect(Array.from(target)).toEqual([1, 2, 3]); - }); - - it("sets values from source to target with offset", () => { - const target = new Uint8Array([0, 0, 0, 0, 0]); - const source = [9, 8]; - setTypedArray(target, source, 2); - expect(Array.from(target)).toEqual([0, 0, 9, 8, 0]); - }); - - it("clamps source if it would overflow target", () => { - const target = new Uint8Array(4); - const source = [1, 2, 3, 4, 5, 6]; - setTypedArray(target, source, 2); - expect(Array.from(target)).toEqual([0, 0, 1, 2]); - }); - - it("does nothing if offset < 0", () => { - const target = new Uint8Array([1, 2, 3]); - const source = [4, 5, 6]; - setTypedArray(target, source, -1); - expect(Array.from(target)).toEqual([1, 2, 3]); - }); - - it("does nothing if offset >= target.length", () => { - const target = new Uint8Array([1, 2, 3]); - const source = [4, 5, 6]; - setTypedArray(target, source, 3); - expect(Array.from(target)).toEqual([1, 2, 3]); - }); - - it("does nothing if source is empty", () => { - const target = new Uint8Array([1, 2, 3]); - const source: number[] = []; - setTypedArray(target, source, 1); - expect(Array.from(target)).toEqual([1, 2, 3]); - }); - - it("works with typed array as source", () => { - const target = new Uint8Array(4); - const source = new Uint8Array([7, 8, 9]); - setTypedArray(target, source, 1); - expect(Array.from(target)).toEqual([0, 7, 8, 9]); - }); - - it("clamps source typed array if it would overflow target", () => { - const target = new Uint8Array(3); - const source = new Uint8Array([1, 2, 3, 4, 5]); - setTypedArray(target, source, 1); - expect(Array.from(target)).toEqual([0, 1, 2]); - }); - - it("works with BigUint64Array and bigint[]", () => { - if (typeof BigUint64Array !== "undefined") { - const target = new BigUint64Array(3); - const source = [1n, 2n, 3n, 4n]; + it("does nothing if offset >= target.length", () => { + const target = new Uint8Array([1, 2, 3]); + const source = [4, 5, 6]; + setTypedArray(target, source, 3); + expect(Array.from(target)).toEqual([1, 2, 3]); + }); + it("does nothing if source is empty", () => { + const target = new Uint8Array([1, 2, 3]); + const source: number[] = []; setTypedArray(target, source, 1); - expect(Array.from(target)).toEqual([0n, 1n, 2n]); - } + expect(Array.from(target)).toEqual([1, 2, 3]); + }); + + it("works with typed array as source", () => { + const target = new Uint8Array(4); + const source = new Uint8Array([7, 8, 9]); + setTypedArray(target, source, 1); + expect(Array.from(target)).toEqual([0, 7, 8, 9]); + }); + + it("clamps source typed array if it would overflow target", () => { + const target = new Uint8Array(3); + const source = new Uint8Array([1, 2, 3, 4, 5]); + setTypedArray(target, source, 1); + expect(Array.from(target)).toEqual([0, 1, 2]); + }); + + it("works with BigUint64Array and bigint[]", () => { + if (typeof BigUint64Array !== "undefined") { + const target = new BigUint64Array(3); + const source = [1n, 2n, 3n, 4n]; + + setTypedArray(target, source, 1); + expect(Array.from(target)).toEqual([0n, 1n, 2n]); + } + }); + }); + + describe("coerceArray", () => { + it("returns the same array if input is already an array", () => { + const arr = [1, 2, 3]; + const result = coerceArray(arr); + expect(result).toBe(arr); + expect(result).toEqual([1, 2, 3]); + }); + + it("wraps a non-array input in an array", () => { + const input = 42; + const result = coerceArray(input); + expect(result).toEqual([42]); + }); + + it("wraps an object in an array", () => { + const obj = { a: 1 }; + const result = coerceArray(obj); + expect(result).toEqual([{ a: 1 }]); + }); + + it("wraps a string in an array", () => { + const str = "hello"; + const result = coerceArray(str); + expect(result).toEqual(["hello"]); + }); + + it("wraps null in an array", () => { + const result = coerceArray(null); + expect(result).toEqual([null]); + }); + + it("wraps undefined in an array", () => { + const result = coerceArray(undefined); + expect(result).toEqual([undefined]); + }); + + it("returns the same array for empty array input", () => { + const arr: number[] = []; + const result = coerceArray(arr); + expect(result).toBe(arr); + expect(result).toEqual([]); + }); + + describe("typing", () => { + it("infers correct type for array input", () => { + const arr = [1, 2, 3]; + const result = coerceArray(arr); + expectTypeOf(result).toEqualTypeOf(); + }); + + it("infers correct type for non-array input", () => { + const input = "test"; + const result = coerceArray(input); + expectTypeOf(result).toEqualTypeOf<[string]>(); + }); + + it("infers correct type for object input", () => { + const obj = { key: "value" }; + const result = coerceArray(obj); + expectTypeOf(result).toEqualTypeOf<[{ key: string }]>(); + }); + + it("infers correct type for null input", () => { + const result = coerceArray(null); + expectTypeOf(result).toEqualTypeOf<[null]>(); + }); + + it("infers correct type for undefined input", () => { + const result = coerceArray(undefined); + expectTypeOf(result).toEqualTypeOf<[undefined]>(); + }); + + it("infers correct type for empty array input", () => { + const arr: number[] = []; + const result = coerceArray(arr); + expectTypeOf(result).toEqualTypeOf(); + }); + }); + }); + + describe("isTypedArray", () => { + it.each([ + ["Int8Array", Int8Array], + ["Uint8ClampedArray", Uint8ClampedArray], + ["Uint16Array", Uint16Array], + ["Uint32Array", Uint32Array], + ["Float64Array", Float64Array], + ["Float32Array", Float32Array], + ])("returns true for %s", (_, ArrayType) => { + if (typeof ArrayType !== "undefined") { + const arr = new ArrayType([1, 2, 3]); + expect(isTypedArray(arr)).toBe(true); + } + }); + + it.each([ + ["BigUint64Array", BigUint64Array], + ["BigInt64Array", BigInt64Array], + ])("returns true for %s", (_, ArrayType) => { + const arr = new ArrayType([1n, 2n, 3n]); + expect(isTypedArray(arr)).toBe(true); + }); + + it.each([ + ["strings", "hello"], + ["null", null], + ["undefined", undefined], + ["a number", 123], + ["an object", { a: 1 }], + ["a plain array", [1, 2, 3]], + ["a DataView", new DataView(new ArrayBuffer(8))], + ["an ArrayLike", { length: 2, 0: 1, 1: 2 }], + ])("returns false for %s", (_, input) => { + expect(isTypedArray(input)).toBe(false); + }); + + it("returns true for an object that extends a typed array", () => { + class MyUint8Array extends Uint8Array { + customMethod() { + return "custom"; + } + } + const arr = new MyUint8Array([1, 2, 3]); + expect(isTypedArray(arr)).toBe(true); + }); }); }); From cea47507df9b4da3a6820c07481997f300c94a36 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Mon, 22 Sep 2025 10:35:56 -0500 Subject: [PATCH 13/21] Update type of ivs in save-data.ts --- src/@types/save-data.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/@types/save-data.ts b/src/@types/save-data.ts index ae359c20949..c737f5097ad 100644 --- a/src/@types/save-data.ts +++ b/src/@types/save-data.ts @@ -123,7 +123,7 @@ export interface Starter { pokerus: boolean; nickname?: string; teraType?: PokemonType; - ivs: number[]; + ivs: Uint8Array; } export type RunHistoryData = Record; From fd4c037959d2805e8b37c3d1849b36ceab857db2 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Mon, 22 Sep 2025 10:36:49 -0500 Subject: [PATCH 14/21] Update part-timer-encounter.test.ts --- test/mystery-encounter/encounters/part-timer-encounter.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/mystery-encounter/encounters/part-timer-encounter.test.ts b/test/mystery-encounter/encounters/part-timer-encounter.test.ts index 78369570fa9..05af5757b2a 100644 --- a/test/mystery-encounter/encounters/part-timer-encounter.test.ts +++ b/test/mystery-encounter/encounters/part-timer-encounter.test.ts @@ -168,7 +168,7 @@ describe("Part-Timer - Mystery Encounter", () => { // Override party levels to 50 so stats can be fully reflective scene.getPlayerParty().forEach(p => { p.level = 50; - p.ivs = [0, 0, 0, 0, 0, 0]; + p.ivs = Uint8Array.of(0, 0, 0, 0, 0, 0); p.calculateStats(); }); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 3 }); From ccaef596602689fe7b7422b656659ed5cc49b509 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Mon, 22 Sep 2025 10:39:51 -0500 Subject: [PATCH 15/21] Convert uses of StatusEffect[] to Uint8Array --- src/data/abilities/ability.ts | 43 ++++++++++--------- src/data/moves/move.ts | 9 ++-- .../mystery-encounter-requirements.ts | 5 ++- .../pokemon-forms/form-change-triggers.ts | 5 ++- src/typings/phaser/index.d.ts | 7 +++ src/utils/common.ts | 4 +- 6 files changed, 42 insertions(+), 31 deletions(-) diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index c4deeef3c1f..81d35d3a24c 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -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 type { GenericUint8Array, ReadonlyGenericInt8Array, ReadonlyGenericUint8Array } from "#types/typed-arrays"; import { coerceArray } from "#utils/array"; import type { Constructor } from "#utils/common"; import { BooleanHolder, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common"; @@ -1210,13 +1211,13 @@ export class PostDefendTerrainChangeAbAttr extends PostDefendAbAttr { export class PostDefendContactApplyStatusEffectAbAttr extends PostDefendAbAttr { private chance: number; - private effects: StatusEffect[]; + private readonly effects: ReadonlyGenericUint8Array; constructor(chance: number, ...effects: StatusEffect[]) { super(true); this.chance = chance; - this.effects = effects; + this.effects = new Uint8Array(effects); } override canApply({ pokemon, move, opponent: attacker }: PostMoveInteractionAbAttrParams): boolean { @@ -2180,14 +2181,14 @@ export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr { export class PostAttackApplyStatusEffectAbAttr extends PostAttackAbAttr { private contactRequired: boolean; private chance: number; - private effects: StatusEffect[]; + private readonly effects: ReadonlyGenericUint8Array; constructor(contactRequired: boolean, chance: number, ...effects: StatusEffect[]) { super(); this.contactRequired = contactRequired; this.chance = chance; - this.effects = effects; + this.effects = new Uint8Array(effects); } override canApply(params: PostMoveInteractionAbAttrParams): boolean { @@ -2940,7 +2941,7 @@ export class PostSummonTerrainChangeAbAttr extends PostSummonAbAttr { * Heals a status effect if the Pokemon is afflicted with it upon switch in (or gain) */ export class PostSummonHealStatusAbAttr extends PostSummonRemoveEffectAbAttr { - private immuneEffects: StatusEffect[]; + private readonly immuneEffects: ReadonlyGenericUint8Array; private statusHealed: StatusEffect; /** @@ -2948,12 +2949,12 @@ export class PostSummonHealStatusAbAttr extends PostSummonRemoveEffectAbAttr { */ constructor(...immuneEffects: StatusEffect[]) { super(); - this.immuneEffects = immuneEffects; + this.immuneEffects = new Uint8Array(immuneEffects); } public override canApply({ pokemon }: AbAttrBaseParams): boolean { const status = pokemon.status?.effect; - return status != null && (this.immuneEffects.length === 0 || this.immuneEffects.includes(status)); + return status != null && this.immuneEffects.includes(status); } public override apply({ pokemon }: AbAttrBaseParams): void { @@ -3049,7 +3050,7 @@ export class PostSummonCopyAbilityAbAttr extends PostSummonAbAttr { * Removes supplied status effects from the user's field. */ export class PostSummonUserFieldRemoveStatusEffectAbAttr extends PostSummonAbAttr { - private statusEffect: StatusEffect[]; + private readonly statusEffect: ReadonlyGenericUint8Array; /** * @param statusEffect - The status effects to be removed from the user's field. @@ -3057,7 +3058,7 @@ export class PostSummonUserFieldRemoveStatusEffectAbAttr extends PostSummonAbAtt constructor(...statusEffect: StatusEffect[]) { super(false); - this.statusEffect = statusEffect; + this.statusEffect = new Uint8Array(statusEffect); } override canApply({ pokemon }: AbAttrBaseParams): boolean { @@ -3637,7 +3638,7 @@ export class PreSetStatusAbAttr extends AbAttr { * Provides immunity to status effects to specified targets. */ export class PreSetStatusEffectImmunityAbAttr extends PreSetStatusAbAttr { - protected immuneEffects: StatusEffect[]; + protected readonly immuneEffects: ReadonlyGenericUint8Array; /** * @param immuneEffects - An array of {@linkcode StatusEffect}s to prevent application. @@ -3646,7 +3647,7 @@ export class PreSetStatusEffectImmunityAbAttr extends PreSetStatusAbAttr { constructor(...immuneEffects: StatusEffect[]) { super(); - this.immuneEffects = immuneEffects; + this.immuneEffects = new Uint8Array(immuneEffects); } override canApply({ effect, cancelled }: PreSetStatusAbAttrParams): boolean { @@ -3705,7 +3706,7 @@ export interface UserFieldStatusEffectImmunityAbAttrParams extends AbAttrBasePar */ export class UserFieldStatusEffectImmunityAbAttr extends CancelInteractionAbAttr { private declare readonly _: never; - protected immuneEffects: StatusEffect[]; + protected readonly immuneEffects: ReadonlyGenericUint8Array; /** * @param immuneEffects - An array of {@linkcode StatusEffect}s to prevent application. @@ -3714,7 +3715,7 @@ export class UserFieldStatusEffectImmunityAbAttr extends CancelInteractionAbAttr constructor(...immuneEffects: StatusEffect[]) { super(); - this.immuneEffects = immuneEffects; + this.immuneEffects = new Uint8Array(immuneEffects); } override canApply({ effect, cancelled }: UserFieldStatusEffectImmunityAbAttrParams): boolean { @@ -3998,7 +3999,7 @@ export class BlockNonDirectDamageAbAttr extends CancelInteractionAbAttr { * This attribute will block any status damage that you put in the parameter. */ export class BlockStatusDamageAbAttr extends CancelInteractionAbAttr { - private effects: StatusEffect[]; + private readonly effects: ReadonlyGenericUint8Array; /** * @param effects - The status effect(s) that will be blocked from damaging the ability pokemon @@ -4006,7 +4007,7 @@ export class BlockStatusDamageAbAttr extends CancelInteractionAbAttr { constructor(...effects: StatusEffect[]) { super(false); - this.effects = effects; + this.effects = new Uint8Array(effects); } override canApply({ pokemon, cancelled }: AbAttrParamsWithCancel): boolean { @@ -4539,7 +4540,7 @@ export class PostTurnAbAttr extends AbAttr { * @sealed */ export class PostTurnStatusHealAbAttr extends PostTurnAbAttr { - private effects: StatusEffect[]; + private readonly effects: GenericUint8Array; /** * @param effects - The status effect(s) that will qualify healing the ability pokemon @@ -4547,7 +4548,7 @@ export class PostTurnStatusHealAbAttr extends PostTurnAbAttr { constructor(...effects: StatusEffect[]) { super(false); - this.effects = effects; + this.effects = new Uint8Array(effects); } override canApply({ pokemon }: AbAttrBaseParams): boolean { @@ -5808,14 +5809,14 @@ export interface IgnoreTypeStatusEffectImmunityAbAttrParams extends AbAttrParams * @sealed */ export class IgnoreTypeStatusEffectImmunityAbAttr extends AbAttr { - private statusEffect: StatusEffect[]; - private defenderType: PokemonType[]; + private readonly statusEffect: ReadonlyGenericUint8Array; + private readonly defenderType: ReadonlyGenericInt8Array; constructor(statusEffect: StatusEffect[], defenderType: PokemonType[]) { super(false); - this.statusEffect = statusEffect; - this.defenderType = defenderType; + this.statusEffect = new Uint8Array(statusEffect); + this.defenderType = new Int8Array(defenderType); } override canApply({ statusEffect, defenderType, cancelled }: IgnoreTypeStatusEffectImmunityAbAttrParams): boolean { diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 9e0942eb4c8..3d6886abe2b 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -95,6 +95,7 @@ import i18next from "i18next"; import { applyChallenges } from "#utils/challenge-utils"; import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; import type { AbstractConstructor } from "#types/type-helpers"; +import type { ReadonlyGenericUint8Array } from "#types/typed-arrays"; /** * A function used to conditionally determine execution of a given {@linkcode MoveAttr}. @@ -2595,11 +2596,11 @@ export class StatusEffectAttr extends MoveEffectAttr { * Used for {@linkcode Moves.TRI_ATTACK} and {@linkcode Moves.DIRE_CLAW}. */ export class MultiStatusEffectAttr extends StatusEffectAttr { - public effects: StatusEffect[]; + public readonly effects: ReadonlyGenericUint8Array; constructor(effects: StatusEffect[], selfTarget?: boolean) { super(effects[0], selfTarget); - this.effects = effects; + this.effects = new Uint8Array(effects); } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { @@ -2925,7 +2926,7 @@ export class StealEatBerryAttr extends EatBerryAttr { */ export class HealStatusEffectAttr extends MoveEffectAttr { /** List of Status Effects to cure */ - private effects: StatusEffect[]; + private readonly effects: ReadonlyGenericUint8Array; /** * @param selfTarget - Whether this move targets the user @@ -2933,7 +2934,7 @@ export class HealStatusEffectAttr extends MoveEffectAttr { */ constructor(selfTarget: boolean, effects: StatusEffect | StatusEffect[]) { super(selfTarget, { lastHitOnly: true }); - this.effects = coerceArray(effects) + this.effects = new Uint8Array(coerceArray(effects)); } /** diff --git a/src/data/mystery-encounters/mystery-encounter-requirements.ts b/src/data/mystery-encounters/mystery-encounter-requirements.ts index e0deee3a4b9..a752ce428df 100644 --- a/src/data/mystery-encounters/mystery-encounter-requirements.ts +++ b/src/data/mystery-encounters/mystery-encounter-requirements.ts @@ -15,6 +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 type { ReadonlyGenericUint8Array } from "#types/typed-arrays"; import { coerceArray } from "#utils/array"; export interface EncounterRequirement { @@ -696,7 +697,7 @@ export class AbilityRequirement extends EncounterPokemonRequirement { } export class StatusEffectRequirement extends EncounterPokemonRequirement { - requiredStatusEffect: StatusEffect[]; + requiredStatusEffect: ReadonlyGenericUint8Array; minNumberOfPokemon: number; invertQuery: boolean; @@ -704,7 +705,7 @@ export class StatusEffectRequirement extends EncounterPokemonRequirement { super(); this.minNumberOfPokemon = minNumberOfPokemon; this.invertQuery = invertQuery; - this.requiredStatusEffect = coerceArray(statusEffect); + this.requiredStatusEffect = new Uint8Array(coerceArray(statusEffect)); } override meetsRequirement(): boolean { diff --git a/src/data/pokemon-forms/form-change-triggers.ts b/src/data/pokemon-forms/form-change-triggers.ts index 3a6c84f80d2..6be6385241e 100644 --- a/src/data/pokemon-forms/form-change-triggers.ts +++ b/src/data/pokemon-forms/form-change-triggers.ts @@ -11,6 +11,7 @@ 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 { ReadonlyGenericUint8Array } from "#types/typed-arrays"; import { coerceArray } from "#utils/array"; import type { Constructor } from "#utils/common"; import { toCamelCase } from "#utils/strings"; @@ -122,12 +123,12 @@ export class SpeciesFormChangeActiveTrigger extends SpeciesFormChangeTrigger { } export class SpeciesFormChangeStatusEffectTrigger extends SpeciesFormChangeTrigger { - public statusEffects: StatusEffect[]; + public readonly statusEffects: ReadonlyGenericUint8Array; public invert: boolean; constructor(statusEffects: StatusEffect | StatusEffect[], invert = false) { super(); - this.statusEffects = coerceArray(statusEffects); + this.statusEffects = new Uint8Array(coerceArray(statusEffects)); this.invert = invert; // this.description = i18next.t("pokemonEvolutions:forms.statusEffect"); } diff --git a/src/typings/phaser/index.d.ts b/src/typings/phaser/index.d.ts index caddaedfc59..706ee1a35ba 100644 --- a/src/typings/phaser/index.d.ts +++ b/src/typings/phaser/index.d.ts @@ -54,6 +54,13 @@ declare module "phaser" { } } + namespace Math { + interface RandomDataGenerator { + pick(array: ArrayLike): T; + weightedPick(array: ArrayLike): T; + } + } + namespace Input { namespace Gamepad { interface GamepadPlugin { diff --git a/src/utils/common.ts b/src/utils/common.ts index 9c6e03714db..29ab3cf462b 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -125,11 +125,11 @@ export function randSeedFloat(): number { return Phaser.Math.RND.frac(); } -export function randItem(items: T[]): T { +export function randItem(items: ArrayLike): T { return items.length === 1 ? items[0] : items[randInt(items.length)]; } -export function randSeedItem(items: T[]): T { +export function randSeedItem(items: ArrayLike): T { return items.length === 1 ? items[0] : Phaser.Math.RND.pick(items); } From 9921b57ef3687a11243468f0980fa4b41ae15e10 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Mon, 22 Sep 2025 10:43:26 -0500 Subject: [PATCH 16/21] Update ssui to use uint8array for ivs --- src/ui/handlers/starter-select-ui-handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/handlers/starter-select-ui-handler.ts b/src/ui/handlers/starter-select-ui-handler.ts index d0bef69aa81..4e7fd837a65 100644 --- a/src/ui/handlers/starter-select-ui-handler.ts +++ b/src/ui/handlers/starter-select-ui-handler.ts @@ -2759,7 +2759,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { pokerus: this.pokerusSpecies.includes(species), nickname: this.starterPreferences[species.speciesId]?.nickname, teraType, - ivs: dexEntry.ivs, + ivs: new Uint8Array(dexEntry.ivs), }; this.starters.push(starter); From ba7be773dcb6901a9181eb2f6511a161d13e22c2 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Mon, 22 Sep 2025 12:41:40 -0500 Subject: [PATCH 17/21] Revert use of typed arrays --- src/@types/common.ts | 3 + src/@types/save-data.ts | 2 +- src/battle-scene.ts | 13 ++- src/data/abilities/ability.ts | 45 +++++---- src/data/moves/move.ts | 11 ++- .../mystery-encounter-requirements.ts | 19 ++-- .../utils/encounter-phase-utils.ts | 2 +- src/data/pokemon-forms.ts | 3 +- .../pokemon-forms/form-change-triggers.ts | 7 +- src/data/pokemon/pokemon-data.ts | 12 +-- .../positional-tags/load-positional-tag.ts | 2 +- src/field/arena.ts | 3 +- src/field/pokemon.ts | 33 ++++--- src/system/game-data.ts | 2 +- src/system/pokemon-data.ts | 6 +- src/ui/containers/stats-container.ts | 2 +- src/ui/handlers/battle-message-ui-handler.ts | 4 +- src/ui/handlers/starter-select-ui-handler.ts | 2 +- src/utils/array.ts | 2 +- src/utils/common.ts | 9 +- test/abilities/beast-boost.test.ts | 6 +- test/battle/battle-order.test.ts | 26 +++--- test/escape-calculations.test.ts | 92 +++++++++++-------- test/moves/rollout.test.ts | 4 +- .../fun-and-games-encounter.test.ts | 2 +- .../encounters/part-timer-encounter.test.ts | 6 +- 26 files changed, 164 insertions(+), 154 deletions(-) diff --git a/src/@types/common.ts b/src/@types/common.ts index 93d88a3b680..4126733397e 100644 --- a/src/@types/common.ts +++ b/src/@types/common.ts @@ -1 +1,4 @@ export type ConditionFn = (args?: any[]) => boolean; + +/** Alias for the constructor of a class */ +export type Constructor = new (...args: unknown[]) => T; diff --git a/src/@types/save-data.ts b/src/@types/save-data.ts index c737f5097ad..ae359c20949 100644 --- a/src/@types/save-data.ts +++ b/src/@types/save-data.ts @@ -123,7 +123,7 @@ export interface Starter { pokerus: boolean; nickname?: string; teraType?: PokemonType; - ivs: Uint8Array; + ivs: number[]; } export type RunHistoryData = Record; diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 225f806d491..6da361dbf2f 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -118,9 +118,9 @@ import type { TrainerData } from "#system/trainer-data"; import type { Voucher } from "#system/voucher"; import { vouchers } from "#system/voucher"; import { trainerConfigs } from "#trainers/trainer-config"; +import type { Constructor } from "#types/common"; 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"; @@ -133,7 +133,6 @@ import { UI } from "#ui/ui"; import { addUiThemeOverrides } from "#ui/ui-theme"; import { BooleanHolder, - type Constructor, fixedInt, formatMoney, getIvsFromId, @@ -867,7 +866,7 @@ export class BattleScene extends SceneBase { gender?: Gender, shiny?: boolean, variant?: Variant, - ivs?: ReadonlyUint8Array | number[], + ivs?: number[], nature?: Nature, dataSource?: Pokemon | PokemonData, postProcess?: (playerPokemon: PlayerPokemon) => void, @@ -898,12 +897,12 @@ export class BattleScene extends SceneBase { if (Overrides.IVS_OVERRIDE.some(value => !isBetween(value, 0, 31))) { throw new Error("All IVs in the player IV override must be between 0 and 31!"); } - pokemon.ivs = new Uint8Array(Overrides.IVS_OVERRIDE); + pokemon.ivs = Overrides.IVS_OVERRIDE; } else { if (!isBetween(Overrides.IVS_OVERRIDE, 0, 31)) { throw new Error("The Player IV override must be a value between 0 and 31!"); } - pokemon.ivs = new Uint8Array(6).fill(Overrides.IVS_OVERRIDE); + pokemon.ivs = new Array(6).fill(Overrides.IVS_OVERRIDE); } if (Overrides.NATURE_OVERRIDE !== null) { @@ -963,12 +962,12 @@ export class BattleScene extends SceneBase { if (Overrides.ENEMY_IVS_OVERRIDE.some(value => !isBetween(value, 0, 31))) { throw new Error("All IVs in the enemy IV override must be between 0 and 31!"); } - pokemon.ivs = new Uint8Array(Overrides.ENEMY_IVS_OVERRIDE); + pokemon.ivs = Overrides.ENEMY_IVS_OVERRIDE; } else { if (!isBetween(Overrides.ENEMY_IVS_OVERRIDE, 0, 31)) { throw new Error("The Enemy IV override must be a value between 0 and 31!"); } - pokemon.ivs = new Uint8Array(6).fill(Overrides.ENEMY_IVS_OVERRIDE); + pokemon.ivs = new Array(6).fill(Overrides.ENEMY_IVS_OVERRIDE); } if (Overrides.ENEMY_NATURE_OVERRIDE !== null) { diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 81d35d3a24c..d602cfb9651 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -62,11 +62,10 @@ import type { PokemonDefendCondition, PokemonStatStageChangeCondition, } from "#types/ability-types"; +import type { Constructor } from "#types/common"; import type { Localizable } from "#types/locales"; import type { Closed, Exact } from "#types/type-helpers"; -import type { GenericUint8Array, ReadonlyGenericInt8Array, ReadonlyGenericUint8Array } from "#types/typed-arrays"; import { coerceArray } from "#utils/array"; -import type { Constructor } from "#utils/common"; import { BooleanHolder, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common"; import { toCamelCase } from "#utils/strings"; import i18next from "i18next"; @@ -1211,13 +1210,13 @@ export class PostDefendTerrainChangeAbAttr extends PostDefendAbAttr { export class PostDefendContactApplyStatusEffectAbAttr extends PostDefendAbAttr { private chance: number; - private readonly effects: ReadonlyGenericUint8Array; + private readonly effects: readonly StatusEffect[]; constructor(chance: number, ...effects: StatusEffect[]) { super(true); this.chance = chance; - this.effects = new Uint8Array(effects); + this.effects = effects; } override canApply({ pokemon, move, opponent: attacker }: PostMoveInteractionAbAttrParams): boolean { @@ -2181,14 +2180,14 @@ export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr { export class PostAttackApplyStatusEffectAbAttr extends PostAttackAbAttr { private contactRequired: boolean; private chance: number; - private readonly effects: ReadonlyGenericUint8Array; + private readonly effects: readonly StatusEffect[]; constructor(contactRequired: boolean, chance: number, ...effects: StatusEffect[]) { super(); this.contactRequired = contactRequired; this.chance = chance; - this.effects = new Uint8Array(effects); + this.effects = effects; } override canApply(params: PostMoveInteractionAbAttrParams): boolean { @@ -2941,7 +2940,7 @@ export class PostSummonTerrainChangeAbAttr extends PostSummonAbAttr { * Heals a status effect if the Pokemon is afflicted with it upon switch in (or gain) */ export class PostSummonHealStatusAbAttr extends PostSummonRemoveEffectAbAttr { - private readonly immuneEffects: ReadonlyGenericUint8Array; + private readonly immuneEffects: readonly StatusEffect[]; private statusHealed: StatusEffect; /** @@ -2949,7 +2948,7 @@ export class PostSummonHealStatusAbAttr extends PostSummonRemoveEffectAbAttr { */ constructor(...immuneEffects: StatusEffect[]) { super(); - this.immuneEffects = new Uint8Array(immuneEffects); + this.immuneEffects = immuneEffects; } public override canApply({ pokemon }: AbAttrBaseParams): boolean { @@ -3050,7 +3049,7 @@ export class PostSummonCopyAbilityAbAttr extends PostSummonAbAttr { * Removes supplied status effects from the user's field. */ export class PostSummonUserFieldRemoveStatusEffectAbAttr extends PostSummonAbAttr { - private readonly statusEffect: ReadonlyGenericUint8Array; + private readonly statusEffect: readonly StatusEffect[]; /** * @param statusEffect - The status effects to be removed from the user's field. @@ -3058,7 +3057,7 @@ export class PostSummonUserFieldRemoveStatusEffectAbAttr extends PostSummonAbAtt constructor(...statusEffect: StatusEffect[]) { super(false); - this.statusEffect = new Uint8Array(statusEffect); + this.statusEffect = statusEffect; } override canApply({ pokemon }: AbAttrBaseParams): boolean { @@ -3638,7 +3637,7 @@ export class PreSetStatusAbAttr extends AbAttr { * Provides immunity to status effects to specified targets. */ export class PreSetStatusEffectImmunityAbAttr extends PreSetStatusAbAttr { - protected readonly immuneEffects: ReadonlyGenericUint8Array; + protected readonly immuneEffects: readonly StatusEffect[]; /** * @param immuneEffects - An array of {@linkcode StatusEffect}s to prevent application. @@ -3647,7 +3646,7 @@ export class PreSetStatusEffectImmunityAbAttr extends PreSetStatusAbAttr { constructor(...immuneEffects: StatusEffect[]) { super(); - this.immuneEffects = new Uint8Array(immuneEffects); + this.immuneEffects = immuneEffects; } override canApply({ effect, cancelled }: PreSetStatusAbAttrParams): boolean { @@ -3706,7 +3705,7 @@ export interface UserFieldStatusEffectImmunityAbAttrParams extends AbAttrBasePar */ export class UserFieldStatusEffectImmunityAbAttr extends CancelInteractionAbAttr { private declare readonly _: never; - protected readonly immuneEffects: ReadonlyGenericUint8Array; + protected readonly immuneEffects: readonly StatusEffect[]; /** * @param immuneEffects - An array of {@linkcode StatusEffect}s to prevent application. @@ -3715,7 +3714,7 @@ export class UserFieldStatusEffectImmunityAbAttr extends CancelInteractionAbAttr constructor(...immuneEffects: StatusEffect[]) { super(); - this.immuneEffects = new Uint8Array(immuneEffects); + this.immuneEffects = immuneEffects; } override canApply({ effect, cancelled }: UserFieldStatusEffectImmunityAbAttrParams): boolean { @@ -3999,7 +3998,7 @@ export class BlockNonDirectDamageAbAttr extends CancelInteractionAbAttr { * This attribute will block any status damage that you put in the parameter. */ export class BlockStatusDamageAbAttr extends CancelInteractionAbAttr { - private readonly effects: ReadonlyGenericUint8Array; + private readonly effects: readonly StatusEffect[]; /** * @param effects - The status effect(s) that will be blocked from damaging the ability pokemon @@ -4007,7 +4006,7 @@ export class BlockStatusDamageAbAttr extends CancelInteractionAbAttr { constructor(...effects: StatusEffect[]) { super(false); - this.effects = new Uint8Array(effects); + this.effects = effects; } override canApply({ pokemon, cancelled }: AbAttrParamsWithCancel): boolean { @@ -4540,7 +4539,7 @@ export class PostTurnAbAttr extends AbAttr { * @sealed */ export class PostTurnStatusHealAbAttr extends PostTurnAbAttr { - private readonly effects: GenericUint8Array; + private readonly effects: readonly StatusEffect[]; /** * @param effects - The status effect(s) that will qualify healing the ability pokemon @@ -4548,7 +4547,7 @@ export class PostTurnStatusHealAbAttr extends PostTurnAbAttr { constructor(...effects: StatusEffect[]) { super(false); - this.effects = new Uint8Array(effects); + this.effects = effects; } override canApply({ pokemon }: AbAttrBaseParams): boolean { @@ -5809,14 +5808,14 @@ export interface IgnoreTypeStatusEffectImmunityAbAttrParams extends AbAttrParams * @sealed */ export class IgnoreTypeStatusEffectImmunityAbAttr extends AbAttr { - private readonly statusEffect: ReadonlyGenericUint8Array; - private readonly defenderType: ReadonlyGenericInt8Array; + private readonly statusEffect: readonly StatusEffect[]; + private readonly defenderType: readonly PokemonType[]; - constructor(statusEffect: StatusEffect[], defenderType: PokemonType[]) { + constructor(statusEffect: readonly StatusEffect[], defenderType: readonly PokemonType[]) { super(false); - this.statusEffect = new Uint8Array(statusEffect); - this.defenderType = new Int8Array(defenderType); + this.statusEffect = statusEffect; + this.defenderType = defenderType; } override canApply({ statusEffect, defenderType, cancelled }: IgnoreTypeStatusEffectImmunityAbAttrParams): boolean { diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 3d6886abe2b..03d4efd65db 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -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, type Constructor, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common"; +import { BooleanHolder, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common"; +import type { Constructor } from "#types/common"; import { coerceArray } from "#utils/array"; import { getEnumValues } from "#utils/enums"; import { toCamelCase, toTitleCase } from "#utils/strings"; @@ -2596,11 +2597,11 @@ export class StatusEffectAttr extends MoveEffectAttr { * Used for {@linkcode Moves.TRI_ATTACK} and {@linkcode Moves.DIRE_CLAW}. */ export class MultiStatusEffectAttr extends StatusEffectAttr { - public readonly effects: ReadonlyGenericUint8Array; + public readonly effects: readonly StatusEffect[]; constructor(effects: StatusEffect[], selfTarget?: boolean) { super(effects[0], selfTarget); - this.effects = new Uint8Array(effects); + this.effects = effects; } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { @@ -2926,7 +2927,7 @@ export class StealEatBerryAttr extends EatBerryAttr { */ export class HealStatusEffectAttr extends MoveEffectAttr { /** List of Status Effects to cure */ - private readonly effects: ReadonlyGenericUint8Array; + private readonly effects: readonly StatusEffect[]; /** * @param selfTarget - Whether this move targets the user @@ -2934,7 +2935,7 @@ export class HealStatusEffectAttr extends MoveEffectAttr { */ constructor(selfTarget: boolean, effects: StatusEffect | StatusEffect[]) { super(selfTarget, { lastHitOnly: true }); - this.effects = new Uint8Array(coerceArray(effects)); + this.effects = coerceArray(effects); } /** diff --git a/src/data/mystery-encounters/mystery-encounter-requirements.ts b/src/data/mystery-encounters/mystery-encounter-requirements.ts index a752ce428df..218a2e7cbed 100644 --- a/src/data/mystery-encounters/mystery-encounter-requirements.ts +++ b/src/data/mystery-encounters/mystery-encounter-requirements.ts @@ -15,7 +15,6 @@ 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 type { ReadonlyGenericUint8Array } from "#types/typed-arrays"; import { coerceArray } from "#utils/array"; export interface EncounterRequirement { @@ -697,15 +696,15 @@ export class AbilityRequirement extends EncounterPokemonRequirement { } export class StatusEffectRequirement extends EncounterPokemonRequirement { - requiredStatusEffect: ReadonlyGenericUint8Array; + requiredStatusEffect: readonly StatusEffect[]; minNumberOfPokemon: number; invertQuery: boolean; - constructor(statusEffect: StatusEffect | StatusEffect[], minNumberOfPokemon = 1, invertQuery = false) { + constructor(statusEffect: StatusEffect | readonly StatusEffect[], minNumberOfPokemon = 1, invertQuery = false) { super(); this.minNumberOfPokemon = minNumberOfPokemon; this.invertQuery = invertQuery; - this.requiredStatusEffect = new Uint8Array(coerceArray(statusEffect)); + this.requiredStatusEffect = coerceArray(statusEffect); } override meetsRequirement(): boolean { @@ -718,7 +717,7 @@ export class StatusEffectRequirement extends EncounterPokemonRequirement { return x; } - override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + override queryParty(partyPokemon: readonly PlayerPokemon[]): PlayerPokemon[] { if (!this.invertQuery) { return partyPokemon.filter(pokemon => { return this.requiredStatusEffect.some(statusEffect => { @@ -762,11 +761,11 @@ export class StatusEffectRequirement extends EncounterPokemonRequirement { * If you want to trigger the event based on the form change enabler, use PersistentModifierRequirement. */ export class CanFormChangeWithItemRequirement extends EncounterPokemonRequirement { - requiredFormChangeItem: FormChangeItem[]; + requiredFormChangeItem: readonly FormChangeItem[]; minNumberOfPokemon: number; invertQuery: boolean; - constructor(formChangeItem: FormChangeItem | FormChangeItem[], minNumberOfPokemon = 1, invertQuery = false) { + constructor(formChangeItem: FormChangeItem | readonly FormChangeItem[], minNumberOfPokemon = 1, invertQuery = false) { super(); this.minNumberOfPokemon = minNumberOfPokemon; this.invertQuery = invertQuery; @@ -793,7 +792,7 @@ export class CanFormChangeWithItemRequirement extends EncounterPokemonRequiremen ); } - override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + override queryParty(partyPokemon: readonly PlayerPokemon[]): PlayerPokemon[] { if (!this.invertQuery) { return partyPokemon.filter( pokemon => @@ -878,13 +877,13 @@ export class HeldItemRequirement extends EncounterPokemonRequirement { } export class AttackTypeBoosterHeldItemTypeRequirement extends EncounterPokemonRequirement { - requiredHeldItemTypes: PokemonType[]; + requiredHeldItemTypes: readonly PokemonType[]; minNumberOfPokemon: number; invertQuery: boolean; requireTransferable: boolean; constructor( - heldItemTypes: PokemonType | PokemonType[], + heldItemTypes: PokemonType | readonly PokemonType[], minNumberOfPokemon = 1, invertQuery = false, requireTransferable = true, diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 0cf90b0fcb1..58dbfc4182a 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -319,7 +319,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): // Set IVs if (config.ivs) { - enemyPokemon.ivs = new Uint8Array(config.ivs); + enemyPokemon.ivs = config.ivs; } // Set Status diff --git a/src/data/pokemon-forms.ts b/src/data/pokemon-forms.ts index 7eb8ed263d5..1b392f6fb21 100644 --- a/src/data/pokemon-forms.ts +++ b/src/data/pokemon-forms.ts @@ -24,7 +24,8 @@ import { SpeciesFormKey } from "#enums/species-form-key"; import { SpeciesId } from "#enums/species-id"; import { WeatherType } from "#enums/weather-type"; import type { Pokemon } from "#field/pokemon"; -import type { Constructor, nil } from "#utils/common"; +import type { Constructor } from "#types/common"; +import type { nil } from "#utils/common"; export type SpeciesFormChangeConditionPredicate = (p: Pokemon) => boolean; export type SpeciesFormChangeConditionEnforceFunc = (p: Pokemon) => void; diff --git a/src/data/pokemon-forms/form-change-triggers.ts b/src/data/pokemon-forms/form-change-triggers.ts index 6be6385241e..de0deb412a1 100644 --- a/src/data/pokemon-forms/form-change-triggers.ts +++ b/src/data/pokemon-forms/form-change-triggers.ts @@ -11,9 +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 { ReadonlyGenericUint8Array } from "#types/typed-arrays"; +import type { Constructor } from "#types/common"; import { coerceArray } from "#utils/array"; -import type { Constructor } from "#utils/common"; import { toCamelCase } from "#utils/strings"; import i18next from "i18next"; @@ -123,12 +122,12 @@ export class SpeciesFormChangeActiveTrigger extends SpeciesFormChangeTrigger { } export class SpeciesFormChangeStatusEffectTrigger extends SpeciesFormChangeTrigger { - public readonly statusEffects: ReadonlyGenericUint8Array; + public readonly statusEffects: readonly StatusEffect[]; public invert: boolean; constructor(statusEffects: StatusEffect | StatusEffect[], invert = false) { super(); - this.statusEffects = new Uint8Array(coerceArray(statusEffects)); + this.statusEffects = coerceArray(statusEffects); this.invert = invert; // this.description = i18next.t("pokemonEvolutions:forms.statusEffect"); } diff --git a/src/data/pokemon/pokemon-data.ts b/src/data/pokemon/pokemon-data.ts index ed94a87f2af..4fbb70bccb2 100644 --- a/src/data/pokemon/pokemon-data.ts +++ b/src/data/pokemon/pokemon-data.ts @@ -16,7 +16,6 @@ import type { AttackMoveResult } from "#types/attack-move-result"; import type { IllusionData } from "#types/illusion-data"; import type { TurnMove } from "#types/turn-move"; import type { CoerceNullPropertiesToUndefined } from "#types/type-helpers"; -import { setTypedArray } from "#utils/array"; import { getPokemonSpeciesForm } from "#utils/pokemon-utils"; /** @@ -130,7 +129,7 @@ export class PokemonSummonData { public passiveAbility: AbilityId | undefined; public gender: Gender | undefined; public fusionGender: Gender | undefined; - public stats: Uint32Array = new Uint32Array(6); + public stats: number[] = [0, 0, 0, 0, 0, 0]; public moveset: PokemonMove[] | null; // If not initialized this value will not be populated from save data. @@ -166,11 +165,6 @@ export class PokemonSummonData { continue; } - if (key === "stats") { - setTypedArray(this.stats, source.stats); - continue; - } - if (key === "illusion" && typeof value === "object") { // Make a copy so as not to mutate provided value const illusionData = { @@ -227,10 +221,8 @@ export class PokemonSummonData { // We coerce null to undefined in the type, as the for loop below replaces `null` with `undefined` ...(this as Omit< CoerceNullPropertiesToUndefined, - "speciesForm" | "fusionSpeciesForm" | "illusion" | "stats" + "speciesForm" | "fusionSpeciesForm" | "illusion" >), - // TypedArrays do not serialize to JSON as an array. - stats: Array.from(this.stats), speciesForm: speciesForm == null ? undefined : { id: speciesForm.speciesId, formIdx: speciesForm.formIndex }, fusionSpeciesForm: fusionSpeciesForm == null diff --git a/src/data/positional-tags/load-positional-tag.ts b/src/data/positional-tags/load-positional-tag.ts index ef3609d93e7..90c889db0e9 100644 --- a/src/data/positional-tags/load-positional-tag.ts +++ b/src/data/positional-tags/load-positional-tag.ts @@ -1,7 +1,7 @@ import { DelayedAttackTag, type PositionalTag, WishTag } from "#data/positional-tags/positional-tag"; import { PositionalTagType } from "#enums/positional-tag-type"; +import type { Constructor } from "#types/common"; import type { ObjectValues } from "#types/type-helpers"; -import type { Constructor } from "#utils/common"; /** * Load the attributes of a {@linkcode PositionalTag}. diff --git a/src/field/arena.ts b/src/field/arena.ts index 3e214ff1ea7..d307ce9d0e8 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -35,8 +35,9 @@ import { TagAddedEvent, TagRemovedEvent, TerrainChangedEvent, WeatherChangedEven import type { Pokemon } from "#field/pokemon"; import { FieldEffectModifier } from "#modifiers/modifier"; import type { Move } from "#moves/move"; +import type { Constructor } from "#types/common"; import type { AbstractConstructor } from "#types/type-helpers"; -import { type Constructor, NumberHolder, randSeedInt } from "#utils/common"; +import { NumberHolder, randSeedInt } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; export class Arena { diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index f223bbfd64f..9f5656aae12 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -141,22 +141,21 @@ import type { PokemonData } from "#system/pokemon-data"; import { RibbonData } from "#system/ribbons/ribbon-data"; import { awardRibbonsToSpeciesLine } from "#system/ribbons/ribbon-methods"; import type { AbAttrMap, AbAttrString, TypeMultiplierAbAttrParams } from "#types/ability-types"; +import type { Constructor } from "#types/common"; import type { getAttackDamageParams, getBaseDamageParams } from "#types/damage-params"; 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 { coerceArray } from "#utils/array"; import { applyChallenges } from "#utils/challenge-utils"; import { BooleanHolder, - type Constructor, deltaRgb, fixedInt, getIvsFromId, @@ -204,8 +203,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { public levelExp: number; public gender: Gender; public hp: number; - public stats = Uint32Array.of(1, 1, 1, 1, 1, 1); - public ivs = Uint8Array.of(0, 0, 0, 0, 0, 0); + public stats: number[]; + public ivs: number[]; public nature: Nature; public moveset: PokemonMove[]; /** @@ -312,7 +311,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { gender?: Gender, shiny?: boolean, variant?: Variant, - ivs?: ReadonlyUint8Array | number[], + ivs?: number[], nature?: Nature, dataSource?: Pokemon | PokemonData, ) { @@ -346,8 +345,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { if (dataSource) { this.id = dataSource.id; this.hp = dataSource.hp; - setTypedArray(this.stats, dataSource.stats); - setTypedArray(this.ivs, dataSource.ivs ?? getIvsFromId(dataSource.id)); + this.stats = dataSource.stats; + this.ivs = dataSource.ivs; this.passive = !!dataSource.passive; if (this.variant === undefined) { this.variant = 0; @@ -386,7 +385,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { this.stellarTypesBoosted = dataSource.stellarTypesBoosted ?? []; } else { this.id = randSeedInt(4294967296); - setTypedArray(this.ivs, ivs ?? getIvsFromId(this.id)); + this.ivs = ivs || getIvsFromId(this.id); if (this.gender === undefined) { this.gender = this.species.generateGender(); @@ -1320,7 +1319,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @param bypassSummonData - Whether to prefer actual stats (`true`) or in-battle overridden stats (`false`); default `true` * @returns The numeric values of this {@linkcode Pokemon}'s stats as an array. */ - getStats(bypassSummonData = true): Uint32Array { + getStats(bypassSummonData = true): number[] { if (!bypassSummonData) { // Only grab summon data stats if nonzero return this.summonData.stats.map((s, i) => s || this.stats[i]); @@ -1552,6 +1551,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } calculateStats(): void { + if (!this.stats) { + this.stats = [0, 0, 0, 0, 0, 0]; + } + // Get and manipulate base stats const baseStats = this.calculateBaseStats(); // Using base stats, calculate and store stats one by one @@ -1584,7 +1587,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { globalScene.applyModifier(PokemonIncrementingStatModifier, this.isPlayer(), this, s, statHolder); } - statHolder.value = Phaser.Math.Clamp(statHolder.value, 1, 0xffffffff); + statHolder.value = Phaser.Math.Clamp(statHolder.value, 1, Number.MAX_SAFE_INTEGER); this.setStat(s, statHolder.value); } @@ -5701,7 +5704,7 @@ export class PlayerPokemon extends Pokemon { gender?: Gender, shiny?: boolean, variant?: Variant, - ivs?: ReadonlyUint8Array | number[], + ivs?: number[], nature?: Nature, dataSource?: Pokemon | PokemonData, ) { @@ -6321,9 +6324,9 @@ export class EnemyPokemon extends Pokemon { if (this.hasTrainer() && globalScene.currentBattle) { const { waveIndex } = globalScene.currentBattle; - const ivs = new Uint8Array(6); - for (let i = 0; i < 6; i++) { - ivs[i] = this.randBattleSeedIntRange(Math.floor(waveIndex / 10), 31); + const ivs: number[] = []; + while (ivs.length < 6) { + ivs.push(randSeedIntRange(Math.floor(waveIndex / 10), 31)); } this.ivs = ivs; } diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 5f89c0a6da8..3ffa7482706 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -1914,7 +1914,7 @@ export class GameData { _unlockSpeciesNature(species.speciesId); } - updateSpeciesDexIvs(speciesId: SpeciesId, ivs: Uint8Array): void { + updateSpeciesDexIvs(speciesId: SpeciesId, ivs: number[]): void { let dexEntry: DexEntry; do { dexEntry = globalScene.gameData.dexData[speciesId]; diff --git a/src/system/pokemon-data.ts b/src/system/pokemon-data.ts index 4aa036f621c..0ddfedeff84 100644 --- a/src/system/pokemon-data.ts +++ b/src/system/pokemon-data.ts @@ -99,8 +99,8 @@ export class PokemonData { this.levelExp = source.levelExp; this.gender = source.gender; this.hp = source.hp; - this.stats = Array.from(source.stats); - this.ivs = Array.from(source.ivs); + this.stats = source.stats; + this.ivs = source.ivs; // TODO: Can't we move some of this verification stuff to an upgrade script? this.nature = source.nature ?? Nature.HARDY; @@ -162,7 +162,7 @@ export class PokemonData { this.gender, this.shiny, this.variant, - new Uint8Array(this.ivs.slice(0, 6)), + this.ivs, this.nature, this, playerPokemon => { diff --git a/src/ui/containers/stats-container.ts b/src/ui/containers/stats-container.ts index 55b099b95c5..fa53c8c8fd4 100644 --- a/src/ui/containers/stats-container.ts +++ b/src/ui/containers/stats-container.ts @@ -104,7 +104,7 @@ export class StatsContainer extends Phaser.GameObjects.Container { } } - updateIvs(ivs: Uint8Array | number[], originalIvs?: number[]): void { + updateIvs(ivs: number[], originalIvs?: number[]): void { if (ivs) { const ivChartData = new Array(6) .fill(null) diff --git a/src/ui/handlers/battle-message-ui-handler.ts b/src/ui/handlers/battle-message-ui-handler.ts index 79e7e318915..f845f22a730 100644 --- a/src/ui/handlers/battle-message-ui-handler.ts +++ b/src/ui/handlers/battle-message-ui-handler.ts @@ -195,7 +195,7 @@ export class BattleMessageUiHandler extends MessageUiHandler { super.showDialogue(text, name, delay, callback, callbackDelay, prompt, promptDelay); } - promptLevelUpStats(partyMemberIndex: number, prevStats: ArrayLike, showTotals: boolean): Promise { + promptLevelUpStats(partyMemberIndex: number, prevStats: number[], showTotals: boolean): Promise { return new Promise(resolve => { if (!globalScene.showLevelUpStats) { return resolve(); @@ -219,7 +219,7 @@ export class BattleMessageUiHandler extends MessageUiHandler { }); } - promptIvs(pokemonId: number, ivs: ArrayLike): Promise { + promptIvs(pokemonId: number, ivs: number[]): Promise { return new Promise(resolve => { globalScene.executeWithSeedOffset(() => { let levelUpStatsValuesText = ""; diff --git a/src/ui/handlers/starter-select-ui-handler.ts b/src/ui/handlers/starter-select-ui-handler.ts index 4e7fd837a65..d0bef69aa81 100644 --- a/src/ui/handlers/starter-select-ui-handler.ts +++ b/src/ui/handlers/starter-select-ui-handler.ts @@ -2759,7 +2759,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { pokerus: this.pokerusSpecies.includes(species), nickname: this.starterPreferences[species.speciesId]?.nickname, teraType, - ivs: new Uint8Array(dexEntry.ivs), + ivs: dexEntry.ivs, }; this.starters.push(starter); diff --git a/src/utils/array.ts b/src/utils/array.ts index 75807d2da21..ece0bdd24c5 100644 --- a/src/utils/array.ts +++ b/src/utils/array.ts @@ -12,7 +12,7 @@ import type { * 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(input: T): T extends any[] ? T : [T]; +export function coerceArray(input: T): T extends readonly any[] ? T : [T]; export function coerceArray(input: T): T | [T] { return Array.isArray(input) ? input : [input]; } diff --git a/src/utils/common.ts b/src/utils/common.ts index 29ab3cf462b..cd6b726938e 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -176,15 +176,15 @@ export function getPlayTimeString(totalSeconds: number): string { * @param id 32-bit number * @returns An array of six numbers corresponding to 5-bit chunks from {@linkcode id} */ -export function getIvsFromId(id: number): Uint8Array { - return Uint8Array.of( +export function getIvsFromId(id: number): [number, number, number, number, number, number] { + return [ (id & 0x3e000000) >>> 25, (id & 0x01f00000) >>> 20, (id & 0x000f8000) >>> 15, (id & 0x00007c00) >>> 10, (id & 0x000003e0) >>> 5, id & 0x0000001f, - ); + ]; } export function formatLargeNumber(count: number, threshold: number): string { @@ -292,9 +292,6 @@ export async function localPing(): Promise { } } -/** Alias for the constructor of a class */ -export type Constructor = new (...args: unknown[]) => T; - export class BooleanHolder { public value: boolean; diff --git a/test/abilities/beast-boost.test.ts b/test/abilities/beast-boost.test.ts index 7ec3ac355ff..aeb4d854b1a 100644 --- a/test/abilities/beast-boost.test.ts +++ b/test/abilities/beast-boost.test.ts @@ -38,7 +38,7 @@ describe("Abilities - Beast Boost", () => { const playerPokemon = game.field.getPlayerPokemon(); // Set the pokemon's highest stat to DEF, so it should be picked by Beast Boost - vi.spyOn(playerPokemon, "stats", "get").mockReturnValue(Uint32Array.of(10000, 100, 1000, 200, 100, 100)); + vi.spyOn(playerPokemon, "stats", "get").mockReturnValue([10000, 100, 1000, 200, 100, 100]); console.log(playerPokemon.stats); expect(playerPokemon.getStatStage(Stat.DEF)).toBe(0); @@ -56,7 +56,7 @@ describe("Abilities - Beast Boost", () => { const playerPokemon = game.field.getPlayerPokemon(); // If the opponent uses Guard Split, the pokemon's second highest stat (SPATK) should be chosen - vi.spyOn(playerPokemon, "stats", "get").mockReturnValue(Uint32Array.of(10000, 100, 201, 200, 100, 100)); + vi.spyOn(playerPokemon, "stats", "get").mockReturnValue([10000, 100, 201, 200, 100, 100]); expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(0); @@ -75,7 +75,7 @@ describe("Abilities - Beast Boost", () => { const playerPokemon = game.field.getPlayerPokemon(); // Set up tie between SPATK, SPDEF, and SPD, where SPATK should win - vi.spyOn(playerPokemon, "stats", "get").mockReturnValue(Uint32Array.of(10000, 1, 1, 100, 100, 100)); + vi.spyOn(playerPokemon, "stats", "get").mockReturnValue([10000, 1, 1, 100, 100, 100]); expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(0); diff --git a/test/battle/battle-order.test.ts b/test/battle/battle-order.test.ts index 382604bffcd..de13b22df79 100644 --- a/test/battle/battle-order.test.ts +++ b/test/battle/battle-order.test.ts @@ -39,8 +39,8 @@ describe("Battle order", () => { const enemyPokemon = game.field.getEnemyPokemon(); const enemyStartHp = enemyPokemon.hp; - vi.spyOn(playerPokemon, "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, 50)); // set playerPokemon's speed to 50 - vi.spyOn(enemyPokemon, "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, 150)); // set enemyPokemon's speed to 150 + vi.spyOn(playerPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 50]); // set playerPokemon's speed to 50 + vi.spyOn(enemyPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set enemyPokemon's speed to 150 game.move.select(MoveId.TACKLE); await game.phaseInterceptor.to("MoveEndPhase", false); @@ -55,8 +55,8 @@ describe("Battle order", () => { const playerStartHp = playerPokemon.hp; const enemyPokemon = game.field.getEnemyPokemon(); const enemyStartHp = enemyPokemon.hp; - vi.spyOn(playerPokemon, "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, 150)); // set playerPokemon's speed to 150 - vi.spyOn(enemyPokemon, "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, 50)); // set enemyPokemon's speed to 50 + vi.spyOn(playerPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set playerPokemon's speed to 150 + vi.spyOn(enemyPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 50]); // set enemyPokemon's speed to 50 game.move.select(MoveId.TACKLE); @@ -74,8 +74,8 @@ describe("Battle order", () => { const enemyPokemon = game.scene.getEnemyField(); const enemyHps = enemyPokemon.map(p => p.hp); - playerPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, 50))); // set both playerPokemons' speed to 50 - enemyPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, 150))); // set both enemyPokemons' speed to 150 + playerPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 50])); // set both playerPokemons' speed to 50 + enemyPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150])); // set both enemyPokemons' speed to 150 game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE, 1); @@ -96,9 +96,9 @@ describe("Battle order", () => { const playerPokemon = game.scene.getPlayerField(); const enemyPokemon = game.scene.getEnemyField(); - playerPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, 100))); //set both playerPokemons' speed to 100 - vi.spyOn(enemyPokemon[0], "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, 100)); // set enemyPokemon's speed to 100 - vi.spyOn(enemyPokemon[1], "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, 150)); // set enemyPokemon's speed to 150 + playerPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100])); //set both playerPokemons' speed to 100 + vi.spyOn(enemyPokemon[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100]); // set enemyPokemon's speed to 100 + vi.spyOn(enemyPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set enemyPokemon's speed to 150 game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE, 1); @@ -114,10 +114,10 @@ describe("Battle order", () => { const playerPokemon = game.scene.getPlayerField(); const enemyPokemon = game.scene.getEnemyField(); - vi.spyOn(playerPokemon[0], "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, 100)); // set one playerPokemon's speed to 100 - vi.spyOn(playerPokemon[1], "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, 150)); // set other playerPokemon's speed to 150 - vi.spyOn(enemyPokemon[0], "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, 100)); // set one enemyPokemon's speed to 100 - vi.spyOn(enemyPokemon[1], "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, 150)); // set other enemyPokemon's speed to 150 + vi.spyOn(playerPokemon[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100]); // set one playerPokemon's speed to 100 + vi.spyOn(playerPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set other playerPokemon's speed to 150 + vi.spyOn(enemyPokemon[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100]); // set one enemyPokemon's speed to 100 + vi.spyOn(enemyPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set other enemyPokemon's speed to 150 game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE, 1); diff --git a/test/escape-calculations.test.ts b/test/escape-calculations.test.ts index 062a86c6fa2..e1e521f4394 100644 --- a/test/escape-calculations.test.ts +++ b/test/escape-calculations.test.ts @@ -38,7 +38,7 @@ describe("Escape chance calculations", () => { const enemyField = game.scene.getEnemyField(); const enemySpeed = 100; // set enemyPokemon's speed to 100 - vi.spyOn(enemyField[0], "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, enemySpeed)); + vi.spyOn(enemyField[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, enemySpeed]); const commandPhase = game.scene.phaseManager.getCurrentPhase() as CommandPhase; commandPhase.handleCommand(Command.RUN, 0); @@ -81,9 +81,14 @@ describe("Escape chance calculations", () => { // set the number of escape attempts to the required amount game.scene.currentBattle.escapeAttempts = check.escapeAttempts; // set playerPokemon's speed to a multiple of the enemySpeed - vi.spyOn(playerPokemon[0], "stats", "get").mockReturnValue( - Uint32Array.of(20, 20, 20, 20, 20, check.pokemonSpeedRatio * enemySpeed), - ); + vi.spyOn(playerPokemon[0], "stats", "get").mockReturnValue([ + 20, + 20, + 20, + 20, + 20, + check.pokemonSpeedRatio * enemySpeed, + ]); const chance = phase.calculateEscapeChance(game.scene.currentBattle.escapeAttempts); expect(chance).toBe(check.expectedEscapeChance); } @@ -102,9 +107,9 @@ describe("Escape chance calculations", () => { // this is used to find the ratio of the player's first pokemon const playerASpeedPercentage = 0.4; // set enemyAPokemon's speed to 70 - vi.spyOn(enemyField[0], "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, enemyASpeed)); + vi.spyOn(enemyField[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, enemyASpeed]); // set enemyBPokemon's speed to 30 - vi.spyOn(enemyField[1], "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, enemyBSpeed)); + vi.spyOn(enemyField[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, enemyBSpeed]); const commandPhase = game.scene.phaseManager.getCurrentPhase() as CommandPhase; commandPhase.handleCommand(Command.RUN, 0); @@ -146,20 +151,23 @@ describe("Escape chance calculations", () => { // sets the number of escape attempts to the required amount game.scene.currentBattle.escapeAttempts = check.escapeAttempts; // set the first playerPokemon's speed to a multiple of the enemySpeed - vi.spyOn(playerPokemon[0], "stats", "get").mockReturnValue( - Uint32Array.of( - 20, - 20, - 20, - 20, - 20, - Math.floor(check.pokemonSpeedRatio * totalEnemySpeed * playerASpeedPercentage), - ), - ); + vi.spyOn(playerPokemon[0], "stats", "get").mockReturnValue([ + 20, + 20, + 20, + 20, + 20, + Math.floor(check.pokemonSpeedRatio * totalEnemySpeed * playerASpeedPercentage), + ]); // set the second playerPokemon's speed to the remaining value of speed - vi.spyOn(playerPokemon[1], "stats", "get").mockReturnValue( - Uint32Array.of(20, 20, 20, 20, 20, check.pokemonSpeedRatio * totalEnemySpeed - playerPokemon[0].stats[5]), - ); + vi.spyOn(playerPokemon[1], "stats", "get").mockReturnValue([ + 20, + 20, + 20, + 20, + 20, + check.pokemonSpeedRatio * totalEnemySpeed - playerPokemon[0].stats[5], + ]); const chance = phase.calculateEscapeChance(game.scene.currentBattle.escapeAttempts); // checks to make sure the escape values are the same expect(chance).toBe(check.expectedEscapeChance); @@ -176,7 +184,7 @@ describe("Escape chance calculations", () => { const enemyField = game.scene.getEnemyField()!; const enemySpeed = 100; // set enemyPokemon's speed to 100 - vi.spyOn(enemyField[0], "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, enemySpeed)); + vi.spyOn(enemyField[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, enemySpeed]); const commandPhase = game.scene.phaseManager.getCurrentPhase() as CommandPhase; commandPhase.handleCommand(Command.RUN, 0); @@ -233,9 +241,14 @@ describe("Escape chance calculations", () => { // sets the number of escape attempts to the required amount game.scene.currentBattle.escapeAttempts = check.escapeAttempts; // set playerPokemon's speed to a multiple of the enemySpeed - vi.spyOn(playerPokemon[0], "stats", "get").mockReturnValue( - Uint32Array.of(20, 20, 20, 20, 20, check.pokemonSpeedRatio * enemySpeed), - ); + vi.spyOn(playerPokemon[0], "stats", "get").mockReturnValue([ + 20, + 20, + 20, + 20, + 20, + check.pokemonSpeedRatio * enemySpeed, + ]); const chance = phase.calculateEscapeChance(game.scene.currentBattle.escapeAttempts); expect(chance).toBe(check.expectedEscapeChance); } @@ -254,9 +267,9 @@ describe("Escape chance calculations", () => { // this is used to find the ratio of the player's first pokemon const playerASpeedPercentage = 0.8; // set enemyAPokemon's speed to 70 - vi.spyOn(enemyField[0], "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, enemyASpeed)); + vi.spyOn(enemyField[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, enemyASpeed]); // set enemyBPokemon's speed to 30 - vi.spyOn(enemyField[1], "stats", "get").mockReturnValue(Uint32Array.of(20, 20, 20, 20, 20, enemyBSpeed)); + vi.spyOn(enemyField[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, enemyBSpeed]); const commandPhase = game.scene.phaseManager.getCurrentPhase() as CommandPhase; commandPhase.handleCommand(Command.RUN, 0); @@ -311,20 +324,23 @@ describe("Escape chance calculations", () => { // sets the number of escape attempts to the required amount game.scene.currentBattle.escapeAttempts = check.escapeAttempts; // set the first playerPokemon's speed to a multiple of the enemySpeed - vi.spyOn(playerPokemon[0], "stats", "get").mockReturnValue( - Uint32Array.of( - 20, - 20, - 20, - 20, - 20, - Math.floor(check.pokemonSpeedRatio * totalEnemySpeed * playerASpeedPercentage), - ), - ); + vi.spyOn(playerPokemon[0], "stats", "get").mockReturnValue([ + 20, + 20, + 20, + 20, + 20, + Math.floor(check.pokemonSpeedRatio * totalEnemySpeed * playerASpeedPercentage), + ]); // set the second playerPokemon's speed to the remaining value of speed - vi.spyOn(playerPokemon[1], "stats", "get").mockReturnValue( - Uint32Array.of(20, 20, 20, 20, 20, check.pokemonSpeedRatio * totalEnemySpeed - playerPokemon[0].stats[5]), - ); + vi.spyOn(playerPokemon[1], "stats", "get").mockReturnValue([ + 20, + 20, + 20, + 20, + 20, + check.pokemonSpeedRatio * totalEnemySpeed - playerPokemon[0].stats[5], + ]); const chance = phase.calculateEscapeChance(game.scene.currentBattle.escapeAttempts); // checks to make sure the escape values are the same expect(chance).toBe(check.expectedEscapeChance); diff --git a/test/moves/rollout.test.ts b/test/moves/rollout.test.ts index 9e6d8b87e06..0e01725a188 100644 --- a/test/moves/rollout.test.ts +++ b/test/moves/rollout.test.ts @@ -45,10 +45,10 @@ describe("Moves - Rollout", () => { await game.classicMode.startBattle(); const playerPkm = game.field.getPlayerPokemon(); - vi.spyOn(playerPkm, "stats", "get").mockReturnValue(Uint32Array.of(500000, 1, 1, 1, 1, 1)); // HP, ATK, DEF, SPATK, SPDEF, SPD + vi.spyOn(playerPkm, "stats", "get").mockReturnValue([500000, 1, 1, 1, 1, 1]); // HP, ATK, DEF, SPATK, SPDEF, SPD const enemyPkm = game.field.getEnemyPokemon(); - vi.spyOn(enemyPkm, "stats", "get").mockReturnValue(Uint32Array.of(500000, 1, 1, 1, 1, 1)); // HP, ATK, DEF, SPATK, SPDEF, SPD + vi.spyOn(enemyPkm, "stats", "get").mockReturnValue([500000, 1, 1, 1, 1, 1]); // HP, ATK, DEF, SPATK, SPDEF, SPD vi.spyOn(enemyPkm, "getHeldItems").mockReturnValue([]); //no berries enemyPkm.hp = enemyPkm.getMaxHp(); diff --git a/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts b/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts index fb1e5034c9d..bc1a2893627 100644 --- a/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts +++ b/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts @@ -144,7 +144,7 @@ describe("Fun And Games! - Mystery Encounter", () => { expect(game).toBeAtPhase("CommandPhase"); expect(game.field.getEnemyPokemon().species.speciesId).toBe(SpeciesId.WOBBUFFET); - expect(game.field.getEnemyPokemon().ivs).toEqual(Uint8Array.of(0, 0, 0, 0, 0, 0)); + expect(game.field.getEnemyPokemon().ivs).toEqual([0, 0, 0, 0, 0, 0]); expect(game.field.getEnemyPokemon().nature).toBe(Nature.MILD); game.onNextPrompt("MessagePhase", UiMode.MESSAGE, () => { diff --git a/test/mystery-encounter/encounters/part-timer-encounter.test.ts b/test/mystery-encounter/encounters/part-timer-encounter.test.ts index 05af5757b2a..15d2664364c 100644 --- a/test/mystery-encounter/encounters/part-timer-encounter.test.ts +++ b/test/mystery-encounter/encounters/part-timer-encounter.test.ts @@ -122,7 +122,7 @@ describe("Part-Timer - Mystery Encounter", () => { // Override party levels to 50 so stats can be fully reflective scene.getPlayerParty().forEach(p => { p.level = 50; - p.ivs = Uint8Array.of(20, 20, 20, 20, 20, 20); + p.ivs = [20, 20, 20, 20, 20, 20]; p.calculateStats(); }); await runMysteryEncounterToEnd(game, 1, { pokemonNo: 2 }); @@ -168,7 +168,7 @@ describe("Part-Timer - Mystery Encounter", () => { // Override party levels to 50 so stats can be fully reflective scene.getPlayerParty().forEach(p => { p.level = 50; - p.ivs = Uint8Array.of(0, 0, 0, 0, 0, 0); + p.ivs = [0, 0, 0, 0, 0, 0]; p.calculateStats(); }); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 3 }); @@ -188,7 +188,7 @@ describe("Part-Timer - Mystery Encounter", () => { // Override party levels to 50 so stats can be fully reflective scene.getPlayerParty().forEach(p => { p.level = 50; - p.ivs = Uint8Array.of(20, 20, 20, 20, 20, 20); + p.ivs = [20, 20, 20, 20, 20, 20]; p.calculateStats(); }); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 4 }); From 6896e9abaabedc71a50715d6ce1d9cb5e812c0c2 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Mon, 22 Sep 2025 13:11:58 -0500 Subject: [PATCH 18/21] Move `nil` to @types/common --- src/@types/common.ts | 2 ++ src/data/battle-anims.ts | 3 ++- src/data/pokemon-forms.ts | 3 +-- src/phases/move-effect-phase.ts | 2 +- src/timed-event-manager.ts | 2 +- src/utils/common.ts | 2 -- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/@types/common.ts b/src/@types/common.ts index 4126733397e..c1373d93297 100644 --- a/src/@types/common.ts +++ b/src/@types/common.ts @@ -2,3 +2,5 @@ export type ConditionFn = (args?: any[]) => boolean; /** Alias for the constructor of a class */ export type Constructor = new (...args: unknown[]) => T; + +export type nil = null | undefined; diff --git a/src/data/battle-anims.ts b/src/data/battle-anims.ts index ac93a0cac1c..e1966805b40 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -7,8 +7,9 @@ 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 type { nil } from "#types/common"; import { coerceArray } from "#utils/array"; -import { getFrameMs, type nil } from "#utils/common"; +import { getFrameMs } from "#utils/common"; import { getEnumKeys, getEnumValues } from "#utils/enums"; import { toKebabCase } from "#utils/strings"; import Phaser from "phaser"; diff --git a/src/data/pokemon-forms.ts b/src/data/pokemon-forms.ts index 1b392f6fb21..0517459aaa1 100644 --- a/src/data/pokemon-forms.ts +++ b/src/data/pokemon-forms.ts @@ -24,8 +24,7 @@ import { SpeciesFormKey } from "#enums/species-form-key"; import { SpeciesId } from "#enums/species-id"; import { WeatherType } from "#enums/weather-type"; import type { Pokemon } from "#field/pokemon"; -import type { Constructor } from "#types/common"; -import type { nil } from "#utils/common"; +import type { Constructor, nil } from "#types/common"; export type SpeciesFormChangeConditionPredicate = (p: Pokemon) => boolean; export type SpeciesFormChangeConditionEnforceFunc = (p: Pokemon) => void; diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index be6d0164698..2115694d6c7 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -37,9 +37,9 @@ import { getMoveTargets, isFieldTargeted } from "#moves/move-utils"; import { PokemonMove } from "#moves/pokemon-move"; import { PokemonPhase } from "#phases/pokemon-phase"; import { DamageAchv } from "#system/achv"; +import type { nil } from "#types/common"; import type { DamageResult } from "#types/damage-result"; import type { TurnMove } from "#types/turn-move"; -import type { nil } from "#utils/common"; import { BooleanHolder, NumberHolder } from "#utils/common"; import i18next from "i18next"; diff --git a/src/timed-event-manager.ts b/src/timed-event-manager.ts index 75f4afa772e..d0ebb5607bb 100644 --- a/src/timed-event-manager.ts +++ b/src/timed-event-manager.ts @@ -7,8 +7,8 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; import { TextStyle } from "#enums/text-style"; import { WeatherType } from "#enums/weather-type"; +import type { nil } from "#types/common"; import { addTextObject } from "#ui/text"; -import type { nil } from "#utils/common"; import i18next from "i18next"; export enum EventType { diff --git a/src/utils/common.ts b/src/utils/common.ts index cd6b726938e..ae586b60455 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -3,8 +3,6 @@ import { MoneyFormat } from "#enums/money-format"; import type { Variant } from "#sprites/variant"; import i18next from "i18next"; -export type nil = null | undefined; - export const MissingTextureKey = "__MISSING"; // TODO: Draft tests for these utility functions From e938ff608d9ae8f08b2b61a55ab23fcf019476c6 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Mon, 22 Sep 2025 14:12:37 -0500 Subject: [PATCH 19/21] Make more arrays readonly --- src/data/abilities/ability.ts | 2 +- src/data/balance/pokemon-species.ts | 2 +- src/data/data-lists.ts | 6 +- src/data/moves/move.ts | 2 +- .../encounters/bug-type-superfan-encounter.ts | 69 +++++++------------ src/data/pokemon-forms.ts | 2 +- src/data/trainers/trainer-config.ts | 2 +- src/field/arena.ts | 2 +- src/utils/common.ts | 16 ++--- 9 files changed, 41 insertions(+), 62 deletions(-) diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index d602cfb9651..339a6cb7f79 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -6699,7 +6699,7 @@ function getPokemonWithWeatherBasedForms() { // biome-ignore format: prevent biome from removing the newlines (e.g. prevent `new Ability(...).attr(...)`) export function initAbilities() { - allAbilities.push( + (allAbilities as Ability[]).push( new Ability(AbilityId.NONE, 3), new Ability(AbilityId.STENCH, 3) .attr(PostAttackApplyBattlerTagAbAttr, false, (user, target, move) => !move.hasAttr("FlinchAttr") && !move.hitsSubstitute(user, target) ? 10 : 0, BattlerTagType.FLINCHED), diff --git a/src/data/balance/pokemon-species.ts b/src/data/balance/pokemon-species.ts index c6c17986257..1d89b77e98c 100644 --- a/src/data/balance/pokemon-species.ts +++ b/src/data/balance/pokemon-species.ts @@ -8,7 +8,7 @@ import { SpeciesId } from "#enums/species-id"; // biome-ignore format: manually formatted export function initSpecies() { - allSpecies.push( + (allSpecies as PokemonSpecies[]).push( new PokemonSpecies(SpeciesId.BULBASAUR, 1, false, false, false, "Seed Pokémon", PokemonType.GRASS, PokemonType.POISON, 0.7, 6.9, AbilityId.OVERGROW, AbilityId.NONE, AbilityId.CHLOROPHYLL, 318, 45, 49, 49, 65, 65, 45, 45, 50, 64, GrowthRate.MEDIUM_SLOW, 87.5, false), new PokemonSpecies(SpeciesId.IVYSAUR, 1, false, false, false, "Seed Pokémon", PokemonType.GRASS, PokemonType.POISON, 1, 13, AbilityId.OVERGROW, AbilityId.NONE, AbilityId.CHLOROPHYLL, 405, 60, 62, 63, 80, 80, 60, 45, 50, 142, GrowthRate.MEDIUM_SLOW, 87.5, false), new PokemonSpecies(SpeciesId.VENUSAUR, 1, false, false, false, "Seed Pokémon", PokemonType.GRASS, PokemonType.POISON, 2, 100, AbilityId.OVERGROW, AbilityId.NONE, AbilityId.CHLOROPHYLL, 525, 80, 82, 83, 100, 100, 80, 45, 50, 263, GrowthRate.MEDIUM_SLOW, 87.5, true, true, diff --git a/src/data/data-lists.ts b/src/data/data-lists.ts index ae3d0acc77f..7c9a9520735 100644 --- a/src/data/data-lists.ts +++ b/src/data/data-lists.ts @@ -3,9 +3,9 @@ import type { PokemonSpecies } from "#data/pokemon-species"; import type { ModifierTypes } from "#modifiers/modifier-type"; import type { Move } from "#moves/move"; -export const allAbilities: Ability[] = []; -export const allMoves: Move[] = []; -export const allSpecies: PokemonSpecies[] = []; +export const allAbilities: readonly Ability[] = []; +export const allMoves: readonly Move[] = []; +export const allSpecies: readonly PokemonSpecies[] = []; // TODO: Figure out what this is used for and provide an appropriate tsdoc comment export const modifierTypes = {} as ModifierTypes; diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 03d4efd65db..252bd5d6b4b 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -8394,7 +8394,7 @@ const MoveAttrs = Object.freeze({ export type MoveAttrConstructorMap = typeof MoveAttrs; export function initMoves() { - allMoves.push( + (allMoves as Move[]).push( new SelfStatusMove(MoveId.NONE, PokemonType.NORMAL, MoveCategory.STATUS, -1, -1, 0, 1), new AttackMove(MoveId.POUND, PokemonType.NORMAL, MoveCategory.PHYSICAL, 40, 100, 35, -1, 0, 1), new AttackMove(MoveId.KARATE_CHOP, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 50, 100, 25, -1, 0, 1) diff --git a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts index 00e98048ada..6ab029a7ff9 100644 --- a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts +++ b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts @@ -78,7 +78,7 @@ const POOL_1_POKEMON = [ SpeciesId.RIBOMBEE, SpeciesId.SPIDOPS, SpeciesId.LOKIX, -]; +] as const; const POOL_2_POKEMON = [ SpeciesId.SCYTHER, @@ -104,41 +104,20 @@ const POOL_2_POKEMON = [ SpeciesId.CENTISKORCH, SpeciesId.FROSMOTH, SpeciesId.KLEAVOR, -]; +] as const; -const POOL_3_POKEMON: { species: SpeciesId; formIndex?: number }[] = [ - { - species: SpeciesId.PINSIR, - formIndex: 1, - }, - { - species: SpeciesId.SCIZOR, - formIndex: 1, - }, - { - species: SpeciesId.HERACROSS, - formIndex: 1, - }, - { - species: SpeciesId.ORBEETLE, - formIndex: 1, - }, - { - species: SpeciesId.CENTISKORCH, - formIndex: 1, - }, - { - species: SpeciesId.DURANT, - }, - { - species: SpeciesId.VOLCARONA, - }, - { - species: SpeciesId.GOLISOPOD, - }, -]; +const POOL_3_POKEMON = [ + { species: SpeciesId.PINSIR, formIndex: 1 }, + { species: SpeciesId.SCIZOR, formIndex: 1 }, + { species: SpeciesId.HERACROSS, formIndex: 1 }, + { species: SpeciesId.ORBEETLE, formIndex: 1 }, + { species: SpeciesId.CENTISKORCH, formIndex: 1 }, + { species: SpeciesId.DURANT } as { species: SpeciesId.DURANT; formIndex: undefined }, + { species: SpeciesId.VOLCARONA } as { species: SpeciesId.VOLCARONA; formIndex: undefined }, + { species: SpeciesId.GOLISOPOD } as { species: SpeciesId.GOLISOPOD; formIndex: undefined }, +] as const; -const POOL_4_POKEMON = [SpeciesId.GENESECT, SpeciesId.SLITHER_WING, SpeciesId.BUZZWOLE, SpeciesId.PHEROMOSA]; +const POOL_4_POKEMON = [SpeciesId.GENESECT, SpeciesId.SLITHER_WING, SpeciesId.BUZZWOLE, SpeciesId.PHEROMOSA] as const; const PHYSICAL_TUTOR_MOVES = [ MoveId.MEGAHORN, @@ -146,7 +125,7 @@ const PHYSICAL_TUTOR_MOVES = [ MoveId.BUG_BITE, MoveId.FIRST_IMPRESSION, MoveId.LUNGE, -]; +] as const; const SPECIAL_TUTOR_MOVES = [ MoveId.SILVER_WIND, @@ -154,7 +133,7 @@ const SPECIAL_TUTOR_MOVES = [ MoveId.BUG_BUZZ, MoveId.POLLEN_PUFF, MoveId.STRUGGLE_BUG, -]; +] as const; const STATUS_TUTOR_MOVES = [ MoveId.STRING_SHOT, @@ -162,14 +141,20 @@ const STATUS_TUTOR_MOVES = [ MoveId.RAGE_POWDER, MoveId.STICKY_WEB, MoveId.SILK_TRAP, -]; +] as const; -const MISC_TUTOR_MOVES = [MoveId.LEECH_LIFE, MoveId.U_TURN, MoveId.HEAL_ORDER, MoveId.QUIVER_DANCE, MoveId.INFESTATION]; +const MISC_TUTOR_MOVES = [ + MoveId.LEECH_LIFE, + MoveId.U_TURN, + MoveId.HEAL_ORDER, + MoveId.QUIVER_DANCE, + MoveId.INFESTATION, +] as const; /** * Wave breakpoints that determine how strong to make the Bug-Type Superfan's team */ -const WAVE_LEVEL_BREAKPOINTS = [30, 50, 70, 100, 120, 140, 160]; +const WAVE_LEVEL_BREAKPOINTS = [30, 50, 70, 100, 120, 140, 160] as const; /** * Bug Type Superfan encounter. @@ -517,8 +502,8 @@ function getTrainerConfigForWave(waveIndex: number) { const config = trainerConfigs[TrainerType.BUG_TYPE_SUPERFAN].clone(); config.name = i18next.t("trainerNames:bugTypeSuperfan"); - let pool3Copy = POOL_3_POKEMON.slice(0); - pool3Copy = randSeedShuffle(pool3Copy); + const pool3Copy = randSeedShuffle(POOL_3_POKEMON.slice()); + // Bang is fine here, as we know pool3Copy has at least 1 entry const pool3Mon = pool3Copy.pop()!; if (waveIndex < WAVE_LEVEL_BREAKPOINTS[0]) { @@ -579,7 +564,6 @@ function getTrainerConfigForWave(waveIndex: number) { }), ); } else if (waveIndex < WAVE_LEVEL_BREAKPOINTS[5]) { - pool3Copy = randSeedShuffle(pool3Copy); const pool3Mon2 = pool3Copy.pop()!; config .setPartyTemplates(new TrainerPartyTemplate(5, PartyMemberStrength.AVERAGE)) @@ -657,7 +641,6 @@ function getTrainerConfigForWave(waveIndex: number) { ) .setPartyMemberFunc(4, getRandomPartyMemberFunc(POOL_4_POKEMON, TrainerSlot.TRAINER, true)); } else { - pool3Copy = randSeedShuffle(pool3Copy); const pool3Mon2 = pool3Copy.pop()!; config .setPartyTemplates( diff --git a/src/data/pokemon-forms.ts b/src/data/pokemon-forms.ts index 0517459aaa1..909967b84fa 100644 --- a/src/data/pokemon-forms.ts +++ b/src/data/pokemon-forms.ts @@ -35,7 +35,7 @@ export class SpeciesFormChange { public formKey: string; public trigger: SpeciesFormChangeTrigger; public quiet: boolean; - public readonly conditions: SpeciesFormChangeCondition[]; + public readonly conditions: readonly SpeciesFormChangeCondition[]; constructor( speciesId: SpeciesId, diff --git a/src/data/trainers/trainer-config.ts b/src/data/trainers/trainer-config.ts index 982ef989971..1e6e06abd10 100644 --- a/src/data/trainers/trainer-config.ts +++ b/src/data/trainers/trainer-config.ts @@ -1000,7 +1000,7 @@ let t = 0; * @param postProcess */ export function getRandomPartyMemberFunc( - speciesPool: SpeciesId[], + speciesPool: readonly SpeciesId[], trainerSlot: TrainerSlot = TrainerSlot.TRAINER, ignoreEvolution = false, postProcess?: (enemyPokemon: EnemyPokemon) => void, diff --git a/src/field/arena.ts b/src/field/arena.ts index d307ce9d0e8..4cd814ab907 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -588,7 +588,7 @@ export class Arena { overrideTint(): [number, number, number] { switch (Overrides.ARENA_TINT_OVERRIDE) { case TimeOfDay.DUSK: - return [98, 48, 73].map(c => Math.round((c + 128) / 2)) as [number, number, number]; + return [113, 88, 101]; case TimeOfDay.NIGHT: return [64, 64, 64]; case TimeOfDay.DAWN: diff --git a/src/utils/common.ts b/src/utils/common.ts index ae586b60455..61ce2177a1e 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -132,20 +132,16 @@ export function randSeedItem(items: ArrayLike): T { } /** - * Shuffle a list using the seeded rng. Utilises the Fisher-Yates algorithm. + * Shuffle a list in place using the seeded rng and the Fisher-Yates algorithm. * @param items An array of items. - * @returns A new shuffled array of items. + * @returns `items` shuffled in place. */ export function randSeedShuffle(items: T[]): T[] { - if (items.length <= 1) { - return items; - } - const newArray = items.slice(0); for (let i = items.length - 1; i > 0; i--) { const j = Phaser.Math.RND.integerInRange(0, i); - [newArray[i], newArray[j]] = [newArray[j], newArray[i]]; + [items[i], items[j]] = [items[j], items[i]]; } - return newArray; + return items; } export function getFrameMs(frameCount: number): number { @@ -338,7 +334,7 @@ export function rgbToHsv(r: number, g: number, b: number) { * @param rgb1 First RGB color in array * @param rgb2 Second RGB color in array */ -export function deltaRgb(rgb1: number[], rgb2: number[]): number { +export function deltaRgb(rgb1: readonly number[], rgb2: readonly number[]): number { const [r1, g1, b1] = rgb1; const [r2, g2, b2] = rgb2; const drp2 = Math.pow(r1 - r2, 2); @@ -362,7 +358,7 @@ export function rgbHexToRgba(hex: string) { }; } -export function rgbaToInt(rgba: number[]): number { +export function rgbaToInt(rgba: readonly number[]): number { return (rgba[0] << 24) + (rgba[1] << 16) + (rgba[2] << 8) + rgba[3]; } From 8003215c39a6c169b8611e6f020d263e2a077d4d Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Mon, 22 Sep 2025 17:25:07 -0500 Subject: [PATCH 20/21] fix: remnant change to immune effects --- src/data/abilities/ability.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 339a6cb7f79..db186432e1c 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -2953,7 +2953,8 @@ export class PostSummonHealStatusAbAttr extends PostSummonRemoveEffectAbAttr { public override canApply({ pokemon }: AbAttrBaseParams): boolean { const status = pokemon.status?.effect; - return status != null && this.immuneEffects.includes(status); + const immuneEffects = this.immuneEffects; + return status != null && (immuneEffects.length === 0 || immuneEffects.includes(status)); } public override apply({ pokemon }: AbAttrBaseParams): void { From ac2f09ad339d95c1481e79b068f0695dd6c0c42d Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Mon, 22 Sep 2025 18:42:50 -0500 Subject: [PATCH 21/21] Even more array improvements --- src/@types/trainer-funcs.ts | 4 +- src/battle.ts | 17 +- src/constants.ts | 2 +- src/data/balance/biomes.ts | 49 ++--- src/data/battler-tags.ts | 8 +- src/data/egg.ts | 6 +- src/data/pokemon-forms.ts | 5 +- .../pokemon-forms/form-change-triggers.ts | 15 +- src/field/pokemon.ts | 3 +- src/field/trainer.ts | 4 +- src/modifier/modifier-type.ts | 12 +- src/phases/battle-phase.ts | 2 +- src/timed-event-manager.ts | 203 +++++++++--------- src/ui/handlers/pokedex-page-ui-handler.ts | 6 +- src/utils/speed-order.ts | 11 +- 15 files changed, 178 insertions(+), 169 deletions(-) diff --git a/src/@types/trainer-funcs.ts b/src/@types/trainer-funcs.ts index aa839cbd158..cc4ba91aff3 100644 --- a/src/@types/trainer-funcs.ts +++ b/src/@types/trainer-funcs.ts @@ -7,8 +7,8 @@ import type { TrainerPartyTemplate } from "#trainers/trainer-party-template"; export type PartyTemplateFunc = () => TrainerPartyTemplate; export type PartyMemberFunc = (level: number, strength: PartyMemberStrength) => EnemyPokemon; -export type GenModifiersFunc = (party: EnemyPokemon[]) => PersistentModifier[]; -export type GenAIFunc = (party: EnemyPokemon[]) => void; +export type GenModifiersFunc = (party: readonly EnemyPokemon[]) => PersistentModifier[]; +export type GenAIFunc = (party: readonly EnemyPokemon[]) => void; export interface TrainerTierPools { [key: number]: SpeciesId[]; diff --git a/src/battle.ts b/src/battle.ts index 1b789707806..27a2498ffdb 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -526,24 +526,25 @@ export class FixedBattleConfig { /** * Helper function to generate a random trainer for evil team trainers and the elite 4/champion - * @param trainerPool The TrainerType or list of TrainerTypes that can possibly be generated - * @param randomGender whether or not to randomly (50%) generate a female trainer (for use with evil team grunts) - * @param seedOffset the seed offset to use for the random generation of the trainer - * @returns the generated trainer + * @param trainerPool - An array of trainer types to choose from. If an entry is an array, a random trainer type will be chosen from that array + * @param randomGender - Whether or not to randomly (50%) generate a female trainer (for use with evil team grunts) + * @param seedOffset - The seed offset to use for the random generation of the trainer + * @returns A function to get a random trainer */ export function getRandomTrainerFunc( - trainerPool: (TrainerType | TrainerType[])[], + trainerPool: readonly (TrainerType | readonly TrainerType[])[], randomGender = false, seedOffset = 0, ): GetTrainerFunc { return () => { const rand = randSeedInt(trainerPool.length); - const trainerTypes: TrainerType[] = []; + const trainerTypes: TrainerType[] = new Array(trainerPool.length); globalScene.executeWithSeedOffset(() => { - for (const trainerPoolEntry of trainerPool) { + for (let i = 0; i < trainerPool.length; i++) { + const trainerPoolEntry = trainerPool[i]; const trainerType = Array.isArray(trainerPoolEntry) ? randSeedItem(trainerPoolEntry) : trainerPoolEntry; - trainerTypes.push(trainerType); + trainerTypes[i] = trainerType; } }, seedOffset); diff --git a/src/constants.ts b/src/constants.ts index 17cf08aa7e2..8cc2cb550cb 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -23,7 +23,7 @@ export const TYPE_BOOST_ITEM_BOOST_PERCENT = 20; /** * The default species that a new player can choose from */ -export const defaultStarterSpecies: SpeciesId[] = [ +export const defaultStarterSpecies: readonly SpeciesId[] = [ SpeciesId.BULBASAUR, SpeciesId.CHARMANDER, SpeciesId.SQUIRTLE, diff --git a/src/data/balance/biomes.ts b/src/data/balance/biomes.ts index 9af2dbe221c..b7d74634589 100644 --- a/src/data/balance/biomes.ts +++ b/src/data/balance/biomes.ts @@ -5,6 +5,7 @@ import { PokemonType } from "#enums/pokemon-type"; import { SpeciesId } from "#enums/species-id"; import { TimeOfDay } from "#enums/time-of-day"; import { TrainerType } from "#enums/trainer-type"; +import type { Mutable } from "#types/type-helpers"; import { randSeedInt } from "#utils/common"; import { getEnumValues } from "#utils/enums"; import { toCamelCase } from "#utils/strings"; @@ -88,19 +89,19 @@ export enum BiomePoolTier { export const uncatchableSpecies: SpeciesId[] = []; interface SpeciesTree { - [key: number]: SpeciesId[] + readonly [key: number]: SpeciesId[] } export interface PokemonPools { - [key: number]: (SpeciesId | SpeciesTree)[] + readonly [key: number]: (SpeciesId | SpeciesTree)[] } interface BiomeTierPokemonPools { - [key: number]: PokemonPools + readonly [key: number]: PokemonPools } interface BiomePokemonPools { - [key: number]: BiomeTierPokemonPools + readonly [key: number]: BiomeTierPokemonPools } export interface BiomeTierTod { @@ -110,17 +111,17 @@ export interface BiomeTierTod { } export interface CatchableSpecies{ - [key: number]: BiomeTierTod[] + readonly [key: number]: readonly BiomeTierTod[] } export const catchableSpecies: CatchableSpecies = {}; export interface BiomeTierTrainerPools { - [key: number]: TrainerType[] + readonly [key: number]: readonly TrainerType[] } export interface BiomeTrainerPools { - [key: number]: BiomeTierTrainerPools + readonly [key: number]: BiomeTierTrainerPools } export const biomePokemonPools: BiomePokemonPools = { @@ -7621,12 +7622,10 @@ export function initBiomes() { ? biomeLinks[biome] as (BiomeId | [ BiomeId, number ])[] : [ biomeLinks[biome] as BiomeId ]; for (const linkedBiomeEntry of linkedBiomes) { - const linkedBiome = !Array.isArray(linkedBiomeEntry) - ? linkedBiomeEntry as BiomeId - : linkedBiomeEntry[0]; - const biomeChance = !Array.isArray(linkedBiomeEntry) - ? 1 - : linkedBiomeEntry[1]; + const linkedBiome = Array.isArray(linkedBiomeEntry) + ? linkedBiomeEntry[0] : linkedBiomeEntry as BiomeId; + const biomeChance = Array.isArray(linkedBiomeEntry) + ? linkedBiomeEntry[1] : 1; if (!biomeDepths.hasOwnProperty(linkedBiome) || biomeChance < biomeDepths[linkedBiome][1] || (depth < biomeDepths[linkedBiome][0] && biomeChance === biomeDepths[linkedBiome][1])) { biomeDepths[linkedBiome] = [ depth + 1, biomeChance ]; traverseBiome(linkedBiome, depth + 1); @@ -7638,15 +7637,15 @@ export function initBiomes() { biomeDepths[BiomeId.END] = [ Object.values(biomeDepths).map(d => d[0]).reduce((max: number, value: number) => Math.max(max, value), 0) + 1, 1 ]; for (const biome of getEnumValues(BiomeId)) { - biomePokemonPools[biome] = {}; - biomeTrainerPools[biome] = {}; + (biomePokemonPools[biome] as Mutable) = {}; + (biomeTrainerPools[biome] as Mutable) = {}; for (const tier of getEnumValues(BiomePoolTier)) { - biomePokemonPools[biome][tier] = {}; - biomeTrainerPools[biome][tier] = []; + (biomePokemonPools[biome][tier] as Mutable) = {}; + (biomeTrainerPools[biome][tier] as Mutable) = []; for (const tod of getEnumValues(TimeOfDay)) { - biomePokemonPools[biome][tier][tod] = []; + (biomePokemonPools[biome][tier][tod] as Mutable) = []; } } } @@ -7663,8 +7662,9 @@ export function initBiomes() { uncatchableSpecies.push(speciesId); } + type mutableSpecies = Mutable; // array of biome options for the current species - catchableSpecies[speciesId] = []; + (catchableSpecies[speciesId] as mutableSpecies) = []; for (const b of biomeEntries) { const biome = b[0]; @@ -7675,7 +7675,7 @@ export function initBiomes() { : [ b[2] ] : [ TimeOfDay.ALL ]; - catchableSpecies[speciesId].push({ + (catchableSpecies[speciesId] as mutableSpecies).push({ biome: biome as BiomeId, tier: tier as BiomePoolTier, tod: timesOfDay as TimeOfDay[] @@ -7735,12 +7735,13 @@ export function initBiomes() { }; for (let s = 1; s < entry.length; s++) { const speciesId = entry[s]; + // biome-ignore lint/nursery/noShadow: one-off const prevolution = entry.flatMap((s: string | number) => pokemonEvolutions[s]).find(e => e && e.speciesId === speciesId); const level = prevolution.level - (prevolution.level === 1 ? 1 : 0) + (prevolution.wildDelay * 10) - (tier >= BiomePoolTier.BOSS ? 10 : 0); - if (!newEntry.hasOwnProperty(level)) { - newEntry[level] = [ speciesId ]; - } else { + if (newEntry.hasOwnProperty(level)) { newEntry[level].push(speciesId); + } else { + newEntry[level] = [ speciesId ]; } } biomeTierTimePool[e] = newEntry; @@ -7763,7 +7764,7 @@ export function initBiomes() { } const biomeTierPool = biomeTrainerPools[biome][tier]; - biomeTierPool.push(trainerType); + (biomeTierPool as Mutable).push(trainerType); } //outputPools(); } diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index f9bd31beb17..ae5a3882ccc 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -2154,8 +2154,8 @@ export class HighestStatBoostTag extends AbilityBattlerTag { } export class WeatherHighestStatBoostTag extends HighestStatBoostTag { - #weatherTypes: WeatherType[]; - public get weatherTypes(): WeatherType[] { + readonly #weatherTypes: readonly WeatherType[]; + public get weatherTypes(): readonly WeatherType[] { return this.#weatherTypes; } @@ -2166,8 +2166,8 @@ export class WeatherHighestStatBoostTag extends HighestStatBoostTag { } export class TerrainHighestStatBoostTag extends HighestStatBoostTag { - #terrainTypes: TerrainType[]; - public get terrainTypes(): TerrainType[] { + readonly #terrainTypes: readonly TerrainType[]; + public get terrainTypes(): readonly TerrainType[] { return this.#terrainTypes; } diff --git a/src/data/egg.ts b/src/data/egg.ts index d4d37071d11..1e8dcc1665c 100644 --- a/src/data/egg.ts +++ b/src/data/egg.ts @@ -485,14 +485,14 @@ export class Egg { * and being the same each time */ let totalWeight = 0; - const speciesWeights: number[] = []; - for (const speciesId of speciesPool) { + const speciesWeights = new Array(speciesPool.length); + for (const [idx, speciesId] of speciesPool.entries()) { // Accounts for species that have starter costs outside of the normal range for their EggTier const speciesCostClamped = Phaser.Math.Clamp(speciesStarterCosts[speciesId], minStarterValue, maxStarterValue); const weight = Math.floor( (((maxStarterValue - speciesCostClamped) / (maxStarterValue - minStarterValue + 1)) * 1.5 + 1) * 100, ); - speciesWeights.push(totalWeight + weight); + speciesWeights[idx] = totalWeight + weight; totalWeight += weight; } diff --git a/src/data/pokemon-forms.ts b/src/data/pokemon-forms.ts index 909967b84fa..f223347c60f 100644 --- a/src/data/pokemon-forms.ts +++ b/src/data/pokemon-forms.ts @@ -25,6 +25,7 @@ import { SpeciesId } from "#enums/species-id"; import { WeatherType } from "#enums/weather-type"; import type { Pokemon } from "#field/pokemon"; import type { Constructor, nil } from "#types/common"; +import type { Mutable } from "#types/type-helpers"; export type SpeciesFormChangeConditionPredicate = (p: Pokemon) => boolean; export type SpeciesFormChangeConditionEnforceFunc = (p: Pokemon) => void; @@ -116,7 +117,7 @@ function getSpeciesDependentFormChangeCondition(species: SpeciesId): SpeciesForm } interface PokemonFormChanges { - [key: string]: SpeciesFormChange[]; + [key: string]: readonly SpeciesFormChange[]; } // biome-ignore format: manually formatted @@ -608,6 +609,6 @@ export function initPokemonForms() { ); } } - formChanges.push(...newFormChanges); + (formChanges as Mutable).push(...newFormChanges); } } diff --git a/src/data/pokemon-forms/form-change-triggers.ts b/src/data/pokemon-forms/form-change-triggers.ts index de0deb412a1..149c44bd978 100644 --- a/src/data/pokemon-forms/form-change-triggers.ts +++ b/src/data/pokemon-forms/form-change-triggers.ts @@ -240,9 +240,9 @@ export class SpeciesFormChangeWeatherTrigger extends SpeciesFormChangeTrigger { /** The ability that triggers the form change */ public ability: AbilityId; /** The list of weathers that trigger the form change */ - public weathers: WeatherType[]; + public readonly weathers: readonly WeatherType[]; - constructor(ability: AbilityId, weathers: WeatherType[]) { + constructor(ability: AbilityId, weathers: readonly WeatherType[]) { super(); this.ability = ability; this.weathers = weathers; @@ -278,9 +278,9 @@ export class SpeciesFormChangeRevertWeatherFormTrigger extends SpeciesFormChange /** The ability that triggers the form change*/ public ability: AbilityId; /** The list of weathers that will also trigger a form change to original form */ - public weathers: WeatherType[]; + public readonly weathers: readonly WeatherType[]; - constructor(ability: AbilityId, weathers: WeatherType[]) { + constructor(ability: AbilityId, weathers: readonly WeatherType[]) { super(); this.ability = ability; this.weathers = weathers; @@ -310,9 +310,10 @@ export class SpeciesFormChangeRevertWeatherFormTrigger extends SpeciesFormChange } export function getSpeciesFormChangeMessage(pokemon: Pokemon, formChange: SpeciesFormChange, preName: string): string { - const isMega = formChange.formKey.indexOf(SpeciesFormKey.MEGA) > -1; - const isGmax = formChange.formKey.indexOf(SpeciesFormKey.GIGANTAMAX) > -1; - const isEmax = formChange.formKey.indexOf(SpeciesFormKey.ETERNAMAX) > -1; + const formKey = formChange.formKey; + const isMega = formKey.indexOf(SpeciesFormKey.MEGA) > -1; + const isGmax = formKey.indexOf(SpeciesFormKey.GIGANTAMAX) > -1; + const isEmax = formKey.indexOf(SpeciesFormKey.ETERNAMAX) > -1; const isRevert = !isMega && formChange.formKey === pokemon.species.forms[0].formKey; if (isMega) { return i18next.t("battlePokemonForm:megaChange", { diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 9f5656aae12..40fdb35a6e1 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2140,7 +2140,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * Suppresses an ability and calls its onlose attributes */ public suppressAbility() { - [true, false].forEach(passive => applyOnLoseAbAttrs({ pokemon: this, passive })); + applyOnLoseAbAttrs({ pokemon: this, passive: true }); + applyOnLoseAbAttrs({ pokemon: this, passive: false }); this.summonData.abilitySuppressed = true; } diff --git a/src/field/trainer.ts b/src/field/trainer.ts index f5b2e5dad99..d3fe36259dc 100644 --- a/src/field/trainer.ts +++ b/src/field/trainer.ts @@ -642,14 +642,14 @@ export class Trainer extends Phaser.GameObjects.Container { } } - genModifiers(party: EnemyPokemon[]): PersistentModifier[] { + genModifiers(party: readonly EnemyPokemon[]): PersistentModifier[] { if (this.config.genModifiersFunc) { return this.config.genModifiersFunc(party); } return []; } - genAI(party: EnemyPokemon[]) { + genAI(party: readonly EnemyPokemon[]) { if (this.config.genAIFuncs) { this.config.genAIFuncs.forEach(f => f(party)); } diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index d67011bc145..0b1656e5464 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -277,7 +277,7 @@ export class ModifierType { } } -type ModifierTypeGeneratorFunc = (party: Pokemon[], pregenArgs?: any[]) => ModifierType | null; +type ModifierTypeGeneratorFunc = (party: readonly Pokemon[], pregenArgs?: any[]) => ModifierType | null; export class ModifierTypeGenerator extends ModifierType { private genTypeFunc: ModifierTypeGeneratorFunc; @@ -287,7 +287,7 @@ export class ModifierTypeGenerator extends ModifierType { this.genTypeFunc = genTypeFunc; } - generateType(party: Pokemon[], pregenArgs?: any[]) { + generateType(party: readonly Pokemon[], pregenArgs?: any[]) { const ret = this.genTypeFunc(party, pregenArgs); if (ret) { ret.id = this.id; @@ -2360,7 +2360,11 @@ const tierWeights = [768 / 1024, 195 / 1024, 48 / 1024, 12 / 1024, 1 / 1024]; */ export const itemPoolChecks: Map = new Map(); -export function regenerateModifierPoolThresholds(party: Pokemon[], poolType: ModifierPoolType, rerollCount = 0) { +export function regenerateModifierPoolThresholds( + party: readonly Pokemon[], + poolType: ModifierPoolType, + rerollCount = 0, +) { const pool = getModifierPoolForType(poolType); itemPoolChecks.forEach((_v, k) => { itemPoolChecks.set(k, false); @@ -2906,7 +2910,7 @@ export class ModifierTypeOption { * @param party The player's party. * @returns A number between 0 and 14 based on the party's total luck value, or a random number between 0 and 14 if the player is in Daily Run mode. */ -export function getPartyLuckValue(party: Pokemon[]): number { +export function getPartyLuckValue(party: readonly Pokemon[]): number { if (globalScene.gameMode.isDaily) { const DailyLuck = new NumberHolder(0); globalScene.executeWithSeedOffset( diff --git a/src/phases/battle-phase.ts b/src/phases/battle-phase.ts index 26794ed9bc5..eb87aca9ca4 100644 --- a/src/phases/battle-phase.ts +++ b/src/phases/battle-phase.ts @@ -12,7 +12,7 @@ export abstract class BattlePhase extends Phase { const tintSprites = globalScene.currentBattle.trainer.getTintSprites(); for (let i = 0; i < sprites.length; i++) { const visible = !trainerSlot || !i === (trainerSlot === TrainerSlot.TRAINER) || sprites.length < 2; - [sprites[i], tintSprites[i]].map(sprite => { + [sprites[i], tintSprites[i]].forEach(sprite => { if (visible) { sprite.x = trainerSlot || sprites.length < 2 ? 0 : i ? 16 : -16; } diff --git a/src/timed-event-manager.ts b/src/timed-event-manager.ts index d0ebb5607bb..f225bf8db61 100644 --- a/src/timed-event-manager.ts +++ b/src/timed-event-manager.ts @@ -18,59 +18,59 @@ export enum EventType { } interface EventBanner { - bannerKey?: string; - xOffset?: number; - yOffset?: number; - scale?: number; - availableLangs?: string[]; + readonly bannerKey?: string; + readonly xOffset?: number; + readonly yOffset?: number; + readonly scale?: number; + readonly availableLangs?: readonly string[]; } interface EventEncounter { - species: SpeciesId; - blockEvolution?: boolean; - formIndex?: number; + readonly species: SpeciesId; + readonly blockEvolution?: boolean; + readonly formIndex?: number; } interface EventMysteryEncounterTier { - mysteryEncounter: MysteryEncounterType; - tier?: MysteryEncounterTier; - disable?: boolean; + readonly mysteryEncounter: MysteryEncounterType; + readonly tier?: MysteryEncounterTier; + readonly disable?: boolean; } interface EventWaveReward { - wave: number; - type: string; + readonly wave: number; + readonly type: string; } -type EventMusicReplacement = [string, string]; +type EventMusicReplacement = readonly [string, string]; interface EventChallenge { - challenge: Challenges; - value: number; + readonly challenge: Challenges; + readonly value: number; } interface TimedEvent extends EventBanner { - name: string; - eventType: EventType; - shinyMultiplier?: number; - classicFriendshipMultiplier?: number; - luckBoost?: number; - upgradeUnlockedVouchers?: boolean; - startDate: Date; - endDate: Date; - eventEncounters?: EventEncounter[]; - delibirdyBuff?: string[]; - weather?: WeatherPoolEntry[]; - mysteryEncounterTierChanges?: EventMysteryEncounterTier[]; - luckBoostedSpecies?: SpeciesId[]; - boostFusions?: boolean; //MODIFIER REWORK PLEASE - classicWaveRewards?: EventWaveReward[]; // Rival battle rewards - trainerShinyChance?: number; // Odds over 65536 of trainer mon generating as shiny - music?: EventMusicReplacement[]; - dailyRunChallenges?: EventChallenge[]; + readonly name: string; + readonly eventType: EventType; + readonly shinyMultiplier?: number; + readonly classicFriendshipMultiplier?: number; + readonly luckBoost?: number; + readonly upgradeUnlockedVouchers?: boolean; + readonly startDate: Date; + readonly endDate: Date; + readonly eventEncounters?: readonly EventEncounter[]; + readonly delibirdyBuff?: readonly string[]; + readonly weather?: readonly WeatherPoolEntry[]; + readonly mysteryEncounterTierChanges?: readonly EventMysteryEncounterTier[]; + readonly luckBoostedSpecies?: readonly SpeciesId[]; + readonly boostFusions?: boolean; //MODIFIER REWORK PLEASE + readonly classicWaveRewards?: readonly EventWaveReward[]; // Rival battle rewards + readonly trainerShinyChance?: number; // Odds over 65536 of trainer mon generating as shiny + readonly music?: readonly EventMusicReplacement[]; + readonly dailyRunChallenges?: readonly EventChallenge[]; } -const timedEvents: TimedEvent[] = [ +const timedEvents: readonly TimedEvent[] = [ { name: "Winter Holiday Update", eventType: EventType.SHINY, @@ -385,7 +385,8 @@ const timedEvents: TimedEvent[] = [ export class TimedEventManager { isActive(event: TimedEvent) { - return event.startDate < new Date() && new Date() < event.endDate; + const now = new Date(); + return event.startDate < now && now < event.endDate; } activeEvent(): TimedEvent | undefined { @@ -427,19 +428,17 @@ export class TimedEventManager { getEventBannerLangs(): string[] { const ret: string[] = []; - ret.push(...timedEvents.find(te => this.isActive(te) && te.availableLangs != null)?.availableLangs!); + ret.push(...(timedEvents.find(te => this.isActive(te) && te.availableLangs != null)?.availableLangs ?? [])); return ret; } getEventEncounters(): EventEncounter[] { const ret: EventEncounter[] = []; - timedEvents - .filter(te => this.isActive(te)) - .map(te => { - if (te.eventEncounters != null) { - ret.push(...te.eventEncounters); - } - }); + for (const te of timedEvents) { + if (this.isActive(te) && te.eventEncounters != null) { + ret.push(...te.eventEncounters); + } + } return ret; } @@ -472,13 +471,11 @@ export class TimedEventManager { */ getDelibirdyBuff(): string[] { const ret: string[] = []; - timedEvents - .filter(te => this.isActive(te)) - .map(te => { - if (te.delibirdyBuff != null) { - ret.push(...te.delibirdyBuff); - } - }); + for (const te of timedEvents) { + if (this.isActive(te) && te.delibirdyBuff != null) { + ret.push(...te.delibirdyBuff); + } + } return ret; } @@ -488,39 +485,35 @@ export class TimedEventManager { */ getWeather(): WeatherPoolEntry[] { const ret: WeatherPoolEntry[] = []; - timedEvents - .filter(te => this.isActive(te)) - .map(te => { - if (te.weather != null) { - ret.push(...te.weather); - } - }); + for (const te of timedEvents) { + if (this.isActive(te) && te.weather != null) { + ret.push(...te.weather); + } + } return ret; } getAllMysteryEncounterChanges(): EventMysteryEncounterTier[] { const ret: EventMysteryEncounterTier[] = []; - timedEvents - .filter(te => this.isActive(te)) - .map(te => { - if (te.mysteryEncounterTierChanges != null) { - ret.push(...te.mysteryEncounterTierChanges); - } - }); + for (const te of timedEvents) { + if (this.isActive(te) && te.mysteryEncounterTierChanges != null) { + ret.push(...te.mysteryEncounterTierChanges); + } + } return ret; } getEventMysteryEncountersDisabled(): MysteryEncounterType[] { const ret: MysteryEncounterType[] = []; - timedEvents - .filter(te => this.isActive(te) && te.mysteryEncounterTierChanges != null) - .map(te => { - te.mysteryEncounterTierChanges?.map(metc => { + for (const te of timedEvents) { + if (this.isActive(te) && te.mysteryEncounterTierChanges != null) { + for (const metc of te.mysteryEncounterTierChanges) { if (metc.disable) { ret.push(metc.mysteryEncounter); } - }); - }); + } + } + } return ret; } @@ -529,15 +522,15 @@ export class TimedEventManager { normal: MysteryEncounterTier, ): MysteryEncounterTier { let ret = normal; - timedEvents - .filter(te => this.isActive(te) && te.mysteryEncounterTierChanges != null) - .map(te => { - te.mysteryEncounterTierChanges?.map(metc => { + for (const te of timedEvents) { + if (this.isActive(te) && te.mysteryEncounterTierChanges != null) { + for (const metc of te.mysteryEncounterTierChanges) { if (metc.mysteryEncounter === encounterType) { ret = metc.tier ?? normal; } - }); - }); + } + } + } return ret; } @@ -551,15 +544,16 @@ export class TimedEventManager { } getEventLuckBoostedSpecies(): SpeciesId[] { - const ret: SpeciesId[] = []; - timedEvents - .filter(te => this.isActive(te)) - .map(te => { - if (te.luckBoostedSpecies != null) { - ret.push(...te.luckBoostedSpecies.filter(s => !ret.includes(s))); + const ret = new Set(); + + for (const te of timedEvents) { + if (this.isActive(te) && te.luckBoostedSpecies != null) { + for (const s of te.luckBoostedSpecies) { + ret.add(s); } - }); - return ret; + } + } + return Array.from(ret); } areFusionsBoosted(): boolean { @@ -574,45 +568,50 @@ export class TimedEventManager { */ getFixedBattleEventRewards(wave: number): string[] { const ret: string[] = []; - timedEvents - .filter(te => this.isActive(te) && te.classicWaveRewards != null) - .map(te => { - ret.push(...te.classicWaveRewards!.filter(cwr => cwr.wave === wave).map(cwr => cwr.type)); - }); + for (const te of timedEvents) { + if (this.isActive(te) && te.classicWaveRewards != null) { + ret.push(...te.classicWaveRewards.filter(cwr => cwr.wave === wave).map(cwr => cwr.type)); + } + } return ret; } - // Gets the extra shiny chance for trainers due to event (odds/65536) + /** + * Get the extra shiny chance for trainers due to event + */ getClassicTrainerShinyChance(): number { let ret = 0; - const tsEvents = timedEvents.filter(te => this.isActive(te) && te.trainerShinyChance != null); - tsEvents.map(t => (ret += t.trainerShinyChance!)); + for (const te of timedEvents) { + const shinyChance = te.trainerShinyChance; + if (shinyChance && this.isActive(te)) { + ret += shinyChance; + } + } return ret; } getEventBgmReplacement(bgm: string): string { let ret = bgm; - timedEvents.map(te => { + for (const te of timedEvents) { if (this.isActive(te) && te.music != null) { - te.music.map(mr => { + for (const mr of te.music) { if (mr[0] === bgm) { console.log(`it is ${te.name} so instead of ${mr[0]} we play ${mr[1]}`); ret = mr[1]; } - }); + } } - }); + } return ret; } /** - * Activates any challenges on {@linkcode globalScene.gameMode} for the currently active event + * Activate any challenges on {@linkcode globalScene.gameMode} for the currently active event */ startEventChallenges(): void { - const challenges = this.activeEvent()?.dailyRunChallenges; - challenges?.forEach((eventChal: EventChallenge) => - globalScene.gameMode.setChallengeValue(eventChal.challenge, eventChal.value), - ); + for (const eventChal of this.activeEvent()?.dailyRunChallenges ?? []) { + globalScene.gameMode.setChallengeValue(eventChal.challenge, eventChal.value); + } } } diff --git a/src/ui/handlers/pokedex-page-ui-handler.ts b/src/ui/handlers/pokedex-page-ui-handler.ts index 684ead7d45a..393e0b713b4 100644 --- a/src/ui/handlers/pokedex-page-ui-handler.ts +++ b/src/ui/handlers/pokedex-page-ui-handler.ts @@ -240,8 +240,8 @@ export class PokedexPageUiHandler extends MessageUiHandler { private passive: AbilityId; private hasPassive: boolean; private hasAbilities: number[]; - private biomes: BiomeTierTod[]; - private preBiomes: BiomeTierTod[]; + private biomes: readonly BiomeTierTod[]; + private preBiomes: readonly BiomeTierTod[]; private baseStats: number[]; private baseTotal: number; private evolutions: SpeciesFormEvolution[]; @@ -893,7 +893,7 @@ export class PokedexPageUiHandler extends MessageUiHandler { } // Function to ensure that forms appear in the appropriate biome and tod - sanitizeBiomes(biomes: BiomeTierTod[], speciesId: number): BiomeTierTod[] { + sanitizeBiomes(biomes: readonly BiomeTierTod[], speciesId: number): readonly BiomeTierTod[] { if (speciesId === SpeciesId.BURMY || speciesId === SpeciesId.WORMADAM) { return biomes.filter(b => { const formIndex = (() => { diff --git a/src/utils/speed-order.ts b/src/utils/speed-order.ts index 1d894369bb3..de1e978510d 100644 --- a/src/utils/speed-order.ts +++ b/src/utils/speed-order.ts @@ -16,22 +16,23 @@ interface hasPokemon { * @returns The sorted array of {@linkcode Pokemon} */ export function sortInSpeedOrder(pokemonList: T[], shuffleFirst = true): T[] { - pokemonList = shuffleFirst ? shufflePokemonList(pokemonList) : pokemonList; + if (shuffleFirst) { + shufflePokemonList(pokemonList); + } sortBySpeed(pokemonList); return pokemonList; } /** + * Shuffle the list of pokemon *in place* * @param pokemonList - The array of Pokemon or objects containing Pokemon - * @returns The shuffled array + * @returns The same array instance that was passed in, shuffled. */ function shufflePokemonList(pokemonList: T[]): T[] { // This is seeded with the current turn to prevent an inconsistency where it // was varying based on how long since you last reloaded globalScene.executeWithSeedOffset( - () => { - pokemonList = randSeedShuffle(pokemonList); - }, + () => randSeedShuffle(pokemonList), globalScene.currentBattle.turn * 1000 + pokemonList.length, globalScene.waveSeed, );