diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index ff86937622b..7b32f224b8d 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -1246,7 +1246,7 @@ export class MoveTypeChangeAbAttr extends PreAttackAbAttr { /** * Determine if the move type change attribute can be applied - * + * * Can be applied if: * - The ability's condition is met, e.g. pixilate only boosts normal moves, * - The move is not forbidden from having its type changed by an ability, e.g. {@linkcode Moves.MULTI_ATTACK} @@ -1262,7 +1262,7 @@ export class MoveTypeChangeAbAttr extends PreAttackAbAttr { */ override canApplyPreAttack(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _defender: Pokemon | null, move: Move, _args: [NumberHolder?, NumberHolder?, ...any]): boolean { return (!this.condition || this.condition(pokemon, _defender, move)) && - !noAbilityTypeOverrideMoves.has(move.id) && + !noAbilityTypeOverrideMoves.has(move.id) && (!pokemon.isTerastallized || (move.id !== Moves.TERA_BLAST && (move.id !== Moves.TERA_STARSTORM || pokemon.getTeraType() !== PokemonType.STELLAR || !pokemon.hasSpecies(Species.TERAPAGOS)))); @@ -7444,7 +7444,7 @@ export function initAbilities() { new Ability(Abilities.ORICHALCUM_PULSE, 9) .attr(PostSummonWeatherChangeAbAttr, WeatherType.SUNNY) .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.SUNNY) - .conditionalAttr(getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN), StatMultiplierAbAttr, Stat.ATK, 4 / 3), + .conditionalAttr(getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN), StatMultiplierAbAttr, Stat.ATK, 4 / 3), // No game freak rounding jank new Ability(Abilities.HADRON_ENGINE, 9) .attr(PostSummonTerrainChangeAbAttr, TerrainType.ELECTRIC) .attr(PostBiomeChangeTerrainChangeAbAttr, TerrainType.ELECTRIC) diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 19c94a8a045..4b4e8fdcd1a 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -827,32 +827,37 @@ class ToxicSpikesTag extends ArenaTrapTag { } override activateTrap(pokemon: Pokemon, simulated: boolean): boolean { - if (pokemon.isGrounded()) { - if (simulated) { - return true; - } - if (pokemon.isOfType(PokemonType.POISON)) { - this.neutralized = true; - if (globalScene.arena.removeTag(this.tagType)) { - globalScene.queueMessage( - i18next.t("arenaTag:toxicSpikesActivateTrapPoison", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - moveName: this.getMoveName(), - }), - ); - return true; - } - } else if (!pokemon.status) { - const toxic = this.layers > 1; - if ( - pokemon.trySetStatus(!toxic ? StatusEffect.POISON : StatusEffect.TOXIC, true, null, 0, this.getMoveName()) - ) { - return true; - } - } + if (!pokemon.isGrounded()) { + return false; + } + if (simulated) { + return true; } - return false; + // poision types will neutralize toxic spikes + if (pokemon.isOfType(PokemonType.POISON)) { + this.neutralized = true; + if (globalScene.arena.removeTag(this.tagType)) { + globalScene.queueMessage( + i18next.t("arenaTag:toxicSpikesActivateTrapPoison", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), + moveName: this.getMoveName(), + }), + ); + } + return true; + } + + if (pokemon.status) { + return false; + } + + return pokemon.trySetStatus( + this.layers === 1 ? StatusEffect.POISON : StatusEffect.TOXIC, + true, + null, + this.getMoveName(), + ); } getMatchupScoreMultiplier(pokemon: Pokemon): number { diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 235cb954ea5..9bb00d9a6da 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -2440,31 +2440,36 @@ export class WaterShurikenMultiHitTypeAttr extends ChangeMultiHitTypeAttr { export class StatusEffectAttr extends MoveEffectAttr { public effect: StatusEffect; - public turnsRemaining?: number; - public overrideStatus: boolean = false; + private overrideStatus: boolean; - constructor(effect: StatusEffect, selfTarget?: boolean, turnsRemaining?: number, overrideStatus: boolean = false) { + constructor(effect: StatusEffect, selfTarget = false, overrideStatus = false) { super(selfTarget); this.effect = effect; - this.turnsRemaining = turnsRemaining; - this.overrideStatus = overrideStatus; } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true); const statusCheck = moveChance < 0 || moveChance === 100 || user.randBattleSeedInt(100) < moveChance; + if (!statusCheck) { + return false; + } + const quiet = move.category !== MoveCategory.STATUS; - if (statusCheck) { - const pokemon = this.selfTarget ? user : target; - if (user !== target && move.category === MoveCategory.STATUS && !target.canSetStatus(this.effect, quiet, false, user, true)) { - return false; - } - if (((!pokemon.status || this.overrideStatus) || (pokemon.status.effect === this.effect && moveChance < 0)) - && pokemon.trySetStatus(this.effect, true, user, this.turnsRemaining, null, this.overrideStatus, quiet)) { - applyPostAttackAbAttrs(ConfusionOnStatusEffectAbAttr, user, target, move, null, false, this.effect); - return true; - } + + // TODO: why + const pokemon = this.selfTarget ? user : target; + if (user !== target && move.category === MoveCategory.STATUS && !target.canSetStatus(this.effect, quiet, this.overrideStatus, user, true)) { + return false; + } + + // TODO: What does a chance of -1 have to do with any of this??? + if ( + (!pokemon.status || (pokemon.status.effect === this.effect && moveChance < 0)) + && pokemon.trySetStatus(this.effect, true, user, null, this.overrideStatus, quiet) + ) { + applyPostAttackAbAttrs(ConfusionOnStatusEffectAbAttr, user, target, move, null, false, this.effect); + return true; } return false; } @@ -2478,11 +2483,37 @@ export class StatusEffectAttr extends MoveEffectAttr { } } +/** + * Attribute to put the target to sleep for a fixed duration and cure its status. + * Used for {@linkcode Moves.REST}. + */ +export class RestAttr extends StatusEffectAttr { + private duration: number; + + constructor( + duration: number, + overrideStatus: boolean + ){ + // Sleep is the only duration-based status ATM + super(StatusEffect.SLEEP, true, overrideStatus); + this.duration = duration; + } + + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + const didStatus = super.apply(user, target, move, args); + if (didStatus && user.status?.effect === this.effect) { + user.status.sleepTurnsRemaining = this.duration; + } + return didStatus; + } +} + + export class MultiStatusEffectAttr extends StatusEffectAttr { public effects: StatusEffect[]; - constructor(effects: StatusEffect[], selfTarget?: boolean, turnsRemaining?: number, overrideStatus?: boolean) { - super(effects[0], selfTarget, turnsRemaining, overrideStatus); + constructor(effects: StatusEffect[], selfTarget?: boolean) { + super(effects[0], selfTarget); this.effects = effects; } @@ -8704,7 +8735,7 @@ export function initMoves() { .attr(MultiHitAttr, MultiHitType._2) .makesContact(false), new SelfStatusMove(Moves.REST, PokemonType.PSYCHIC, -1, 5, -1, 0, 1) - .attr(StatusEffectAttr, StatusEffect.SLEEP, true, 3, true) + .attr(RestAttr, 3, true) .attr(HealAttr, 1, true) .condition((user, target, move) => !user.isFullHp() && user.canSetStatus(StatusEffect.SLEEP, true, true, user)) .triageMove(), diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index fcca0c5614a..7c698ac5dad 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -5433,12 +5433,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Checks if a status effect can be applied to the Pokemon. + * Check if a status effect can be applied to this {@linckode Pokemon}. * - * @param effect The {@linkcode StatusEffect} whose applicability is being checked - * @param quiet Whether in-battle messages should trigger or not - * @param overrideStatus Whether the Pokemon's current status can be overriden - * @param sourcePokemon The Pokemon that is setting the status effect + * @param effect - The {@linkcode StatusEffect} whose applicability is being checked + * @param quiet - Whether to suppress in-battle messages for status checks; default `false` + * @param overrideStatus - Whether to allow overriding the Pokemon's current status with a different one; default `false` + * @param sourcePokemon - The {@linkcode Pokemon} applying the status effect to the target * @param ignoreField Whether any field effects (weather, terrain, etc.) should be considered */ canSetStatus( @@ -5586,7 +5586,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { effect?: StatusEffect, asPhase = false, sourcePokemon: Pokemon | null = null, - turnsRemaining = 0, sourceText: string | null = null, overrideStatus?: boolean, quiet = true, @@ -5618,7 +5617,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { new ObtainStatusEffectPhase( this.getBattlerIndex(), effect, - turnsRemaining, sourceText, sourcePokemon, ), diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 94bb0e3419a..162554aa9c6 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -1756,7 +1756,7 @@ export class TurnStatusEffectModifier extends PokemonHeldItemModifier { * @returns `true` if the status effect was applied successfully */ override apply(pokemon: Pokemon): boolean { - return pokemon.trySetStatus(this.effect, true, undefined, undefined, this.type.name); + return pokemon.trySetStatus(this.effect, true, pokemon, this.type.name); } getMaxHeldItemCount(_pokemon: Pokemon): number { diff --git a/src/phases/obtain-status-effect-phase.ts b/src/phases/obtain-status-effect-phase.ts index 47cae2dcbf6..452b48f9109 100644 --- a/src/phases/obtain-status-effect-phase.ts +++ b/src/phases/obtain-status-effect-phase.ts @@ -10,58 +10,54 @@ import { SpeciesFormChangeStatusEffectTrigger } from "#app/data/pokemon-forms"; import { applyPostSetStatusAbAttrs, PostSetStatusAbAttr } from "#app/data/abilities/ability"; import { isNullOrUndefined } from "#app/utils/common"; +/** The phase where pokemon obtain status effects. */ export class ObtainStatusEffectPhase extends PokemonPhase { private statusEffect?: StatusEffect; - private turnsRemaining?: number; private sourceText?: string | null; private sourcePokemon?: Pokemon | null; constructor( battlerIndex: BattlerIndex, statusEffect?: StatusEffect, - turnsRemaining?: number, sourceText?: string | null, sourcePokemon?: Pokemon | null, ) { super(battlerIndex); this.statusEffect = statusEffect; - this.turnsRemaining = turnsRemaining; this.sourceText = sourceText; this.sourcePokemon = sourcePokemon; } start() { const pokemon = this.getPokemon(); - if (pokemon && !pokemon.status) { - if (pokemon.trySetStatus(this.statusEffect, false, this.sourcePokemon)) { - if (this.turnsRemaining) { - pokemon.status!.sleepTurnsRemaining = this.turnsRemaining; - } - pokemon.updateInfo(true); - new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect! - 1), pokemon).play(false, () => { - globalScene.queueMessage( - getStatusEffectObtainText( - this.statusEffect, - getPokemonNameWithAffix(pokemon), - this.sourceText ?? undefined, - ), - ); - if (!isNullOrUndefined(this.statusEffect) && this.statusEffect !== StatusEffect.FAINT) { - globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeStatusEffectTrigger, true); - // If mold breaker etc was used to set this status, it shouldn't apply to abilities activated afterwards - globalScene.arena.setIgnoreAbilities(false); - applyPostSetStatusAbAttrs(PostSetStatusAbAttr, pokemon, this.statusEffect, this.sourcePokemon); - } - this.end(); - }); - return; - } - } else if (pokemon.status?.effect === this.statusEffect) { + if (pokemon.status?.effect === this.statusEffect) { globalScene.queueMessage( getStatusEffectOverlapText(this.statusEffect ?? StatusEffect.NONE, getPokemonNameWithAffix(pokemon)), ); + this.end(); + return; } - this.end(); + + if (!pokemon.trySetStatus(this.statusEffect, false, this.sourcePokemon)) { + // status application passes + this.end(); + return; + } + + pokemon.updateInfo(true); + new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect! - 1), pokemon).play(false, () => { + globalScene.queueMessage( + getStatusEffectObtainText(this.statusEffect, getPokemonNameWithAffix(pokemon), this.sourceText ?? undefined), + ); + if (!isNullOrUndefined(this.statusEffect) && this.statusEffect !== StatusEffect.FAINT) { + globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeStatusEffectTrigger, true); + // If mold breaker etc was used to set this status, it shouldn't apply to abilities activated afterwards + // TODO: We may need to reset this for Ice Fang, etc. + globalScene.arena.setIgnoreAbilities(false); + applyPostSetStatusAbAttrs(PostSetStatusAbAttr, pokemon, this.statusEffect, this.sourcePokemon); + } + this.end(); + }); } } diff --git a/test/abilities/corrosion.test.ts b/test/abilities/corrosion.test.ts index c72aef9f0a3..d054cbd5a39 100644 --- a/test/abilities/corrosion.test.ts +++ b/test/abilities/corrosion.test.ts @@ -1,6 +1,7 @@ import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -22,7 +23,7 @@ describe("Abilities - Corrosion", () => { beforeEach(() => { game = new GameManager(phaserGame); game.override - .moveset([Moves.SPLASH]) + .moveset([Moves.SPLASH, Moves.TOXIC, Moves.TOXIC_SPIKES]) .battleStyle("single") .disableCrits() .enemySpecies(Species.GRIMER) @@ -30,9 +31,35 @@ describe("Abilities - Corrosion", () => { .enemyMoveset(Moves.TOXIC); }); - it("If a Poison- or Steel-type Pokémon with this Ability poisons a target with Synchronize, Synchronize does not gain the ability to poison Poison- or Steel-type Pokémon.", async () => { + it.each<{ name: string; species: Species }>([ + { name: "Poison", species: Species.GRIMER }, + { name: "Steel", species: Species.KLINK }, + ])("should grant the user the ability to poison $name-type opponents", async ({ species }) => { game.override.ability(Abilities.SYNCHRONIZE); - await game.classicMode.startBattle([Species.FEEBAS]); + await game.classicMode.startBattle([species]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + expect(enemyPokemon.status).toBeUndefined(); + + game.move.select(Moves.TOXIC); + await game.phaseInterceptor.to("BerryPhase"); + expect(enemyPokemon.status).toBeDefined(); + }); + + it("should not affect Toxic Spikes", async () => { + await game.classicMode.startBattle([Species.SALANDIT]); + + game.move.select(Moves.TOXIC_SPIKES); + await game.doKillOpponents(); + await game.toNextWave(); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + expect(enemyPokemon.status).toBeUndefined(); + }); + + it("should not affect an opponent's Synchronize ability", async () => { + game.override.ability(Abilities.SYNCHRONIZE); + await game.classicMode.startBattle([Species.ARBOK]); const playerPokemon = game.scene.getPlayerPokemon(); const enemyPokemon = game.scene.getEnemyPokemon(); @@ -43,4 +70,16 @@ describe("Abilities - Corrosion", () => { expect(playerPokemon!.status).toBeDefined(); expect(enemyPokemon!.status).toBeUndefined(); }); + + it("should affect the user's held a Toxic Orb", async () => { + game.override.startingHeldItems([{ name: "TOXIC_ORB", count: 1 }]); + await game.classicMode.startBattle([Species.SALAZZLE]); + + const salazzle = game.scene.getPlayerPokemon()!; + expect(salazzle.status?.effect).toBeUndefined(); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + expect(salazzle.status?.effect).toBe(StatusEffect.TOXIC); + }); }); diff --git a/test/moves/rest.test.ts b/test/moves/rest.test.ts new file mode 100644 index 00000000000..33dfb46e09c --- /dev/null +++ b/test/moves/rest.test.ts @@ -0,0 +1,134 @@ +import { MoveResult } from "#app/field/pokemon"; +import { Abilities } from "#enums/abilities"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; +import GameManager from "#test/testUtils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Rest", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([Moves.REST, Moves.SLEEP_TALK]) + .ability(Abilities.BALL_FETCH) + .battleStyle("single") + .disableCrits() + .enemySpecies(Species.EKANS) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should fully heal the user, cure its status and put it to sleep", async () => { + await game.classicMode.startBattle([Species.SNORLAX]); + + const snorlax = game.scene.getPlayerPokemon()!; + snorlax.hp = 1; + snorlax.trySetStatus(StatusEffect.POISON); + expect(snorlax.status?.effect).toBe(StatusEffect.POISON); + + game.move.select(Moves.REST); + await game.phaseInterceptor.to("BerryPhase"); + + expect(snorlax.isFullHp()).toBe(true); + expect(snorlax.status?.effect).toBe(StatusEffect.SLEEP); + }); + + it("should preserve non-volatile conditions", async () => { + await game.classicMode.startBattle([Species.SNORLAX]); + + const snorlax = game.scene.getPlayerPokemon()!; + snorlax.hp = 1; + snorlax.addTag(BattlerTagType.CONFUSED, 999); + + game.move.select(Moves.REST); + await game.phaseInterceptor.to("BerryPhase"); + + expect(snorlax.getTag(BattlerTagType.CONFUSED)).toBeDefined(); + }); + + it.each<{ name: string; status?: StatusEffect; ability?: Abilities; dmg?: number }>([ + { name: "is at full HP", dmg: 0 }, + { name: "is affected by Electric Terrain", ability: Abilities.ELECTRIC_SURGE }, + { name: "is affected by Misty Terrain", ability: Abilities.MISTY_SURGE }, + { name: "has Comatose", ability: Abilities.COMATOSE }, + ])("should fail if the user $name", async ({ status = StatusEffect.NONE, ability = Abilities.NONE, dmg = 1 }) => { + game.override.ability(ability); + await game.classicMode.startBattle([Species.SNORLAX]); + + const snorlax = game.scene.getPlayerPokemon()!; + snorlax.trySetStatus(status); + + snorlax.hp = snorlax.getMaxHp() - dmg; + + game.move.select(Moves.REST); + await game.phaseInterceptor.to("BerryPhase"); + + expect(snorlax.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }); + + it("should fail if called while already asleep", async () => { + await game.classicMode.startBattle([Species.SNORLAX]); + + const snorlax = game.scene.getPlayerPokemon()!; + snorlax.hp = 1; + snorlax.trySetStatus(StatusEffect.SLEEP); + + game.move.select(Moves.SLEEP_TALK); + await game.phaseInterceptor.to("BerryPhase"); + + expect(snorlax.isFullHp()).toBe(false); + expect(snorlax.status?.effect).toBeUndefined(); + expect(snorlax.getLastXMoves().map(tm => tm.result)).toEqual([MoveResult.FAIL, MoveResult.SUCCESS]); + }); + + it("should succeed if called the turn after waking up", async () => { + await game.classicMode.startBattle([Species.SNORLAX]); + + const snorlax = game.scene.getPlayerPokemon()!; + snorlax.hp = 1; + + // Turn 1 + game.move.select(Moves.REST); + await game.toNextTurn(); + + snorlax.hp = 1; + expect(snorlax.status?.effect).toBe(StatusEffect.SLEEP); + + // Turn 2 + game.move.select(Moves.REST); + await game.toNextTurn(); + + expect(snorlax.status?.effect).toBe(StatusEffect.SLEEP); + + // Turn 3 + game.move.select(Moves.REST); + await game.toNextTurn(); + + expect(snorlax.status?.effect).toBe(StatusEffect.SLEEP); + + // Turn 4 (wakeup) + game.move.select(Moves.REST); + await game.toNextTurn(); + + expect(snorlax.status?.effect).toBe(StatusEffect.SLEEP); + expect(snorlax.isFullHp()).toBe(true); + expect(snorlax.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + expect(snorlax.status?.sleepTurnsRemaining).toBe(3); + }); +}); diff --git a/test/moves/sleep_talk.test.ts b/test/moves/sleep_talk.test.ts index cbe3b6d7d3a..945bf858d39 100644 --- a/test/moves/sleep_talk.test.ts +++ b/test/moves/sleep_talk.test.ts @@ -36,6 +36,31 @@ describe("Moves - Sleep Talk", () => { .enemyLevel(100); }); + it("should call a random valid move if the user is asleep", async () => { + game.override.moveset([Moves.SLEEP_TALK, Moves.DIG, Moves.FLY, Moves.SWORDS_DANCE]); // Dig and Fly are invalid moves, Swords Dance should always be called + await game.classicMode.startBattle([Species.FEEBAS]); + + game.move.select(Moves.SLEEP_TALK); + await game.toNextTurn(); + const feebas = game.scene.getPlayerPokemon()!; + + expect(feebas.getStatStage(Stat.ATK)).toBe(2); + expect(feebas.getLastXMoves()).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + move: Moves.SWORDS_DANCE, + result: MoveResult.SUCCESS, + virtual: true, + }), + expect.objectContaining({ + move: Moves.SLEEP_TALK, + result: MoveResult.SUCCESS, + virtual: false, + }), + ]), + ); + }); + it("should fail when the user is not asleep", async () => { game.override.statusEffect(StatusEffect.NONE); await game.classicMode.startBattle([Species.FEEBAS]); @@ -54,15 +79,6 @@ describe("Moves - Sleep Talk", () => { expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL); }); - it("should call a random valid move if the user is asleep", async () => { - game.override.moveset([Moves.SLEEP_TALK, Moves.DIG, Moves.FLY, Moves.SWORDS_DANCE]); // Dig and Fly are invalid moves, Swords Dance should always be called - await game.classicMode.startBattle([Species.FEEBAS]); - - game.move.select(Moves.SLEEP_TALK); - await game.toNextTurn(); - expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)); - }); - it("should apply secondary effects of a move", async () => { game.override.moveset([Moves.SLEEP_TALK, Moves.DIG, Moves.FLY, Moves.WOOD_HAMMER]); // Dig and Fly are invalid moves, Wood Hammer should always be called await game.classicMode.startBattle();