[Bug][Refactor] Fix loading arena tags (#6110)

* Improve type safety; add missing loadTag overrides to wish and neutralizing gas

* More automatic type safety for arena tags

* Fixup wording of lead comment in arena-tag.ts

* Apply kev's suggestions from code review

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Allow abstract constructors for arena methods

* Address dean's comments from code review

* Add missing newline after interface definition

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com>

* Format with biome

* Update tsdoc of ConditionalProtectTag

Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com>

* Apply kev's suggestions from code review

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com>
This commit is contained in:
Sirz Benjie 2025-07-22 12:26:27 -06:00 committed by GitHub
parent 6937effa16
commit 069e8a47d6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 503 additions and 244 deletions

48
src/@types/arena-tags.ts Normal file
View File

@ -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<ArenaTagType, NonSerializableArenaTagType>;
/**
* 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;

View File

@ -44,3 +44,34 @@ export type Mutable<T> = {
export type InferKeys<O extends Record<keyof any, unknown>, V extends EnumValues<O>> = {
[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<T> = {
[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<Class> = {
[K in keyof Class as Class[K] extends AnyFn ? never : K]: Class[K] extends Array<infer U>
? NonFunctionPropertiesRecursive<U>[]
: Class[K] extends object
? NonFunctionPropertiesRecursive<Class[K]>
: Class[K];
};
export type AbstractConstructor<T> = abstract new (...args: any[]) => T;

View File

@ -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<this>).sourceName = getPokemonNameWithAffix(source);
(this as Mutable<this>).healHp = toDmgValue(source.getMaxHp() / 2);
(this as Mutable<this>).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<WishTag>): void {
super.loadTag(source);
(this as Mutable<this>).battlerIndex = source.battlerIndex;
(this as Mutable<this>).healHp = source.healHp;
(this as Mutable<this>).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<ArenaTrapTag>): 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<SuppressAbilitiesTag>): void {
super.loadTag(source);
(this as Mutable<this>).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<this>).sourceCount++;
this.playActivationMessage(source);
}
public onSourceLeave(arena: Arena): void {
this.sourceCount--;
(this as Mutable<this>).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;
};

View File

@ -13,6 +13,11 @@ export enum TerrainType {
PSYCHIC,
}
export interface SerializedTerrain {
terrainType: TerrainType;
turnsLeft: number;
}
export class Terrain {
public terrainType: TerrainType;
public turnsLeft: number;

View File

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

View File

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

View File

@ -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<ArenaTag>,
tagType: ArenaTagType | Constructor<ArenaTag> | AbstractConstructor<ArenaTag>,
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<ArenaTag>, simulated: boolean, ...args: unknown[]): void {
applyTags(
tagType: ArenaTagType | Constructor<ArenaTag> | AbstractConstructor<ArenaTag>,
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>): ArenaTag | undefined {
getTag(tagType: ArenaTagType | Constructor<ArenaTag> | AbstractConstructor<ArenaTag>): 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<ArenaTag>, side: ArenaTagSide): ArenaTag | undefined {
getTagOnSide(
tagType: ArenaTagType | Constructor<ArenaTag> | AbstractConstructor<ArenaTag>,
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),

View File

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

View File

@ -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<Weather> | null;
terrain: NonFunctionProperties<Terrain> | 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;
}
}

View File

@ -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[] = [];