Merge branch 'beta' into future-sight

This commit is contained in:
NightKev 2025-07-30 21:33:51 -07:00
commit 3e44da240a
7 changed files with 123 additions and 83 deletions

View File

@ -1,6 +1,5 @@
import type { ArenaTagTypeMap } from "#data/arena-tag";
import type { ArenaTagType } from "#enums/arena-tag-type";
import type { NonFunctionProperties } from "#types/type-helpers";
/** Subset of {@linkcode ArenaTagType}s that apply some negative effect to pokemon that switch in ({@link https://bulbapedia.bulbagarden.net/wiki/List_of_moves_that_cause_entry_hazards#List_of_traps | entry hazards} and Imprison. */
export type ArenaTrapTagType =
@ -27,13 +26,13 @@ export type NonSerializableArenaTagType = ArenaTagType.NONE | TurnProtectArenaTa
export type SerializableArenaTagType = Exclude<ArenaTagType, NonSerializableArenaTagType>;
/**
* Type-safe representation of the serializable data of an ArenaTag
* Type-safe representation of an arbitrary, serialized Arena Tag
*/
export type ArenaTagTypeData = NonFunctionProperties<
export type ArenaTagTypeData = Parameters<
ArenaTagTypeMap[keyof {
[K in keyof ArenaTagTypeMap as K extends SerializableArenaTagType ? K : never]: ArenaTagTypeMap[K];
}]
>;
}]["loadTag"]
>[0];
/** Dummy, typescript-only declaration to ensure that
* {@linkcode ArenaTagTypeMap} has a map for all ArenaTagTypes.

View File

@ -108,6 +108,15 @@ export type SerializableBattlerTagType = keyof {
*/
export type NonSerializableBattlerTagType = Exclude<BattlerTagType, SerializableBattlerTagType>;
/**
* Type-safe representation of an arbitrary, serialized Battler Tag
*/
export type BattlerTagTypeData = Parameters<
BattlerTagTypeMap[keyof {
[K in keyof BattlerTagTypeMap as K extends SerializableBattlerTagType ? K : never]: BattlerTagTypeMap[K];
}]["loadTag"]
>[0];
/**
* Dummy, typescript-only declaration to ensure that
* {@linkcode BattlerTagTypeMap} has an entry for all `BattlerTagType`s.

View File

@ -27,37 +27,57 @@ import type {
ArenaTrapTagType,
SerializableArenaTagType,
} from "#types/arena-tags";
import type { Mutable, NonFunctionProperties } from "#types/type-helpers";
import type { Mutable } from "#types/type-helpers";
import { BooleanHolder, NumberHolder, toDmgValue } from "#utils/common";
import i18next from "i18next";
/*
ArenaTags are are meant for effects that are tied to the arena (as opposed to a specific pokemon).
Examples include (but are not limited to)
- Cross-turn effects that persist even if the user/target switches out, such as Happy Hour
- Effects that are applied to a specific side of the field, such as Crafty Shield, Reflect, and Spikes
- Field-Effects, like Gravity and Trick Room
Any arena tag that persists across turns *must* extend from `SerializableArenaTag` in the class definition signature.
Serializable ArenaTags have strict rules for their fields.
These rules ensure that only the data necessary to reconstruct the tag is serialized, and that the
session loader is able to deserialize saved tags correctly.
If the data is static (i.e. it is always the same for all instances of the class, such as the
type that is weakened by Mud Sport/Water Sport), then it must not be defined as a field, and must
instead be defined as a getter.
A static property is also acceptable, though static properties are less ergonomic with inheritance.
If the data is mutable (i.e. it can change over the course of the tag's lifetime), then it *must*
be defined as a field, and it must be set in the `loadTag` method.
Such fields cannot be marked as `private/protected`, as if they were, typescript would omit them from
types that are based off of the class, namely, `ArenaTagTypeData`. It is preferrable to trade the
type-safety of private/protected fields for the type safety when deserializing arena tags from save data.
For data that is mutable only within a turn (e.g. SuppressAbilitiesTag's beingRemoved field),
where it does not make sense to be serialized, the field should use ES2020's private field syntax (a `#` prepended to the field name).
If the field should be accessible outside of the class, then a public getter should be used.
/**
* @module
* ArenaTags are are meant for effects that are tied to the arena (as opposed to a specific pokemon).
* Examples include (but are not limited to)
* - Cross-turn effects that persist even if the user/target switches out, such as Happy Hour
* - Effects that are applied to a specific side of the field, such as Crafty Shield, Reflect, and Spikes
* - Field-Effects, like Gravity and Trick Room
*
* Any arena tag that persists across turns *must* extend from `SerializableArenaTag` in the class definition signature.
*
* Serializable ArenaTags have strict rules for their fields.
* These rules ensure that only the data necessary to reconstruct the tag is serialized, and that the
* session loader is able to deserialize saved tags correctly.
*
* If the data is static (i.e. it is always the same for all instances of the class, such as the
* type that is weakened by Mud Sport/Water Sport), then it must not be defined as a field, and must
* instead be defined as a getter.
* A static property is also acceptable, though static properties are less ergonomic with inheritance.
*
* If the data is mutable (i.e. it can change over the course of the tag's lifetime), then it *must*
* be defined as a field, and it must be set in the `loadTag` method.
* Such fields cannot be marked as `private`/`protected`; if they were, Typescript would omit them from
* types that are based off of the class, namely, `ArenaTagTypeData`. It is preferrable to trade the
* type-safety of private/protected fields for the type safety when deserializing arena tags from save data.
*
* For data that is mutable only within a turn (e.g. SuppressAbilitiesTag's beingRemoved field),
* where it does not make sense to be serialized, the field should use ES2020's
* [private field syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_elements#private_fields).
* If the field should be accessible outside of the class, then a public getter should be used.
*
* If any new serializable fields *are* added, then the class *must* override the
* `loadTag` method to set the new fields. Its signature *must* match the example below,
* ```
* class ExampleTag extends SerializableArenaTag {
* // Example, if we add 2 new fields that should be serialized:
* public a: string;
* public b: number;
* // Then we must also define a loadTag method with one of the following signatures
* public override loadTag(source: BaseArenaTag & Pick<ExampleTag, "tagType" | "a" | "b"): void;
* public override loadTag<const T extends this>(source: BaseArenaTag & Pick<T, "tagType" | "a" | "b">): void;
* public override loadTag(source: NonFunctionProperties<ExampleTag>): void;
* }
* ```
* Notes
* - If the class has any subclasses, then the second form of `loadTag` *must* be used.
* - The third form *must not* be used if the class has any getters, as typescript would expect such fields to be
* present in `source`.
*/
/** Interface containing the serializable fields of ArenaTagData. */
@ -142,9 +162,9 @@ export abstract class ArenaTag implements BaseArenaTag {
/**
* When given a arena tag or json representing one, load the data for it.
* This is meant to be inherited from by any arena tag with custom attributes
* @param source - The {@linkcode BaseArenaTag} being loaded
* @param source - The arena tag being loaded
*/
loadTag(source: BaseArenaTag): void {
loadTag<const T extends this>(source: BaseArenaTag & Pick<T, "tagType">): void {
this.turnCount = source.turnCount;
this.sourceMove = source.sourceMove;
this.sourceId = source.sourceId;
@ -764,7 +784,7 @@ export abstract class ArenaTrapTag extends SerializableArenaTag {
: Phaser.Math.Linear(0, 1 / Math.pow(2, this.layers), Math.min(pokemon.getHpRatio(), 0.5) * 2);
}
loadTag(source: NonFunctionProperties<ArenaTrapTag>): void {
public loadTag<T extends this>(source: BaseArenaTag & Pick<T, "tagType" | "layers" | "maxLayers">): void {
super.loadTag(source);
this.layers = source.layers;
this.maxLayers = source.maxLayers;
@ -1490,7 +1510,7 @@ export class SuppressAbilitiesTag extends SerializableArenaTag {
this.#beingRemoved = false;
}
public override loadTag(source: NonFunctionProperties<SuppressAbilitiesTag>): void {
public override loadTag(source: BaseArenaTag & Pick<SuppressAbilitiesTag, "tagType" | "sourceCount">): void {
super.loadTag(source);
(this as Mutable<this>).sourceCount = source.sourceCount;
}

View File

@ -69,6 +69,24 @@ import { BooleanHolder, coerceArray, getFrameMs, isNullOrUndefined, NumberHolder
* be declared as a public readonly propety. Then, in the `loadTag` method (or any method inside the class that needs to adjust the property)
* use `(this as Mutable<this>).propertyName = value;`
* These rules ensure that Typescript is aware of the shape of the serialized version of the class.
*
* If any new serializable fields *are* added, then the class *must* override the
* `loadTag` method to set the new fields. Its signature *must* match the example below:
* ```
* class ExampleTag extends SerializableBattlerTag {
* // Example, if we add 2 new fields that should be serialized:
* public a: string;
* public b: number;
* // Then we must also define a loadTag method with one of the following signatures
* public override loadTag(source: BaseBattlerTag & Pick<ExampleTag, "tagType" | "a" | "b"): void;
* public override loadTag<const T extends this>(source: BaseBattlerTag & Pick<T, "tagType" | "a" | "b">): void;
* public override loadTag(source: NonFunctionProperties<ExampleTag>): void;
* }
* ```
* Notes
* - If the class has any subclasses, then the second form of `loadTag` *must* be used.
* - The third form *must not* be used if the class has any getters, as typescript would expect such fields to be
* present in `source`.
*/
/** Interface containing the serializable fields of BattlerTag */
@ -168,7 +186,7 @@ export class BattlerTag implements BaseBattlerTag {
* Should be inherited from by any battler tag with custom attributes.
* @param source - An object containing the fields needed to reconstruct this tag.
*/
loadTag(source: BaseBattlerTag): void {
public loadTag<const T extends this>(source: BaseBattlerTag & Pick<T, "tagType">): void {
this.turnCount = source.turnCount;
this.sourceMove = source.sourceMove;
this.sourceId = source.sourceId;
@ -386,7 +404,7 @@ export class DisabledTag extends MoveRestrictionBattlerTag {
});
}
override loadTag(source: NonFunctionProperties<DisabledTag>): void {
public override loadTag(source: BaseBattlerTag & Pick<DisabledTag, "tagType" | "moveId">): void {
super.loadTag(source);
(this as Mutable<this>).moveId = source.moveId;
}
@ -437,7 +455,7 @@ export class GorillaTacticsTag extends MoveRestrictionBattlerTag {
* Loads the Gorilla Tactics Battler Tag along with its unique class variable moveId
* @param source - Object containing the fields needed to reconstruct this tag.
*/
override loadTag(source: NonFunctionProperties<GorillaTacticsTag>): void {
public override loadTag(source: BaseBattlerTag & Pick<GorillaTacticsTag, "tagType" | "moveId">): void {
super.loadTag(source);
(this as Mutable<GorillaTacticsTag>).moveId = source.moveId;
}
@ -971,6 +989,12 @@ export class InfatuatedTag extends SerializableBattlerTag {
}
}
/**
* Battler tag for the "Seeded" effect applied by {@linkcode MoveId.LEECH_SEED | Leech Seed} and
* {@linkcode MoveId.SAPPY_SEED | Sappy Seed}
*
* @sealed
*/
export class SeedTag extends SerializableBattlerTag {
public override readonly tagType = BattlerTagType.SEEDED;
public readonly sourceIndex: BattlerIndex;
@ -983,7 +1007,7 @@ export class SeedTag extends SerializableBattlerTag {
* When given a battler tag or json representing one, load the data for it.
* @param source - An object containing the fields needed to reconstruct this tag.
*/
override loadTag(source: NonFunctionProperties<SeedTag>): void {
public override loadTag(source: BaseBattlerTag & Pick<SeedTag, "tagType" | "sourceIndex">): void {
super.loadTag(source);
(this as Mutable<this>).sourceIndex = source.sourceIndex;
}
@ -1194,6 +1218,7 @@ export class FrenzyTag extends SerializableBattlerTag {
/**
* Applies the effects of {@linkcode MoveId.ENCORE} onto the target Pokemon.
* Encore forces the target Pokemon to use its most-recent move for 3 turns.
* @sealed
*/
export class EncoreTag extends MoveRestrictionBattlerTag {
public override readonly tagType = BattlerTagType.ENCORE;
@ -1210,7 +1235,7 @@ export class EncoreTag extends MoveRestrictionBattlerTag {
);
}
override loadTag(source: NonFunctionProperties<EncoreTag>): void {
public override loadTag(source: NonFunctionProperties<EncoreTag>): void {
super.loadTag(source);
this.moveId = source.moveId;
}
@ -2085,7 +2110,7 @@ export class HighestStatBoostTag extends AbilityBattlerTag {
* When given a battler tag or json representing one, load the data for it.
* @param source - An object containing the fields needed to reconstruct this tag.
*/
loadTag(source: NonFunctionProperties<HighestStatBoostTag>): void {
public override loadTag<T extends this>(source: BaseBattlerTag & Pick<T, "tagType" | "stat" | "multiplier">): void {
super.loadTag(source);
this.stat = source.stat as Stat;
this.multiplier = source.multiplier;
@ -2564,6 +2589,7 @@ export class IceFaceBlockDamageTag extends FormBlockDamageTag {
/**
* Battler tag indicating a Tatsugiri with {@link https://bulbapedia.bulbagarden.net/wiki/Commander_(Ability) | Commander}
* has entered the tagged Pokemon's mouth.
* @sealed
*/
export class CommandedTag extends SerializableBattlerTag {
public override readonly tagType = BattlerTagType.COMMANDED;
@ -2607,6 +2633,7 @@ export class CommandedTag extends SerializableBattlerTag {
* - Stat changes on removal of (all) stacks.
* - Removing stacks decreases DEF and SPDEF, independently, by one stage for each stack that successfully changed
* the stat when added.
* @sealed
*/
export class StockpilingTag extends SerializableBattlerTag {
public override readonly tagType = BattlerTagType.STOCKPILING;
@ -2632,7 +2659,7 @@ export class StockpilingTag extends SerializableBattlerTag {
}
};
override loadTag(source: NonFunctionProperties<StockpilingTag>): void {
public override loadTag(source: NonFunctionProperties<StockpilingTag>): void {
super.loadTag(source);
this.stockpiledCount = source.stockpiledCount || 0;
this.statChangeCounts = {
@ -2979,7 +3006,7 @@ export class AutotomizedTag extends SerializableBattlerTag {
this.onAdd(pokemon);
}
loadTag(source: NonFunctionProperties<AutotomizedTag>): void {
public override loadTag(source: NonFunctionProperties<AutotomizedTag>): void {
super.loadTag(source);
this.autotomizeCount = source.autotomizeCount;
}
@ -3129,7 +3156,7 @@ export class SubstituteTag extends SerializableBattlerTag {
* When given a battler tag or json representing one, load the data for it.
* @param source - An object containing the necessary properties to load the tag
*/
override loadTag(source: NonFunctionProperties<SubstituteTag>): void {
public override loadTag(source: BaseBattlerTag & Pick<SubstituteTag, "tagType" | "hp">): void {
super.loadTag(source);
this.hp = source.hp;
}

View File

@ -425,9 +425,8 @@ export abstract class Move implements Localizable {
/**
* Sets the {@linkcode MoveFlags.MAKES_CONTACT} flag for the calling Move
* @param setFlag Default `true`, set to `false` if the move doesn't make contact
* @see {@linkcode AbilityId.STATIC}
* @returns The {@linkcode Move} that called this function
* @param setFlag - Whether the move should make contact; default `true`
* @returns `this`
*/
makesContact(setFlag: boolean = true): this {
this.setFlag(MoveFlags.MAKES_CONTACT, setFlag);
@ -3634,8 +3633,7 @@ export class CutHpStatStageBoostAttr extends StatStageChangeAttr {
/**
* Attribute implementing the stat boosting effect of {@link https://bulbapedia.bulbagarden.net/wiki/Order_Up_(move) | Order Up}.
* If the user has a Pokemon with {@link https://bulbapedia.bulbagarden.net/wiki/Commander_(Ability) | Commander} in their mouth,
* one of the user's stats are increased by 1 stage, depending on the "commanding" Pokemon's form. This effect does not respect
* effect chance, but Order Up itself may be boosted by Sheer Force.
* one of the user's stats are increased by 1 stage, depending on the "commanding" Pokemon's form.
*/
export class OrderUpStatBoostAttr extends MoveEffectAttr {
constructor() {
@ -9289,7 +9287,7 @@ export function initMoves() {
new SelfStatusMove(MoveId.STOCKPILE, PokemonType.NORMAL, -1, 20, -1, 0, 3)
.condition(user => (user.getTag(StockpilingTag)?.stockpiledCount ?? 0) < 3)
.attr(AddBattlerTagAttr, BattlerTagType.STOCKPILING, true),
new AttackMove(MoveId.SPIT_UP, PokemonType.NORMAL, MoveCategory.SPECIAL, -1, -1, 10, -1, 0, 3)
new AttackMove(MoveId.SPIT_UP, PokemonType.NORMAL, MoveCategory.SPECIAL, -1, 100, 10, -1, 0, 3)
.condition(hasStockpileStacksCondition)
.attr(SpitUpPowerAttr, 100)
.attr(RemoveBattlerTagAttr, [ BattlerTagType.STOCKPILING ], true),
@ -9532,7 +9530,7 @@ export function initMoves() {
new AttackMove(MoveId.SAND_TOMB, PokemonType.GROUND, MoveCategory.PHYSICAL, 35, 85, 15, -1, 0, 3)
.attr(TrapAttr, BattlerTagType.SAND_TOMB)
.makesContact(false),
new AttackMove(MoveId.SHEER_COLD, PokemonType.ICE, MoveCategory.SPECIAL, 200, 20, 5, -1, 0, 3)
new AttackMove(MoveId.SHEER_COLD, PokemonType.ICE, MoveCategory.SPECIAL, 200, 30, 5, -1, 0, 3)
.attr(IceNoEffectTypeAttr)
.attr(OneHitKOAttr)
.attr(SheerColdAccuracyAttr),
@ -10457,7 +10455,7 @@ export function initMoves() {
.attr(RemoveBattlerTagAttr, [ BattlerTagType.FLYING, BattlerTagType.FLOATING, BattlerTagType.TELEKINESIS ])
.makesContact(false)
.target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(MoveId.THOUSAND_WAVES, PokemonType.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
new AttackMove(MoveId.THOUSAND_WAVES, PokemonType.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, 100, 0, 6)
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1, 1, true)
.makesContact(false)
.target(MoveTarget.ALL_NEAR_ENEMIES),
@ -10992,7 +10990,8 @@ export function initMoves() {
new StatusMove(MoveId.LIFE_DEW, PokemonType.WATER, -1, 10, -1, 0, 8)
.attr(HealAttr, 0.25, true, false)
.target(MoveTarget.USER_AND_ALLIES)
.ignoresProtect(),
.ignoresProtect()
.triageMove(),
new SelfStatusMove(MoveId.OBSTRUCT, PokemonType.DARK, 100, 10, -1, 4, 8)
.attr(ProtectAttr, BattlerTagType.OBSTRUCT)
.condition(failIfLastCondition),
@ -11070,7 +11069,8 @@ export function initMoves() {
new StatusMove(MoveId.JUNGLE_HEALING, PokemonType.GRASS, -1, 10, -1, 0, 8)
.attr(HealAttr, 0.25, true, false)
.attr(HealStatusEffectAttr, false, getNonVolatileStatusEffects())
.target(MoveTarget.USER_AND_ALLIES),
.target(MoveTarget.USER_AND_ALLIES)
.triageMove(),
new AttackMove(MoveId.WICKED_BLOW, PokemonType.DARK, MoveCategory.PHYSICAL, 75, 100, 5, -1, 0, 8)
.attr(CritOnlyAttr)
.punchingMove(),
@ -11298,7 +11298,7 @@ export function initMoves() {
.makesContact(false),
new AttackMove(MoveId.LUMINA_CRASH, PokemonType.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 10, 100, 0, 9)
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -2),
new AttackMove(MoveId.ORDER_UP, PokemonType.DRAGON, MoveCategory.PHYSICAL, 80, 100, 10, 100, 0, 9)
new AttackMove(MoveId.ORDER_UP, PokemonType.DRAGON, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 9)
.attr(OrderUpStatBoostAttr)
.makesContact(false),
new AttackMove(MoveId.JET_PUNCH, PokemonType.WATER, MoveCategory.PHYSICAL, 60, 100, 15, -1, 1, 9)
@ -11481,7 +11481,7 @@ export function initMoves() {
.attr(IvyCudgelTypeAttr)
.attr(HighCritAttr)
.makesContact(false),
new ChargingAttackMove(MoveId.ELECTRO_SHOT, PokemonType.ELECTRIC, MoveCategory.SPECIAL, 130, 100, 10, 100, 0, 9)
new ChargingAttackMove(MoveId.ELECTRO_SHOT, PokemonType.ELECTRIC, MoveCategory.SPECIAL, 130, 100, 10, -1, 0, 9)
.chargeText(i18next.t("moveTriggers:absorbedElectricity", { pokemonName: "{USER}" }))
.chargeAttr(StatStageChangeAttr, [ Stat.SPATK ], 1, true)
.chargeAttr(WeatherInstantChargeAttr, [ WeatherType.RAIN, WeatherType.HEAVY_RAIN ]),

View File

@ -4,15 +4,19 @@
*/
export enum MoveFlags {
NONE = 0,
/**
* Whether the move makes contact.
* Set by default on all contact moves, and unset by default on all special moves.
*/
MAKES_CONTACT = 1 << 0,
IGNORE_PROTECT = 1 << 1,
/**
* Sound-based moves have the following effects:
* - Pokemon with the {@linkcode AbilityId.SOUNDPROOF Soundproof Ability} are unaffected by other Pokemon's sound-based moves.
* - Pokemon affected by {@linkcode MoveId.THROAT_CHOP Throat Chop} cannot use sound-based moves for two turns.
* - Sound-based moves used by a Pokemon with {@linkcode AbilityId.LIQUID_VOICE Liquid Voice} become Water-type moves.
* - Sound-based moves used by a Pokemon with {@linkcode AbilityId.PUNK_ROCK Punk Rock} are boosted by 30%. Pokemon with Punk Rock also take half damage from sound-based moves.
* - All sound-based moves (except Howl) can hit Pokemon behind an active {@linkcode MoveId.SUBSTITUTE Substitute}.
* - Pokemon with the {@linkcode AbilityId.SOUNDPROOF | Soundproof} Ability are unaffected by other Pokemon's sound-based moves.
* - Pokemon affected by {@linkcode MoveId.THROAT_CHOP | Throat Chop} cannot use sound-based moves for two turns.
* - Sound-based moves used by a Pokemon with {@linkcode AbilityId.LIQUID_VOICE | Liquid Voice} become Water-type moves.
* - Sound-based moves used by a Pokemon with {@linkcode AbilityId.PUNK_ROCK | Punk Rock} are boosted by 30%. Pokemon with Punk Rock also take half damage from sound-based moves.
* - All sound-based moves (except Howl) can hit Pokemon behind an active {@linkcode MoveId.SUBSTITUTE | Substitute}.
*
* cf https://bulbapedia.bulbagarden.net/wiki/Sound-based_move
*/

View File

@ -65,23 +65,4 @@ describe("Moves - Order Up", () => {
affectedStats.forEach(st => expect(dondozo.getStatStage(st)).toBe(st === stat ? 3 : 2));
},
);
it("should be boosted by Sheer Force while still applying a stat boost", async () => {
game.override.passiveAbility(AbilityId.SHEER_FORCE).starterForms({ [SpeciesId.TATSUGIRI]: 0 });
await game.classicMode.startBattle([SpeciesId.TATSUGIRI, SpeciesId.DONDOZO]);
const [tatsugiri, dondozo] = game.scene.getPlayerField();
expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY);
expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined();
game.move.select(MoveId.ORDER_UP, 1, BattlerIndex.ENEMY);
expect(game.scene.currentBattle.turnCommands[0]?.skip).toBeTruthy();
await game.phaseInterceptor.to("BerryPhase", false);
expect(dondozo.waveData.abilitiesApplied.has(AbilityId.SHEER_FORCE)).toBeTruthy();
expect(dondozo.getStatStage(Stat.ATK)).toBe(3);
});
});