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] 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]); + } + }); +});