Merge branch 'beta' into test-matchers-v2

This commit is contained in:
Bertie690 2025-07-27 23:29:33 -04:00 committed by GitHub
commit ccc8890362
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 149 additions and 4 deletions

View File

@ -76,11 +76,22 @@ export type NonFunctionPropertiesRecursive<Class> = {
export type AbstractConstructor<T> = abstract new (...args: any[]) => T;
/**
* Type helper that iterates through the fields of the type and coerces any `null` properties to `undefined` (including in union types).
*
* @remarks
* This is primarily useful when an object with nullable properties wants to be serialized and have its `null`
* properties coerced to `undefined`.
*/
export type CoerceNullPropertiesToUndefined<T extends object> = {
[K in keyof T]: null extends T[K] ? Exclude<T[K], null> | undefined : T[K];
};
/**
* Type helper to mark all properties in `T` optional, while still mandating that at least 1
* of its properties be present.
*
* Distinct from {@linkcode Partial} as this requires at least 1 property to _not_ be undefined.
* @typeParam T - The type to render partial.
* @typeParam T - The type to render partial
*/
export type AtLeastOne<T> = Partial<T> & EnumValues<{ [K in keyof T]: Pick<T, K> }>;

View File

@ -1,18 +1,29 @@
import { type BattlerTag, loadBattlerTag } from "#data/battler-tags";
import { allSpecies } from "#data/data-lists";
import type { Gender } from "#data/gender";
import { PokemonMove } from "#data/moves/pokemon-move";
import type { PokemonSpeciesForm } from "#data/pokemon-species";
import { getPokemonSpeciesForm, type PokemonSpeciesForm } from "#data/pokemon-species";
import type { TypeDamageMultiplier } from "#data/type";
import type { AbilityId } from "#enums/ability-id";
import type { BerryType } from "#enums/berry-type";
import type { MoveId } from "#enums/move-id";
import type { Nature } from "#enums/nature";
import type { PokemonType } from "#enums/pokemon-type";
import type { SpeciesId } from "#enums/species-id";
import type { AttackMoveResult } from "#types/attack-move-result";
import type { IllusionData } from "#types/illusion-data";
import type { TurnMove } from "#types/turn-move";
import type { CoerceNullPropertiesToUndefined } from "#types/type-helpers";
import { isNullOrUndefined } from "#utils/common";
/**
* The type that {@linkcode PokemonSpeciesForm} is converted to when an object containing it serializes it.
*/
type SerializedSpeciesForm = {
id: SpeciesId;
formIdx: number;
};
/**
* Permanent data that can customize a Pokemon in non-standard ways from its Species.
* Includes abilities, nature, changed types, etc.
@ -41,9 +52,59 @@ export class CustomPokemonData {
}
}
/**
* Deserialize a pokemon species form from an object containing `id` and `formIdx` properties.
* @param value - The value to deserialize
* @returns The `PokemonSpeciesForm` or `null` if the fields could not be properly discerned
*/
function deserializePokemonSpeciesForm(value: SerializedSpeciesForm | PokemonSpeciesForm): PokemonSpeciesForm | null {
// @ts-expect-error: We may be deserializing a PokemonSpeciesForm, but we catch later on
let { id, formIdx } = value;
if (isNullOrUndefined(id) || isNullOrUndefined(formIdx)) {
// @ts-expect-error: Typescript doesn't know that in block, `value` must be a PokemonSpeciesForm
id = value.speciesId;
// @ts-expect-error: Same as above (plus we are accessing a protected property)
formIdx = value._formIndex;
}
// If for some reason either of these fields are null/undefined, we cannot reconstruct the species form
if (isNullOrUndefined(id) || isNullOrUndefined(formIdx)) {
return null;
}
return getPokemonSpeciesForm(id, formIdx);
}
interface SerializedIllusionData extends Omit<IllusionData, "fusionSpecies"> {
/** The id of the illusioned fusion species, or `undefined` if not a fusion */
fusionSpecies?: SpeciesId;
}
interface SerializedPokemonSummonData {
statStages: number[];
moveQueue: TurnMove[];
tags: BattlerTag[];
abilitySuppressed: boolean;
speciesForm?: SerializedSpeciesForm;
fusionSpeciesForm?: SerializedSpeciesForm;
ability?: AbilityId;
passiveAbility?: AbilityId;
gender?: Gender;
fusionGender?: Gender;
stats: number[];
moveset?: PokemonMove[];
types: PokemonType[];
addedType?: PokemonType;
illusion?: SerializedIllusionData;
illusionBroken: boolean;
berriesEatenLast: BerryType[];
moveHistory: TurnMove[];
}
/**
* Persistent in-battle data for a {@linkcode Pokemon}.
* Resets on switch or new battle.
*
* @sealed
*/
export class PokemonSummonData {
/** [Atk, Def, SpAtk, SpDef, Spd, Acc, Eva] */
@ -86,7 +147,7 @@ export class PokemonSummonData {
*/
public moveHistory: TurnMove[] = [];
constructor(source?: PokemonSummonData | Partial<PokemonSummonData>) {
constructor(source?: PokemonSummonData | SerializedPokemonSummonData) {
if (isNullOrUndefined(source)) {
return;
}
@ -97,6 +158,30 @@ export class PokemonSummonData {
continue;
}
if (key === "speciesForm" || key === "fusionSpeciesForm") {
this[key] = deserializePokemonSpeciesForm(value);
}
if (key === "illusion" && typeof value === "object") {
// Make a copy so as not to mutate provided value
const illusionData = {
...value,
};
if (!isNullOrUndefined(illusionData.fusionSpecies)) {
switch (typeof illusionData.fusionSpecies) {
case "object":
illusionData.fusionSpecies = allSpecies[illusionData.fusionSpecies.speciesId];
break;
case "number":
illusionData.fusionSpecies = allSpecies[illusionData.fusionSpecies];
break;
default:
illusionData.fusionSpecies = undefined;
}
}
this[key] = illusionData as IllusionData;
}
if (key === "moveset") {
this.moveset = value?.map((m: any) => PokemonMove.loadMove(m));
continue;
@ -110,6 +195,49 @@ export class PokemonSummonData {
this[key] = value;
}
}
/**
* Serialize this PokemonSummonData to JSON, converting {@linkcode PokemonSpeciesForm} and {@linkcode IllusionData.fusionSpecies}
* into simpler types instead of serializing all of their fields.
*
* @remarks
* - `IllusionData.fusionSpecies` is serialized as just the species ID
* - `PokemonSpeciesForm` and `PokemonSpeciesForm.fusionSpeciesForm` are converted into {@linkcode SerializedSpeciesForm} objects
*/
public toJSON(): SerializedPokemonSummonData {
// Pokemon species forms are never saved, only the species ID.
const illusion = this.illusion;
const speciesForm = this.speciesForm;
const fusionSpeciesForm = this.fusionSpeciesForm;
const illusionSpeciesForm = illusion?.fusionSpecies;
const t = {
// the "as omit" is required to avoid TS resolving the overwritten properties to "never"
// We coerce null to undefined in the type, as the for loop below replaces `null` with `undefined`
...(this as Omit<
CoerceNullPropertiesToUndefined<PokemonSummonData>,
"speciesForm" | "fusionSpeciesForm" | "illusion"
>),
speciesForm: isNullOrUndefined(speciesForm)
? undefined
: { id: speciesForm.speciesId, formIdx: speciesForm.formIndex },
fusionSpeciesForm: isNullOrUndefined(fusionSpeciesForm)
? undefined
: { id: fusionSpeciesForm.speciesId, formIdx: fusionSpeciesForm.formIndex },
illusion: isNullOrUndefined(illusion)
? undefined
: {
...(this.illusion as Omit<typeof illusion, "fusionSpecies">),
fusionSpecies: illusionSpeciesForm?.speciesId,
},
};
// Replace `null` with `undefined`, as `undefined` never gets serialized
for (const [key, value] of Object.entries(t)) {
if (value === null) {
t[key] = undefined;
}
}
return t;
}
}
// TODO: Merge this inside `summmonData` but exclude from save if/when a save data serializer is added

View File

@ -98,7 +98,9 @@ export function toTitleCase(str: string): string {
*/
export function toCamelCase(str: string) {
return splitWords(str)
.map((word, index) => (index === 0 ? word.toLowerCase() : capitalizeFirstLetter(word)))
.map((word, index) =>
index === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
)
.join("");
}

View File

@ -9,6 +9,10 @@
{
"tagName": "@linkcode",
"syntaxKind": "inline"
},
{
"tagName": "@module",
"syntaxKind": "modifier"
}
]
}