From acd27eb1daa552548043512c7d5650470a425547 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Sun, 2 Feb 2025 12:04:11 -0600 Subject: [PATCH] Finish magic bounce impl --- src/data/ability.ts | 5 +- src/phases/move-effect-phase.ts | 123 +++++++++++++++++------- src/phases/move-phase.ts | 2 +- src/test/abilities/magic_bounce.test.ts | 108 +++++++++++++++++---- 4 files changed, 182 insertions(+), 56 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 94318156734..4b5a97a29ec 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -5818,10 +5818,9 @@ export function initAbilities() { new Ability(Abilities.MAGIC_BOUNCE, 5) .attr(ReflectStatusMoveAbAttr) .ignorable() - // Interactions with stomping tantrum, instruct, and other moves that + // Interactions with stomping tantrum, instruct, encore, and probably other moves that // rely on move history - .edgeCase() - .unimplemented(), + .edgeCase(), new Ability(Abilities.SAP_SIPPER, 5) .attr(TypeImmunityStatStageChangeAbAttr, Type.GRASS, Stat.ATK, 1) .ignorable(), diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index a247532df52..1a479688081 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -62,15 +62,16 @@ import { } from "#app/modifier/modifier"; import { PokemonPhase } from "#app/phases/pokemon-phase"; import { BooleanHolder, executeIf, isNullOrUndefined, NumberHolder } from "#app/utils"; +import { type nil } from "#app/utils"; import { BattlerTagType } from "#enums/battler-tag-type"; import type { Moves } from "#enums/moves"; import i18next from "i18next"; import { Stat } from "#app/enums/stat"; import { ArenaTagType } from "#app/enums/arena-tag-type"; -import { MessagePhase } from "./message-phase"; import type { Phase } from "#app/phase"; import { ShowAbilityPhase } from "./show-ability-phase"; import { MovePhase } from "./move-phase"; +import { MoveEndPhase } from "./move-end-phase"; export class MoveEffectPhase extends PokemonPhase { public move: PokemonMove; @@ -190,7 +191,7 @@ export class MoveEffectPhase extends PokemonPhase { && (targets[0]?.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) && !targets[0]?.getTag(SemiInvulnerableTag); - const mayBounce = move.hasFlag(MoveFlags.REFLECTABLE) && !this.reflected; + const mayBounce = move.hasFlag(MoveFlags.REFLECTABLE) && !this.reflected && targets.some(t => t.hasAbilityWithAttr(ReflectStatusMoveAbAttr) || !!t.getTag(BattlerTagType.MAGIC_COAT)); /** * If no targets are left for the move to hit (FAIL), or the invoked move is non-reflectable, single-target @@ -223,22 +224,21 @@ export class MoveEffectPhase extends PokemonPhase { // Prevent ENEMY_SIDE targeted moves from occurring twice in double battles // and determine which enemy will magic bounce based on speed order, respecting trick room const trueTargets: Pokemon[] = move.moveTarget !== MoveTarget.ENEMY_SIDE ? targets : (() => { - const magicCoatTargets = targets.filter(t => t.getTag(BattlerTagType.MAGIC_COAT) !== null); + const magicCoatTargets = targets.filter(t => t.getTag(BattlerTagType.MAGIC_COAT) || t.hasAbilityWithAttr(ReflectStatusMoveAbAttr)); // only magic coat effect cares about order if (!mayBounce || magicCoatTargets.length === 0) { return [ targets[0] ]; + } else if (magicCoatTargets.length === 1) { + return magicCoatTargets; } - const reversed = globalScene.arena.hasTag(ArenaTagType.TRICK_ROOM); - const sortedTargets = targets - .sort((a: Pokemon, b: Pokemon) => { - const aSpeed = a?.getEffectiveStat(Stat.SPD) ?? 0; - const bSpeed = b?.getEffectiveStat(Stat.SPD) ?? 0; - return reversed ? aSpeed - bSpeed : bSpeed - aSpeed; - }) - .filter(t => t.getTag(BattlerTagType.MAGIC_COAT) !== null); - return sortedTargets.length === 1 ? sortedTargets : [ sortedTargets[globalScene.randBattleSeedInt(sortedTargets.length)] ]; + // Filter the list of magic coat targets to those with the highest speed, or lowest if trick room is active. + const speeds = magicCoatTargets.map(p => p.getEffectiveStat(Stat.SPD) ?? 0); + const targetSpeed = globalScene.arena.hasTag(ArenaTagType.TRICK_ROOM) ? Math.min(...speeds) : Math.max(...speeds); + const filteredTargets = magicCoatTargets.filter((_, idx) => speeds[idx] === targetSpeed); + // In the event of a speed tie, choose a pokemon at random that will bounce the move. + return filteredTargets.length === 1 ? filteredTargets : [ filteredTargets[globalScene.randBattleSeedInt(filteredTargets.length)] ]; })(); const queuedPhases: Phase[] = []; @@ -255,7 +255,7 @@ export class MoveEffectPhase extends PokemonPhase { } /** Is the target protected by Protect, etc. or a relevant conditional protection effect? */ - const isProtected = ( + const isProtected = !([ MoveTarget.ENEMY_SIDE, MoveTarget.BOTH_SIDES ].includes(this.move.getMove().moveTarget)) && ( bypassIgnoreProtect.value || !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target)) && (hasConditionalProtectApplied.value @@ -268,21 +268,30 @@ export class MoveEffectPhase extends PokemonPhase { const isCommanding = globalScene.currentBattle.double && target.getAlly()?.getTag(BattlerTagType.COMMANDED)?.getSourcePokemon() === target; /** Is the target reflecting status moves from the magic coat move? */ - const isReflecting = target.getTag(BattlerTagType.MAGIC_COAT) !== null; + const isReflecting = !!target.getTag(BattlerTagType.MAGIC_COAT); /** Is the target's magic bounce ability not ignored and able to reflect this move? */ - const canMagicBounce = !(isReflecting || move.checkFlag(MoveFlags.IGNORE_ABILITIES, user, target) && target.hasAbilityWithAttr(ReflectStatusMoveAbAttr)); + const canMagicBounce = !isReflecting && !move.checkFlag(MoveFlags.IGNORE_ABILITIES, user, target) && target.hasAbilityWithAttr(ReflectStatusMoveAbAttr); - /** Is the target protected and reflecting the effect */ - const willBounce = !isProtected && !this.reflected && !isCommanding && (isReflecting || canMagicBounce); + const semiInvulnerableTag = target.getTag(SemiInvulnerableTag); + /** Is the target in a semi-invulnerable state that isn't being bypassed by this move? */ + const activeSemiInvulnerability = !!semiInvulnerableTag && + !(this.checkBypassSemiInvuln(semiInvulnerableTag) + || this.checkBypassAccAndInvuln(target)); - /** If the move will bounce, then queue the bounce and move on to the next target*/ + /** Is the target reflecting the effect, not protected, and not in an semi-invulnerable state?*/ + const willBounce = (!isProtected && !this.reflected && !isCommanding + && move.hasFlag(MoveFlags.REFLECTABLE) + && (isReflecting || canMagicBounce) + && !activeSemiInvulnerability); + + // If the move will bounce, then queue the bounce and move on to the next target if (!target.switchOutStatus && willBounce) { const newTargets = move.isMultiTarget() ? getMoveTargets(target, move.id).targets : [ user.getBattlerIndex() ]; - if (!isReflecting && canMagicBounce) { + if (!isReflecting) { queuedPhases.push(new ShowAbilityPhase(target.getBattlerIndex(), target.getPassiveAbility().hasAttr(ReflectStatusMoveAbAttr))); } - queuedPhases.push(new MessagePhase(i18next.t("battle:magicCoatBounce", { pokemonNameWithAffix: getPokemonNameWithAffix(target) }))); + queuedPhases.push(new MovePhase(target, newTargets, new PokemonMove(move.id, 0, 0, true), true, true, true)); continue; } @@ -290,7 +299,7 @@ export class MoveEffectPhase extends PokemonPhase { /** Is the pokemon immune due to an ablility, and also not in a semi invulnerable state? */ const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr) && (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) - && !target.getTag(SemiInvulnerableTag); + && !semiInvulnerableTag; /** @@ -419,7 +428,9 @@ export class MoveEffectPhase extends PokemonPhase { } // Apply queued phases - queuedPhases.forEach(p => globalScene.unshiftPhase(p)); + if (queuedPhases.length) { + globalScene.appendToPhase(queuedPhases, MoveEndPhase); + } // Apply the move's POST_TARGET effects on the move's last hit, after all targeted effects have resolved const postTarget = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive()) ? applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET, user, null, move) : @@ -635,12 +646,7 @@ export class MoveEffectPhase extends PokemonPhase { } } - if (user.hasAbilityWithAttr(AlwaysHitAbAttr) || target.hasAbilityWithAttr(AlwaysHitAbAttr)) { - return true; - } - - // If the user should ignore accuracy on a target, check who the user targeted last turn and see if they match - if (user.getTag(BattlerTagType.IGNORE_ACCURACY) && (user.getLastXMoves().find(() => true)?.targets || []).indexOf(target.getBattlerIndex()) !== -1) { + if (this.checkBypassAccAndInvuln(target)) { return true; } @@ -648,15 +654,12 @@ export class MoveEffectPhase extends PokemonPhase { return true; } - if (target.getTag(BattlerTagType.TELEKINESIS) && !target.getTag(SemiInvulnerableTag) && !this.move.getMove().hasAttr(OneHitKOAttr)) { + const semiInvulnerableTag = target.getTag(SemiInvulnerableTag); + if (target.getTag(BattlerTagType.TELEKINESIS) && !semiInvulnerableTag && !this.move.getMove().hasAttr(OneHitKOAttr)) { return true; } - const semiInvulnerableTag = target.getTag(SemiInvulnerableTag); - if (semiInvulnerableTag - && !this.move.getMove().getAttrs(HitsTagAttr).some(hta => hta.tagType === semiInvulnerableTag.tagType) - && !(this.move.getMove().hasAttr(ToxicAccuracyAttr) && user.isOfType(Type.POISON)) - ) { + if (this.checkBypassSemiInvuln(semiInvulnerableTag)) { return false; } @@ -672,6 +675,58 @@ export class MoveEffectPhase extends PokemonPhase { return rand < (moveAccuracy * accuracyMultiplier); } + /** + * Check whether the move should bypass *both* the accuracy *and* semi-invulnerable states. + * @param target - The {@linkcode Pokemon} targeted by the invoked move + * @returns `true` if the move should bypass accuracy and semi-invulnerability + * + * Accuracy and semi-invulnerability can be bypassed by: + * - An ability like {@linkcode Abilities.NO_GUARD | No Guard} + * - A poison type using {@linkcode Moves.TOXIC | Toxic} + * - A move like {@linkcode Moves.LOCK_ON | Lock-On} or {@linkcode Moves.MIND_READER | Mind Reader}. + * - A move like {@linkcode Moves.SPIEKS | Spikes} or {@linkcode Moves.SANDSTORM | Sandstorm} that targets the field + * + * Does *not* check against effects {@linkcode Moves.GLAIVE_RUSH | Glaive Rush} status (which + * should not bypass semi-invulnerability), or interactions like Earthquake hitting against Dig, + * (which should not bypass the accuracy check). + * + * @see {@linkcode hitCheck} + */ + public checkBypassAccAndInvuln(target: Pokemon) { + const user = this.getUserPokemon(); + if (!user) { + return false; + } + if ([ MoveTarget.USER, MoveTarget.ENEMY_SIDE, MoveTarget.USER_SIDE, MoveTarget.BOTH_SIDES ].includes(this.move.getMove().moveTarget)) { + return true; + } + if (user.hasAbilityWithAttr(AlwaysHitAbAttr) || target.hasAbilityWithAttr(AlwaysHitAbAttr)) { + return true; + } + if ((this.move.getMove().hasAttr(ToxicAccuracyAttr) && user.isOfType(Type.POISON))) { + return true; + } + // TODO: Fix lock on / mind reader check. + if (user.getTag(BattlerTagType.IGNORE_ACCURACY) && (user.getLastXMoves().find(() => true)?.targets || []).indexOf(target.getBattlerIndex()) !== -1) { + return true; + } + } + + /** + * Check whether the move is able to ignore the given `semiInvulnerableTag` + * @param semiInvulnerableTag - The semiInvulnerbale tag to check against + * @returns `true` if the move can ignore the semi-invulnerable state + */ + public checkBypassSemiInvuln(semiInvulnerableTag: SemiInvulnerableTag | nil): boolean { + if (!semiInvulnerableTag) { + return false; + } + const move = this.move.getMove(); + /** Does the move target the field instead of the target itself? */ + const isIndirectTarget = move.moveTarget in [ MoveTarget.USER, MoveTarget.ENEMY_SIDE, MoveTarget.USER_SIDE, MoveTarget.BOTH_SIDES ]; + return isIndirectTarget || move.getAttrs(HitsTagAttr).some(hta => hta.tagType === semiInvulnerableTag.tagType); + } + /** @returns The {@linkcode Pokemon} using this phase's invoked move */ public getUserPokemon(): Pokemon | null { if (this.battlerIndex > BattlerIndex.ENEMY_2) { diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index fdd8029edb4..b7effa58e36 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -547,7 +547,7 @@ export class MovePhase extends BattlePhase { return; } - globalScene.queueMessage(i18next.t("battle:useMove", { + globalScene.queueMessage(i18next.t(this.reflected ? "battle:magicCoatExe" : "battle:useMove", { pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon), moveName: this.move.getName() }), 500); diff --git a/src/test/abilities/magic_bounce.test.ts b/src/test/abilities/magic_bounce.test.ts index 0ca54c10bec..818d66f4e11 100644 --- a/src/test/abilities/magic_bounce.test.ts +++ b/src/test/abilities/magic_bounce.test.ts @@ -1,14 +1,15 @@ import { BattlerIndex } from "#app/battle"; +import { allAbilities } from "#app/data/ability"; import { ArenaTagSide } from "#app/data/arena-tag"; import { allMoves } from "#app/data/move"; import { ArenaTagType } from "#app/enums/arena-tag-type"; import { BattlerTagType } from "#app/enums/battler-tag-type"; import { Stat } from "#app/enums/stat"; +import { StatusEffect } from "#app/enums/status-effect"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import GameManager from "#test/utils/gameManager"; -import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; describe("Abilities - Magic Bounce", () => { @@ -134,11 +135,12 @@ describe("Abilities - Magic Bounce", () => { game.move.select(Moves.SPIKES); await game.phaseInterceptor.to("BerryPhase"); - expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.PLAYER)).toBe(1); - expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY)).toBe(0); + expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.PLAYER)!["layers"]).toBe(1); + expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY)).toBeUndefined(); }); it("should not bounce back curse", async() => { + game.override.starterSpecies(Species.GASTLY); await game.classicMode.startBattle([ Species.GASTLY ]); game.override.moveset([ Moves.CURSE ]); @@ -148,9 +150,67 @@ describe("Abilities - Magic Bounce", () => { expect(game.scene.getEnemyPokemon()!.getTag(BattlerTagType.CURSED)).toBeDefined(); }); - it("should cause stomping tantrum to double in power if the bounced move fails", async () => { + it("should not cause encore to be interrupted after bouncing", async () => { + game.override.moveset([ Moves.SPLASH, Moves.GROWL, Moves.ENCORE ]); + game.override.enemyMoveset([ Moves.TACKLE, Moves.GROWL ]); + // game.override.ability(Abilities.MOLD_BREAKER); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + // Give the player MOLD_BREAKER for this turn to bypass Magic Bounce. + vi.spyOn(playerPokemon, "getAbility").mockReturnValue(allAbilities[Abilities.MOLD_BREAKER]); + + // turn 1 + game.move.select(Moves.ENCORE); + await game.forceEnemyMove(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextTurn(); + expect(enemyPokemon.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(Moves.TACKLE); + + // turn 2 + vi.spyOn(playerPokemon, "getAbility").mockRestore(); + game.move.select(Moves.GROWL); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("BerryPhase"); + expect(enemyPokemon.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(Moves.TACKLE); + expect(enemyPokemon.getLastXMoves()[0].move).toBe(Moves.TACKLE); + + }); + + // TODO: encore is failing if the last move was virtual. + it.todo("should not cause the bounced move to count for encore", async () => { + game.override.moveset([ Moves.SPLASH, Moves.GROWL, Moves.ENCORE ]); + game.override.enemyMoveset([ Moves.GROWL, Moves.TACKLE ]); + game.override.enemyAbility(Abilities.MAGIC_BOUNCE); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + // turn 1 + game.move.select(Moves.GROWL); + await game.forceEnemyMove(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextTurn(); + + // Give the player MOLD_BREAKER for this turn to bypass Magic Bounce. + vi.spyOn(playerPokemon, "getAbility").mockReturnValue(allAbilities[Abilities.MOLD_BREAKER]); + + // turn 2 + game.move.select(Moves.ENCORE); + await game.forceEnemyMove(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("BerryPhase"); + expect(enemyPokemon.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(Moves.TACKLE); + expect(enemyPokemon.getLastXMoves()[0].move).toBe(Moves.TACKLE); + }); + + // TODO: stomping tantrum should consider moves that were bounced. + it.todo("should cause stomping tantrum to double in power when the last move was bounced", async () => { game.override.battleType("single"); await game.classicMode.startBattle([ Species.MAGIKARP ]); + game.override.moveset([ Moves.STOMPING_TANTRUM, Moves.CHARM ]); const stomping_tantrum = allMoves[Moves.STOMPING_TANTRUM]; vi.spyOn(stomping_tantrum, "calculateBattlePower"); @@ -163,26 +223,25 @@ describe("Abilities - Magic Bounce", () => { expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(150); }); - it("should properly cause the enemy's stomping tantrum to be doubled in power after bouncing and failing", async () => { - game.override.battleType("double"); - game.override.enemyMoveset([ Moves.GROWL, Moves.STOMPING_TANTRUM, Moves.CHARM, Moves.SPLASH ]); - game.override.enemyLevel(50); - await game.classicMode.startBattle([ Species.MAGIKARP ]); + // TODO: stomping tantrum should consider moves that were bounced. + it.todo("should properly cause the enemy's stomping tantrum to be doubled in power after bouncing and failing", async () => { + game.override.enemyMoveset([ Moves.STOMPING_TANTRUM, Moves.SPLASH, Moves.CHARM ]); + await game.classicMode.startBattle([ Species.BULBASAUR ]); const stomping_tantrum = allMoves[Moves.STOMPING_TANTRUM]; + const enemy = game.scene.getEnemyPokemon()!; vi.spyOn(stomping_tantrum, "calculateBattlePower"); - game.move.select(Moves.CHARM, 0, BattlerIndex.ENEMY); - game.move.select(Moves.SPLASH, 1); + game.move.select(Moves.SPORE); + await game.forceEnemyMove(Moves.CHARM); await game.phaseInterceptor.to("TurnEndPhase"); + expect(enemy.getLastXMoves(1)[0].result).toBe("success"); - game.move.select(Moves.STOMPING_TANTRUM, 0, BattlerIndex.ENEMY_2); await game.phaseInterceptor.to("BerryPhase"); - expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(150); + expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(75); await game.toNextTurn(); - game.move.select(Moves.GROWL, 0); - game.move.select(Moves.SPLASH, 1); + game.move.select(Moves.GROWL); await game.phaseInterceptor.to("BerryPhase"); expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(75); }); @@ -213,7 +272,7 @@ describe("Abilities - Magic Bounce", () => { vi.spyOn(attacker, "getAccuracyMultiplier").mockReturnValue(0.0); game.move.select(Moves.SPORE); await game.phaseInterceptor.to("BerryPhase"); - expect(game.scene.getPlayerPokemon()!.status).toBeUndefined(); + expect(game.scene.getPlayerPokemon()!.status?.effect).toBe(StatusEffect.SLEEP); }); it("should take the accuracy of the magic bounce user into account", async () => { @@ -240,14 +299,27 @@ describe("Abilities - Magic Bounce", () => { game.move.select(Moves.TRICK_ROOM, 1); await game.phaseInterceptor.to("TurnEndPhase"); - expect(game.scene.arena.getTagOnSide(ArenaTagType.STICKY_WEB, ArenaTagSide.PLAYER)?.getSourcePokemon()?.id).toBe(enemy_1.id); + expect(game.scene.arena.getTagOnSide(ArenaTagType.STICKY_WEB, ArenaTagSide.PLAYER)?.getSourcePokemon()?.getBattlerIndex()).toBe(BattlerIndex.ENEMY); game.scene.arena.removeTagOnSide(ArenaTagType.STICKY_WEB, ArenaTagSide.PLAYER, true); // turn 2 game.move.select(Moves.STICKY_WEB, 0); game.move.select(Moves.TRICK_ROOM, 1); await game.phaseInterceptor.to("BerryPhase"); - expect(game.scene.arena.getTagOnSide(ArenaTagType.STICKY_WEB, ArenaTagSide.PLAYER)?.getSourcePokemon()?.id).toBe(enemy_2.id); + expect(game.scene.arena.getTagOnSide(ArenaTagType.STICKY_WEB, ArenaTagSide.PLAYER)?.getSourcePokemon()?.getBattlerIndex()).toBe(BattlerIndex.ENEMY_2); + }); + + it("should bounce back moves like spikes when the magic bounce user is semi-invulnerable", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + game.override.moveset([ Moves.SPIKES ]); + game.override.enemyMoveset([ Moves.FLY ]); + + game.move.select(Moves.SPIKES); + await game.forceEnemyMove(Moves.FLY); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.PLAYER)!["layers"]).toBe(1); }); });