From 0c42f16fdd27c5cc9fc5e33e4fecc6c7ffde5436 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Mon, 21 Jul 2025 17:36:08 -0600 Subject: [PATCH] [WIP] --- package.json | 4 +- pnpm-lock.yaml | 11 ++ sample_session_data.json | 165 ++++++++++++++++++ scripts/zod-test.ts | 61 +++++++ src/@types/turn-move.ts | 2 +- src/data/pokemon/pokemon-data.ts | 16 ++ src/system/schemas/common.ts | 27 +++ src/system/schemas/game-version.ts | 8 + src/system/schemas/v1.10/battler-index.ts | 15 ++ src/system/schemas/v1.10/battler-tag.ts | 142 +++++++++++++++ src/system/schemas/v1.10/berry-type.ts | 19 ++ src/system/schemas/v1.10/biome-id.ts | 55 ++++++ .../schemas/v1.10/custom-pokemon-data.ts | 17 ++ src/system/schemas/v1.10/move-result.ts | 8 + src/system/schemas/v1.10/pokeball-type.ts | 17 ++ .../schemas/v1.10/pokemon-battle-data.ts | 18 ++ src/system/schemas/v1.10/pokemon-data.ts | 123 +++++++++++++ src/system/schemas/v1.10/pokemon-gender.ts | 11 ++ src/system/schemas/v1.10/pokemon-move.ts | 17 ++ src/system/schemas/v1.10/pokemon-nature.ts | 33 ++++ src/system/schemas/v1.10/pokemon-stats.ts | 39 +++++ .../schemas/v1.10/pokemon-summon-data.ts | 27 +++ src/system/schemas/v1.10/pokemon-type.ts | 26 +++ src/system/schemas/v1.10/session-save-data.ts | 0 src/system/schemas/v1.10/status-effect.ts | 31 ++++ src/system/schemas/v1.10/turn-move.ts | 25 +++ tsconfig.json | 1 + 27 files changed, 916 insertions(+), 2 deletions(-) create mode 100644 sample_session_data.json create mode 100644 scripts/zod-test.ts create mode 100644 src/system/schemas/common.ts create mode 100644 src/system/schemas/game-version.ts create mode 100644 src/system/schemas/v1.10/battler-index.ts create mode 100644 src/system/schemas/v1.10/battler-tag.ts create mode 100644 src/system/schemas/v1.10/berry-type.ts create mode 100644 src/system/schemas/v1.10/biome-id.ts create mode 100644 src/system/schemas/v1.10/custom-pokemon-data.ts create mode 100644 src/system/schemas/v1.10/move-result.ts create mode 100644 src/system/schemas/v1.10/pokeball-type.ts create mode 100644 src/system/schemas/v1.10/pokemon-battle-data.ts create mode 100644 src/system/schemas/v1.10/pokemon-data.ts create mode 100644 src/system/schemas/v1.10/pokemon-gender.ts create mode 100644 src/system/schemas/v1.10/pokemon-move.ts create mode 100644 src/system/schemas/v1.10/pokemon-nature.ts create mode 100644 src/system/schemas/v1.10/pokemon-stats.ts create mode 100644 src/system/schemas/v1.10/pokemon-summon-data.ts create mode 100644 src/system/schemas/v1.10/pokemon-type.ts create mode 100644 src/system/schemas/v1.10/session-save-data.ts create mode 100644 src/system/schemas/v1.10/status-effect.ts create mode 100644 src/system/schemas/v1.10/turn-move.ts diff --git a/package.json b/package.json index d3494da677c..4aa0c1ba321 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ }, "dependencies": { "@material/material-color-utilities": "^0.2.7", + "ajv": "^8.17.1", "compare-versions": "^6.1.1", "crypto-js": "^4.2.0", "i18next": "^24.2.3", @@ -58,7 +59,8 @@ "json-stable-stringify": "^1.3.0", "jszip": "^3.10.1", "phaser": "^3.90.0", - "phaser3-rex-plugins": "^1.80.16" + "phaser3-rex-plugins": "^1.80.16", + "zod": "^4.0.5" }, "engines": { "node": ">=22.0.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 900be6fd76e..623316a2fdb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@material/material-color-utilities': specifier: ^0.2.7 version: 0.2.7 + ajv: + specifier: ^8.17.1 + version: 8.17.1 compare-versions: specifier: ^6.1.1 version: 6.1.1 @@ -41,6 +44,9 @@ importers: phaser3-rex-plugins: specifier: ^1.80.16 version: 1.80.16(graphology-types@0.24.8) + zod: + specifier: ^4.0.5 + version: 4.0.5 devDependencies: '@biomejs/biome': specifier: 2.0.0 @@ -2002,6 +2008,9 @@ packages: resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} engines: {node: '>=18'} + zod@4.0.5: + resolution: {integrity: sha512-/5UuuRPStvHXu7RS+gmvRf4NXrNxpSllGwDnCBcJZtQsKrviYXm54yDGV2KYNLT5kq0lHGcl7lqWJLgSaG+tgA==} + snapshots: '@ampproject/remapping@2.3.0': @@ -3828,3 +3837,5 @@ snapshots: yargs-parser: 21.1.1 yoctocolors-cjs@2.1.2: {} + + zod@4.0.5: {} diff --git a/sample_session_data.json b/sample_session_data.json new file mode 100644 index 00000000000..34e9db92b21 --- /dev/null +++ b/sample_session_data.json @@ -0,0 +1,165 @@ +{ + "seed": "XsQk92D0JOcTKUGejq4HpzvC", + "playTime": 0, + "gameMode": 0, + "party": [ + { + "id": 674110050, + "player": true, + "species": 479, + "formIndex": 0, + "abilityIndex": 0, + "passive": true, + "shiny": false, + "variant": 0, + "pokeball": 0, + "level": 5, + "exp": 125, + "levelExp": 0, + "gender": -1, + "hp": 21, + "stats": [21, 13, 13, 16, 14, 13], + "ivs": [31, 31, 25, 31, 31, 26], + "nature": 2, + "moveset": [], + "status": null, + "friendship": 50, + "metLevel": 5, + "metBiome": -1, + "metSpecies": 479, + "metWave": -1, + "luck": 2, + "pauseEvolutions": false, + "pokerus": false, + "usedTMs": [], + "teraType": 12, + "isTerastallized": false, + "stellarTypesBoosted": [], + "mysteryEncounterPokemonData": null, + "fusionMysteryEncounterPokemonData": null, + "fusionLuck": 0, + "fusionTeraType": 0, + "boss": false, + "bossSegments": 0, + "summonData": { + "statStages": [0, 0, 0, 0, 0, 0, 0], + "moveQueue": [], + "tags": [], + "abilitySuppressed": false, + "speciesForm": null, + "fusionSpeciesForm": null, + "stats": [0, 0, 0, 0, 0, 0], + "types": [], + "addedType": null, + "illusion": null, + "illusionBroken": false, + "berriesEatenLast": [], + "moveHistory": [] + }, + "battleData": { "hitCount": 0, "hasEatenBerry": false, "berriesEaten": [] }, + "customPokemonData": { + "spriteScale": -1, + "hitsRecCount": null, + "ability": -1, + "passive": -1, + "nature": -1, + "types": [] + }, + "fusionCustomPokemonData": { + "spriteScale": -1, + "hitsRecCount": null, + "ability": -1, + "passive": -1, + "nature": -1, + "types": [] + } + } + ], + "enemyParty": [ + { + "id": 1442439343, + "player": false, + "species": 876, + "formIndex": 1, + "abilityIndex": 1, + "shiny": false, + "variant": 0, + "pokeball": 0, + "level": 2, + "exp": 7, + "levelExp": 0, + "gender": 1, + "hp": 15, + "stats": [15, 7, 7, 9, 10, 7], + "ivs": [10, 31, 19, 24, 5, 15], + "nature": 22, + "moveset": [{ "moveId": 500, "ppUsed": 0, "ppUp": 0 }, { "moveId": 589, "ppUsed": 0, "ppUp": 0 }], + "status": null, + "friendship": 140, + "metLevel": 2, + "metBiome": 0, + "metSpecies": 876, + "metWave": 1, + "luck": 0, + "pauseEvolutions": false, + "pokerus": false, + "usedTMs": [], + "teraType": 13, + "isTerastallized": false, + "stellarTypesBoosted": [], + "mysteryEncounterPokemonData": null, + "fusionMysteryEncounterPokemonData": null, + "fusionLuck": 0, + "fusionTeraType": 0, + "boss": false, + "bossSegments": 0, + "summonData": { + "statStages": [0, 0, 0, 0, 0, 0, 0], + "moveQueue": [], + "tags": [], + "abilitySuppressed": false, + "speciesForm": null, + "fusionSpeciesForm": null, + "stats": [0, 0, 0, 0, 0, 0], + "types": [], + "addedType": null, + "illusion": null, + "illusionBroken": false, + "berriesEatenLast": [], + "moveHistory": [] + }, + "battleData": { "hitCount": 0, "hasEatenBerry": false, "berriesEaten": [] }, + "customPokemonData": { + "spriteScale": -1, + "hitsRecCount": null, + "ability": -1, + "passive": -1, + "nature": -1, + "types": [] + }, + "fusionCustomPokemonData": { + "spriteScale": -1, + "hitsRecCount": null, + "ability": -1, + "passive": -1, + "nature": -1, + "types": [] + } + } + ], + "modifiers": [], + "enemyModifiers": [], + "arena": { "biome": 0, "weather": null, "terrain": null, "playerTerasUsed": 0, "tags": [] }, + "pokeballCounts": { "0": 5, "1": 0, "2": 0, "3": 0, "4": 5 }, + "money": 1000, + "score": 0, + "waveIndex": 1, + "battleType": 0, + "trainer": null, + "gameVersion": "1.9.6", + "timestamp": 1751940739555, + "challenges": [], + "mysteryEncounterType": -1, + "mysteryEncounterSaveData": { "encounteredEvents": [], "encounterSpawnChance": 3, "queuedEncounters": [] }, + "playerFaints": 0 +} diff --git a/scripts/zod-test.ts b/scripts/zod-test.ts new file mode 100644 index 00000000000..252245f2fca --- /dev/null +++ b/scripts/zod-test.ts @@ -0,0 +1,61 @@ +import z from "zod"; + +/* +Schema for session data +*/ + +/* +Schema of a `PokemonMove` as of version 1.10 +*/ + +// const IVSet = z.tuple([IVSchema, IVSchema, IVSchema, IVSchema, IVSchema, IVSchema]).catch((e) => { +// e.value +// } + +export const Z$NonNegativeCatchNeg1 = /*@__PURE__*/ z + .int() + .nonnegative() + .optional() + .catch(-1); + +console.log(Z$NonNegativeCatchNeg1.safeParse(undefined)); + +// export const PokemonMoveSchema = z.object({ +// moveId: z.number().int().min(0).optional().overwrite(() => 15), // Move ID, default to 0 +// ppUsed: z.number().int().catch(0), // PP used, default to 0 +// ppUp: z.number().int().default(0).catch(0), // PP Up count, default to 0 +// maxPpOverride: z.int().min(1).optional(), // Optional max PP override, can be null +// }); + +// // export const PokemonDataSchema = z.object({ +// // id: z.int(), +// // player: z.boolean(), +// // species: z.int(), +// // nickname: z.string(), +// // formIndex: z.int().default(0), +// // abilityIndex: z.int().min(0).max(2).default(0).catch, +// // }).check(data => { +// // // clamp form index to be between 0 and the max form index for the species +// // const species = getPokemonSpecies(data.value.species); +// // // Clamp formindex to be between 0 and the max form index for the species + +// // }) + +// const mockData = { +// // ppUsed: 5, +// ppUp: 2, +// iv: [1, 15, 14, 14, 15, 16, 17] +// }; + +// try { +// // Parse and validate the mock data +// const result = PokemonMoveSchema.parse(mockData); +// console.log("Validation successful:", result); +// } catch (error: unknown) { +// if (error instanceof ZodError) { +// console.error(error.message); +// console.error(z.treeifyError(error)); +// } else { +// console.error("Validation failed:", error); +// } +// } diff --git a/src/@types/turn-move.ts b/src/@types/turn-move.ts index a5a69eb3749..f26e3b9e640 100644 --- a/src/@types/turn-move.ts +++ b/src/@types/turn-move.ts @@ -10,4 +10,4 @@ export interface TurnMove { useMode: MoveUseMode; result?: MoveResult; turn?: number; -} +} \ No newline at end of file diff --git a/src/data/pokemon/pokemon-data.ts b/src/data/pokemon/pokemon-data.ts index 972d7627bcd..03c806d614e 100644 --- a/src/data/pokemon/pokemon-data.ts +++ b/src/data/pokemon/pokemon-data.ts @@ -51,6 +51,22 @@ export class CustomPokemonData { this.types = data?.types ?? []; this.hitsRecCount = data?.hitsRecCount ?? null; } + + /** + * Returns whether this CustomPokemonData has only the default properties. + * Primarily used to avoid serializing this data if it is not customized. + */ + static isDefault(data: CustomPokemonData | Partial): boolean { + return ( + data.spriteScale === -1 && + data.ability === -1 && + data.passive === -1 && + data.nature === -1 && + Array.isArray(data.types) && + data.types.length === 0 && + (data.hitsRecCount === null || data.hitsRecCount === 0) + ); + } } /** diff --git a/src/system/schemas/common.ts b/src/system/schemas/common.ts new file mode 100644 index 00000000000..19f95241d8b --- /dev/null +++ b/src/system/schemas/common.ts @@ -0,0 +1,27 @@ +import { z } from "zod"; + +/* +Schemas for commonly used primitive types, to avoid repeated instantiations. +*/ + +/** Reusable schema for a positive integer, equivalent to `z.int().positive()`.*/ +export const Z$PositiveInt = /*@__PURE__*/ z + .int() + .positive(); + +/** Reusable schema for a non-negative integer, equivalent to `z.int().nonnegative()`.*/ +export const Z$NonNegativeInt = /*@__PURE__*/ z + .int() + .nonnegative(); + +/** Reusable schema for a boolean that coerces non-boolean inputs to `false` */ +export const Z$BoolCatchToFalse = /*@__PURE__*/ z + .boolean() + .catch(false); + +/** Reusable schema for an optional non-negative integer that coerces invalid inputs to `undefined` */ +export const Z$OptionalNonNegativeIntCatchToUndef = /*@__PURE__*/ z + .int() + .nonnegative() + .optional() + .catch(undefined); diff --git a/src/system/schemas/game-version.ts b/src/system/schemas/game-version.ts new file mode 100644 index 00000000000..0b3e9e471ca --- /dev/null +++ b/src/system/schemas/game-version.ts @@ -0,0 +1,8 @@ +import { z } from "zod"; + +/** + * Zod schema matching a version string + * + * @remarks Equivalent to `z.string().regex(/^\d+\.\d+\.\d+$/)` + */ +export const Z$GameVersion = z.string().regex(/^\d+\.\d+\.\d+$/); diff --git a/src/system/schemas/v1.10/battler-index.ts b/src/system/schemas/v1.10/battler-index.ts new file mode 100644 index 00000000000..f7b590de12f --- /dev/null +++ b/src/system/schemas/v1.10/battler-index.ts @@ -0,0 +1,15 @@ +// biome-ignore lint/correctness/noUnusedImports: used in tsdoc comment +import { BattlerIndex } from "#enums/battler-index"; +import { z } from "zod"; + +/** + * Zod schema for the {@linkcode BattlerIndex} as of version 1.10 + * + * @remarks + * - `-1`: Attacker + * - `0`: Player + * - `1`: Player 2 + * - `2`: Enemy + * - `3`: Enemy 2 + */ +export const Z$BattlerIndex = z.literal([-1, 0, 1, 2, 3]); diff --git a/src/system/schemas/v1.10/battler-tag.ts b/src/system/schemas/v1.10/battler-tag.ts new file mode 100644 index 00000000000..915ba29e6e1 --- /dev/null +++ b/src/system/schemas/v1.10/battler-tag.ts @@ -0,0 +1,142 @@ +import { Z$NonNegativeInt, Z$PositiveInt } from "#system/schemas/common"; +import { Z$BattlerIndex } from "#system/schemas/v1.10/battler-index"; +import { z } from "zod"; + +/* +Schemas for battler tags are a bit more cumbersome, +as we need to have schemas for each subclass that has a different shape. +*/ + +/** + * Zod schema for the {@linkcode BattlerTagType} enum as of version 1.10 + */ +const Z$BattlerTagType = z.literal([ + "NONE", + "RECHARGING", + "FLINCHED", + "INTERRUPTED", + "CONFUSED", + "INFATUATED", + "SEEDED", + "NIGHTMARE", + "FRENZY", + "CHARGING", + "ENCORE", + "HELPING_HAND", + "INGRAIN", + "OCTOLOCK", + "AQUA_RING", + "DROWSY", + "TRAPPED", + "BIND", + "WRAP", + "FIRE_SPIN", + "WHIRLPOOL", + "CLAMP", + "SAND_TOMB", + "MAGMA_STORM", + "SNAP_TRAP", + "THUNDER_CAGE", + "INFESTATION", + "PROTECTED", + "SPIKY_SHIELD", + "KINGS_SHIELD", + "OBSTRUCT", + "SILK_TRAP", + "BANEFUL_BUNKER", + "BURNING_BULWARK", + "ENDURING", + "STURDY", + "PERISH_SONG", + "TRUANT", + "SLOW_START", + "PROTOSYNTHESIS", + "QUARK_DRIVE", + "FLYING", + "UNDERGROUND", + "UNDERWATER", + "HIDDEN", + "FIRE_BOOST", + "CRIT_BOOST", + "ALWAYS_CRIT", + "IGNORE_ACCURACY", + "BYPASS_SLEEP", + "IGNORE_FLYING", + "SALT_CURED", + "CURSED", + "CHARGED", + "ROOSTED", + "FLOATING", + "MINIMIZED", + "DESTINY_BOND", + "CENTER_OF_ATTENTION", + "ICE_FACE", + "DISGUISE", + "STOCKPILING", + "RECEIVE_DOUBLE_DAMAGE", + "ALWAYS_GET_HIT", + "DISABLED", + "SUBSTITUTE", + "IGNORE_GHOST", + "IGNORE_DARK", + "GULP_MISSILE_ARROKUDA", + "GULP_MISSILE_PIKACHU", + "BEAK_BLAST_CHARGING", + "SHELL_TRAP", + "DRAGON_CHEER", + "NO_RETREAT", + "GORILLA_TACTICS", + "UNBURDEN", + "THROAT_CHOPPED", + "TAR_SHOT", + "BURNED_UP", + "DOUBLE_SHOCKED", + "AUTOTOMIZED", + "MYSTERY_ENCOUNTER_POST_SUMMON", + "POWER_TRICK", + "HEAL_BLOCK", + "TORMENT", + "TAUNT", + "IMPRISON", + "SYRUP_BOMB", + "ELECTRIFIED", + "TELEKINESIS", + "COMMANDED", + "GRUDGE", + "PSYCHO_SHIFT", + "ENDURE_TOKEN", + "POWDER", + "MAGIC_COAT", +]); + +/** + * Zod schema for {@linkcode BattlerTagLapseType} as of version 1.10 + * @remarks + * - `0`: Faint + * - `1`: Move + * - `2`: Pre-Move + * - `3`: After Move + * - `4`: Move Effect + * - `5`: Turn End + * - `6`: Hit + * - `7`: After Hit + * - `8`: Custom + */ +const Z$BattlerTagLapseType = z.literal([0, 1, 2, 3, 4, 5, 6, 7, 8]); + +// DamagingTrapTag may have a `commonAnim` field, though it's always instantiated. +const Z$BattlerTag = z.object({ + tagType: Z$BattlerTagType, + lapseTypes: z.array(Z$BattlerTagLapseType), + turnCount: Z$PositiveInt, + // Source move can be `none` for tags not applied by move, so allow `0` here. + sourceMove: Z$NonNegativeInt, + sourceId: z.int().optional().catch(undefined), + isBatonPassable: z.boolean(), +}); + +export const Z$SeedTag = z.object({ + ...Z$BattlerTag.shape, + seedType: z.literal("SEEDED"), + sourceIndex: Z$BattlerIndex, +}) diff --git a/src/system/schemas/v1.10/berry-type.ts b/src/system/schemas/v1.10/berry-type.ts new file mode 100644 index 00000000000..2f11530f94b --- /dev/null +++ b/src/system/schemas/v1.10/berry-type.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +/** + * Zod schema for Berry types as of version 1.10. + * + * @remarks + * - `0`: Sitrus + * - `1`: Lum + * - `2`: Enigma + * - `3`: Liechi + * - `4`: Ganlon + * - `5`: Petaya + * - `6`: Apicot + * - `7`: Salac + * - `8`: Lansat + * - `9`: Starf + * - `10`: Leppa + */ +export const Z$BerryType = z.literal([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); diff --git a/src/system/schemas/v1.10/biome-id.ts b/src/system/schemas/v1.10/biome-id.ts new file mode 100644 index 00000000000..2957d77dba3 --- /dev/null +++ b/src/system/schemas/v1.10/biome-id.ts @@ -0,0 +1,55 @@ +import { z } from "zod"; + +/** + * Schema for biome IDs, as of version 1.10 + * + * @remarks + * - `0`: Town, + * - `1`: Plains, + * - `2`: Grass, + * - `3`: Tall Grass, + * - `4`: Metropolis, + * - `5`: Forest, + * - `6`: Sea, + * - `7`: Swamp, + * - `8`: Beach, + * - `9`: Lake, + * - `10`: Seabed, + * - `11`: Mountain, + * - `12`: Badlands, + * - `13`: Cave, + * - `14`: Desert, + * - `15`: Ice Cave, + * - `16`: Meadow, + * - `17`: Power Plant, + * - `18`: Volcano, + * - `19`: Graveyard, + * - `20`: Dojo, + * - `21`: Factory, + * - `22`: Ruins, + * - `23`: Wasteland, + * - `24`: Abyss, + * - `25`: Space, + * - `26`: Construction Site, + * - `27`: Jungle, + * - `28`: Fairy Cave, + * - `29`: Temple, + * - `30`: Slum, + * - `31`: Snowy Forest, + * - `40`: Island, + * - `41`: Laboratory, + * - `50`: End + */ +export const Z$BiomeID = z.literal([ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 40, 41, 50, +]); + +/** + * Schema for a pokemon's met biome, as of version 1.10 + * Same as {@linkcode Z$BiomeID}, additionally allowing `-1` for starters. + */ +export const Z$MetBiome = z.union([ + z.literal(-1), // For starters + Z$BiomeID, // All other biomes +]); diff --git a/src/system/schemas/v1.10/custom-pokemon-data.ts b/src/system/schemas/v1.10/custom-pokemon-data.ts new file mode 100644 index 00000000000..cda2dcd2c3d --- /dev/null +++ b/src/system/schemas/v1.10/custom-pokemon-data.ts @@ -0,0 +1,17 @@ +import { Z$OptionalNonNegativeIntCatchToUndef } from "#schemas/common"; +import { z } from "zod"; + +/** + * Zod schema for custom Pokémon data as of version 1.10. + * + * @remarks All fields are optional, but catch to `undefined` + * on malformed inputs, as the `CustomPokemonData` allows partial data and + * uses defaults for missing fields. + */ +export const Z$CustomPokemonData = z.object({ + // sprite scale of -1 is allowed, but it's the default meaning there is no override for it + spriteScale: z.number().nonnegative().optional().catch(undefined), + ability: Z$OptionalNonNegativeIntCatchToUndef.catch(undefined), + passive: Z$OptionalNonNegativeIntCatchToUndef.catch(undefined), + nature: Z$OptionalNonNegativeIntCatchToUndef.catch(undefined), +}); \ No newline at end of file diff --git a/src/system/schemas/v1.10/move-result.ts b/src/system/schemas/v1.10/move-result.ts new file mode 100644 index 00000000000..9e7e3391454 --- /dev/null +++ b/src/system/schemas/v1.10/move-result.ts @@ -0,0 +1,8 @@ +// biome-ignore lint/correctness/noUnusedImports: used in tsdoc comment +import type { MoveResult } from "#enums/move-result"; +import { z } from "zod"; + +/** + * Zod schema for the {@linkcode MoveResult} enum as of version 1.10 + */ +export const Z$MoveResult = z.literal([0, 1, 2, 3, 4]); diff --git a/src/system/schemas/v1.10/pokeball-type.ts b/src/system/schemas/v1.10/pokeball-type.ts new file mode 100644 index 00000000000..38d944e7cbd --- /dev/null +++ b/src/system/schemas/v1.10/pokeball-type.ts @@ -0,0 +1,17 @@ +import { z } from "zod"; + +/** + * Schema for Pokéball types that a pokemon can be caught in, as of version 1.10. Excludes Luxury Ball + * - `0`: POKEBALL, + * - `1`: GREAT_BALL, + * - `2`: ULTRA_BALL, + * - `3`: ROGUE_BALL, + * - `4`: MASTER_BALL + */ +export const Z$PokeballType = z.literal([ + 0, // POKEBALL + 1, // GREAT_BALL + 2, // ULTRA_BALL + 3, // ROGUE BALL + 4, // MASTER_BALL +]); diff --git a/src/system/schemas/v1.10/pokemon-battle-data.ts b/src/system/schemas/v1.10/pokemon-battle-data.ts new file mode 100644 index 00000000000..01721dd37d1 --- /dev/null +++ b/src/system/schemas/v1.10/pokemon-battle-data.ts @@ -0,0 +1,18 @@ +import { Z$BerryType } from "#schemas/berry-type"; +import { Z$NonNegativeInt } from "#schemas/common"; +import { z } from "zod"; + +/** + * Zod schema for Pokémon battle data as of version 1.10. + * + * @remarks + * All fields are optional and fallback to undefined if malformed, as `BattleData`'s + * constructor supports taking partial data, and using defaults for missing fields. + */ +export const Z$PokemonBattleData = z.object({ + hitCount: Z$NonNegativeInt.optional().catch(undefined), + hasEatenBerry: z.boolean().optional().catch(undefined), + berriesEaten: z.array(Z$BerryType).optional().catch(undefined), +}); + +export type ParsedPokemonBattleData = z.output; diff --git a/src/system/schemas/v1.10/pokemon-data.ts b/src/system/schemas/v1.10/pokemon-data.ts new file mode 100644 index 00000000000..9f4fa01b128 --- /dev/null +++ b/src/system/schemas/v1.10/pokemon-data.ts @@ -0,0 +1,123 @@ +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$Gender } from "#system/schemas/v1.10/pokemon-gender"; +import z from "zod"; +import { NatureSchema } from "./pokemon-nature"; +import { IVSetSchema, statSetSchema } from "./pokemon-stats"; +import { Z$PokemonType } from "./pokemon-type"; +import { StatusSchema } from "./status-effect"; + +/** + * Not meant to actually be used itself. + * Instead, use either {@linkcode Z$PlayerPokemonData} or {@linkcode Z$EnemyPokemonData}. + * `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({ + // malformed pokemon ids are _not_ supported. + id: z.uint32(), + species: z.uint32(), + nickname: z.string().optional(), + formIndex: Z$NonNegativeInt.catch(0), + // Between 0 and 2, with malformed inputs defaulting to 0 + abilityIndex: z.int().min(0).max(2).catch(0), + passive: Z$BoolCatchToFalse, + shiny: Z$BoolCatchToFalse, + variant: z.literal([0, 1, 2]).catch(0), + pokeball: Z$PokeballType.catch(0), + + // No fallbacks for level + level: Z$NonNegativeInt, + // TODO: Fallback to minimum experience for level if parsing error? + exp: Z$NonNegativeInt, + // Fallback to 0, patch in transformer + levelExp: Z$NonNegativeInt.catch(0), + // Fallback to -1, patch to a default gender in the transformer. + gender: Z$Gender.catch(-1), + // hp can be 0 if fainted + hp: Z$NonNegativeInt, + stats: statSetSchema, + ivs: IVSetSchema, + nature: NatureSchema, + moveset: z.array(Z$PokemonMove).catch([]), + status: z.union([z.null(), StatusSchema]).catch(null), + friendship: z.int().min(0).max(255).catch(0), + + //#region "met" information + metLevel: z.int().positive().catch(1), + metBiome: z.union([z.int().nonnegative(), z.literal(-1)]).catch(-1), // -1 for starters + metSpecies: z.uint32(), + metWave: z.int().min(-1).default(0), + //#endregion "met" information + + luck: Z$NonNegativeInt.catch(0), + teraType: Z$PokemonType, + isTerastallized: Z$BoolCatchToFalse, + stellarTypesBoosted: z.array(Z$PokemonType).catch([]), + + //#region "fusion" information + fusionSpecies: z.uint32().optional(), + fusionFormIndex: Z$NonNegativeInt.optional(), + fusionAbilityIndex: z.int().min(0).max(2).optional().catch(0), + fusionShiny: Z$BoolCatchToFalse, + fusionLuck: Z$NonNegativeInt.catch(0), + //#endregion "fusion" information + + pokerus: z.boolean().catch(false), +}); + +export const Z$PlayerPokemonData = z.object({ + ...Z$PokemonData.shape, + player: z.transform((): true => true), + pauseEvolutions: Z$BoolCatchToFalse, + // Assume player pokemon can never be bosses + boss: z.transform((): false => false), + bossSegments: z.transform((): 0 => 0), + // 0 is unknown, -1 is starter + metWave: z.int().min(-1).default(0), + usedTms: z.array(Z$PositiveInt).catch([]), + // Fallback for empty pokemon movesets handled by transformer + moveset: z.array(Z$PokemonMove).catch([]), +}); + +export const Z$EnemyPokemonData = z.object({ + ...Z$PokemonData.shape, + player: z.transform((): false => false), + boss: z.boolean().catch(false), + 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; + +export type PreParsedPlayerPokemonData = z.input; +export type ParsedPlayerPokemon = z.output; + +export type PreParsedEnemyPokemonData = z.input; +export type ParsedEnemyPokemon = z.output; diff --git a/src/system/schemas/v1.10/pokemon-gender.ts b/src/system/schemas/v1.10/pokemon-gender.ts new file mode 100644 index 00000000000..0e09626d233 --- /dev/null +++ b/src/system/schemas/v1.10/pokemon-gender.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; + +/** + * Schema for a Pokémon's Gender, as of version 1.10 + * + * @remarks + * - `-1`: Genderless, + * - `0`: Male, + * - `1`: Female + */ +export const Z$Gender = z.literal([-1, 0, 1]); diff --git a/src/system/schemas/v1.10/pokemon-move.ts b/src/system/schemas/v1.10/pokemon-move.ts new file mode 100644 index 00000000000..9eabecfa152 --- /dev/null +++ b/src/system/schemas/v1.10/pokemon-move.ts @@ -0,0 +1,17 @@ +import { z } from "zod"; + + +/** + * Zod schema for a Pokémon move, as of version 1.10. + */ +export const Z$PokemonMove = z.object({ + moveId: z.number().int().min(0), // Move ID, default to 0 + ppUsed: z.number().int().default(0).catch(0), // PP used, default to 0 + ppUp: z.number().int().default(0).catch(0), // PP Up count, default to 0 + maxPpOverride: z.int().min(1).optional(), // Optional max PP override, can be null +}); + +// If necessary, we can define `transforms` which implicitly create an instance of the class. + +export type ParsedPokemonMove = z.output; + diff --git a/src/system/schemas/v1.10/pokemon-nature.ts b/src/system/schemas/v1.10/pokemon-nature.ts new file mode 100644 index 00000000000..beb17751c92 --- /dev/null +++ b/src/system/schemas/v1.10/pokemon-nature.ts @@ -0,0 +1,33 @@ +import { z } from "zod"; + +/** + * Zod schema for a Pokémon's nature, as of version 1.10 + * - `0`: Hardy, + * - `1`: Lonely, + * - `2`: Brave, + * - `3`: Adamant, + * - `4`: Naughty, + * - `5`: Bold, + * - `6`: Docile, + * - `7`: Relaxed, + * - `8`: Impish, + * - `9`: Lax, + * - `10`: Timid, + * - `11`: Hasty, + * - `12`: Serious, + * - `13`: Jolly, + * - `14`: Naive, + * - `15`: Modest, + * - `16`: Mild, + * - `17`: Quiet, + * - `18`: Bashful, + * - `19`: Rash, + * - `20`: Calm, + * - `21`: Gentle, + * - `22`: Sassy, + * - `23`: Careful, + * - `24`: Quirky + */ +export const NatureSchema = z.literal([ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, +]); diff --git a/src/system/schemas/v1.10/pokemon-stats.ts b/src/system/schemas/v1.10/pokemon-stats.ts new file mode 100644 index 00000000000..a52f5b9cb33 --- /dev/null +++ b/src/system/schemas/v1.10/pokemon-stats.ts @@ -0,0 +1,39 @@ +import { Z$PositiveInt } from "#schemas/common"; +import { z } from "zod"; + +/** + * Schema for a Pokémon's Individual Values (IVs), as of version 1.10 + * + * @remarks + * - Each IV is an integer between 0 and 31, inclusive. + * - Malformed values parse to 15 by default. + */ +export const IVSchema = z.int().min(0).max(31).catch(15); + +/** + * Schema for a set of 6 Pokémon IVs, as of version 1.10 + * + * @remarks + * Malformed IV sets default to [15, 15, 15, 15, 15, 15]. + */ +export const IVSetSchema = z + .tuple([IVSchema, IVSchema, IVSchema, IVSchema, IVSchema, IVSchema]) + .catch([15, 15, 15, 15, 15, 15]); + +/** + * Schema for a Pokémon's stats, as of version 1.10 + * - [0]: HP, + * - [1]: Attack, + * - [2]: Defense, + * - [3]: Special Attack, + * - [4]: Special Defense, + * - [5]: Speed + */ +export const statSetSchema = z.tuple([ + Z$PositiveInt, // HP + Z$PositiveInt, // Attack + Z$PositiveInt, // Defense + Z$PositiveInt, // Special Attack + Z$PositiveInt, // Special Defense + Z$PositiveInt, // Speed +]); diff --git a/src/system/schemas/v1.10/pokemon-summon-data.ts b/src/system/schemas/v1.10/pokemon-summon-data.ts new file mode 100644 index 00000000000..15a5be30f4a --- /dev/null +++ b/src/system/schemas/v1.10/pokemon-summon-data.ts @@ -0,0 +1,27 @@ +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]) +/** + * Zod schema for Pokémon summon data as of version 1.10. + * + */ +export const Z$PokemonSummonData = z.object({ + statStages: Z$StatStageSet.optional().catch(undefined), + moveQueue: z.array(Z$TurnMove).optional().catch(undefined), + + //#region Overrides for transform + + //#endregion Overrides for transform + +}); \ No newline at end of file diff --git a/src/system/schemas/v1.10/pokemon-type.ts b/src/system/schemas/v1.10/pokemon-type.ts new file mode 100644 index 00000000000..d9fc82bc08a --- /dev/null +++ b/src/system/schemas/v1.10/pokemon-type.ts @@ -0,0 +1,26 @@ +import { z } from "zod"; + +/** + * Schema for a Pokémon's type, as of version 1.10 + * - `-1`: Unknown (aka typeless), + * - `0`: Normal, + * - `1`: Fighting, + * - `2`: Flying, + * - `3`: Poison, + * - `4`: Ground, + * - `5`: Rock, + * - `6`: Bug, + * - `7`: Ghost, + * - `8`: Steel, + * - `9`: Fire, + * - `10`: Water, + * - `11`: Grass, + * - `12`: Electric, + * - `13`: Psychic, + * - `14`: Ice, + * - `15`: Dragon, + * - `16`: Dark, + * - `17`: Fairy + * - `18`: Stellar + */ +export const Z$PokemonType = z.literal([-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]); diff --git a/src/system/schemas/v1.10/session-save-data.ts b/src/system/schemas/v1.10/session-save-data.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/system/schemas/v1.10/status-effect.ts b/src/system/schemas/v1.10/status-effect.ts new file mode 100644 index 00000000000..2a3324ddd2e --- /dev/null +++ b/src/system/schemas/v1.10/status-effect.ts @@ -0,0 +1,31 @@ +// biome-ignore lint/correctness/noUnusedImports: used in tsdoc comment +import type { Status } from "#data/status-effect"; +import { StatusEffect } from "#enums/status-effect"; +import { z } from "zod"; +import { Z$NonNegativeInt, Z$PositiveInt } from "../common"; + +/** + * Zod schema for the {@linkcode StatusEffect} enum + * + * @remarks + * - `0`: NONE, + * - `1`: POISON, + * - `2`: TOXIC, + * - `3`: PARALYSIS, + * - `4`: SLEEP, + * - `5`: FREEZE, + * - `6`: BURN, + * - `7`: FAINT + */ +const Z$StatusEffect = z.int().min(StatusEffect.NONE).max(StatusEffect.FAINT).catch(StatusEffect.NONE); + +// Note: This does not validate that sleepTurnsRemaining exists when effect is SLEEP. +// This is game logic that should perhaps exist in the constructor. +/** + * Zod schema for the {@linkcode Status} class + */ +export const StatusSchema = z.object({ + effect: Z$StatusEffect, + toxicTurnCount: Z$NonNegativeInt.catch(0), + sleepTurnsRemaining: Z$PositiveInt.optional().catch(0), +}); diff --git a/src/system/schemas/v1.10/turn-move.ts b/src/system/schemas/v1.10/turn-move.ts new file mode 100644 index 00000000000..da64c695936 --- /dev/null +++ b/src/system/schemas/v1.10/turn-move.ts @@ -0,0 +1,25 @@ +// biome-ignore-start lint/correctness/noUnusedImports: used in tsdoc comment +import type { MoveUseMode } from "#enums/move-use-mode"; +import type { TurnMove } from "#types/turn-move"; +// biome-ignore-end lint/correctness/noUnusedImports: used in tsdoc comment + +import { Z$NonNegativeInt, Z$PositiveInt } from "#system/schemas/common"; +import { Z$BattlerIndex } from "#system/schemas/v1.10/battler-index"; +import { Z$MoveResult } from "#system/schemas/v1.10/move-result"; +import { z } from "zod"; + +/** + * Zod schema for the {@linkcode MoveUseMode} enum + */ +export const Z$MoveUseMode = z.literal([1, 2, 3, 4, 5]); + +/** + * Zod schema for `{@linkcode TurnMove} as of version 1.10. + */ +export const Z$TurnMove = z.object({ + move: Z$PositiveInt, + targets: z.array(Z$BattlerIndex), + useMode: Z$MoveUseMode, + result: Z$MoveResult.optional().catch(undefined), + turn: Z$NonNegativeInt.optional().catch(undefined) +}); diff --git a/tsconfig.json b/tsconfig.json index dcbf7456df8..780f33598c8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -48,6 +48,7 @@ "./system/version-migration/*.ts", "./system/*.ts" ], + "#schemas/*": ["./system/schemas/*.ts", "./system/schemas/v1.10/*.ts"], "#trainers/*": ["./data/trainers/*.ts"], "#types/*": ["./@types/helpers/*.ts", "./@types/*.ts", "./typings/phaser/*.ts"], "#ui/*": ["./ui/battle-info/*.ts", "./ui/settings/*.ts", "./ui/*.ts"],