This commit is contained in:
Sirz Benjie 2025-07-31 17:31:56 -06:00
parent 5321f00636
commit 2d3002ce2d
No known key found for this signature in database
GPG Key ID: 4A524B4D196C759E
6 changed files with 353 additions and 116 deletions

View File

@ -126,6 +126,11 @@ export type BattlerTagTypeData = Parameters<
}]["loadTag"]
>[0];
/**
* Subset of {@linkcode BattlerTagType}s whose associated `BattlerTag` adds a serializable `moveId` field
*/
export type BattlerTagTypeWithMoveId = BattlerTagType.DISABLED | BattlerTagType.GORILLA_TACTICS | BattlerTagType.ENCORE;
/**
* Dummy, typescript-only declaration to ensure that
* {@linkcode BattlerTagTypeMap} has an entry for all `BattlerTagType`s.

View File

@ -0,0 +1,48 @@
import type { ObjectValues } from "#types/type-helpers";
import type { z } from "zod";
/**
* Fake a discriminated union type for Zod Schemas.
* In essence, allows a particular field that is really a union
* to coerce to a zod type that emits a UNION of schemas.
*
* @typeParam Choices - The set of enum values that the union can take.
* @typeParam BaseZodSchema - The base zod schema to extend.
* @typeParam FieldName - The name of the field that acts as the discriminator. **Must be a string literal**
*
* @remarks
* There are times where we want to allow a field in the zod schema to take one of
* a set of literal values, but we want to treat it as a discriminated union.
*
* For instance, if we have a zod schema like this:
* z.object({
* field: z.literal(["Foo", "Bar"])
* })
*
* then pass it into some function which expects field to be either "Foo" or "Bar",
* then it would not work. This is because
* { field: "Foo" | "Bar" }
* would not be considered compatible with
* { field: "Foo" } | { field: "Bar"}
*
* While it is possible to make use of Zod's discriminated union, this
* ends up defining way too many additional types, which is not readable,
* and is substantially harder to maintain.
*
* Instead, this type helper can be used as a type assertion on the actual zod schema.
* Note that it *does* require defining a base schema separately, which does not isnclude
* the discriminator field.
*
* This is a hacky way to get around typescript's limitations.
*/
export type DiscriminatedUnionFake<
Choices extends string | number,
BaseZodSchema extends Record<string, z.ZodType>,
FieldName extends string,
> = ObjectValues<{
[Choice in Choices]: z.ZodObject<
BaseZodSchema & {
[F in FieldName]: z.ZodLiteral<Choice>;
}
>;
}>;

View File

