diff --git a/src/@types/common.ts b/src/@types/common.ts index 93d88a3b680..c1373d93297 100644 --- a/src/@types/common.ts +++ b/src/@types/common.ts @@ -1 +1,6 @@ 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/@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/@types/typed-arrays.ts b/src/@types/typed-arrays.ts new file mode 100644 index 00000000000..860b8bf0557 --- /dev/null +++ b/src/@types/typed-arrays.ts @@ -0,0 +1,524 @@ +/* + * 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; + +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. + * + * @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 289c9a8f051..6da361dbf2f 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -118,6 +118,7 @@ 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 { AbilityBar } from "#ui/ability-bar"; @@ -132,7 +133,6 @@ import { UI } from "#ui/ui"; import { addUiThemeOverrides } from "#ui/ui-theme"; import { BooleanHolder, - type Constructor, fixedInt, formatMoney, getIvsFromId, 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/abilities/ability.ts b/src/data/abilities/ability.ts index f6494548b99..db186432e1c 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -62,18 +62,11 @@ 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 { Constructor } from "#utils/common"; -import { - BooleanHolder, - coerceArray, - NumberHolder, - randSeedFloat, - randSeedInt, - randSeedItem, - toDmgValue, -} from "#utils/common"; +import { coerceArray } from "#utils/array"; +import { BooleanHolder, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common"; import { toCamelCase } from "#utils/strings"; import i18next from "i18next"; @@ -1217,7 +1210,7 @@ export class PostDefendTerrainChangeAbAttr extends PostDefendAbAttr { export class PostDefendContactApplyStatusEffectAbAttr extends PostDefendAbAttr { private chance: number; - private effects: StatusEffect[]; + private readonly effects: readonly StatusEffect[]; constructor(chance: number, ...effects: StatusEffect[]) { super(true); @@ -2187,7 +2180,7 @@ export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr { export class PostAttackApplyStatusEffectAbAttr extends PostAttackAbAttr { private contactRequired: boolean; private chance: number; - private effects: StatusEffect[]; + private readonly effects: readonly StatusEffect[]; constructor(contactRequired: boolean, chance: number, ...effects: StatusEffect[]) { super(); @@ -2947,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 immuneEffects: StatusEffect[]; + private readonly immuneEffects: readonly StatusEffect[]; private statusHealed: StatusEffect; /** @@ -2960,7 +2953,8 @@ export class PostSummonHealStatusAbAttr extends PostSummonRemoveEffectAbAttr { public override canApply({ pokemon }: AbAttrBaseParams): boolean { const status = pokemon.status?.effect; - return status != null && (this.immuneEffects.length === 0 || this.immuneEffects.includes(status)); + const immuneEffects = this.immuneEffects; + return status != null && (immuneEffects.length === 0 || immuneEffects.includes(status)); } public override apply({ pokemon }: AbAttrBaseParams): void { @@ -3056,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: readonly StatusEffect[]; /** * @param statusEffect - The status effects to be removed from the user's field. @@ -3644,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: readonly StatusEffect[]; /** * @param immuneEffects - An array of {@linkcode StatusEffect}s to prevent application. @@ -3712,7 +3706,7 @@ export interface UserFieldStatusEffectImmunityAbAttrParams extends AbAttrBasePar */ export class UserFieldStatusEffectImmunityAbAttr extends CancelInteractionAbAttr { private declare readonly _: never; - protected immuneEffects: StatusEffect[]; + protected readonly immuneEffects: readonly StatusEffect[]; /** * @param immuneEffects - An array of {@linkcode StatusEffect}s to prevent application. @@ -4005,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: readonly StatusEffect[]; /** * @param effects - The status effect(s) that will be blocked from damaging the ability pokemon @@ -4546,7 +4540,7 @@ export class PostTurnAbAttr extends AbAttr { * @sealed */ export class PostTurnStatusHealAbAttr extends PostTurnAbAttr { - private effects: StatusEffect[]; + private readonly effects: readonly StatusEffect[]; /** * @param effects - The status effect(s) that will qualify healing the ability pokemon @@ -5815,10 +5809,10 @@ export interface IgnoreTypeStatusEffectImmunityAbAttrParams extends AbAttrParams * @sealed */ export class IgnoreTypeStatusEffectImmunityAbAttr extends AbAttr { - private statusEffect: StatusEffect[]; - private defenderType: PokemonType[]; + 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 = statusEffect; @@ -6706,7 +6700,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/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/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/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/battle-anims.ts b/src/data/battle-anims.ts index 573a1730796..e1966805b40 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -7,7 +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 { coerceArray, getFrameMs, type nil } from "#utils/common"; +import type { nil } from "#types/common"; +import { coerceArray } from "#utils/array"; +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/battler-tags.ts b/src/data/battler-tags.ts index 8abd98f4683..ae5a3882ccc 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 */ @@ -2153,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; } @@ -2165,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/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/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/moves/move.ts b/src/data/moves/move.ts index 075876d8ddd..252bd5d6b4b 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -87,13 +87,16 @@ 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, 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"; 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}. @@ -2594,7 +2597,7 @@ 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: readonly StatusEffect[]; constructor(effects: StatusEffect[], selfTarget?: boolean) { super(effects[0], selfTarget); @@ -2924,7 +2927,7 @@ export class StealEatBerryAttr extends EatBerryAttr { */ export class HealStatusEffectAttr extends MoveEffectAttr { /** List of Status Effects to cure */ - private effects: StatusEffect[]; + private readonly effects: readonly StatusEffect[]; /** * @param selfTarget - Whether this move targets the user @@ -2932,7 +2935,7 @@ export class HealStatusEffectAttr extends MoveEffectAttr { */ constructor(selfTarget: boolean, effects: StatusEffect | StatusEffect[]) { super(selfTarget, { lastHitOnly: true }); - this.effects = coerceArray(effects) + this.effects = coerceArray(effects); } /** @@ -8391,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/mystery-encounters/encounters/fun-and-games-encounter.ts b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts index f2363ade500..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 = [0, 0, 0, 0, 0, 0]; + wobbuffet.ivs.fill(0); wobbuffet.setNature(Nature.MILD); wobbuffet.setAlpha(0); wobbuffet.setVisible(false); diff --git a/src/data/mystery-encounters/mystery-encounter-requirements.ts b/src/data/mystery-encounters/mystery-encounter-requirements.ts index 85906044b77..218a2e7cbed 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 @@ -696,11 +696,11 @@ export class AbilityRequirement extends EncounterPokemonRequirement { } export class StatusEffectRequirement extends EncounterPokemonRequirement { - requiredStatusEffect: StatusEffect[]; + 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; @@ -717,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 => { @@ -761,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; @@ -792,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 => @@ -877,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/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 0ba0dec896a..58dbfc4182a 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.ts b/src/data/pokemon-forms.ts index 7eb8ed263d5..f223347c60f 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, nil } from "#types/common"; +import type { Mutable } from "#types/type-helpers"; export type SpeciesFormChangeConditionPredicate = (p: Pokemon) => boolean; export type SpeciesFormChangeConditionEnforceFunc = (p: Pokemon) => void; @@ -35,7 +36,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, @@ -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 f51b090878f..149c44bd978 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 type { Constructor } from "#types/common"; +import { coerceArray } from "#utils/array"; import { toCamelCase } from "#utils/strings"; import i18next from "i18next"; @@ -121,7 +122,7 @@ export class SpeciesFormChangeActiveTrigger extends SpeciesFormChangeTrigger { } export class SpeciesFormChangeStatusEffectTrigger extends SpeciesFormChangeTrigger { - public statusEffects: StatusEffect[]; + public readonly statusEffects: readonly StatusEffect[]; public invert: boolean; constructor(statusEffects: StatusEffect | StatusEffect[], invert = false) { @@ -239,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; @@ -277,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; @@ -309,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/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/data/trainers/trainer-config.ts b/src/data/trainers/trainer-config.ts index b5786d1f0a2..1e6e06abd10 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"; @@ -999,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 3e214ff1ea7..4cd814ab907 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 { @@ -587,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/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 cbad6caaafa..30ef8cd670e 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -141,6 +141,7 @@ 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"; @@ -151,11 +152,10 @@ 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 } from "#utils/array"; import { applyChallenges } from "#utils/challenge-utils"; import { BooleanHolder, - type Constructor, - coerceArray, deltaRgb, fixedInt, getIvsFromId, @@ -2141,7 +2141,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/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/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/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/timed-event-manager.ts b/src/timed-event-manager.ts index 75f4afa772e..f225bf8db61 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 { @@ -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/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/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/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..ece0bdd24c5 --- /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 readonly 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 569333209bf..61ce2177a1e 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 @@ -125,29 +123,25 @@ 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); } /** - * 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 { @@ -176,7 +170,7 @@ 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[] { +export function getIvsFromId(id: number): [number, number, number, number, number, number] { return [ (id & 0x3e000000) >>> 25, (id & 0x01f00000) >>> 20, @@ -292,9 +286,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; @@ -343,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); @@ -367,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]; } @@ -506,12 +497,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]; -} 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; } 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, ); 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/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/array.test.ts b/test/utils/array.test.ts new file mode 100644 index 00000000000..9265830ec86 --- /dev/null +++ b/test/utils/array.test.ts @@ -0,0 +1,279 @@ +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.ts` + * @module + */ + +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(); + }); + }); + }); + + 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]); + } + }); + }); + + 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); + }); + }); +});