From c690b19d740be3c9ffac664b96f5fe4e49467eec Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Thu, 31 Jul 2025 23:29:35 -0600 Subject: [PATCH] [WIP] --- src/@types/battler-tags.ts | 51 ++++++++++- src/@types/helpers/type-helpers.ts | 30 ++++--- src/system/schemas/common.ts | 23 ++--- src/system/schemas/pokemon/battler-tag.ts | 102 ++++++++++------------ 4 files changed, 115 insertions(+), 91 deletions(-) diff --git a/src/@types/battler-tags.ts b/src/@types/battler-tags.ts index bdd69fdf76c..6bb25a18ad7 100644 --- a/src/@types/battler-tags.ts +++ b/src/@types/battler-tags.ts @@ -1,8 +1,16 @@ // biome-ignore-start lint/correctness/noUnusedImports: Used in a TSDoc comment -import type { AbilityBattlerTag, BattlerTagTypeMap, SerializableBattlerTag, TypeBoostTag } from "#data/battler-tags"; +import type { + AbilityBattlerTag, + BattlerTag, + BattlerTagTypeMap, + SerializableBattlerTag, + TypeBoostTag, +} from "#data/battler-tags"; import type { AbilityId } from "#enums/ability-id"; // biome-ignore-end lint/correctness/noUnusedImports: end import type { BattlerTagType } from "#enums/battler-tag-type"; +import type { MoveId } from "#enums/move-id"; +import type { MatchShape } from "#types/type-helpers"; /** * Subset of {@linkcode BattlerTagType}s that restrict the use of moves. @@ -97,6 +105,17 @@ export type CritStageBoostTagType = BattlerTagType.CRIT_BOOST | BattlerTagType.D /** Subset of {@linkcode BattlerTagType}s that remove one of the users' types */ export type RemovedTypeTagType = BattlerTagType.DOUBLE_SHOCKED | BattlerTagType.BURNED_UP; +/** + * Subset of {@linkcode BattlerTagType}s that provide type boosts + */ +export type TypeBoostTagType = BattlerTagType.FIRE_BOOST | BattlerTagType.CHARGED; + +/** Subset of {@linkcode BattlerTagType}s that boost the user's critical stage */ +export type CritStageBoostTagType = BattlerTagType.CRIT_BOOST | BattlerTagType.DRAGON_CHEER; + +/** Subset of {@linkcode BattlerTagType}s that remove one of the users' types */ +export type RemovedTypeTagType = BattlerTagType.DOUBLE_SHOCKED | BattlerTagType.BURNED_UP; + /** * Subset of {@linkcode BattlerTagType}s related to abilities that boost the highest stat. */ @@ -122,14 +141,38 @@ export type NonSerializableBattlerTagType = Exclude[0]; /** - * Subset of {@linkcode BattlerTagType}s whose associated `BattlerTag` adds a serializable `moveId` field + * Subset of {@linkcode BattlerTagType}s whose associated `BattlerTag` + * adds no additional fields that are serialized. */ -export type BattlerTagTypeWithMoveId = BattlerTagType.DISABLED | BattlerTagType.GORILLA_TACTICS | BattlerTagType.ENCORE; +export type BasicBattlerTag = keyof { + [K in SerializableBattlerTagType as MatchShape< + Parameters[0], + Parameters[0] + > extends never + ? never + : K]: any; +}; + +/** + * Subset of {@linkcode BattlerTagType}s whose associated `BattlerTag` adds a serializable `moveId` field + * (and no other serialized fields). + * + * @remarks + * Intended to be used to simplify type safety in the zod schemas. + */ +export type BattlerTagTypeWithMoveId = keyof { + [K in SerializableBattlerTagType as MatchShape< + Parameters[0], + Parameters[0] & { moveId: MoveId } + > extends never + ? never + : K]: any; +}; /** * Dummy, typescript-only declaration to ensure that diff --git a/src/@types/helpers/type-helpers.ts b/src/@types/helpers/type-helpers.ts index 68a82124a7a..f42232b75b4 100644 --- a/src/@types/helpers/type-helpers.ts +++ b/src/@types/helpers/type-helpers.ts @@ -44,20 +44,12 @@ export type InferKeys> = { }[keyof O]; /** - * 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. - * + * Utility type to obtain the values of a given object. \ + * Functions similar to `keyof E`, except producing the values instead of the keys. * @remarks - * 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 - * ``` + * This can be used to convert an `enum` interface produced by `typeof Enum` into the union type representing its members. */ -export type ObjectValues = T[keyof T]; +export type ObjectValues = E[keyof E]; /** * Type helper that matches any `Function` type. @@ -72,7 +64,6 @@ 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 = { @@ -103,3 +94,16 @@ export type AbstractConstructor = abstract new (...args: any[]) => T; export type CoerceNullPropertiesToUndefined = { [K in keyof T]: null extends T[K] ? Exclude | undefined : T[K]; }; + +/** + * Type helper that takes two types and ensures that their shapes match *exactly*. + * Resolves to T1 if T1 and T2 have the same shape, otherwise resolves to `never`. + * + * @remarks + * Meant to be used in the `as` clause of a mapped type + */ +export type MatchShape = T1 extends T2 + ? Exclude extends never + ? T1 + : never + : never; diff --git a/src/system/schemas/common.ts b/src/system/schemas/common.ts index 230639ce874..f9eb0021c64 100644 --- a/src/system/schemas/common.ts +++ b/src/system/schemas/common.ts @@ -5,29 +5,16 @@ 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(); +export const Z$PositiveInt = z.int().positive(); /** Reusable schema for a non-negative integer, equivalent to `z.int().nonnegative()`.*/ -export const Z$NonNegativeInt = /*@__PURE__*/ z - .int() - .nonnegative(); +export const Z$NonNegativeInt = z.int().nonnegative(); /** Reusable schema for a boolean that coerces non-boolean inputs to `false` */ -export const Z$BoolCatchToFalse = /*@__PURE__*/ z - .boolean() - .catch(false); +export const Z$BoolCatchToFalse = z.boolean().catch(false); /** Reusable schema for a positive number, equivalent to `z.number().positive()`. */ -export const Z$PositiveNumber = /*@__PURE__*/ z - .number() - .positive() - .catch(0); +export const Z$PositiveNumber = z.number().positive().catch(0); /** 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); +export const Z$OptionalNonNegativeIntCatchToUndef = z.int().nonnegative().optional().catch(undefined); diff --git a/src/system/schemas/pokemon/battler-tag.ts b/src/system/schemas/pokemon/battler-tag.ts index 4203b054b18..fae75821f28 100644 --- a/src/system/schemas/pokemon/battler-tag.ts +++ b/src/system/schemas/pokemon/battler-tag.ts @@ -1,12 +1,9 @@ +import { loadBattlerTag } from "#data/battler-tags"; 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 { BasicBattlerTag, BattlerTagTypeWithMoveId, HighestStatBoostTagType } from "#types/battler-tags"; import type { DiscriminatedUnionFake } from "#types/schema-helpers"; import { z } from "zod"; @@ -15,22 +12,17 @@ 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; - /** * Zod enum of {@linkcode BattlerTagType}s whose associated `BattlerTag` adds no * additional fields that are serialized. - * */ -const Z$BaseBattlerTags = /** @__PURE__ */ z.literal([ +const BasicBattlerTag = 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, @@ -46,12 +38,9 @@ const Z$BaseBattlerTags = /** @__PURE__ */ z.literal([ 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, @@ -60,7 +49,6 @@ const Z$BaseBattlerTags = /** @__PURE__ */ z.literal([ BattlerTagType.CRIT_BOOST, BattlerTagType.ALWAYS_CRIT, BattlerTagType.IGNORE_ACCURACY, - BattlerTagType.BYPASS_SLEEP, BattlerTagType.IGNORE_FLYING, BattlerTagType.SALT_CURED, BattlerTagType.CURSED, @@ -70,24 +58,19 @@ const Z$BaseBattlerTags = /** @__PURE__ */ z.literal([ 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, @@ -95,26 +78,10 @@ const Z$BaseBattlerTags = /** @__PURE__ */ z.literal([ BattlerTagType.IMPRISON, BattlerTagType.SYRUP_BOMB, BattlerTagType.TELEKINESIS, - BattlerTagType.COMMANDED, BattlerTagType.GRUDGE, -] satisfies SerializableBattlerTagType[]); +] satisfies BasicBattlerTag[]); -/** - * 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 = /** @__PURE__ */ z.literal([0, 1, 2, 3, 4, 5, 6, 7, 8]); - -const Z$BaseBattlerTag = /** @__PURE__ */ z.object({ +const Z$BaseBattlerTag = z.object({ turnCount: Z$PositiveInt, // Source move can be `none` for tags not applied by move, so allow `0` here. sourceMove: Z$NonNegativeInt.optional().catch(undefined), @@ -126,41 +93,56 @@ const Z$BaseTagWithMoveId = z.object({ 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" ->; +/** + * Zod schema for a basic {@linkcode BattlerTag} (i.e., one that does not have + * additional fields beyond the base `BattlerTag`). + */ +const Z$PlainBattlerTag = z.object({ + ...Z$BaseBattlerTag.shape, + tagType: BasicBattlerTag, +}) as DiscriminatedUnionFake; -const Z$SeedTag = /** @__PURE__ */ z.object({ +/** Subset of battler tags that have a moveID field */ +const Z$TagWithMoveId = z.object({ + ...Z$BaseTagWithMoveId.shape, + tagType: z.literal([ + BattlerTagType.DISABLED, + BattlerTagType.GORILLA_TACTICS, + BattlerTagType.ENCORE, + ] satisfies BattlerTagTypeWithMoveId[]), + moveId: Z$PositiveInt, +}) as DiscriminatedUnionFake; + +/** + * Zod schema for {@linkcode SeedTag} as of version 1.10. + */ +const Z$SeedTag = z.object({ ...Z$BaseBattlerTag.shape, tagType: z.literal(BattlerTagType.SEEDED), sourceIndex: Z$BattlerIndex, }); -const Z$BaseHighestStatBoostTag = /** @__PURE__ */ z.object({ +/** + * Zod schema for {@linkcode HighestStatBoostTag} as of version 1.10. + */ +const Z$BaseHighestStatBoostTag = z.object({ ...Z$BaseBattlerTag.shape, stat: Z$Stat, multiplier: z.number(), }); -const Z$HighestStatBoostTag = /** @__PURE__ */ z.object({ +const Z$HighestStatBoostTag = z.object({ ...Z$BaseHighestStatBoostTag.shape, tagType: z.literal([BattlerTagType.QUARK_DRIVE, BattlerTagType.PROTOSYNTHESIS] satisfies HighestStatBoostTagType[]), }) as DiscriminatedUnionFake; -const Z$CommandedTag = /** @__PURE__ */ z.object({ +const Z$CommandedTag = z.object({ ...Z$BaseBattlerTag.shape, tagType: z.literal(BattlerTagType.COMMANDED), tatsugiriFormKey: z.string().catch("curly"), }); -const Z$StockpilingTag = /** @__PURE__ */ z.object({ +const Z$StockpilingTag = z.object({ ...Z$BaseBattlerTag.shape, tagType: z.literal(BattlerTagType.STOCKPILING), stockpiledCount: Z$PositiveInt.catch(1), @@ -170,14 +152,22 @@ const Z$StockpilingTag = /** @__PURE__ */ z.object({ }), }); -const Z$AutotomizedTag = /** @__PURE__ */ z.object({ +const Z$AutotomizedTag = z.object({ ...Z$BaseBattlerTag.shape, tagType: z.literal(BattlerTagType.AUTOTOMIZED), autotomizeCount: Z$PositiveInt.catch(1), }); -const Z$SubstituteTag = /** @__PURE__ */ z.object({ +const Z$SubstituteTag = z.object({ ...Z$BaseBattlerTag.shape, tagType: z.literal(BattlerTagType.SUBSTITUTE), - hp: Z$PositiveInt, + // hp: Z$PositiveInt, }); + +export const Z$BattlerTag = z.discriminatedUnion("tagType", [Z$SubstituteTag]); + +declare const t: any; + +const o = Z$BattlerTag.parse(t); + +const r = loadBattlerTag(Z$SubstituteTag.parse(t));