diff --git a/src/data/pokemon/pokemon-data.ts b/src/data/pokemon/pokemon-data.ts index 03c806d614e..846aed3bed4 100644 --- a/src/data/pokemon/pokemon-data.ts +++ b/src/data/pokemon/pokemon-data.ts @@ -97,10 +97,10 @@ interface SerializedIllusionData extends Omit { } interface SerializedPokemonSummonData { - statStages: number[]; - moveQueue: TurnMove[]; - tags: BattlerTag[]; - abilitySuppressed: boolean; + statStages?: number[]; + moveQueue?: TurnMove[]; + tags?: BattlerTag[]; + abilitySuppressed?: boolean; speciesForm?: SerializedSpeciesForm; fusionSpeciesForm?: SerializedSpeciesForm; ability?: AbilityId; @@ -109,12 +109,12 @@ interface SerializedPokemonSummonData { fusionGender?: Gender; stats: number[]; moveset?: PokemonMove[]; - types: PokemonType[]; + types?: PokemonType[]; addedType?: PokemonType; illusion?: SerializedIllusionData; - illusionBroken: boolean; - berriesEatenLast: BerryType[]; - moveHistory: TurnMove[]; + illusionBroken?: boolean; + berriesEatenLast?: BerryType[]; + moveHistory?: TurnMove[]; } /** @@ -250,9 +250,12 @@ export class PokemonSummonData { }, }; // Replace `null` with `undefined`, as `undefined` never gets serialized + // Replace empty arrays with `[]` for (const [key, value] of Object.entries(t)) { if (value === null) { t[key] = undefined; + } else if (Array.isArray(value) && value.length === 0) { + t[key] = []; } } return t; diff --git a/src/system/schema-migrators/custom-pokemon-data.ts b/src/system/schema-migrators/custom-pokemon-data.ts new file mode 100644 index 00000000000..b979e3d1b8e --- /dev/null +++ b/src/system/schema-migrators/custom-pokemon-data.ts @@ -0,0 +1,30 @@ +import type { Z$PokemonData } from "#system/schemas/v1.10/pokemon-data"; +import { NatureSchema } from "#system/schemas/v1.10/pokemon-nature"; +import type z from "zod"; + +/** + * Very early saves did not have `customPokemonData` and instead + * stored things like nature overrides and abilities from MEs directly. + * This migrator moves the properties into the `customPokemonData` field. + */ +export function PreCustomPokemonDataMigrator( + data: z.output, +): asserts data is z.output { + // Value of `-1` indicated no override, so we can ignore it. + const nature = NatureSchema.safeParse(data.natureOverride); + if (nature.success) { + const customPokemonData = data.customPokemonData; + // If natureOverride is valid, use it + if ( + customPokemonData && + typeof customPokemonData === "object" && + ((customPokemonData as { nature?: number }).nature ?? -1) === -1 + ) { + customPokemonData; + } else { + data.customPokemonData = { + nature: nature.data, + }; + } + } +} diff --git a/src/system/schema-migrators/summon-data.ts b/src/system/schema-migrators/summon-data.ts new file mode 100644 index 00000000000..6268185a8a3 --- /dev/null +++ b/src/system/schema-migrators/summon-data.ts @@ -0,0 +1,69 @@ +import { Z$PositiveInt } from "#system/schemas/common"; +import type { Z$IllusionData } from "#system/schemas/v1.10/illusion-data"; +import { getPokemonSpecies } from "#utils/pokemon-utils"; +import { z } from "zod"; + +/** + * In version 1.9, serialized illusion data looked like this, where `basePokemon` held + * information about the pokemon that had the illusion ability, while the pokemon's own + * properties were modified. (though were overwritten on save). + * + * ```ts + * interface IllusionData { + * basePokemon: { + * name: string; + * nickname: string; + * shiny: boolean; + * variant: Variant; + * fusionShiny: boolean; + * fusionVariant: Variant; + * } + * species: Species; + * formIndex: number; + * gender: Gender; + * pokeball: PokeballType; + * fusionSpecies?: PokemonSpecies; + * fusionFormIndex?: number; + * fusionGender?: Gender; + * level?: number + * ``` + * + * As this version of the data is compatible with version 1.10, we only need to specify the new properties. + */ +export const Z$V1_9_IllusionData = z.looseObject({ + species: Z$PositiveInt, + fusionSpecies: z + .object({ + speciesId: Z$PositiveInt, + }) + .optional() + .catch(undefined), +}); +type V1_9_IllusionData = z.input; + +/** + * Migrate illusion data from version 1.9 to 1.10 + * + * @remarks + * Extract `speciesId` from `fusionSpecies`, and use defaults for all fields from the illusioned pokemon. + * + */ +export function V1_10_IllusionDataMigrator(arg: V1_9_IllusionData): Partial> { + return Z$V1_9_IllusionData.transform(data => { + // Needed to fetch a default name. + const pokemon = getPokemonSpecies(data.species); + return { + ...(data as Omit), + // unwrap fusion species + fusionSpecies: data.fusionSpecies?.speciesId, + // and use defaults for all fields from the illusioned pokemon + name: pokemon.name, + nickname: pokemon.name, + shiny: false, + fusionShiny: false, + variant: 0, + fusionVariant: 0, + fusionFormIndex: 0, + }; + }).parse(arg); +} diff --git a/src/system/schemas/v1.10/illusion-data.ts b/src/system/schemas/v1.10/illusion-data.ts new file mode 100644 index 00000000000..073db626dd9 --- /dev/null +++ b/src/system/schemas/v1.10/illusion-data.ts @@ -0,0 +1,25 @@ +import { Z$NonNegativeInt, Z$PositiveInt } from "#system/schemas/common"; +import { Z$PokeballType } from "#system/schemas/v1.10/pokeball-type"; +import { Z$Gender } from "#system/schemas/v1.10/pokemon-gender"; +import { z } from "zod"; + + +// TODO: Write migrator for illusion data's fusionSpecies field +// that transforms incoming fusion species + +export const Z$IllusionData = z.object({ + name: z.string(), + nickname: z.string(), + shiny: z.boolean(), + variant: z.literal([0, 1, 2]).catch(0), + species: Z$PositiveInt, + formIndex: Z$NonNegativeInt.catch(0), + gender: Z$Gender, + pokeball: Z$PokeballType, + fusionSpecies: Z$PositiveInt.optional().catch(undefined), + fusionFormIndex: Z$NonNegativeInt.optional().catch(undefined), + fusionShiny: z.boolean().optional().catch(false), + fusionVariant: z.literal([0, 1, 2]).optional().catch(0), + fusionGender: Z$Gender.optional().catch(undefined), + level: Z$PositiveInt.optional().catch(undefined), +}); diff --git a/src/system/schemas/v1.10/pokemon-data.ts b/src/system/schemas/v1.10/pokemon-data.ts index 824a56b5614..b9476711c13 100644 --- a/src/system/schemas/v1.10/pokemon-data.ts +++ b/src/system/schemas/v1.10/pokemon-data.ts @@ -1,6 +1,7 @@ import { Z$BoolCatchToFalse, Z$NonNegativeInt, Z$PositiveInt } from "#schemas/common"; import { Z$PokeballType } from "#schemas/pokeball-type"; import { Z$PokemonMove } from "#schemas/pokemon-move"; +import { Z$PokemonBattleData } from "#system/schemas/v1.10/pokemon-battle-data"; import { Z$Gender } from "#system/schemas/v1.10/pokemon-gender"; import z from "zod"; import { NatureSchema } from "./pokemon-nature"; @@ -14,7 +15,7 @@ import { StatusSchema } from "./status-effect"; * `looseObject` used here to allow properties specific to player or enemy Pokémon * to be handled by their respective schemas. */ -const Z$PokemonData = z.looseObject({ +export const Z$PokemonData = z.looseObject({ // malformed pokemon ids are _not_ supported. id: z.uint32(), species: z.uint32(), @@ -65,6 +66,9 @@ const Z$PokemonData = z.looseObject({ //#endregion "fusion" information pokerus: z.boolean().catch(false), + summonData: Z$PokemonSummonData, + battleData: Z$PokemonBattleData, + summonDataSpeciesFormIndex: Z$NonNegativeInt, }); export const Z$PlayerPokemonData = z.object({ @@ -88,29 +92,6 @@ export const Z$EnemyPokemonData = z.object({ bossSegments: z.int().nonnegative().default(0), }); -// TODO: Replace output assertion type with the type of pokemon data that has CustomPokemonData. -export function PreCustomPokemonDataMigrator( - data: z.output, -): asserts data is z.output { - // Value of `-1` indicated no override, so we can ignore it. - const nature = NatureSchema.safeParse(data.natureOverride); - if (nature.success) { - const customPokemonData = data.customPokemonData; - // If natureOverride is valid, use it - if ( - customPokemonData && - typeof customPokemonData === "object" && - ((customPokemonData as { nature?: number }).nature ?? -1) === -1 - ) { - customPokemonData; - } else { - data.customPokemonData = { - nature: nature.data, - }; - } - } -} - export type PreParsedPokemonData = z.input; export type ParsedPokemonData = z.output; diff --git a/src/system/schemas/v1.10/pokemon-summon-data.ts b/src/system/schemas/v1.10/pokemon-summon-data.ts index 2cd078f3631..b03a93ae2a2 100644 --- a/src/system/schemas/v1.10/pokemon-summon-data.ts +++ b/src/system/schemas/v1.10/pokemon-summon-data.ts @@ -1,47 +1,36 @@ import { Z$NonNegativeInt } from "#system/schemas/common"; import { Z$Gender } from "#system/schemas/v1.10/pokemon-gender"; -import { Z$PokemonSpeciesForm } from "#system/schemas/v1.10/pokemon-species-form"; +import { Z$PokemonMove } from "#system/schemas/v1.10/pokemon-move"; import { Z$StatSet } from "#system/schemas/v1.10/pokemon-stats"; import { Z$PokemonType } from "#system/schemas/v1.10/pokemon-type"; import { Z$TurnMove } from "#system/schemas/v1.10/turn-move"; import { z } from "zod"; -const Z$StatStage = z.int().min(-6).max(6).catch(0); - -const Z$StatStageSet = z.tuple([ - Z$StatStage, - Z$StatStage, - Z$StatStage, - Z$StatStage, - Z$StatStage, - Z$StatStage, - Z$StatStage, -]); - -// Pre version 1.10 pokemon summon data migration needs to rename -// input fields +export const Z$SerializedSpeciesForm = z.object({ + id: Z$NonNegativeInt, + formIndex: Z$NonNegativeInt.catch(0), +}); /** * Zod schema for Pokémon summon data as of version 1.10. * + * @remarks + * All fields other than `stats` are optional, and catch to `undefined` on parse error, + * allowing {@linkcode PokemonSummonData} to fill in defaults. + * */ export const Z$PokemonSummonData = z.object({ - statStages: Z$StatStageSet.optional().catch(undefined), + statSages: z.array(z.int().min(-6).max(6).catch(0)).optional().catch(undefined), moveQueue: z.array(Z$TurnMove).optional().catch(undefined), - // todo: tags abilitySuppressed: z.boolean().optional().catch(undefined), - - //#region Overrides for transform - speciesForm: Z$PokemonSpeciesForm.nullable().catch(null), - fusionSpeciesForm: Z$PokemonSpeciesForm.nullable().catch(null), + speciesForm: Z$SerializedSpeciesForm.optional().catch(undefined), ability: Z$NonNegativeInt.optional().catch(undefined), passiveAbility: Z$NonNegativeInt.optional().catch(undefined), gender: Z$Gender.optional().catch(undefined), fusionGender: Z$Gender.optional().catch(undefined), - stats: Z$StatSet.optional().catch(undefined), - moveset: z.array(Z$TurnMove).nullable().catch(null), - //#endregion Overrides for transform - + stats: Z$StatSet, + moveset: z.array(Z$PokemonMove).optional().catch(undefined), types: z.array(Z$PokemonType).optional().catch(undefined), - addedType: Z$PokemonType.nullable().catch(null), + addedType: Z$PokemonType.optional().catch(undefined), + illusion });