diff --git a/src/@types/arena-tags.ts b/src/@types/arena-tags.ts new file mode 100644 index 00000000000..ab4339b2fef --- /dev/null +++ b/src/@types/arena-tags.ts @@ -0,0 +1,48 @@ +import type { ArenaTagTypeMap } from "#data/arena-tag"; +import type { ArenaTagType } from "#enums/arena-tag-type"; +import type { NonFunctionProperties } from "./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 = + | ArenaTagType.STICKY_WEB + | ArenaTagType.SPIKES + | ArenaTagType.TOXIC_SPIKES + | ArenaTagType.STEALTH_ROCK + | ArenaTagType.IMPRISON; + +/** Subset of {@linkcode ArenaTagType}s that are considered delayed attacks */ +export type ArenaDelayedAttackTagType = ArenaTagType.FUTURE_SIGHT | ArenaTagType.DOOM_DESIRE; + +/** Subset of {@linkcode ArenaTagType}s that create {@link https://bulbapedia.bulbagarden.net/wiki/Category:Screen-creating_moves | screens}. */ +export type ArenaScreenTagType = ArenaTagType.REFLECT | ArenaTagType.LIGHT_SCREEN | ArenaTagType.AURORA_VEIL; + +/** Subset of {@linkcode ArenaTagType}s for moves that add protection */ +export type TurnProtectArenaTagType = + | ArenaTagType.QUICK_GUARD + | ArenaTagType.WIDE_GUARD + | ArenaTagType.MAT_BLOCK + | ArenaTagType.CRAFTY_SHIELD; + +/** Subset of {@linkcode ArenaTagType}s that cannot persist across turns, and thus should not be serialized in {@linkcode SessionSaveData}. */ +export type NonSerializableArenaTagType = ArenaTagType.NONE | TurnProtectArenaTagType | ArenaTagType.ION_DELUGE; + +/** Subset of {@linkcode ArenaTagType}s that may persist across turns, and thus must be serialized in {@linkcode SessionSaveData}. */ +export type SerializableArenaTagType = Exclude; + +/** + * Type-safe representation of the serializable data of an ArenaTag + */ +export type ArenaTagTypeData = NonFunctionProperties< + ArenaTagTypeMap[keyof { + [K in keyof ArenaTagTypeMap as K extends SerializableArenaTagType ? K : never]: ArenaTagTypeMap[K]; + }] +>; + +/** Dummy, typescript-only declaration to ensure that + * {@linkcode ArenaTagTypeMap} has a map for all ArenaTagTypes. + * + * If an arena tag is missing from the map, typescript will throw an error on this statement. + * + * ⚠️ Does not actually exist at runtime, so it must not be used! + */ +declare const EnsureAllArenaTagTypesAreMapped: ArenaTagTypeMap[ArenaTagType] & never; diff --git a/src/@types/type-helpers.ts b/src/@types/type-helpers.ts index 077bef62f1f..3a5c88e3f15 100644 --- a/src/@types/type-helpers.ts +++ b/src/@types/type-helpers.ts @@ -44,3 +44,34 @@ export type Mutable = { export type InferKeys, V extends EnumValues> = { [K in keyof O]: O[K] extends V ? K : never; }[keyof O]; + +/** + * Type helper that matches any `Function` type. Equivalent to `Function`, but will not raise a warning from Biome. + */ +export type AnyFn = (...args: any[]) => any; + +/** + * Type helper to extract non-function properties from a type. + * + * @remarks + * 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. + * - Nested properties are not recursively extracted. For this, use {@linkcode NonFunctionPropertiesRecursive} + */ +export type NonFunctionProperties = { + [K in keyof T as T[K] extends AnyFn ? never : K]: T[K]; +}; + +/** + * Type helper to extract out non-function properties from a type, recursively applying to nested properties. + */ +export type NonFunctionPropertiesRecursive = { + [K in keyof Class as Class[K] extends AnyFn ? never : K]: Class[K] extends Array + ? NonFunctionPropertiesRecursive[] + : Class[K] extends object + ? NonFunctionPropertiesRecursive + : Class[K]; +}; + +export type AbstractConstructor = abstract new (...args: any[]) => T; diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index ab50d279cc5..b25e79649a0 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -19,19 +19,88 @@ import { Stat } from "#enums/stat"; import { StatusEffect } from "#enums/status-effect"; import type { Arena } from "#field/arena"; import type { Pokemon } from "#field/pokemon"; +import type { + ArenaDelayedAttackTagType, + ArenaScreenTagType, + ArenaTagTypeData, + ArenaTrapTagType, + SerializableArenaTagType, +} from "#types/arena-tags"; +import type { Mutable, NonFunctionProperties } from "#types/type-helpers"; import { BooleanHolder, NumberHolder, toDmgValue } from "#utils/common"; import i18next from "i18next"; -// TODO: Add a class for tags that explicitly REQUIRE a source move (as currently we have a lot of bangs) +/* +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 -export abstract class ArenaTag { - constructor( - public tagType: ArenaTagType, - public turnCount: number, - public sourceMove?: MoveId, - public sourceId?: number, - public side: ArenaTagSide = ArenaTagSide.BOTH, - ) {} +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. +*/ + +/** Interface containing the serializable fields of ArenaTagData. */ +interface BaseArenaTag { + /** + * The tag's remaining duration. Setting to any number `<=0` will make the tag's duration effectively infinite. + */ + turnCount: number; + /** + * The {@linkcode MoveId} that created this tag, or `undefined` if not set by a move. + */ + sourceMove?: MoveId; + /** + * The {@linkcode Pokemon.id | PID} of the {@linkcode Pokemon} having created the tag, or `undefined` if not set by a Pokemon. + * @todo Implement handling for `ArenaTag`s created by non-pokemon sources (most tags will throw errors without a source) + */ + // Note: Intentionally not using `?`, as the property should always exist, but just be undefined if not present. + sourceId: number | undefined; + /** + * The {@linkcode ArenaTagSide | side of the field} that this arena tag affects. + * @defaultValue `ArenaTagSide.BOTH` + */ + side: ArenaTagSide; +} + +/** + * An {@linkcode ArenaTag} represents a semi-persistent effect affecting a given _side_ of the field. + * Unlike {@linkcode BattlerTag}s (which are tied to individual {@linkcode Pokemon}), `ArenaTag`s function independently of + * the Pokemon currently on-field, only cleared on arena reset or through their respective {@linkcode ArenaTag.lapse | lapse} methods. + */ +export abstract class ArenaTag implements BaseArenaTag { + /** The type of the arena tag */ + public abstract readonly tagType: ArenaTagType; + public turnCount: number; + public sourceMove?: MoveId; + public sourceId: number | undefined; + public side: ArenaTagSide; + + constructor(turnCount: number, sourceMove?: MoveId, sourceId?: number, side: ArenaTagSide = ArenaTagSide.BOTH) { + this.turnCount = turnCount; + this.sourceMove = sourceMove; + this.sourceId = sourceId; + this.side = side; + } apply(_arena: Arena, _simulated: boolean, ..._args: unknown[]): boolean { return true; @@ -72,9 +141,9 @@ export abstract class ArenaTag { /** * 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 {ArenaTag | any} source An arena tag + * @param source - The {@linkcode BaseArenaTag} being loaded */ - loadTag(source: ArenaTag | any): void { + loadTag(source: BaseArenaTag): void { this.turnCount = source.turnCount; this.sourceMove = source.sourceMove; this.sourceId = source.sourceId; @@ -107,13 +176,21 @@ export abstract class ArenaTag { } } +/** + * Abstract class for arena tags that can persist across turns. + */ +export abstract class SerializableArenaTag extends ArenaTag { + abstract readonly tagType: SerializableArenaTagType; +} + /** * Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Mist_(move) Mist}. * Prevents Pokémon on the opposing side from lowering the stats of the Pokémon in the Mist. */ -export class MistTag extends ArenaTag { - constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.MIST, turnCount, MoveId.MIST, sourceId, side); +export class MistTag extends SerializableArenaTag { + readonly tagType = ArenaTagType.MIST; + constructor(turnCount: number, sourceId: number | undefined, side: ArenaTagSide) { + super(turnCount, MoveId.MIST, sourceId, side); } onAdd(arena: Arena, quiet = false): void { @@ -170,33 +247,11 @@ export class MistTag extends ArenaTag { /** * Reduces the damage of specific move categories in the arena. - * @extends ArenaTag */ -export class WeakenMoveScreenTag extends ArenaTag { - protected weakenedCategories: MoveCategory[]; - - /** - * Creates a new instance of the WeakenMoveScreenTag class. - * - * @param tagType - The type of the arena tag. - * @param turnCount - The number of turns the tag is active. - * @param sourceMove - The move that created the tag. - * @param sourceId - The ID of the source of the tag. - * @param side - The side (player or enemy) the tag affects. - * @param weakenedCategories - The categories of moves that are weakened by this tag. - */ - constructor( - tagType: ArenaTagType, - turnCount: number, - sourceMove: MoveId, - sourceId: number, - side: ArenaTagSide, - weakenedCategories: MoveCategory[], - ) { - super(tagType, turnCount, sourceMove, sourceId, side); - - this.weakenedCategories = weakenedCategories; - } +export abstract class WeakenMoveScreenTag extends SerializableArenaTag { + public abstract readonly tagType: ArenaScreenTagType; + // Getter to avoid unnecessary serialization and prevent modification + protected abstract get weakenedCategories(): MoveCategory[]; /** * Applies the weakening effect to the move. @@ -233,8 +288,13 @@ export class WeakenMoveScreenTag extends ArenaTag { * Used by {@linkcode MoveId.REFLECT} */ class ReflectTag extends WeakenMoveScreenTag { - constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.REFLECT, turnCount, MoveId.REFLECT, sourceId, side, [MoveCategory.PHYSICAL]); + public readonly tagType = ArenaTagType.REFLECT; + protected get weakenedCategories(): [MoveCategory.PHYSICAL] { + return [MoveCategory.PHYSICAL]; + } + + constructor(turnCount: number, sourceId: number | undefined, side: ArenaTagSide) { + super(turnCount, MoveId.REFLECT, sourceId, side); } onAdd(_arena: Arena, quiet = false): void { @@ -253,8 +313,12 @@ class ReflectTag extends WeakenMoveScreenTag { * Used by {@linkcode MoveId.LIGHT_SCREEN} */ class LightScreenTag extends WeakenMoveScreenTag { - constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.LIGHT_SCREEN, turnCount, MoveId.LIGHT_SCREEN, sourceId, side, [MoveCategory.SPECIAL]); + public readonly tagType = ArenaTagType.LIGHT_SCREEN; + protected get weakenedCategories(): [MoveCategory.SPECIAL] { + return [MoveCategory.SPECIAL]; + } + constructor(turnCount: number, sourceId: number | undefined, side: ArenaTagSide) { + super(turnCount, MoveId.LIGHT_SCREEN, sourceId, side); } onAdd(_arena: Arena, quiet = false): void { @@ -273,11 +337,13 @@ class LightScreenTag extends WeakenMoveScreenTag { * Used by {@linkcode MoveId.AURORA_VEIL} */ class AuroraVeilTag extends WeakenMoveScreenTag { - constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.AURORA_VEIL, turnCount, MoveId.AURORA_VEIL, sourceId, side, [ - MoveCategory.SPECIAL, - MoveCategory.PHYSICAL, - ]); + public readonly tagType = ArenaTagType.AURORA_VEIL; + protected get weakenedCategories(): [MoveCategory.PHYSICAL, MoveCategory.SPECIAL] { + return [MoveCategory.PHYSICAL, MoveCategory.SPECIAL]; + } + + constructor(turnCount: number, sourceId: number | undefined, side: ArenaTagSide) { + super(turnCount, MoveId.AURORA_VEIL, sourceId, side); } onAdd(_arena: Arena, quiet = false): void { @@ -297,21 +363,23 @@ type ProtectConditionFunc = (arena: Arena, moveId: MoveId) => boolean; * Class to implement conditional team protection * applies protection based on the attributes of incoming moves */ -export class ConditionalProtectTag extends ArenaTag { +export abstract class ConditionalProtectTag extends ArenaTag { /** The condition function to determine which moves are negated */ protected protectConditionFunc: ProtectConditionFunc; - /** Does this apply to all moves, including those that ignore other forms of protection? */ + /** + * Whether this protection effect should apply to _all_ moves, including ones that ignore other forms of protection. + * @defaultValue `false` + */ protected ignoresBypass: boolean; constructor( - tagType: ArenaTagType, sourceMove: MoveId, - sourceId: number, + sourceId: number | undefined, side: ArenaTagSide, condition: ProtectConditionFunc, ignoresBypass = false, ) { - super(tagType, 1, sourceMove, sourceId, side); + super(1, sourceMove, sourceId, side); this.protectConditionFunc = condition; this.ignoresBypass = ignoresBypass; @@ -397,8 +465,9 @@ const QuickGuardConditionFunc: ProtectConditionFunc = (_arena, moveId) => { * Condition: The incoming move has increased priority. */ class QuickGuardTag extends ConditionalProtectTag { - constructor(sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.QUICK_GUARD, MoveId.QUICK_GUARD, sourceId, side, QuickGuardConditionFunc); + public readonly tagType = ArenaTagType.QUICK_GUARD; + constructor(sourceId: number | undefined, side: ArenaTagSide) { + super(MoveId.QUICK_GUARD, sourceId, side, QuickGuardConditionFunc); } } @@ -428,8 +497,9 @@ const WideGuardConditionFunc: ProtectConditionFunc = (_arena, moveId): boolean = * can be an ally or enemy. */ class WideGuardTag extends ConditionalProtectTag { - constructor(sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.WIDE_GUARD, MoveId.WIDE_GUARD, sourceId, side, WideGuardConditionFunc); + public readonly tagType = ArenaTagType.WIDE_GUARD; + constructor(sourceId: number | undefined, side: ArenaTagSide) { + super(MoveId.WIDE_GUARD, sourceId, side, WideGuardConditionFunc); } } @@ -450,8 +520,9 @@ const MatBlockConditionFunc: ProtectConditionFunc = (_arena, moveId): boolean => * Condition: The incoming move is a Physical or Special attack move. */ class MatBlockTag extends ConditionalProtectTag { - constructor(sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.MAT_BLOCK, MoveId.MAT_BLOCK, sourceId, side, MatBlockConditionFunc); + public readonly tagType = ArenaTagType.MAT_BLOCK; + constructor(sourceId: number | undefined, side: ArenaTagSide) { + super(MoveId.MAT_BLOCK, sourceId, side, MatBlockConditionFunc); } onAdd(_arena: Arena) { @@ -494,8 +565,9 @@ const CraftyShieldConditionFunc: ProtectConditionFunc = (_arena, moveId) => { * not target all Pokemon or sides of the field. */ class CraftyShieldTag extends ConditionalProtectTag { - constructor(sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.CRAFTY_SHIELD, MoveId.CRAFTY_SHIELD, sourceId, side, CraftyShieldConditionFunc, true); + public readonly tagType = ArenaTagType.CRAFTY_SHIELD; + constructor(sourceId: number | undefined, side: ArenaTagSide) { + super(MoveId.CRAFTY_SHIELD, sourceId, side, CraftyShieldConditionFunc, true); } } @@ -503,17 +575,8 @@ class CraftyShieldTag extends ConditionalProtectTag { * Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Lucky_Chant_(move) Lucky Chant}. * Prevents critical hits against the tag's side. */ -export class NoCritTag extends ArenaTag { - /** - * Constructor method for the NoCritTag class - * @param turnCount `number` the number of turns this effect lasts - * @param sourceMove {@linkcode MoveId} the move that created this effect - * @param sourceId `number` the ID of the {@linkcode Pokemon} that created this effect - * @param side {@linkcode ArenaTagSide} the side to which this effect belongs - */ - constructor(turnCount: number, sourceMove: MoveId, sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.NO_CRIT, turnCount, sourceMove, sourceId, side); - } +export class NoCritTag extends SerializableArenaTag { + public readonly tagType = ArenaTagType.NO_CRIT; /** Queues a message upon adding this effect to the field */ onAdd(_arena: Arena): void { @@ -545,13 +608,17 @@ export class NoCritTag extends ArenaTag { * Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Wish_(move) | Wish}. * Heals the Pokémon in the user's position the turn after Wish is used. */ -class WishTag extends ArenaTag { - private battlerIndex: BattlerIndex; - private triggerMessage: string; - private healHp: number; +class WishTag extends SerializableArenaTag { + // The following fields are meant to be inwardly mutable, but outwardly immutable. + readonly battlerIndex: BattlerIndex; + readonly healHp: number; + readonly sourceName: string; + // End inwardly mutable fields - constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.WISH, turnCount, MoveId.WISH, sourceId, side); + public readonly tagType = ArenaTagType.WISH; + + constructor(turnCount: number, sourceId: number | undefined, side: ArenaTagSide) { + super(turnCount, MoveId.WISH, sourceId, side); } onAdd(_arena: Arena): void { @@ -561,45 +628,38 @@ class WishTag extends ArenaTag { return; } - super.onAdd(_arena); - this.healHp = toDmgValue(source.getMaxHp() / 2); - - globalScene.phaseManager.queueMessage( - i18next.t("arenaTag:wishTagOnAdd", { - pokemonNameWithAffix: getPokemonNameWithAffix(source), - }), - ); + (this as Mutable).sourceName = getPokemonNameWithAffix(source); + (this as Mutable).healHp = toDmgValue(source.getMaxHp() / 2); + (this as Mutable).battlerIndex = source.getBattlerIndex(); } onRemove(_arena: Arena): void { const target = globalScene.getField()[this.battlerIndex]; if (target?.isActive(true)) { - globalScene.phaseManager.queueMessage(this.triggerMessage); + globalScene.phaseManager.queueMessage( + // TODO: Rename key as it triggers on activation + i18next.t("arenaTag:wishTagOnAdd", { + pokemonNameWithAffix: this.sourceName, + }), + ); globalScene.phaseManager.unshiftNew("PokemonHealPhase", target.getBattlerIndex(), this.healHp, null, true, false); } } + + override loadTag(source: NonFunctionProperties): void { + super.loadTag(source); + (this as Mutable).battlerIndex = source.battlerIndex; + (this as Mutable).healHp = source.healHp; + (this as Mutable).sourceName = source.sourceName; + } } /** * Abstract class to implement weakened moves of a specific type. */ -export class WeakenMoveTypeTag extends ArenaTag { - private weakenedType: PokemonType; - - /** - * Creates a new instance of the WeakenMoveTypeTag class. - * - * @param tagType - The type of the arena tag. - * @param turnCount - The number of turns the tag is active. - * @param type - The type being weakened from this tag. - * @param sourceMove - The move that created the tag. - * @param sourceId - The ID of the source of the tag. - */ - constructor(tagType: ArenaTagType, turnCount: number, type: PokemonType, sourceMove: MoveId, sourceId: number) { - super(tagType, turnCount, sourceMove, sourceId); - - this.weakenedType = type; - } +export abstract class WeakenMoveTypeTag extends SerializableArenaTag { + abstract readonly tagType: ArenaTagType.MUD_SPORT | ArenaTagType.WATER_SPORT; + abstract get weakenedType(): PokemonType; /** * Reduces an attack's power by 0.33x if it matches this tag's weakened type. @@ -623,8 +683,12 @@ export class WeakenMoveTypeTag extends ArenaTag { * Weakens Electric type moves for a set amount of turns, usually 5. */ class MudSportTag extends WeakenMoveTypeTag { - constructor(turnCount: number, sourceId: number) { - super(ArenaTagType.MUD_SPORT, turnCount, PokemonType.ELECTRIC, MoveId.MUD_SPORT, sourceId); + public readonly tagType = ArenaTagType.MUD_SPORT; + override get weakenedType(): PokemonType.ELECTRIC { + return PokemonType.ELECTRIC; + } + constructor(turnCount: number, sourceId?: number) { + super(turnCount, MoveId.MUD_SPORT, sourceId); } onAdd(_arena: Arena): void { @@ -641,8 +705,12 @@ class MudSportTag extends WeakenMoveTypeTag { * Weakens Fire type moves for a set amount of turns, usually 5. */ class WaterSportTag extends WeakenMoveTypeTag { - constructor(turnCount: number, sourceId: number) { - super(ArenaTagType.WATER_SPORT, turnCount, PokemonType.FIRE, MoveId.WATER_SPORT, sourceId); + public readonly tagType = ArenaTagType.WATER_SPORT; + override get weakenedType(): PokemonType.FIRE { + return PokemonType.FIRE; + } + constructor(turnCount: number, sourceId?: number) { + super(turnCount, MoveId.WATER_SPORT, sourceId); } onAdd(_arena: Arena): void { @@ -660,8 +728,9 @@ class WaterSportTag extends WeakenMoveTypeTag { * Converts Normal-type moves to Electric type for the rest of the turn. */ export class IonDelugeTag extends ArenaTag { + public readonly tagType = ArenaTagType.ION_DELUGE; constructor(sourceMove?: MoveId) { - super(ArenaTagType.ION_DELUGE, 1, sourceMove); + super(1, sourceMove); } /** Queues an on-add message */ @@ -690,7 +759,8 @@ export class IonDelugeTag extends ArenaTag { /** * Abstract class to implement arena traps. */ -export class ArenaTrapTag extends ArenaTag { +export abstract class ArenaTrapTag extends SerializableArenaTag { + abstract readonly tagType: ArenaTrapTagType; public layers: number; public maxLayers: number; @@ -703,8 +773,8 @@ export class ArenaTrapTag extends ArenaTag { * @param side - The side (player or enemy) the tag affects. * @param maxLayers - The maximum amount of layers this tag can have. */ - constructor(tagType: ArenaTagType, sourceMove: MoveId, sourceId: number, side: ArenaTagSide, maxLayers: number) { - super(tagType, 0, sourceMove, sourceId, side); + constructor(sourceMove: MoveId, sourceId: number | undefined, side: ArenaTagSide, maxLayers: number) { + super(0, sourceMove, sourceId, side); this.layers = 1; this.maxLayers = maxLayers; @@ -743,7 +813,7 @@ export class ArenaTrapTag extends ArenaTag { : Phaser.Math.Linear(0, 1 / Math.pow(2, this.layers), Math.min(pokemon.getHpRatio(), 0.5) * 2); } - loadTag(source: any): void { + loadTag(source: NonFunctionProperties): void { super.loadTag(source); this.layers = source.layers; this.maxLayers = source.maxLayers; @@ -756,8 +826,9 @@ export class ArenaTrapTag extends ArenaTag { * in damage for 1, 2, or 3 layers of Spikes respectively if they are summoned into this trap. */ class SpikesTag extends ArenaTrapTag { - constructor(sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.SPIKES, MoveId.SPIKES, sourceId, side, 3); + public readonly tagType = ArenaTagType.SPIKES; + constructor(sourceId: number | undefined, side: ArenaTagSide) { + super(MoveId.SPIKES, sourceId, side, 3); } onAdd(arena: Arena, quiet = false): void { @@ -814,11 +885,12 @@ class SpikesTag extends ArenaTrapTag { * Pokémon summoned into this trap remove it entirely. */ class ToxicSpikesTag extends ArenaTrapTag { - private neutralized: boolean; + #neutralized: boolean; + public readonly tagType = ArenaTagType.TOXIC_SPIKES; - constructor(sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.TOXIC_SPIKES, MoveId.TOXIC_SPIKES, sourceId, side, 2); - this.neutralized = false; + constructor(sourceId: number | undefined, side: ArenaTagSide) { + super(MoveId.TOXIC_SPIKES, sourceId, side, 2); + this.#neutralized = false; } onAdd(arena: Arena, quiet = false): void { @@ -844,7 +916,7 @@ class ToxicSpikesTag extends ArenaTrapTag { } onRemove(arena: Arena): void { - if (!this.neutralized) { + if (!this.#neutralized) { super.onRemove(arena); } } @@ -855,7 +927,7 @@ class ToxicSpikesTag extends ArenaTrapTag { return true; } if (pokemon.isOfType(PokemonType.POISON)) { - this.neutralized = true; + this.#neutralized = true; if (globalScene.arena.removeTag(this.tagType)) { globalScene.phaseManager.queueMessage( i18next.t("arenaTag:toxicSpikesActivateTrapPoison", { @@ -889,55 +961,15 @@ class ToxicSpikesTag extends ArenaTrapTag { } } -/** - * Arena Tag class for delayed attacks, such as {@linkcode MoveId.FUTURE_SIGHT} or {@linkcode MoveId.DOOM_DESIRE}. - * Delays the attack's effect by a set amount of turns, usually 3 (including the turn the move is used), - * and deals damage after the turn count is reached. - */ -export class DelayedAttackTag extends ArenaTag { - public targetIndex: BattlerIndex; - - constructor( - tagType: ArenaTagType, - sourceMove: MoveId | undefined, - sourceId: number, - targetIndex: BattlerIndex, - side: ArenaTagSide = ArenaTagSide.BOTH, - ) { - super(tagType, 3, sourceMove, sourceId, side); - - this.targetIndex = targetIndex; - this.side = side; - } - - lapse(arena: Arena): boolean { - const ret = super.lapse(arena); - - if (!ret) { - // TODO: This should not add to move history (for Spite) - globalScene.phaseManager.unshiftNew( - "MoveEffectPhase", - this.sourceId!, - [this.targetIndex], - allMoves[this.sourceMove!], - MoveUseMode.FOLLOW_UP, - ); // TODO: are those bangs correct? - } - - return ret; - } - - onRemove(_arena: Arena): void {} -} - /** * Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Stealth_Rock_(move) Stealth Rock}. * Applies up to 1 layer of Stealth Rocks, dealing percentage-based damage to any Pokémon * who is summoned into the trap, based on the Rock type's type effectiveness. */ class StealthRockTag extends ArenaTrapTag { - constructor(sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.STEALTH_ROCK, MoveId.STEALTH_ROCK, sourceId, side, 1); + public readonly tagType = ArenaTagType.STEALTH_ROCK; + constructor(sourceId: number | undefined, side: ArenaTagSide) { + super(MoveId.STEALTH_ROCK, sourceId, side, 1); } onAdd(arena: Arena, quiet = false): void { @@ -1025,8 +1057,9 @@ class StealthRockTag extends ArenaTrapTag { * to any Pokémon who is summoned into this trap. */ class StickyWebTag extends ArenaTrapTag { - constructor(sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.STICKY_WEB, MoveId.STICKY_WEB, sourceId, side, 1); + public readonly tagType = ArenaTagType.STICKY_WEB; + constructor(sourceId: number | undefined, side: ArenaTagSide) { + super(MoveId.STICKY_WEB, sourceId, side, 1); } onAdd(arena: Arena, quiet = false): void { @@ -1093,14 +1126,57 @@ class StickyWebTag extends ArenaTrapTag { } } +/** + * Arena Tag class for delayed attacks, such as {@linkcode MoveId.FUTURE_SIGHT} or {@linkcode MoveId.DOOM_DESIRE}. + * Delays the attack's effect by a set amount of turns, usually 3 (including the turn the move is used), + * and deals damage after the turn count is reached. + */ +export class DelayedAttackTag extends SerializableArenaTag { + public targetIndex: BattlerIndex; + public readonly tagType: ArenaDelayedAttackTagType; + + constructor( + tagType: ArenaTagType.DOOM_DESIRE | ArenaTagType.FUTURE_SIGHT, + sourceMove: MoveId | undefined, + sourceId: number | undefined, + targetIndex: BattlerIndex, + side: ArenaTagSide = ArenaTagSide.BOTH, + ) { + super(3, sourceMove, sourceId, side); + this.tagType = tagType; + this.targetIndex = targetIndex; + this.side = side; + } + + lapse(arena: Arena): boolean { + const ret = super.lapse(arena); + + if (!ret) { + // TODO: This should not add to move history (for Spite) + globalScene.phaseManager.unshiftNew( + "MoveEffectPhase", + this.sourceId!, + [this.targetIndex], + allMoves[this.sourceMove!], + MoveUseMode.FOLLOW_UP, + ); // TODO: are those bangs correct? + } + + return ret; + } + + onRemove(_arena: Arena): void {} +} + /** * Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Trick_Room_(move) Trick Room}. * Reverses the Speed stats for all Pokémon on the field as long as this arena tag is up, * also reversing the turn order for all Pokémon on the field as well. */ -export class TrickRoomTag extends ArenaTag { - constructor(turnCount: number, sourceId: number) { - super(ArenaTagType.TRICK_ROOM, turnCount, MoveId.TRICK_ROOM, sourceId); +export class TrickRoomTag extends SerializableArenaTag { + public readonly tagType = ArenaTagType.TRICK_ROOM; + constructor(turnCount: number, sourceId?: number) { + super(turnCount, MoveId.TRICK_ROOM, sourceId); } /** @@ -1142,9 +1218,10 @@ export class TrickRoomTag extends ArenaTag { * Grounds all Pokémon on the field, including Flying-types and those with * {@linkcode AbilityId.LEVITATE} for the duration of the arena tag, usually 5 turns. */ -export class GravityTag extends ArenaTag { - constructor(turnCount: number) { - super(ArenaTagType.GRAVITY, turnCount, MoveId.GRAVITY); +export class GravityTag extends SerializableArenaTag { + public readonly tagType = ArenaTagType.GRAVITY; + constructor(turnCount: number, sourceId?: number) { + super(turnCount, MoveId.GRAVITY, sourceId); } onAdd(_arena: Arena): void { @@ -1170,9 +1247,10 @@ export class GravityTag extends ArenaTag { * Doubles the Speed of the Pokémon who created this arena tag, as well as all allied Pokémon. * Applies this arena tag for 4 turns (including the turn the move was used). */ -class TailwindTag extends ArenaTag { - constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.TAILWIND, turnCount, MoveId.TAILWIND, sourceId, side); +class TailwindTag extends SerializableArenaTag { + public readonly tagType = ArenaTagType.TAILWIND; + constructor(turnCount: number, sourceId: number | undefined, side: ArenaTagSide) { + super(turnCount, MoveId.TAILWIND, sourceId, side); } onAdd(_arena: Arena, quiet = false): void { @@ -1238,9 +1316,10 @@ class TailwindTag extends ArenaTag { * Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Happy_Hour_(move) Happy Hour}. * Doubles the prize money from trainers and money moves like {@linkcode MoveId.PAY_DAY} and {@linkcode MoveId.MAKE_IT_RAIN}. */ -class HappyHourTag extends ArenaTag { - constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.HAPPY_HOUR, turnCount, MoveId.HAPPY_HOUR, sourceId, side); +class HappyHourTag extends SerializableArenaTag { + public readonly tagType = ArenaTagType.HAPPY_HOUR; + constructor(turnCount: number, sourceId: number | undefined, side: ArenaTagSide) { + super(turnCount, MoveId.HAPPY_HOUR, sourceId, side); } onAdd(_arena: Arena): void { @@ -1253,8 +1332,9 @@ class HappyHourTag extends ArenaTag { } class SafeguardTag extends ArenaTag { - constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.SAFEGUARD, turnCount, MoveId.SAFEGUARD, sourceId, side); + public readonly tagType = ArenaTagType.SAFEGUARD; + constructor(turnCount: number, sourceId: number | undefined, side: ArenaTagSide) { + super(turnCount, MoveId.SAFEGUARD, sourceId, side); } onAdd(_arena: Arena): void { @@ -1275,18 +1355,21 @@ class SafeguardTag extends ArenaTag { } class NoneTag extends ArenaTag { + public readonly tagType = ArenaTagType.NONE; constructor() { - super(ArenaTagType.NONE, 0); + super(0); } } + /** * This arena tag facilitates the application of the move Imprison * Imprison remains in effect as long as the source Pokemon is active and present on the field. * Imprison will apply to any opposing Pokemon that switch onto the field as well. */ class ImprisonTag extends ArenaTrapTag { - constructor(sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.IMPRISON, MoveId.IMPRISON, sourceId, side, 1); + public readonly tagType = ArenaTagType.IMPRISON; + constructor(sourceId: number | undefined, side: ArenaTagSide) { + super(MoveId.IMPRISON, sourceId, side, 1); } /** @@ -1341,7 +1424,9 @@ class ImprisonTag extends ArenaTrapTag { */ override onRemove(): void { const party = this.getAffectedPokemon(); - party.forEach(p => p.removeTag(BattlerTagType.IMPRISON)); + party.forEach(p => { + p.removeTag(BattlerTagType.IMPRISON); + }); } } @@ -1352,9 +1437,10 @@ class ImprisonTag extends ArenaTrapTag { * Damages all non-Fire-type Pokemon on the given side of the field at the end * of each turn for 4 turns. */ -class FireGrassPledgeTag extends ArenaTag { - constructor(sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.FIRE_GRASS_PLEDGE, 4, MoveId.FIRE_PLEDGE, sourceId, side); +class FireGrassPledgeTag extends SerializableArenaTag { + public readonly tagType = ArenaTagType.FIRE_GRASS_PLEDGE; + constructor(sourceId: number | undefined, side: ArenaTagSide) { + super(4, MoveId.FIRE_PLEDGE, sourceId, side); } override onAdd(_arena: Arena): void { @@ -1400,9 +1486,10 @@ class FireGrassPledgeTag extends ArenaTag { * Doubles the secondary effect chance of moves from Pokemon on the * given side of the field for 4 turns. */ -class WaterFirePledgeTag extends ArenaTag { - constructor(sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.WATER_FIRE_PLEDGE, 4, MoveId.WATER_PLEDGE, sourceId, side); +class WaterFirePledgeTag extends SerializableArenaTag { + public readonly tagType = ArenaTagType.WATER_FIRE_PLEDGE; + constructor(sourceId: number | undefined, side: ArenaTagSide) { + super(4, MoveId.WATER_PLEDGE, sourceId, side); } override onAdd(_arena: Arena): void { @@ -1434,9 +1521,10 @@ class WaterFirePledgeTag extends ArenaTag { * and {@link https://bulbapedia.bulbagarden.net/wiki/Water_Pledge_(move) | Water Pledge}. * Quarters the Speed of Pokemon on the given side of the field for 4 turns. */ -class GrassWaterPledgeTag extends ArenaTag { - constructor(sourceId: number, side: ArenaTagSide) { - super(ArenaTagType.GRASS_WATER_PLEDGE, 4, MoveId.GRASS_PLEDGE, sourceId, side); +class GrassWaterPledgeTag extends SerializableArenaTag { + public readonly tagType = ArenaTagType.GRASS_WATER_PLEDGE; + constructor(sourceId: number | undefined, side: ArenaTagSide) { + super(4, MoveId.GRASS_PLEDGE, sourceId, side); } override onAdd(_arena: Arena): void { @@ -1456,9 +1544,10 @@ class GrassWaterPledgeTag extends ArenaTag { * If a Pokémon that's on the field when Fairy Lock is used goes on to faint later in the same turn, * the Pokémon that replaces it will still be unable to switch out in the following turn. */ -export class FairyLockTag extends ArenaTag { - constructor(turnCount: number, sourceId: number) { - super(ArenaTagType.FAIRY_LOCK, turnCount, MoveId.FAIRY_LOCK, sourceId); +export class FairyLockTag extends SerializableArenaTag { + public readonly tagType = ArenaTagType.FAIRY_LOCK; + constructor(turnCount: number, sourceId?: number) { + super(turnCount, MoveId.FAIRY_LOCK, sourceId); } onAdd(_arena: Arena): void { @@ -1472,15 +1561,29 @@ export class FairyLockTag extends ArenaTag { * Keeps track of the number of pokemon on the field with Neutralizing Gas - If it drops to zero, the effect is ended and abilities are reactivated * * Additionally ends onLose abilities when it is activated + * @sealed */ -export class SuppressAbilitiesTag extends ArenaTag { - private sourceCount: number; - private beingRemoved: boolean; +export class SuppressAbilitiesTag extends SerializableArenaTag { + // Source count is allowed to be inwardly mutable, but outwardly immutable + public readonly sourceCount: number; + public readonly tagType = ArenaTagType.NEUTRALIZING_GAS; + // Private field prevents field from appearing during serialization + /** Whether the tag is in the process of being removed */ + #beingRemoved: boolean; + /** Whether the tag is in the process of being removed */ + public get beingRemoved(): boolean { + return this.#beingRemoved; + } - constructor(sourceId: number) { - super(ArenaTagType.NEUTRALIZING_GAS, 0, undefined, sourceId); + constructor(sourceId?: number) { + super(0, undefined, sourceId); this.sourceCount = 1; - this.beingRemoved = false; + this.#beingRemoved = false; + } + + public override loadTag(source: NonFunctionProperties): void { + super.loadTag(source); + (this as Mutable).sourceCount = source.sourceCount; } public override onAdd(_arena: Arena): void { @@ -1492,19 +1595,21 @@ export class SuppressAbilitiesTag extends ArenaTag { if (fieldPokemon && fieldPokemon.id !== pokemon.id) { // TODO: investigate whether we can just remove the foreach and call `applyAbAttrs` directly, providing // the appropriate attributes (preLEaveField and IllusionBreak) - [true, false].forEach(passive => applyOnLoseAbAttrs({ pokemon: fieldPokemon, passive })); + [true, false].forEach(passive => { + applyOnLoseAbAttrs({ pokemon: fieldPokemon, passive }); + }); } } } } public override onOverlap(_arena: Arena, source: Pokemon | null): void { - this.sourceCount++; + (this as Mutable).sourceCount++; this.playActivationMessage(source); } public onSourceLeave(arena: Arena): void { - this.sourceCount--; + (this as Mutable).sourceCount--; if (this.sourceCount <= 0) { arena.removeTag(ArenaTagType.NEUTRALIZING_GAS); } else if (this.sourceCount === 1) { @@ -1522,7 +1627,7 @@ export class SuppressAbilitiesTag extends ArenaTag { } public override onRemove(_arena: Arena, quiet = false) { - this.beingRemoved = true; + this.#beingRemoved = true; if (!quiet) { globalScene.phaseManager.queueMessage(i18next.t("arenaTag:neutralizingGasOnRemove")); } @@ -1530,7 +1635,9 @@ export class SuppressAbilitiesTag extends ArenaTag { for (const pokemon of globalScene.getField(true)) { // There is only one pokemon with this attr on the field on removal, so its abilities are already active if (pokemon && !pokemon.hasAbilityWithAttr("PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr", false)) { - [true, false].forEach(passive => applyOnGainAbAttrs({ pokemon, passive })); + [true, false].forEach(passive => { + applyOnGainAbAttrs({ pokemon, passive }); + }); } } } @@ -1539,10 +1646,6 @@ export class SuppressAbilitiesTag extends ArenaTag { return this.sourceCount > 1; } - public isBeingRemoved() { - return this.beingRemoved; - } - private playActivationMessage(pokemon: Pokemon | null) { if (pokemon) { globalScene.phaseManager.queueMessage( @@ -1559,7 +1662,7 @@ export function getArenaTag( tagType: ArenaTagType, turnCount: number, sourceMove: MoveId | undefined, - sourceId: number, + sourceId: number | undefined, targetIndex?: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH, ): ArenaTag | null { @@ -1575,7 +1678,7 @@ export function getArenaTag( case ArenaTagType.CRAFTY_SHIELD: return new CraftyShieldTag(sourceId, side); case ArenaTagType.NO_CRIT: - return new NoCritTag(turnCount, sourceMove!, sourceId, side); // TODO: is this bang correct? + return new NoCritTag(turnCount, sourceMove, sourceId, side); case ArenaTagType.MUD_SPORT: return new MudSportTag(turnCount, sourceId); case ArenaTagType.WATER_SPORT: @@ -1588,7 +1691,10 @@ export function getArenaTag( return new ToxicSpikesTag(sourceId, side); case ArenaTagType.FUTURE_SIGHT: case ArenaTagType.DOOM_DESIRE: - return new DelayedAttackTag(tagType, sourceMove, sourceId, targetIndex!, side); // TODO:questionable bang + if (!targetIndex) { + return null; // If missing target index, no tag is created + } + return new DelayedAttackTag(tagType, sourceMove, sourceId, targetIndex, side); case ArenaTagType.WISH: return new WishTag(turnCount, sourceId, side); case ArenaTagType.STEALTH_ROCK: @@ -1598,7 +1704,7 @@ export function getArenaTag( case ArenaTagType.TRICK_ROOM: return new TrickRoomTag(turnCount, sourceId); case ArenaTagType.GRAVITY: - return new GravityTag(turnCount); + return new GravityTag(turnCount, sourceId); case ArenaTagType.REFLECT: return new ReflectTag(turnCount, sourceId, side); case ArenaTagType.LIGHT_SCREEN: @@ -1630,10 +1736,10 @@ export function getArenaTag( /** * When given a battler tag or json representing one, creates an actual ArenaTag object with the same data. - * @param {ArenaTag | any} source An arena tag - * @return {ArenaTag} The valid arena tag + * @param source - An arena tag + * @returns The valid arena tag */ -export function loadArenaTag(source: ArenaTag | any): ArenaTag { +export function loadArenaTag(source: (ArenaTag | ArenaTagTypeData) & { targetIndex?: BattlerIndex }): ArenaTag { const tag = getArenaTag( source.tagType, @@ -1646,3 +1752,37 @@ export function loadArenaTag(source: ArenaTag | any): ArenaTag { tag.loadTag(source); return tag; } + +export type ArenaTagTypeMap = { + [ArenaTagType.MUD_SPORT]: MudSportTag; + [ArenaTagType.WATER_SPORT]: WaterSportTag; + [ArenaTagType.ION_DELUGE]: IonDelugeTag; + [ArenaTagType.SPIKES]: SpikesTag; + [ArenaTagType.MIST]: MistTag; + [ArenaTagType.QUICK_GUARD]: QuickGuardTag; + [ArenaTagType.WIDE_GUARD]: WideGuardTag; + [ArenaTagType.MAT_BLOCK]: MatBlockTag; + [ArenaTagType.CRAFTY_SHIELD]: CraftyShieldTag; + [ArenaTagType.NO_CRIT]: NoCritTag; + [ArenaTagType.TOXIC_SPIKES]: ToxicSpikesTag; + [ArenaTagType.FUTURE_SIGHT]: DelayedAttackTag; + [ArenaTagType.DOOM_DESIRE]: DelayedAttackTag; + [ArenaTagType.WISH]: WishTag; + [ArenaTagType.STEALTH_ROCK]: StealthRockTag; + [ArenaTagType.STICKY_WEB]: StickyWebTag; + [ArenaTagType.TRICK_ROOM]: TrickRoomTag; + [ArenaTagType.GRAVITY]: GravityTag; + [ArenaTagType.REFLECT]: ReflectTag; + [ArenaTagType.LIGHT_SCREEN]: LightScreenTag; + [ArenaTagType.AURORA_VEIL]: AuroraVeilTag; + [ArenaTagType.TAILWIND]: TailwindTag; + [ArenaTagType.HAPPY_HOUR]: HappyHourTag; + [ArenaTagType.SAFEGUARD]: SafeguardTag; + [ArenaTagType.IMPRISON]: ImprisonTag; + [ArenaTagType.FIRE_GRASS_PLEDGE]: FireGrassPledgeTag; + [ArenaTagType.WATER_FIRE_PLEDGE]: WaterFirePledgeTag; + [ArenaTagType.GRASS_WATER_PLEDGE]: GrassWaterPledgeTag; + [ArenaTagType.FAIRY_LOCK]: FairyLockTag; + [ArenaTagType.NEUTRALIZING_GAS]: SuppressAbilitiesTag; + [ArenaTagType.NONE]: NoneTag; +}; diff --git a/src/data/terrain.ts b/src/data/terrain.ts index f44fd4f35e7..f5382b1c3ec 100644 --- a/src/data/terrain.ts +++ b/src/data/terrain.ts @@ -13,6 +13,11 @@ export enum TerrainType { PSYCHIC, } +export interface SerializedTerrain { + terrainType: TerrainType; + turnsLeft: number; +} + export class Terrain { public terrainType: TerrainType; public turnsLeft: number; diff --git a/src/data/weather.ts b/src/data/weather.ts index 62a03aa0832..59be56826a4 100644 --- a/src/data/weather.ts +++ b/src/data/weather.ts @@ -11,6 +11,11 @@ import type { Move } from "#moves/move"; import { randSeedInt } from "#utils/common"; import i18next from "i18next"; +export interface SerializedWeather { + weatherType: WeatherType; + turnsLeft: number; +} + export class Weather { public weatherType: WeatherType; public turnsLeft: number; diff --git a/src/enums/arena-tag-type.ts b/src/enums/arena-tag-type.ts index 4180aa00ef5..214826993b3 100644 --- a/src/enums/arena-tag-type.ts +++ b/src/enums/arena-tag-type.ts @@ -1,3 +1,13 @@ +import type { ArenaTagTypeMap } from "#data/arena-tag"; +import type { NonSerializableArenaTagType, SerializableArenaTagType } from "#types/arena-tags"; + +/** + * Enum representing all different types of {@linkcode ArenaTag}s. + * @privateRemarks + * ⚠️ When modifying the fields in this enum, ensure that: + * - The entry is added to / removed from {@linkcode ArenaTagTypeMap} + * - The tag is added to / removed from {@linkcode NonSerializableArenaTagType} or {@linkcode SerializableArenaTagType} +*/ export enum ArenaTagType { NONE = "NONE", MUD_SPORT = "MUD_SPORT", diff --git a/src/field/arena.ts b/src/field/arena.ts index b28ebd0b46b..8f27ddb22e9 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -30,6 +30,7 @@ import { TagAddedEvent, TagRemovedEvent, TerrainChangedEvent, WeatherChangedEven import type { Pokemon } from "#field/pokemon"; import { FieldEffectModifier } from "#modifiers/modifier"; import type { Move } from "#moves/move"; +import type { AbstractConstructor } from "#types/type-helpers"; import { type Constructor, isNullOrUndefined, NumberHolder, randSeedInt } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; @@ -644,7 +645,7 @@ export class Arena { * @param args array of parameters that the called upon tags may need */ applyTagsForSide( - tagType: ArenaTagType | Constructor, + tagType: ArenaTagType | Constructor | AbstractConstructor, side: ArenaTagSide, simulated: boolean, ...args: unknown[] @@ -666,7 +667,11 @@ export class Arena { * @param simulated if `true`, this applies arena tags without changing game state * @param args array of parameters that the called upon tags may need */ - applyTags(tagType: ArenaTagType | Constructor, simulated: boolean, ...args: unknown[]): void { + applyTags( + tagType: ArenaTagType | Constructor | AbstractConstructor, + simulated: boolean, + ...args: unknown[] + ): void { this.applyTagsForSide(tagType, ArenaTagSide.BOTH, simulated, ...args); } @@ -723,7 +728,7 @@ export class Arena { * @param tagType The {@linkcode ArenaTagType} or {@linkcode ArenaTag} to get * @returns either the {@linkcode ArenaTag}, or `undefined` if it isn't there */ - getTag(tagType: ArenaTagType | Constructor): ArenaTag | undefined { + getTag(tagType: ArenaTagType | Constructor | AbstractConstructor): ArenaTag | undefined { return this.getTagOnSide(tagType, ArenaTagSide.BOTH); } @@ -739,7 +744,10 @@ export class Arena { * @param side The {@linkcode ArenaTagSide} to look at * @returns either the {@linkcode ArenaTag}, or `undefined` if it isn't there */ - getTagOnSide(tagType: ArenaTagType | Constructor, side: ArenaTagSide): ArenaTag | undefined { + getTagOnSide( + tagType: ArenaTagType | Constructor | AbstractConstructor, + side: ArenaTagSide, + ): ArenaTag | undefined { return typeof tagType === "string" ? this.tags.find( t => t.tagType === tagType && (side === ArenaTagSide.BOTH || t.side === ArenaTagSide.BOTH || t.side === side), diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 34d8c2b365a..01f137b28fd 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2192,7 +2192,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } const suppressAbilitiesTag = arena.getTag(ArenaTagType.NEUTRALIZING_GAS) as SuppressAbilitiesTag; const suppressOffField = ability.hasAttr("PreSummonAbAttr"); - if ((this.isOnField() || suppressOffField) && suppressAbilitiesTag && !suppressAbilitiesTag.isBeingRemoved()) { + if ((this.isOnField() || suppressOffField) && suppressAbilitiesTag && !suppressAbilitiesTag.beingRemoved) { const thisAbilitySuppressing = ability.hasAttr("PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr"); const hasSuppressingAbility = this.hasAbilityWithAttr("PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr", false); // Neutralizing gas is up - suppress abilities unless they are unsuppressable or this pokemon is responsible for the gas diff --git a/src/system/arena-data.ts b/src/system/arena-data.ts index 9d15ab50fcc..c0ad4a25024 100644 --- a/src/system/arena-data.ts +++ b/src/system/arena-data.ts @@ -1,9 +1,19 @@ import type { ArenaTag } from "#data/arena-tag"; -import { loadArenaTag } from "#data/arena-tag"; +import { loadArenaTag, SerializableArenaTag } from "#data/arena-tag"; import { Terrain } from "#data/terrain"; import { Weather } from "#data/weather"; import type { BiomeId } from "#enums/biome-id"; import { Arena } from "#field/arena"; +import type { ArenaTagTypeData } from "#types/arena-tags"; +import type { NonFunctionProperties } from "#types/type-helpers"; + +export interface SerializedArenaData { + biome: BiomeId; + weather: NonFunctionProperties | null; + terrain: NonFunctionProperties | null; + tags?: ArenaTagTypeData[]; + playerTerasUsed?: number; +} export class ArenaData { public biome: BiomeId; @@ -12,24 +22,26 @@ export class ArenaData { public tags: ArenaTag[]; public playerTerasUsed: number; - constructor(source: Arena | any) { - const sourceArena = source instanceof Arena ? (source as Arena) : null; - this.biome = sourceArena ? sourceArena.biomeType : source.biome; - this.weather = sourceArena - ? sourceArena.weather - : source.weather - ? new Weather(source.weather.weatherType, source.weather.turnsLeft) - : null; - this.terrain = sourceArena - ? sourceArena.terrain - : source.terrain - ? new Terrain(source.terrain.terrainType, source.terrain.turnsLeft) - : null; - this.playerTerasUsed = (sourceArena ? sourceArena.playerTerasUsed : source.playerTerasUsed) ?? 0; - this.tags = []; + constructor(source: Arena | SerializedArenaData) { + // Exclude any unserializable tags from the serialized data (such as ones only lasting 1 turn). + // NOTE: The filter has to be done _after_ map, data loaded from `ArenaTagTypeData` + // is not yet an instance of `ArenaTag` + this.tags = + source.tags + ?.map((t: ArenaTag | ArenaTagTypeData) => loadArenaTag(t)) + ?.filter((tag): tag is SerializableArenaTag => tag instanceof SerializableArenaTag) ?? []; - if (source.tags) { - this.tags = source.tags.map(t => loadArenaTag(t)); + this.playerTerasUsed = source.playerTerasUsed ?? 0; + + if (source instanceof Arena) { + this.biome = source.biomeType; + this.weather = source.weather; + this.terrain = source.terrain; + return; } + + this.biome = source.biome; + this.weather = source.weather ? new Weather(source.weather.weatherType, source.weather.turnsLeft) : null; + this.terrain = source.terrain ? new Terrain(source.terrain.terrainType, source.terrain.turnsLeft) : null; } } diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 62603260cb2..4f251b212b1 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -42,7 +42,7 @@ import * as Modifier from "#modifiers/modifier"; import { MysteryEncounterSaveData } from "#mystery-encounters/mystery-encounter-save-data"; import type { Variant } from "#sprites/variant"; import { achvs } from "#system/achv"; -import { ArenaData } from "#system/arena-data"; +import { ArenaData, type SerializedArenaData } from "#system/arena-data"; import { ChallengeData } from "#system/challenge-data"; import { EggData } from "#system/egg-data"; import { GameStats } from "#system/game-stats"; @@ -1286,7 +1286,7 @@ export class GameData { } case "arena": - return new ArenaData(v); + return new ArenaData(v as SerializedArenaData); case "challenges": { const ret: ChallengeData[] = [];