diff --git a/src/@types/arena-tags.ts b/src/@types/arena-tags.ts index 9ccccb1df5c..cfdce4350da 100644 --- a/src/@types/arena-tags.ts +++ b/src/@types/arena-tags.ts @@ -1,6 +1,5 @@ import type { ArenaTagTypeMap } from "#data/arena-tag"; import type { ArenaTagType } from "#enums/arena-tag-type"; -import type { NonFunctionProperties } from "#types/type-helpers"; /** Subset of {@linkcode ArenaTagType}s that apply some negative effect to pokemon that switch in ({@link https://bulbapedia.bulbagarden.net/wiki/List_of_moves_that_cause_entry_hazards#List_of_traps | entry hazards} and Imprison. */ export type ArenaTrapTagType = @@ -30,13 +29,13 @@ export type NonSerializableArenaTagType = ArenaTagType.NONE | TurnProtectArenaTa export type SerializableArenaTagType = Exclude; /** - * Type-safe representation of the serializable data of an ArenaTag + * Type-safe representation of an arbitrary, serialized Arena Tag */ -export type ArenaTagTypeData = NonFunctionProperties< +export type ArenaTagTypeData = Parameters< ArenaTagTypeMap[keyof { [K in keyof ArenaTagTypeMap as K extends SerializableArenaTagType ? K : never]: ArenaTagTypeMap[K]; - }] ->; + }]["loadTag"] +>[0]; /** Dummy, typescript-only declaration to ensure that * {@linkcode ArenaTagTypeMap} has a map for all ArenaTagTypes. diff --git a/src/@types/battler-tags.ts b/src/@types/battler-tags.ts index d1ff93e0400..0057280e4e5 100644 --- a/src/@types/battler-tags.ts +++ b/src/@types/battler-tags.ts @@ -108,6 +108,15 @@ export type SerializableBattlerTagType = keyof { */ export type NonSerializableBattlerTagType = Exclude; +/** + * Type-safe representation of an arbitrary, serialized Battler Tag + */ +export type BattlerTagTypeData = Parameters< + BattlerTagTypeMap[keyof { + [K in keyof BattlerTagTypeMap as K extends SerializableBattlerTagType ? K : never]: BattlerTagTypeMap[K]; + }]["loadTag"] +>[0]; + /** * Dummy, typescript-only declaration to ensure that * {@linkcode BattlerTagTypeMap} has an entry for all `BattlerTagType`s. diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 2c4c8a04282..b24fd17e0e3 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -26,38 +26,58 @@ import type { ArenaTrapTagType, SerializableArenaTagType, } from "#types/arena-tags"; -import type { Mutable, NonFunctionProperties } from "#types/type-helpers"; +import type { Mutable } from "#types/type-helpers"; import { BooleanHolder, isNullOrUndefined, NumberHolder, toDmgValue } from "#utils/common"; import i18next from "i18next"; -/* -ArenaTags are are meant for effects that are tied to the arena (as opposed to a specific pokemon). -Examples include (but are not limited to) -- Cross-turn effects that persist even if the user/target switches out, such as Wish, Future Sight, and Happy Hour -- Effects that are applied to a specific side of the field, such as Crafty Shield, Reflect, and Spikes -- Field-Effects, like Gravity and Trick Room - -Any arena tag that persists across turns *must* extend from `SerializableArenaTag` in the class definition signature. - -Serializable ArenaTags have strict rules for their fields. -These rules ensure that only the data necessary to reconstruct the tag is serialized, and that the -session loader is able to deserialize saved tags correctly. - -If the data is static (i.e. it is always the same for all instances of the class, such as the -type that is weakened by Mud Sport/Water Sport), then it must not be defined as a field, and must -instead be defined as a getter. -A static property is also acceptable, though static properties are less ergonomic with inheritance. - -If the data is mutable (i.e. it can change over the course of the tag's lifetime), then it *must* -be defined as a field, and it must be set in the `loadTag` method. -Such fields cannot be marked as `private/protected`, as if they were, typescript would omit them from -types that are based off of the class, namely, `ArenaTagTypeData`. It is preferrable to trade the -type-safety of private/protected fields for the type safety when deserializing arena tags from save data. - -For data that is mutable only within a turn (e.g. SuppressAbilitiesTag's beingRemoved field), -where it does not make sense to be serialized, the field should use ES2020's private field syntax (a `#` prepended to the field name). -If the field should be accessible outside of the class, then a public getter should be used. -*/ +/** + * @module + * ArenaTags are are meant for effects that are tied to the arena (as opposed to a specific pokemon). + * Examples include (but are not limited to) + * - Cross-turn effects that persist even if the user/target switches out, such as Wish, Future Sight, and Happy Hour + * - Effects that are applied to a specific side of the field, such as Crafty Shield, Reflect, and Spikes + * - Field-Effects, like Gravity and Trick Room + * + * Any arena tag that persists across turns *must* extend from `SerializableArenaTag` in the class definition signature. + * + * Serializable ArenaTags have strict rules for their fields. + * These rules ensure that only the data necessary to reconstruct the tag is serialized, and that the + * session loader is able to deserialize saved tags correctly. + * + * If the data is static (i.e. it is always the same for all instances of the class, such as the + * type that is weakened by Mud Sport/Water Sport), then it must not be defined as a field, and must + * instead be defined as a getter. + * A static property is also acceptable, though static properties are less ergonomic with inheritance. + * + * If the data is mutable (i.e. it can change over the course of the tag's lifetime), then it *must* + * be defined as a field, and it must be set in the `loadTag` method. + * Such fields cannot be marked as `private`/`protected`; if they were, Typescript would omit them from + * types that are based off of the class, namely, `ArenaTagTypeData`. It is preferrable to trade the + * type-safety of private/protected fields for the type safety when deserializing arena tags from save data. + * + * For data that is mutable only within a turn (e.g. SuppressAbilitiesTag's beingRemoved field), + * where it does not make sense to be serialized, the field should use ES2020's + * [private field syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_elements#private_fields). + * If the field should be accessible outside of the class, then a public getter should be used. + * + * If any new serializable fields *are* added, then the class *must* override the + * `loadTag` method to set the new fields. Its signature *must* match the example below, + * ``` + * class ExampleTag extends SerializableArenaTag { + * // Example, if we add 2 new fields that should be serialized: + * public a: string; + * public b: number; + * // Then we must also define a loadTag method with one of the following signatures + * public override loadTag(source: BaseArenaTag & Pick(source: BaseArenaTag & Pick): void; + * public override loadTag(source: NonFunctionProperties): void; + * } + * ``` + * Notes + * - If the class has any subclasses, then the second form of `loadTag` *must* be used. + * - The third form *must not* be used if the class has any getters, as typescript would expect such fields to be + * present in `source`. + */ /** Interface containing the serializable fields of ArenaTagData. */ interface BaseArenaTag { @@ -141,9 +161,9 @@ export abstract class ArenaTag implements BaseArenaTag { /** * When given a arena tag or json representing one, load the data for it. * This is meant to be inherited from by any arena tag with custom attributes - * @param source - The {@linkcode BaseArenaTag} being loaded + * @param source - The arena tag being loaded */ - loadTag(source: BaseArenaTag): void { + loadTag(source: BaseArenaTag & Pick): void { this.turnCount = source.turnCount; this.sourceMove = source.sourceMove; this.sourceId = source.sourceId; @@ -646,7 +666,9 @@ class WishTag extends SerializableArenaTag { } } - override loadTag(source: NonFunctionProperties): void { + public override loadTag( + source: BaseArenaTag & Pick, + ): void { super.loadTag(source); (this as Mutable).battlerIndex = source.battlerIndex; (this as Mutable).healHp = source.healHp; @@ -813,7 +835,7 @@ export abstract class ArenaTrapTag extends SerializableArenaTag { : Phaser.Math.Linear(0, 1 / Math.pow(2, this.layers), Math.min(pokemon.getHpRatio(), 0.5) * 2); } - loadTag(source: NonFunctionProperties): void { + public loadTag(source: BaseArenaTag & Pick): void { super.loadTag(source); this.layers = source.layers; this.maxLayers = source.maxLayers; @@ -1581,7 +1603,7 @@ export class SuppressAbilitiesTag extends SerializableArenaTag { this.#beingRemoved = false; } - public override loadTag(source: NonFunctionProperties): void { + public override loadTag(source: BaseArenaTag & Pick): void { super.loadTag(source); (this as Mutable).sourceCount = source.sourceCount; } diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 9dfd43eccb3..e21065c184f 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -69,6 +69,24 @@ import { BooleanHolder, coerceArray, getFrameMs, isNullOrUndefined, NumberHolder * be declared as a public readonly propety. Then, in the `loadTag` method (or any method inside the class that needs to adjust the property) * use `(this as Mutable).propertyName = value;` * These rules ensure that Typescript is aware of the shape of the serialized version of the class. + * + * If any new serializable fields *are* added, then the class *must* override the + * `loadTag` method to set the new fields. Its signature *must* match the example below: + * ``` + * class ExampleTag extends SerializableBattlerTag { + * // Example, if we add 2 new fields that should be serialized: + * public a: string; + * public b: number; + * // Then we must also define a loadTag method with one of the following signatures + * public override loadTag(source: BaseBattlerTag & Pick(source: BaseBattlerTag & Pick): void; + * public override loadTag(source: NonFunctionProperties): void; + * } + * ``` + * Notes + * - If the class has any subclasses, then the second form of `loadTag` *must* be used. + * - The third form *must not* be used if the class has any getters, as typescript would expect such fields to be + * present in `source`. */ /** Interface containing the serializable fields of BattlerTag */ @@ -168,7 +186,7 @@ export class BattlerTag implements BaseBattlerTag { * Should be inherited from by any battler tag with custom attributes. * @param source - An object containing the fields needed to reconstruct this tag. */ - loadTag(source: BaseBattlerTag): void { + public loadTag(source: BaseBattlerTag & Pick): void { this.turnCount = source.turnCount; this.sourceMove = source.sourceMove; this.sourceId = source.sourceId; @@ -386,7 +404,7 @@ export class DisabledTag extends MoveRestrictionBattlerTag { }); } - override loadTag(source: NonFunctionProperties): void { + public override loadTag(source: BaseBattlerTag & Pick): void { super.loadTag(source); (this as Mutable).moveId = source.moveId; } @@ -437,7 +455,7 @@ export class GorillaTacticsTag extends MoveRestrictionBattlerTag { * Loads the Gorilla Tactics Battler Tag along with its unique class variable moveId * @param source - Object containing the fields needed to reconstruct this tag. */ - override loadTag(source: NonFunctionProperties): void { + public override loadTag(source: BaseBattlerTag & Pick): void { super.loadTag(source); (this as Mutable).moveId = source.moveId; } @@ -971,6 +989,12 @@ export class InfatuatedTag extends SerializableBattlerTag { } } +/** + * Battler tag for the "Seeded" effect applied by {@linkcode MoveId.LEECH_SEED | Leech Seed} and + * {@linkcode MoveId.SAPPY_SEED | Sappy Seed} + * + * @sealed + */ export class SeedTag extends SerializableBattlerTag { public override readonly tagType = BattlerTagType.SEEDED; public readonly sourceIndex: BattlerIndex; @@ -983,7 +1007,7 @@ export class SeedTag extends SerializableBattlerTag { * When given a battler tag or json representing one, load the data for it. * @param source - An object containing the fields needed to reconstruct this tag. */ - override loadTag(source: NonFunctionProperties): void { + public override loadTag(source: BaseBattlerTag & Pick): void { super.loadTag(source); (this as Mutable).sourceIndex = source.sourceIndex; } @@ -1194,6 +1218,7 @@ export class FrenzyTag extends SerializableBattlerTag { /** * Applies the effects of {@linkcode MoveId.ENCORE} onto the target Pokemon. * Encore forces the target Pokemon to use its most-recent move for 3 turns. + * @sealed */ export class EncoreTag extends MoveRestrictionBattlerTag { public override readonly tagType = BattlerTagType.ENCORE; @@ -1210,7 +1235,7 @@ export class EncoreTag extends MoveRestrictionBattlerTag { ); } - override loadTag(source: NonFunctionProperties): void { + public override loadTag(source: NonFunctionProperties): void { super.loadTag(source); this.moveId = source.moveId; } @@ -2085,7 +2110,7 @@ export class HighestStatBoostTag extends AbilityBattlerTag { * When given a battler tag or json representing one, load the data for it. * @param source - An object containing the fields needed to reconstruct this tag. */ - loadTag(source: NonFunctionProperties): void { + public override loadTag(source: BaseBattlerTag & Pick): void { super.loadTag(source); this.stat = source.stat as Stat; this.multiplier = source.multiplier; @@ -2564,6 +2589,7 @@ export class IceFaceBlockDamageTag extends FormBlockDamageTag { /** * Battler tag indicating a Tatsugiri with {@link https://bulbapedia.bulbagarden.net/wiki/Commander_(Ability) | Commander} * has entered the tagged Pokemon's mouth. + * @sealed */ export class CommandedTag extends SerializableBattlerTag { public override readonly tagType = BattlerTagType.COMMANDED; @@ -2607,6 +2633,7 @@ export class CommandedTag extends SerializableBattlerTag { * - Stat changes on removal of (all) stacks. * - Removing stacks decreases DEF and SPDEF, independently, by one stage for each stack that successfully changed * the stat when added. + * @sealed */ export class StockpilingTag extends SerializableBattlerTag { public override readonly tagType = BattlerTagType.STOCKPILING; @@ -2632,7 +2659,7 @@ export class StockpilingTag extends SerializableBattlerTag { } }; - override loadTag(source: NonFunctionProperties): void { + public override loadTag(source: NonFunctionProperties): void { super.loadTag(source); this.stockpiledCount = source.stockpiledCount || 0; this.statChangeCounts = { @@ -2979,7 +3006,7 @@ export class AutotomizedTag extends SerializableBattlerTag { this.onAdd(pokemon); } - loadTag(source: NonFunctionProperties): void { + public override loadTag(source: NonFunctionProperties): void { super.loadTag(source); this.autotomizeCount = source.autotomizeCount; } @@ -3129,7 +3156,7 @@ export class SubstituteTag extends SerializableBattlerTag { * When given a battler tag or json representing one, load the data for it. * @param source - An object containing the necessary properties to load the tag */ - override loadTag(source: NonFunctionProperties): void { + public override loadTag(source: BaseBattlerTag & Pick): void { super.loadTag(source); this.hp = source.hp; }