This commit is contained in:
Sirz Benjie 2025-07-27 10:27:47 -06:00
parent 09bb498c7a
commit 10af69a777
No known key found for this signature in database
GPG Key ID: 4A524B4D196C759E
6 changed files with 155 additions and 58 deletions

View File

@ -97,10 +97,10 @@ interface SerializedIllusionData extends Omit<IllusionData, "fusionSpecies"> {
}
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;

View File

@ -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<typeof Z$PokemonData>,
): asserts data is z.output<typeof Z$PokemonData> {
// 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,
};
}
}
}

View File

@ -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<typeof Z$V1_9_IllusionData>;
/**
* 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<z.input<typeof Z$IllusionData>> {
return Z$V1_9_IllusionData.transform(data => {
// Needed to fetch a default name.
const pokemon = getPokemonSpecies(data.species);
return {
...(data as Omit<V1_9_IllusionData, "fusionSpecies">),
// 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);
}

View File

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

View File

@ -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<typeof Z$PokemonData>,
): asserts data is z.output<typeof Z$PokemonData> {
// 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<typeof Z$PokemonData>;
export type ParsedPokemonData = z.output<typeof Z$PokemonData>;

View File

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