From 548bd8978fb9f4bb7699c92c3cb506948011091c Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Wed, 7 Aug 2024 07:59:28 -0700 Subject: [PATCH] [Move] Add type immunity removal moves (Foresight, Odor Sleuth, Miracle Eye) (#3379) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update Foresight PR to current beta Implements Foresight, Miracle Eye, and Odor Sleuth * Add placeholder i18n strings * Minor tsdoc updates * Fix placement of evasion level modifier, add tests * Add first batch of translations Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> Co-authored-by: Lugiad' Co-authored-by: José Ricardo Fleury Oliveira * Second batch of translations Co-authored-by: Enoch Co-authored-by: mercurius-00 <80205689+mercurius-00@users.noreply.github.com> * Add Catalan and Japanese translation placeholder strings * Fix issue caused by merge --------- Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> Co-authored-by: Lugiad' Co-authored-by: José Ricardo Fleury Oliveira Co-authored-by: Enoch Co-authored-by: mercurius-00 <80205689+mercurius-00@users.noreply.github.com> --- src/data/battler-tags.ts | 45 +++++++++++++++++++ src/data/move.ts | 39 ++++++++++++++-- src/enums/battler-tag-type.ts | 2 + src/field/pokemon.ts | 11 ++++- src/locales/ca-ES/move-trigger.ts | 1 + src/locales/de/move-trigger.ts | 1 + src/locales/en/move-trigger.ts | 1 + src/locales/es/move-trigger.ts | 1 + src/locales/fr/move-trigger.ts | 1 + src/locales/it/move-trigger.ts | 1 + src/locales/ja/move-trigger.ts | 1 + src/locales/ko/move-trigger.ts | 1 + src/locales/pt_BR/move-trigger.ts | 1 + src/locales/zh_CN/move-trigger.ts | 1 + src/locales/zh_TW/move-trigger.ts | 1 + src/test/moves/foresight.test.ts | 72 ++++++++++++++++++++++++++++++ src/test/moves/miracle_eye.test.ts | 51 +++++++++++++++++++++ 17 files changed, 227 insertions(+), 4 deletions(-) create mode 100644 src/test/moves/foresight.test.ts create mode 100644 src/test/moves/miracle_eye.test.ts diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 4fad0ddbd08..dd5ab8938ae 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1680,6 +1680,47 @@ export class GulpMissileTag extends BattlerTag { } } +/** + * Tag that makes the target drop all of it type immunities + * and all accuracy checks ignore its evasiveness stat. + * + * Applied by moves: {@linkcode Moves.ODOR_SLEUTH | Odor Sleuth}, + * {@linkcode Moves.MIRACLE_EYE | Miracle Eye} and {@linkcode Moves.FORESIGHT | Foresight}. + * + * @extends BattlerTag + * @see {@linkcode ignoreImmunity} + */ +export class ExposedTag extends BattlerTag { + private defenderType: Type; + private allowedTypes: Type[]; + + constructor(tagType: BattlerTagType, sourceMove: Moves, defenderType: Type, allowedTypes: Type[]) { + super(tagType, BattlerTagLapseType.CUSTOM, 1, sourceMove); + this.defenderType = defenderType; + this.allowedTypes = allowedTypes; + } + + /** + * When given a battler tag or json representing one, load the data for it. + * @param {BattlerTag | any} source A battler tag + */ + loadTag(source: BattlerTag | any): void { + super.loadTag(source); + this.defenderType = source.defenderType as Type; + this.allowedTypes = source.allowedTypes as Type[]; + } + + /** + * @param types {@linkcode Type} of the defending Pokemon + * @param moveType {@linkcode Type} of the move targetting it + * @returns `true` if the move should be allowed to target the defender. + */ + ignoreImmunity(type: Type, moveType: Type): boolean { + return type === this.defenderType && this.allowedTypes.includes(moveType); + } +} + + export function getBattlerTag(tagType: BattlerTagType, turnCount: number, sourceMove: Moves, sourceId: number): BattlerTag { switch (tagType) { case BattlerTagType.RECHARGING: @@ -1801,6 +1842,10 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source return new StockpilingTag(sourceMove); case BattlerTagType.OCTOLOCK: return new OctolockTag(sourceId); + case BattlerTagType.IGNORE_GHOST: + return new ExposedTag(tagType, sourceMove, Type.GHOST, [Type.NORMAL, Type.FIGHTING]); + case BattlerTagType.IGNORE_DARK: + return new ExposedTag(tagType, sourceMove, Type.DARK, [Type.PSYCHIC]); case BattlerTagType.GULP_MISSILE_ARROKUDA: case BattlerTagType.GULP_MISSILE_PIKACHU: return new GulpMissileTag(tagType, sourceMove); diff --git a/src/data/move.ts b/src/data/move.ts index dbb29d4c9d1..589c169805d 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -5979,6 +5979,39 @@ export class ResistLastMoveTypeAttr extends MoveEffectAttr { } } +/** + * Drops the target's immunity to types it is immune to + * and makes its evasiveness be ignored during accuracy + * checks. Used by: {@linkcode Moves.ODOR_SLEUTH | Odor Sleuth}, {@linkcode Moves.MIRACLE_EYE | Miracle Eye} and {@linkcode Moves.FORESIGHT | Foresight} + * + * @extends AddBattlerTagAttr + * @see {@linkcode apply} + */ +export class ExposedMoveAttr extends AddBattlerTagAttr { + constructor(tagType: BattlerTagType) { + super(tagType, false, true); + } + + /** + * Applies {@linkcode ExposedTag} to the target. + * @param user {@linkcode Pokemon} using this move + * @param target {@linkcode Pokemon} target of this move + * @param move {@linkcode Move} being used + * @param args N/A + * @returns `true` if the function succeeds + */ + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + if (!super.apply(user, target, move, args)) { + return false; + } + + user.scene.queueMessage(i18next.t("moveTriggers:exposedMove", { pokemonName: getPokemonNameWithAffix(user), targetPokemonName: getPokemonNameWithAffix(target)})); + + return true; + } +} + + const unknownTypeCondition: MoveConditionFunc = (user, target, move) => !user.getTypes().includes(Type.UNKNOWN); export type MoveTargetSet = { @@ -6575,7 +6608,7 @@ export function initMoves() { .attr(StatusEffectAttr, StatusEffect.PARALYSIS) .ballBombMove(), new StatusMove(Moves.FORESIGHT, Type.NORMAL, -1, 40, -1, 0, 2) - .unimplemented(), + .attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST), new SelfStatusMove(Moves.DESTINY_BOND, Type.GHOST, -1, 5, -1, 0, 2) .ignoresProtect() .attr(DestinyBondAttr), @@ -6935,7 +6968,7 @@ export function initMoves() { .attr(StatChangeAttr, BattleStat.SPATK, -2, true) .attr(HealStatusEffectAttr, true, StatusEffect.FREEZE), new StatusMove(Moves.ODOR_SLEUTH, Type.NORMAL, -1, 40, -1, 0, 3) - .unimplemented(), + .attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST), new AttackMove(Moves.ROCK_TOMB, Type.ROCK, MoveCategory.PHYSICAL, 60, 95, 15, 100, 0, 3) .attr(StatChangeAttr, BattleStat.SPD, -1) .makesContact(false), @@ -7045,7 +7078,7 @@ export function initMoves() { .attr(AddArenaTagAttr, ArenaTagType.GRAVITY, 5) .target(MoveTarget.BOTH_SIDES), new StatusMove(Moves.MIRACLE_EYE, Type.PSYCHIC, -1, 40, -1, 0, 4) - .unimplemented(), + .attr(ExposedMoveAttr, BattlerTagType.IGNORE_DARK), new AttackMove(Moves.WAKE_UP_SLAP, Type.FIGHTING, MoveCategory.PHYSICAL, 70, 100, 10, -1, 0, 4) .attr(MovePowerMultiplierAttr, (user, target, move) => targetSleptOrComatoseCondition(user, target, move) ? 2 : 1) .attr(HealStatusEffectAttr, false, StatusEffect.SLEEP), diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index eeba56d6532..405e8cc4822 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -63,6 +63,8 @@ export enum BattlerTagType { STOCKPILING = "STOCKPILING", RECEIVE_DOUBLE_DAMAGE = "RECEIVE_DOUBLE_DAMAGE", ALWAYS_GET_HIT = "ALWAYS_GET_HIT", + IGNORE_GHOST = "IGNORE_GHOST", + IGNORE_DARK = "IGNORE_DARK", GULP_MISSILE_ARROKUDA = "GULP_MISSILE_ARROKUDA", GULP_MISSILE_PIKACHU = "GULP_MISSILE_PIKACHU" } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index ea7acc12588..a53076bc856 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -19,7 +19,7 @@ import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEv import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms"; import { DamagePhase, FaintPhase, LearnMovePhase, MoveEffectPhase, ObtainStatusEffectPhase, StatChangePhase, SwitchSummonPhase, ToggleDoublePositionPhase, MoveEndPhase } from "../phases"; import { BattleStat } from "../data/battle-stat"; -import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag } from "../data/battler-tags"; +import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, ExposedTag } from "../data/battler-tags"; import { WeatherType } from "../data/weather"; import { TempBattleStat } from "../data/temp-battle-stat"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag"; @@ -1259,6 +1259,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (ignoreImmunity.value) { return 1; } + + const exposedTags = this.findTags(tag => tag instanceof ExposedTag) as ExposedTag[]; + if (exposedTags.some(t => t.ignoreImmunity(defType, moveType))) { + return 1; + } } return getTypeDamageMultiplier(moveType, defType); @@ -1865,6 +1870,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { applyMoveAttrs(IgnoreOpponentStatChangesAttr, this, target, sourceMove, targetEvasionLevel); this.scene.applyModifiers(TempBattleStatBoosterModifier, this.isPlayer(), TempBattleStat.ACC, userAccuracyLevel); + if (target.findTag(t => t instanceof ExposedTag)) { + targetEvasionLevel.value = Math.min(0, targetEvasionLevel.value); + } + const accuracyMultiplier = new Utils.NumberHolder(1); if (userAccuracyLevel.value !== targetEvasionLevel.value) { accuracyMultiplier.value = userAccuracyLevel.value > targetEvasionLevel.value diff --git a/src/locales/ca-ES/move-trigger.ts b/src/locales/ca-ES/move-trigger.ts index 1d9d6459d83..7977cca29d4 100644 --- a/src/locales/ca-ES/move-trigger.ts +++ b/src/locales/ca-ES/move-trigger.ts @@ -59,4 +59,5 @@ export const moveTriggers: SimpleTranslationEntries = { "copyType": "{{pokemonName}}'s type became the same as\n{{targetPokemonName}}'s type!", "suppressAbilities": "{{pokemonName}}'s ability\nwas suppressed!", "swapArenaTags": "{{pokemonName}} swapped the battle effects affecting each side of the field!", + "exposedMove": "{{pokemonName}} identified\n{{targetPokemonName}}!", } as const; diff --git a/src/locales/de/move-trigger.ts b/src/locales/de/move-trigger.ts index 427ec6acbde..e8f3298308a 100644 --- a/src/locales/de/move-trigger.ts +++ b/src/locales/de/move-trigger.ts @@ -59,4 +59,5 @@ export const moveTriggers: SimpleTranslationEntries = { "copyType": "{{pokemonName}} hat den Typ von {{targetPokemonName}} angenommen!", "suppressAbilities": "Die Fähigkeit von {{pokemonName}} wirkt nicht mehr!", "swapArenaTags": "{{pokemonName}} hat die Effekte, die auf den beiden Seiten des Kampffeldes wirken, miteinander getauscht!", + "exposedMove": "{{pokemonName}} erkennt {{targetPokemonName}}!", } as const; diff --git a/src/locales/en/move-trigger.ts b/src/locales/en/move-trigger.ts index 1d9d6459d83..7977cca29d4 100644 --- a/src/locales/en/move-trigger.ts +++ b/src/locales/en/move-trigger.ts @@ -59,4 +59,5 @@ export const moveTriggers: SimpleTranslationEntries = { "copyType": "{{pokemonName}}'s type became the same as\n{{targetPokemonName}}'s type!", "suppressAbilities": "{{pokemonName}}'s ability\nwas suppressed!", "swapArenaTags": "{{pokemonName}} swapped the battle effects affecting each side of the field!", + "exposedMove": "{{pokemonName}} identified\n{{targetPokemonName}}!", } as const; diff --git a/src/locales/es/move-trigger.ts b/src/locales/es/move-trigger.ts index 3ff93997cc2..9fef0194b87 100644 --- a/src/locales/es/move-trigger.ts +++ b/src/locales/es/move-trigger.ts @@ -59,4 +59,5 @@ export const moveTriggers: SimpleTranslationEntries = { "copyType": "{{pokemonName}}'s type\nchanged to match {{targetPokemonName}}'s!", "suppressAbilities": "{{pokemonName}}'s ability\nwas suppressed!", "swapArenaTags": "{{pokemonName}} swapped the battle effects affecting each side of the field!", + "exposedMove": "{{pokemonName}} identified\n{{targetPokemonName}}!", } as const; diff --git a/src/locales/fr/move-trigger.ts b/src/locales/fr/move-trigger.ts index d1fbda50b03..d611e19d61a 100644 --- a/src/locales/fr/move-trigger.ts +++ b/src/locales/fr/move-trigger.ts @@ -59,4 +59,5 @@ export const moveTriggers: SimpleTranslationEntries = { "copyType": "{{pokemonName}} prend le type\nde {{targetPokemonName}} !", "suppressAbilities": "Le talent de {{pokemonName}}\na été rendu inactif !", "swapArenaTags": "Les effets affectant chaque côté du terrain\nont été échangés par {{pokemonName}} !", + "exposedMove": "{{targetPokemonName}} est identifié\npar {{pokemonName}} !", } as const; diff --git a/src/locales/it/move-trigger.ts b/src/locales/it/move-trigger.ts index 60679d844c0..98fd37defc4 100644 --- a/src/locales/it/move-trigger.ts +++ b/src/locales/it/move-trigger.ts @@ -59,4 +59,5 @@ export const moveTriggers: SimpleTranslationEntries = { "copyType": "{{pokemonName}} assume il tipo\ndi {{targetPokemonName}}!", "suppressAbilities": "L’abilità di {{pokemonName}}\nperde ogni efficacia!", "swapArenaTags": "{{pokemonName}} ha invertito gli effetti attivi\nnelle due metà del campo!", + "exposedMove": "{{pokemonName}} identified\n{{targetPokemonName}}!", } as const; diff --git a/src/locales/ja/move-trigger.ts b/src/locales/ja/move-trigger.ts index 720ae5df5a8..c0d06b78bf9 100644 --- a/src/locales/ja/move-trigger.ts +++ b/src/locales/ja/move-trigger.ts @@ -59,4 +59,5 @@ export const moveTriggers: SimpleTranslationEntries = { "copyType": "{{pokemonName}}は {{targetPokemonName}}と\n同じタイプに なった!", "suppressAbilities": "{{pokemonName}}の とくせいが きかなくなった!", "swapArenaTags": "{{pokemonName}}は\nおたがいの ばのこうかを いれかえた!", + "exposedMove": "{{pokemonName}} identified\n{{targetPokemonName}}!", } as const; diff --git a/src/locales/ko/move-trigger.ts b/src/locales/ko/move-trigger.ts index 9ebf08b1017..364cb730889 100644 --- a/src/locales/ko/move-trigger.ts +++ b/src/locales/ko/move-trigger.ts @@ -59,4 +59,5 @@ export const moveTriggers: SimpleTranslationEntries = { "copyType": "{{pokemonName}}[[는]]\n{{targetPokemonName}}[[와]] 같은 타입이 되었다!", "suppressAbilities": "{{pokemonName}}의\n특성이 효과를 발휘하지 못하게 되었다!", "swapArenaTags": "{{pokemonName}}[[는]]\n서로의 필드 효과를 교체했다!", + "exposedMove": "{{pokemonName}}[[는]]\n{{targetPokemonName}}의 정체를 꿰뚫어 보았다!", } as const; diff --git a/src/locales/pt_BR/move-trigger.ts b/src/locales/pt_BR/move-trigger.ts index c6f35d8f6d1..579638b00c3 100644 --- a/src/locales/pt_BR/move-trigger.ts +++ b/src/locales/pt_BR/move-trigger.ts @@ -59,4 +59,5 @@ export const moveTriggers: SimpleTranslationEntries = { "copyType": "O tipo de {{pokemonName}}\nmudou para combinar com {{targetPokemonName}}!", "suppressAbilities": "A habilidade de {{pokemonName}}\nfoi suprimida!", "swapArenaTags": "{{pokemonName}} trocou os efeitos de batalha que afetam cada lado do campo!", + "exposedMove": "{{pokemonName}} identificou\n{{targetPokemonName}}!", } as const; diff --git a/src/locales/zh_CN/move-trigger.ts b/src/locales/zh_CN/move-trigger.ts index 0efe24f76f0..23ded8cfb25 100644 --- a/src/locales/zh_CN/move-trigger.ts +++ b/src/locales/zh_CN/move-trigger.ts @@ -59,4 +59,5 @@ export const moveTriggers: SimpleTranslationEntries = { "copyType": "{{pokemonName}}\n变成了{{targetPokemonName}}的属性!", "suppressAbilities": "{{pokemonName}}的特性\n变得无效了!", "swapArenaTags": "{{pokemonName}}\n交换了双方的场地效果!", + "exposedMove": "{{pokemonName}}识破了\n{{targetPokemonName}}的原型!", } as const; diff --git a/src/locales/zh_TW/move-trigger.ts b/src/locales/zh_TW/move-trigger.ts index 019aa84390c..ea355515f71 100644 --- a/src/locales/zh_TW/move-trigger.ts +++ b/src/locales/zh_TW/move-trigger.ts @@ -59,4 +59,5 @@ export const moveTriggers: SimpleTranslationEntries = { "copyType": "{{pokemonName}}變成了{{targetPokemonName}}的屬性!", "suppressAbilities": "{{pokemonName}}的特性\n變得無效了!", "swapArenaTags": "{{pokemonName}}\n交換了雙方的場地效果!", + "exposedMove": "{{pokemonName}}識破了\n{{targetPokemonName}}的原形!", } as const; diff --git a/src/test/moves/foresight.test.ts b/src/test/moves/foresight.test.ts new file mode 100644 index 00000000000..4a2b4f8af03 --- /dev/null +++ b/src/test/moves/foresight.test.ts @@ -0,0 +1,72 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#test/utils/gameManager"; +import { Species } from "#app/enums/species.js"; +import { SPLASH_ONLY } from "../utils/testUtils"; +import { Moves } from "#app/enums/moves.js"; +import { getMovePosition } from "../utils/gameManagerUtils"; +import { MoveEffectPhase } from "#app/phases.js"; + +describe("Internals", () => { + 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 + .disableCrits() + .enemySpecies(Species.GASTLY) + .enemyMoveset(SPLASH_ONLY) + .enemyLevel(5) + .starterSpecies(Species.MAGIKARP) + .moveset([Moves.FORESIGHT, Moves.QUICK_ATTACK, Moves.MACH_PUNCH]); + }); + + it("should allow Normal and Fighting moves to hit Ghost types", async () => { + await game.startBattle(); + + const enemy = game.scene.getEnemyPokemon(); + + game.doAttack(getMovePosition(game.scene, 0, Moves.QUICK_ATTACK)); + await game.toNextTurn(); + expect(enemy.hp).toBe(enemy.getMaxHp()); + + game.doAttack(getMovePosition(game.scene, 0, Moves.FORESIGHT)); + await game.toNextTurn(); + game.doAttack(getMovePosition(game.scene, 0, Moves.QUICK_ATTACK)); + await game.toNextTurn(); + + expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); + enemy.hp = enemy.getMaxHp(); + + game.doAttack(getMovePosition(game.scene, 0, Moves.MACH_PUNCH)); + await game.phaseInterceptor.to(MoveEffectPhase); + + expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); + }); + + it("should ignore target's evasiveness boosts", async () => { + game.override.enemyMoveset(Array(4).fill(Moves.MINIMIZE)); + await game.startBattle(); + + const pokemon = game.scene.getPlayerPokemon(); + vi.spyOn(pokemon, "getAccuracyMultiplier"); + + game.doAttack(getMovePosition(game.scene, 0, Moves.FORESIGHT)); + await game.toNextTurn(); + game.doAttack(getMovePosition(game.scene, 0, Moves.QUICK_ATTACK)); + await game.phaseInterceptor.to(MoveEffectPhase); + + expect(pokemon.getAccuracyMultiplier).toHaveReturnedWith(1); + }); +}); diff --git a/src/test/moves/miracle_eye.test.ts b/src/test/moves/miracle_eye.test.ts new file mode 100644 index 00000000000..188c5736fc4 --- /dev/null +++ b/src/test/moves/miracle_eye.test.ts @@ -0,0 +1,51 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#test/utils/gameManager"; +import { Species } from "#app/enums/species.js"; +import { SPLASH_ONLY } from "../utils/testUtils"; +import { Moves } from "#app/enums/moves.js"; +import { getMovePosition } from "../utils/gameManagerUtils"; +import { MoveEffectPhase } from "#app/phases.js"; + +describe("Internals", () => { + 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 + .disableCrits() + .enemySpecies(Species.UMBREON) + .enemyMoveset(SPLASH_ONLY) + .enemyLevel(5) + .starterSpecies(Species.MAGIKARP) + .moveset([Moves.MIRACLE_EYE, Moves.CONFUSION]); + }); + + it("should allow Psychic moves to hit Dark types", async () => { + await game.startBattle(); + + const enemy = game.scene.getEnemyPokemon(); + + game.doAttack(getMovePosition(game.scene, 0, Moves.CONFUSION)); + await game.toNextTurn(); + expect(enemy.hp).toBe(enemy.getMaxHp()); + + game.doAttack(getMovePosition(game.scene, 0, Moves.MIRACLE_EYE)); + await game.toNextTurn(); + game.doAttack(getMovePosition(game.scene, 0, Moves.CONFUSION)); + await game.phaseInterceptor.to(MoveEffectPhase); + + expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); + }); +});