This commit is contained in:
Sirz Benjie 2025-07-21 17:36:08 -06:00
parent e50ebaa815
commit 0c42f16fdd
No known key found for this signature in database
GPG Key ID: 4A524B4D196C759E
27 changed files with 916 additions and 2 deletions

View File

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

View File

@ -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: {}

165
sample_session_data.json Normal file
View File

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

61
scripts/zod-test.ts Normal file
View File

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

View File

@ -10,4 +10,4 @@ export interface TurnMove {
useMode: MoveUseMode;
result?: MoveResult;
turn?: number;
}
}

View File

@ -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<CustomPokemonData>): 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)
);
}
}
/**

View File

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

View File

@ -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+$/);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<typeof Z$PokemonBattleData>;

View File

@ -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<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>;
export type PreParsedPlayerPokemonData = z.input<typeof Z$PlayerPokemonData>;
export type ParsedPlayerPokemon = z.output<typeof Z$PlayerPokemonData>;
export type PreParsedEnemyPokemonData = z.input<typeof Z$EnemyPokemonData>;
export type ParsedEnemyPokemon = z.output<typeof Z$EnemyPokemonData>;

View File

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

View File

@ -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<typeof Z$PokemonMove>;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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