@ -44,12 +44,20 @@ export type InferKeys<O extends object, V extends ObjectValues<O>> = {
}[keyof O];
/**
* Utility type to obtain the values of a given object. \
* Functions similar to `keyof E`, except producing the values instead of the keys.
* Type helper to construct a union type of the type of each field in an object.
*
* @typeParam T - The type of the object to extract values from.
*
* @remarks
* This can be used to convert an `enum` interface produced by `typeof Enum` into the union type representing its members.
* Can be used to convert an `enum` interface produced by `typeof Enum` into the union type representing its members.
*
* @example
* ```ts
* type oneThruThree = ObjectValues<{ a: 1, b: 2, c: 3 }>
* // ^? 1 | 2 | 3
* ```
*/
export type ObjectValues<E extends object> = E[keyof E];
export type ObjectValues<T extends object> = T[keyof T];
/**
* Type helper that matches any `Function` type.
@ -64,6 +72,7 @@ export type AnyFn = (...args: any[]) => any;
* Useful to produce a type that is roughly the same as the type of `{... obj}`, where `obj` is an instance of `T`.
* A couple of differences:
* - Private and protected properties are not included.
* - Accessors with getters *are* included
* - Nested properties are not recursively extracted. For this, use {@linkcode NonFunctionPropertiesRecursive}
*/
export type NonFunctionProperties<T> = {

View File

@ -0,0 +1,111 @@
// biome-ignore-start lint/correctness/noUnusedImports: used in tsdoc comment
import { type ArenaTrapTag, loadArenaTag, type SerializableArenaTag } from "#data/arena-tag";
import type { ArenaTagSide } from "#enums/arena-tag-side";
import { ArenaTagType } from "#enums/arena-tag-type";
// biome-ignore-end lint/correctness/noUnusedImports: end
import { Z$NonNegativeInt, Z$PositiveInt } from "#system/schemas/common";
import type { ArenaTrapTagType, SerializableArenaTagType } from "#types/arena-tags";
import type { DiscriminatedUnionFake } from "#types/schema-helpers";
import { z } from "zod";
/**
* Zod schema for {@linkcode ArenaTagSide} as of version 1.10
*
* @remarks
* - `0`: BOTH
* - `1`: PLAYER
* - `2`: ENEMY
*/
export const Z$ArenaTagSide = z.literal([0, 1, 2]);
/**
* The base shape of an arena tag, consisting of all of the fields other than
* Zod schema for {@linkcode SerializableArenaTag} as of version 1.10
*/
const Z$BaseArenaTag = z.object({
turnCount: z.int(),
sourceMove: Z$NonNegativeInt.optional().catch(undefined),
sourceId: z.int().or(z.undefined()).catch(undefined),
side: Z$ArenaTagSide,
});
// #region typescript-hackery to extract out the proper zod schema type
/**
* Arena tag type that has no extra additional fields.
*/
type BasicArenaTag =
| Exclude<SerializableArenaTagType, ArenaTagType.NEUTRALIZING_GAS | ArenaTrapTagType>
| ArenaTagType.NONE;
//#endregion: typescript-hackery
/**
* Zod enum for arena tags with no additional properties.
* If a new ArenaTagType that has no additional properties is added,
* this MUST be updated to include it.
*/
const Z$BaseArenaTagEnum = z.literal([
ArenaTagType.NONE,
ArenaTagType.MUD_SPORT,
ArenaTagType.WATER_SPORT,
ArenaTagType.MIST,
ArenaTagType.TRICK_ROOM,
ArenaTagType.GRAVITY,
ArenaTagType.REFLECT,
ArenaTagType.LIGHT_SCREEN,
ArenaTagType.AURORA_VEIL,
ArenaTagType.TAILWIND,
ArenaTagType.HAPPY_HOUR,
ArenaTagType.SAFEGUARD,
ArenaTagType.NO_CRIT,
ArenaTagType.FIRE_GRASS_PLEDGE,
ArenaTagType.WATER_FIRE_PLEDGE,
ArenaTagType.GRASS_WATER_PLEDGE,
ArenaTagType.FAIRY_LOCK,
]) satisfies z.ZodLiteral<BasicArenaTag | ArenaTagType.NONE>;
/**
* Zod schema for the subset of {@linkcode ArenaTagType}s
* that add no additional serializable fields.
*/
const Z$PlainArenaTag = z.object({
...Z$BaseArenaTag.shape,
tagType: Z$BaseArenaTagEnum,
}) as DiscriminatedUnionFake<BasicArenaTag, typeof Z$BaseArenaTag.shape, "tagType">;
const Z$BaseTrapTag = /** __@PURE__ */ z.object({
...Z$BaseArenaTag.shape,
layers: z.int().min(1).max(3).catch(1),
maxLayers: z.int().min(1).max(3),
});
/**
* Zod schema for {@linkcode ArenaTrapTag} as of version 1.10
*/
const Z$ArenaTrapTag = /** __@PURE__ */ z.object({
...Z$BaseTrapTag.shape,
tagType: z.literal([
ArenaTagType.STICKY_WEB,
ArenaTagType.SPIKES,
ArenaTagType.TOXIC_SPIKES,
ArenaTagType.STEALTH_ROCK,
ArenaTagType.IMPRISON,
] satisfies ArenaTrapTagType[]),
}) as DiscriminatedUnionFake<ArenaTrapTagType, typeof Z$BaseTrapTag.shape, "tagType">;
/**
* Zod schema for {@linkcode ArenaTagType.NEUTRALIZING_GAS} as of version 1.10
*/
const Z$SuppressAbilitiesTag = /** __@PURE__ */ z.object({
...Z$BaseArenaTag.shape,
tagType: z.literal(ArenaTagType.NEUTRALIZING_GAS),
sourceCount: Z$PositiveInt,
});
/**
* Zod schema for {@linkcode SerializableArenaTag}s as of version 1.10,
* also permitting "NoneTag".
*/
export const Z$ArenaTag = z.discriminatedUnion("tagType", [Z$ArenaTrapTag, Z$SuppressAbilitiesTag, Z$PlainArenaTag]);

View File

@ -1,5 +1,13 @@
import { BattlerTagType } from "#enums/battler-tag-type";
import { Z$NonNegativeInt, Z$PositiveInt } from "#system/schemas/common";
import { Z$BattlerIndex } from "#system/schemas/pokemon/battler-index";
import { Z$Stat } from "#system/schemas/pokemon/pokemon-stats";
import type {
BattlerTagTypeWithMoveId,
HighestStatBoostTagType,
SerializableBattlerTagType,
} from "#types/battler-tags";
import type { DiscriminatedUnionFake } from "#types/schema-helpers";
import { z } from "zod";
/*
@ -7,107 +15,89 @@ Schemas for battler tags are a bit more cumbersome,
as we need to have schemas for each subclass that has a different shape.
*/
type BasicBattlerTag = Exclude<SerializableBattlerTagType, BattlerTagTypeWithMoveId | HighestStatBoostTagType>;
/**
* Zod schema for the {@linkcode BattlerTagType} enum as of version 1.10
* Zod enum of {@linkcode BattlerTagType}s whose associated `BattlerTag` adds no
* additional fields that are serialized.
*
*/
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",
]);
const Z$BaseBattlerTags = /** @__PURE__ */ z.literal([
BattlerTagType.RECHARGING,
BattlerTagType.CONFUSED,
BattlerTagType.INFATUATED,
BattlerTagType.SEEDED,
BattlerTagType.NIGHTMARE,
BattlerTagType.FRENZY,
BattlerTagType.CHARGING,
BattlerTagType.ENCORE,
BattlerTagType.INGRAIN,
BattlerTagType.OCTOLOCK,
BattlerTagType.AQUA_RING,
BattlerTagType.DROWSY,
BattlerTagType.TRAPPED,
BattlerTagType.BIND,
BattlerTagType.WRAP,
BattlerTagType.FIRE_SPIN,
BattlerTagType.WHIRLPOOL,
BattlerTagType.CLAMP,
BattlerTagType.SAND_TOMB,
BattlerTagType.MAGMA_STORM,
BattlerTagType.SNAP_TRAP,
BattlerTagType.THUNDER_CAGE,
BattlerTagType.INFESTATION,
BattlerTagType.STURDY,
BattlerTagType.PERISH_SONG,
BattlerTagType.TRUANT,
BattlerTagType.SLOW_START,
BattlerTagType.PROTOSYNTHESIS,
BattlerTagType.QUARK_DRIVE,
BattlerTagType.FLYING,
BattlerTagType.UNDERGROUND,
BattlerTagType.UNDERWATER,
BattlerTagType.HIDDEN,
BattlerTagType.FIRE_BOOST,
BattlerTagType.CRIT_BOOST,
BattlerTagType.ALWAYS_CRIT,
BattlerTagType.IGNORE_ACCURACY,
BattlerTagType.BYPASS_SLEEP,
BattlerTagType.IGNORE_FLYING,
BattlerTagType.SALT_CURED,
BattlerTagType.CURSED,
BattlerTagType.CHARGED,
BattlerTagType.FLOATING,
BattlerTagType.MINIMIZED,
BattlerTagType.DESTINY_BOND,
BattlerTagType.ICE_FACE,
BattlerTagType.DISGUISE,
BattlerTagType.STOCKPILING,
BattlerTagType.RECEIVE_DOUBLE_DAMAGE,
BattlerTagType.ALWAYS_GET_HIT,
BattlerTagType.DISABLED,
BattlerTagType.SUBSTITUTE,
BattlerTagType.IGNORE_GHOST,
BattlerTagType.IGNORE_DARK,
BattlerTagType.GULP_MISSILE_ARROKUDA,
BattlerTagType.GULP_MISSILE_PIKACHU,
BattlerTagType.DRAGON_CHEER,
BattlerTagType.NO_RETREAT,
BattlerTagType.GORILLA_TACTICS,
BattlerTagType.UNBURDEN,
BattlerTagType.THROAT_CHOPPED,
BattlerTagType.TAR_SHOT,
BattlerTagType.BURNED_UP,
BattlerTagType.DOUBLE_SHOCKED,
BattlerTagType.AUTOTOMIZED,
BattlerTagType.POWER_TRICK,
BattlerTagType.HEAL_BLOCK,
BattlerTagType.TORMENT,
BattlerTagType.TAUNT,
BattlerTagType.IMPRISON,
BattlerTagType.SYRUP_BOMB,
BattlerTagType.TELEKINESIS,
BattlerTagType.COMMANDED,
BattlerTagType.GRUDGE,
] satisfies SerializableBattlerTagType[]);
/**
* Zod schema for {@linkcode BattlerTagLapseType} as of version 1.10
@ -122,21 +112,72 @@ const Z$BattlerTagType = z.literal([
* - `7`: After Hit
* - `8`: Custom
*/
const Z$BattlerTagLapseType = z.literal([0, 1, 2, 3, 4, 5, 6, 7, 8]);
const Z$BattlerTagLapseType = /** @__PURE__ */ 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),
const Z$BaseBattlerTag = /** @__PURE__ */ z.object({
turnCount: Z$PositiveInt,
// Source move can be `none` for tags not applied by move, so allow `0` here.
sourceMove: Z$NonNegativeInt,
sourceMove: Z$NonNegativeInt.optional().catch(undefined),
sourceId: z.int().optional().catch(undefined),
isBatonPassable: z.boolean(),
});
export const Z$SeedTag = z.object({
...Z$BattlerTag.shape,
seedType: z.literal("SEEDED"),
const Z$BaseTagWithMoveId = z.object({
...Z$BaseBattlerTag.shape,
moveId: Z$PositiveInt,
});
/** Subset of battler tags that have a moveID field */
const Z$TagWithMoveId = /** @__PURE__ */ z.object({
...Z$BaseTagWithMoveId.shape,
tagType: z.literal([BattlerTagType.DISABLED, BattlerTagType.GORILLA_TACTICS, BattlerTagType.ENCORE]),
moveId: Z$PositiveInt,
}) as DiscriminatedUnionFake<
BattlerTagType.DISABLED | BattlerTagType.GORILLA_TACTICS | BattlerTagType.ENCORE,
typeof Z$TagWithMoveId.shape,
"tagType"
>;
const Z$SeedTag = /** @__PURE__ */ z.object({
...Z$BaseBattlerTag.shape,
tagType: z.literal(BattlerTagType.SEEDED),
sourceIndex: Z$BattlerIndex,
});
const Z$BaseHighestStatBoostTag = /** @__PURE__ */ z.object({
...Z$BaseBattlerTag.shape,
stat: Z$Stat,
multiplier: z.number(),
});
const Z$HighestStatBoostTag = /** @__PURE__ */ z.object({
...Z$BaseHighestStatBoostTag.shape,
tagType: z.literal([BattlerTagType.QUARK_DRIVE, BattlerTagType.PROTOSYNTHESIS] satisfies HighestStatBoostTagType[]),
}) as DiscriminatedUnionFake<HighestStatBoostTagType, typeof Z$HighestStatBoostTag.shape, "tagType">;
const Z$CommandedTag = /** @__PURE__ */ z.object({
...Z$BaseBattlerTag.shape,
tagType: z.literal(BattlerTagType.COMMANDED),
tatsugiriFormKey: z.string().catch("curly"),
});
const Z$StockpilingTag = /** @__PURE__ */ z.object({
...Z$BaseBattlerTag.shape,
tagType: z.literal(BattlerTagType.STOCKPILING),
stockpiledCount: Z$PositiveInt.catch(1),
statChangeCounts: z.object({
[2]: z.int().min(-6).max(6).catch(0), // Defense
[4]: z.int().min(-6).max(6).catch(0), // Special Defense
}),
});
const Z$AutotomizedTag = /** @__PURE__ */ z.object({
...Z$BaseBattlerTag.shape,
tagType: z.literal(BattlerTagType.AUTOTOMIZED),
autotomizeCount: Z$PositiveInt.catch(1),
});
const Z$SubstituteTag = /** @__PURE__ */ z.object({
...Z$BaseBattlerTag.shape,
tagType: z.literal(BattlerTagType.SUBSTITUTE),
hp: Z$PositiveInt,
});

View File

@ -1,6 +1,23 @@
// biome-ignore lint/correctness/noUnusedImports: Used in a tsdoc comment
import type { Stat } from "#enums/stat";
import { Z$PositiveInt } from "#schemas/common";
import { z } from "zod";
/**
* Schema for {@linkcode Stat}, as of version 1.10
*
* @remarks
* - `0`: Hp
* - `1`: Attack
* - `2`: Defense
* - `3`: Special Attack
* - `4`: Special Defense
* - `5`: Speed
* - `6`: Accuracy
* - `7`: Evasion
*/
export const Z$Stat = /** @__PURE__ */ z.literal([0, 1, 2, 3, 4, 5, 6, 7]);
/**
* Schema for a Pokémon's Individual Values (IVs), as of version 1.10
*
@ -8,7 +25,11 @@ import { z } from "zod";
* - Each IV is an integer between 0 and 31, inclusive.
* - Malformed values parse to 15 by default.
*/
export const Z$IV = z.int().min(0).max(31).catch(15);
export const Z$IV = /** @__PURE__ */ z
.int()
.min(0)
.max(31)
.catch(15);
/**
* Schema for a set of 6 Pokémon IVs, as of version 1.10
@ -16,7 +37,9 @@ export const Z$IV = z.int().min(0).max(31).catch(15);
* @remarks
* Malformed IV sets default to [15, 15, 15, 15, 15, 15].
*/
export const Z$IVSet = z.tuple([Z$IV, Z$IV, Z$IV, Z$IV, Z$IV, Z$IV]).catch([15, 15, 15, 15, 15, 15]);
export const Z$IVSet = /** @__PURE__ */ z
.tuple([Z$IV, Z$IV, Z$IV, Z$IV, Z$IV, Z$IV])
.catch([15, 15, 15, 15, 15, 15]);
/**
* Schema for a Pokémon's stats, as of version 1.10
@ -27,7 +50,7 @@ export const Z$IVSet = z.tuple([Z$IV, Z$IV, Z$IV, Z$IV, Z$IV, Z$IV]).catch([15,
* - [4]: Special Defense,
* - [5]: Speed
*/
export const Z$StatSet = z.tuple([
export const Z$StatSet = /** @__PURE__ */ z.tuple([
Z$PositiveInt, // HP
Z$PositiveInt, // Attack
Z$PositiveInt, // Defense