[Misc][Refactor] Add scaffolding for TypedArrays and improve typing on methods related to arrays (#6547)

* make IVs use Uint8Array

* Add many typed array helpers

* Move array utils to its own file

* Add suppression comment

* Adjust type of `getStats`

* Adjust test mocks to use typed arrays

* Adjust signatures of some phases to use ArrayLike<T>

* Adjust signature of src/ui/containers/stats-container#updateIvs

* Remove comment gap to try to satisfy typedoc

* Ensure ivs are always set

* fix: fun-and-games me to use typed array

* Add new tests for array utilities

* Update type of ivs in save-data.ts

* Update part-timer-encounter.test.ts

* Convert uses of StatusEffect[] to Uint8Array

* Update ssui to use uint8array for ivs

* Revert use of typed arrays

* Move `nil` to @types/common

* Make more arrays readonly

* fix: remnant change to immune effects

* Even more array improvements

* Apply kev's suggestions from code review

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* address Bertie's comments from code review

* tests: remove undefined check for bigint array types

* fixup abilities.ts

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
This commit is contained in:
Sirz Benjie 2025-10-06 12:21:58 -05:00 committed by GitHub
parent bbd5aefc81
commit e5e0835a96
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
57 changed files with 1439 additions and 537 deletions

2
assets

@ -1 +1 @@
Subproject commit 5ca7463e0a2f113388bfeaba92118f143b8d0278
Subproject commit 90a227d51fcf44c044198afc2753b71fb3da39cd

@ -1 +1 @@
Subproject commit 6a9a91ae6e3d42c771451b32808dae571d3fbd9a
Subproject commit f3b0c55a0f5744a34bb4c526bc3790592bb3c729

View File

@ -12,7 +12,11 @@ import type { applyAbAttrs } from "#abilities/apply-ab-attrs";
export type AbAttrCondition = (pokemon: Pokemon) => boolean;
export type PokemonAttackCondition = (user: Pokemon | null, target: Pokemon | null, move: Move) => boolean;
export type PokemonDefendCondition = (target: Pokemon, user: Pokemon, move: Move) => boolean;
export type PokemonStatStageChangeCondition = (target: Pokemon, statsChanged: BattleStat[], stages: number) => boolean;
export type PokemonStatStageChangeCondition = (
target: Pokemon,
statsChanged: readonly BattleStat[],
stages: number,
) => boolean;
/**
* Union type of all ability attribute class names as strings

View File

@ -12,17 +12,19 @@ export interface BiomeDepths {
[key: number]: [number, number];
}
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;
}
export interface BiomePokemonPools {
[key: number]: BiomeTierPokemonPools;
readonly [key: number]: BiomeTierPokemonPools;
}
export interface BiomeTierTod {
@ -32,13 +34,13 @@ export interface BiomeTierTod {
}
export interface CatchableSpecies {
[key: number]: BiomeTierTod[];
readonly [key: number]: readonly BiomeTierTod[];
}
export interface BiomeTierTrainerPools {
[key: number]: TrainerType[];
readonly [key: number]: readonly TrainerType[];
}
export interface BiomeTrainerPools {
[key: number]: BiomeTierTrainerPools;
readonly [key: number]: BiomeTierTrainerPools;
}

View File

@ -2,3 +2,8 @@ export type ConditionFn = (args?: any[]) => boolean;
/** A union type of all primitives (types that are always passed by value) */
export type Primitive = string | number | boolean | bigint | null | undefined | symbol;
/** Alias for the constructor of a class */
export type Constructor<T> = new (...args: unknown[]) => T;
export type nil = null | undefined;

View File

@ -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[];

524
src/@types/typed-arrays.ts Normal file
View File

@ -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<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>
extends Omit<Int8Array, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> {
subarray(begin?: number, end?: number): ReadonlyInt8Array<TArrayBuffer>;
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<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>
extends Omit<Uint8Array, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> {
subarray(begin?: number, end?: number): ReadonlyUint8Array<TArrayBuffer>;
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<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>
extends Omit<Uint8ClampedArray<TArrayBuffer>, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> {
subarray(begin?: number, end?: number): ReadonlyUint8ClampedArray<TArrayBuffer>;
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<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>
extends Omit<Int16Array<TArrayBuffer>, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> {
subarray(begin?: number, end?: number): ReadonlyInt16Array<TArrayBuffer>;
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<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>
extends Omit<Uint16Array<TArrayBuffer>, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> {
subarray(begin?: number, end?: number): ReadonlyUint16Array<TArrayBuffer>;
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<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>
extends Omit<Int32Array<TArrayBuffer>, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> {
subarray(begin?: number, end?: number): ReadonlyInt32Array<TArrayBuffer>;
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<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>
extends Omit<Uint32Array<TArrayBuffer>, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> {
subarray(begin?: number, end?: number): ReadonlyUint32Array<TArrayBuffer>;
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<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>
extends Omit<Float32Array<TArrayBuffer>, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> {
subarray(begin?: number, end?: number): ReadonlyFloat32Array<TArrayBuffer>;
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<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>
extends Omit<Float64Array, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> {
subarray(begin?: number, end?: number): ReadonlyFloat64Array<TArrayBuffer>;
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<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>
extends Omit<BigInt64Array, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> {
subarray(begin?: number, end?: number): ReadonlyBigInt64Array<TArrayBuffer>;
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<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>
extends Omit<BigUint64Array, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> {
subarray(begin?: number, end?: number): ReadonlyBigUint64Array<TArrayBuffer>;
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<TypedArray, BigIntArray>;
export type ReadonlyNumberCompatibleTypedArray = Exclude<ReadonlyTypedArray, ReadonlyBigIntArray>;
/**
* A partial interface of `Uint8Array` where methods that return the array type have been modified to return a more specific type.
*
* @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<T extends number> 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: T, 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<T>, 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<T>;
[Symbol.iterator](): ArrayIterator<T>;
[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<T extends number> extends PartialUint8Array<T> {
// map(callbackfn: (value: T, index: number, array: this) => T, thisArg?: any): GenericUint8Array<T>;
// 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<T>;
toReversed(): GenericUint8Array<T>;
toSorted(compareFn?: (a: T, b: T) => number): GenericUint8Array<T>;
filter(predicate: (value: T, index: number, array: this) => any, thisArg?: any): GenericUint8Array<T>;
}
/**
* 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<T extends number> 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<T>, 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<T>;
[Symbol.iterator](): ArrayIterator<T>;
[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<T extends number> extends PartialUint16Array<T> {
// map(callbackfn: (value: T, index: number, array: this) => T, thisArg?: any): GenericUint16Array<T>;
// 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<T>;
toReversed(): GenericUint16Array<T>;
toSorted(compareFn?: (a: T, b: T) => number): GenericUint16Array<T>;
filter(predicate: (value: T, index: number, array: this) => any, thisArg?: any): GenericUint16Array<T>;
}
/**
* 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<T extends number> 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<T>, 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<T>;
[Symbol.iterator](): ArrayIterator<T>;
[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<T extends number> extends PartialUint32Array<T> {
// map(callbackfn: (value: T, index: number, array: this) => T, thisArg?: any): GenericUint32Array<T>;
// 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<T>;
toReversed(): GenericUint32Array<T>;
toSorted(compareFn?: (a: T, b: T) => number): GenericUint32Array<T>;
filter(predicate: (value: T, index: number, array: this) => any, thisArg?: any): GenericUint32Array<T>;
}
/**
* 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<T extends number> 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<T>, 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<T>;
[Symbol.iterator](): ArrayIterator<T>;
[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<T extends number> extends PartialInt8Array<T> {
// map(callbackfn: (value: T, index: number, array: this) => T, thisArg?: any): GenericInt8Array<T>;
// 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<T>;
toReversed(): GenericInt8Array<T>;
toSorted(compareFn?: (a: T, b: T) => number): GenericInt8Array<T>;
filter(predicate: (value: T, index: number, array: this) => any, thisArg?: any): GenericInt8Array<T>;
}
/**
* 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<T extends number> 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<T>, 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<T>;
[Symbol.iterator](): ArrayIterator<T>;
[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<T extends number> extends PartialInt16Array<T> {
// map(callbackfn: (value: T, index: number, array: this) => T, thisArg?: any): GenericInt16Array<T>;
// 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<T>;
toReversed(): GenericInt16Array<T>;
toSorted(compareFn?: (a: T, b: T) => number): GenericInt16Array<T>;
filter(predicate: (value: T, index: number, array: this) => any, thisArg?: any): GenericInt16Array<T>;
}
/**
* 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<T extends number> 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<T>, 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<T>;
[Symbol.iterator](): ArrayIterator<T>;
[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<T extends number> extends PartialInt32Array<T> {
// map(callbackfn: (value: T, index: number, array: this) => T, thisArg?: any): GenericInt32Array<T>;
// 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<T>;
toReversed(): GenericInt32Array<T>;
toSorted(compareFn?: (a: T, b: T) => number): GenericInt32Array<T>;
filter(predicate: (value: T, index: number, array: this) => any, thisArg?: any): GenericInt32Array<T>;
}
/**
* 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<T extends number>
extends Omit<GenericUint8Array<T>, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> {
subarray(begin?: number, end?: number): ReadonlyGenericUint8Array<T>;
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<T extends number>
extends Omit<GenericUint16Array<T>, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> {
subarray(begin?: number, end?: number): ReadonlyGenericUint16Array<T>;
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<T extends number>
extends Omit<GenericUint32Array<T>, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> {
subarray(begin?: number, end?: number): ReadonlyGenericUint32Array<T>;
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<T extends number>
extends Omit<GenericInt8Array<T>, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> {
subarray(begin?: number, end?: number): ReadonlyGenericInt8Array<T>;
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<T extends number>
extends Omit<GenericInt16Array<T>, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> {
subarray(begin?: number, end?: number): ReadonlyGenericInt16Array<T>;
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<T extends number>
extends Omit<GenericInt32Array<T>, "fill" | "set" | "sort" | "reverse" | "copyWithin" | "subarray"> {
subarray(begin?: number, end?: number): ReadonlyGenericInt32Array<T>;
readonly [index: number]: T;
}
/**
* A union type of all `GenericTypedArray`s
*/
export type ReadonlyGenericArray<T extends number = number> =
| ReadonlyGenericUint8Array<T>
| ReadonlyGenericUint16Array<T>
| ReadonlyGenericUint32Array<T>
| ReadonlyGenericInt8Array<T>
| ReadonlyGenericInt16Array<T>
| ReadonlyGenericInt32Array<T>;

View File

@ -117,6 +117,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";
@ -131,7 +132,6 @@ import { UI } from "#ui/ui";
import { addUiThemeOverrides } from "#ui/ui-theme";
import {
BooleanHolder,
type Constructor,
fixedInt,
formatMoney,
getBiomeName,

View File

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

View File

@ -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,

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,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";
@ -5736,15 +5737,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<typeof biomePokemonPools[number]>) = {};
(biomeTrainerPools[biome] as Mutable<typeof biomeTrainerPools[number]>) = {};
for (const tier of getEnumValues(BiomePoolTier)) {
biomePokemonPools[biome][tier] = {};
biomeTrainerPools[biome][tier] = [];
(biomePokemonPools[biome][tier] as Mutable<typeof biomePokemonPools[number][number]>) = {};
(biomeTrainerPools[biome][tier] as Mutable<typeof biomeTrainerPools[number][number]>) = [];
for (const tod of getEnumValues(TimeOfDay)) {
biomePokemonPools[biome][tier][tod] = [];
(biomePokemonPools[biome][tier][tod] as Mutable<typeof biomePokemonPools[number][number][number]>) = [];
}
}
}
@ -5761,8 +5762,9 @@ export function initBiomes() {
uncatchableSpecies.push(speciesId);
}
type mutableSpecies = Mutable<typeof catchableSpecies[SpeciesId]>;
// array of biome options for the current species
catchableSpecies[speciesId] = [];
(catchableSpecies[speciesId] as mutableSpecies) = [];
for (const b of biomeEntries) {
const biome = b[0];
@ -5773,7 +5775,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[]
@ -5861,7 +5863,7 @@ export function initBiomes() {
}
const biomeTierPool = biomeTrainerPools[biome][tier];
biomeTierPool.push(trainerType);
(biomeTierPool as Mutable<typeof biomeTierPool>).push(trainerType);
}
//outputPools();
}

View File

@ -17,7 +17,8 @@ import { WeatherType } from "#enums/weather-type";
import type { Pokemon } from "#field/pokemon";
import type { SpeciesStatBoosterItem, SpeciesStatBoosterModifierType } from "#modifiers/modifier-type";
import type { EvoLevelThreshold } from "#types/species-gen-types";
import { coerceArray, randSeedInt } from "#utils/common";
import { coerceArray } from "#utils/array";
import { randSeedInt } from "#utils/common";
import { getPokemonSpecies } from "#utils/pokemon-utils";
import { toCamelCase } from "#utils/strings";
import i18next from "i18next";

View File

@ -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,

View File

@ -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";

View File

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

View File

@ -5,9 +5,9 @@ import type { ModifierTypes } from "#modifiers/modifier-type";
import type { Move } from "#moves/move";
import type { BiomeDepths, CatchableSpecies } from "#types/biomes";
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;

View File

@ -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<number>(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;
}

View File

@ -90,13 +90,16 @@ import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindS
import type { TurnMove } from "#types/turn-move";
import type { AbstractConstructor } from "#types/type-helpers";
import { applyChallenges } from "#utils/challenge-utils";
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 { areAllies } from "#utils/pokemon-utils";
import { toCamelCase, toTitleCase } from "#utils/strings";
import i18next from "i18next";
import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
import { canSpeciesTera, willTerastallize } from "#utils/pokemon-utils";
import type { ReadonlyGenericUint8Array } from "#types/typed-arrays";
/**
* A function used to conditionally determine execution of a given {@linkcode MoveAttr}.
@ -2805,7 +2808,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);
@ -3135,7 +3138,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
@ -3143,7 +3146,7 @@ export class HealStatusEffectAttr extends MoveEffectAttr {
*/
constructor(selfTarget: boolean, effects: StatusEffect | StatusEffect[]) {
super(selfTarget, { lastHitOnly: true });
this.effects = coerceArray(effects)
this.effects = coerceArray(effects);
}
/**
@ -8537,7 +8540,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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<typeof formChanges>).push(...newFormChanges);
}
}

View File

@ -11,7 +11,8 @@ import type { TimeOfDay } from "#enums/time-of-day";
import { WeatherType } from "#enums/weather-type";
import type { Pokemon } from "#field/pokemon";
import type { PokemonFormChangeItemModifier } from "#modifiers/modifier";
import { type Constructor, coerceArray } from "#utils/common";
import 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", {

View File

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

View File

@ -49,7 +49,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";
@ -1002,16 +1003,16 @@ export class TrainerConfig {
* @param postProcess - An optional function to post-process the generated `EnemyPokemon`
*/
export function getRandomPartyMemberFunc(
speciesPool: (SpeciesId | SpeciesId[])[],
speciesPool: readonly (SpeciesId | readonly SpeciesId[])[],
trainerSlot: TrainerSlot = TrainerSlot.TRAINER,
ignoreEvolution = false,
postProcess?: (enemyPokemon: EnemyPokemon) => void,
): (level: number, strength: PartyMemberStrength) => EnemyPokemon {
return (level: number, strength: PartyMemberStrength) => {
let species: SpeciesId | SpeciesId[] | typeof speciesPool = speciesPool;
let species: SpeciesId | readonly SpeciesId[] | typeof speciesPool = speciesPool;
do {
species = randSeedItem(species);
} while (Array.isArray(species));
} while (typeof species !== "number");
if (!ignoreEvolution) {
species = getPokemonSpecies(species).getTrainerSpeciesForLevel(level, true, strength);

View File

@ -36,8 +36,9 @@ import type { Pokemon } from "#field/pokemon";
import { FieldEffectModifier } from "#modifiers/modifier";
import type { Move } from "#moves/move";
import type { BiomeTierTrainerPools, PokemonPools } from "#types/biomes";
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 {
@ -588,7 +589,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:

View File

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

View File

@ -140,6 +140,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;
}

View File

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

View File

@ -278,7 +278,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;
@ -288,7 +288,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;
@ -2355,7 +2355,11 @@ const tierWeights = [768 / 1024, 195 / 1024, 48 / 1024, 12 / 1024, 1 / 1024];
*/
export const itemPoolChecks: Map<ModifierTypeKeys, boolean | undefined> = 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);
@ -2901,7 +2905,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(

View File

@ -12,17 +12,13 @@ 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;
}
sprite.setVisible(visible);
sprite.clearTint();
});
sprites[i].setVisible(visible);
tintSprites[i].setVisible(visible);
sprites[i].clearTint();
tintSprites[i].clearTint();
}
globalScene.tweens.add({
targets: globalScene.currentBattle.trainer,

View File

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

View File

@ -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";

View File

@ -25,20 +25,20 @@ export type StatStageChangeCallback = (
// TODO: Refactor this mess of a phase
export class StatStageChangePhase extends PokemonPhase {
public readonly phaseName = "StatStageChangePhase";
private stats: BattleStat[];
private selfTarget: boolean;
private stages: number;
private showMessage: boolean;
private ignoreAbilities: boolean;
private canBeCopied: boolean;
private onChange: StatStageChangeCallback | null;
private comingFromMirrorArmorUser: boolean;
private comingFromStickyWeb: boolean;
private readonly stats: readonly BattleStat[];
private readonly selfTarget: boolean;
private readonly stages: number;
private readonly showMessage: boolean;
private readonly ignoreAbilities: boolean;
private readonly canBeCopied: boolean;
private readonly onChange: StatStageChangeCallback | null;
private readonly comingFromMirrorArmorUser: boolean;
private readonly comingFromStickyWeb: boolean;
constructor(
battlerIndex: BattlerIndex,
selfTarget: boolean,
stats: BattleStat[],
stats: readonly BattleStat[],
stages: number,
showMessage = true,
ignoreAbilities = false,
@ -294,7 +294,7 @@ export class StatStageChangePhase extends PokemonPhase {
}
}
getStatStageChangeMessages(stats: BattleStat[], stages: number, relStages: number[]): string[] {
getStatStageChangeMessages(stats: readonly BattleStat[], stages: number, relStages: number[]): string[] {
const messages: string[] = [];
const relStageStatIndexes = {};

View File

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

View File

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

View File

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

View File

@ -54,6 +54,13 @@ declare module "phaser" {
}
}
namespace Math {
interface RandomDataGenerator {
pick<T>(array: ArrayLike<T>): T;
weightedPick<T>(array: ArrayLike<T>): T;
}
}
namespace Input {
namespace Gamepad {
interface GamepadPlugin {

View File

@ -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 = (() => {

View File

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

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

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

View File

@ -5,8 +5,6 @@ import type { Variant } from "#sprites/variant";
import { toCamelCase } from "#utils/strings";
import i18next from "i18next";
export type nil = null | undefined;
export const MissingTextureKey = "__MISSING";
// TODO: Draft tests for these utility functions
@ -127,29 +125,25 @@ export function randSeedFloat(): number {
return Phaser.Math.RND.frac();
}
export function randItem<T>(items: T[]): T {
export function randItem<T>(items: ArrayLike<T>): T {
return items.length === 1 ? items[0] : items[randInt(items.length)];
}
export function randSeedItem<T>(items: T[]): T {
export function randSeedItem<T>(items: ArrayLike<T>): T {
return items.length === 1 ? items[0] : Phaser.Math.RND.pick(items);
}
/**
* Shuffle a list using the seeded rng. Utilises the Fisher-Yates algorithm.
* @param items An array of items.
* @returns A new shuffled array of items.
* Shuffle a list in place using the seeded rng and the Fisher-Yates algorithm.
* @param items - An array of items.
* @returns The same `items` array, now shuffled in place.
*/
export function randSeedShuffle<T>(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 {
@ -178,7 +172,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,
@ -294,9 +288,6 @@ export async function localPing(): Promise<void> {
}
}
/** Alias for the constructor of a class */
export type Constructor<T> = new (...args: unknown[]) => T;
export class BooleanHolder {
public value: boolean;
@ -345,7 +336,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);
@ -369,7 +360,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];
}

View File

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

View File

@ -16,22 +16,23 @@ interface hasPokemon {
* @returns The sorted array of {@linkcode Pokemon}
*/
export function sortInSpeedOrder<T extends Pokemon | hasPokemon>(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<T extends Pokemon | hasPokemon>(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,
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

275
test/utils/array.test.ts Normal file
View File

@ -0,0 +1,275 @@
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<number[]>();
});
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<Uint8Array>();
});
it("readonly array input", () => {
const arr: readonly number[] = [1, 2, 3, 4];
const result = subArray(arr, 2);
expectTypeOf(result).toEqualTypeOf<readonly number[]>();
});
it("readonly typed array input", () => {
const arr = new Uint8Array([1, 2, 3, 4]) as ReadonlyUint8Array;
const result = subArray(arr, 2);
expectTypeOf(result).toEqualTypeOf<ReadonlyUint8Array>();
});
});
});
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[]", () => {
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<number[]>();
});
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<number[]>();
});
});
});
describe("isTypedArray", () => {
it.each([
["Int8Array", Int8Array],
["Uint8ClampedArray", Uint8ClampedArray],
["Uint16Array", Uint16Array],
["Uint32Array", Uint32Array],
["Float64Array", Float64Array],
["Float32Array", Float32Array],
])("returns true for %s", (_, ArrayType) => {
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);
});
});
});