diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 898bbbc306b..b059b4cf6b2 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -119,7 +119,7 @@ export class RechargingTag extends BattlerTag { } /** - * BattlerTag representing the "charge phase" of Beak Blast + * BattlerTag representing the "charge phase" of Beak Blast. * Pokemon with this tag will inflict BURN status on any attacker that makes contact. * @see {@link https://bulbapedia.bulbagarden.net/wiki/Beak_Blast_(move) | Beak Blast} */ @@ -156,6 +156,50 @@ export class BeakBlastChargingTag extends BattlerTag { } } +/** + * BattlerTag implementing Shell Trap's pre-move behavior. + * Pokemon with this tag will act immediately after being hit by a physical move. + * @see {@link https://bulbapedia.bulbagarden.net/wiki/Shell_Trap_(move) | Shell Trap} + */ +export class ShellTrapTag extends BattlerTag { + public activated: boolean; + + constructor() { + super(BattlerTagType.SHELL_TRAP, BattlerTagLapseType.TURN_END, 1); + this.activated = false; + } + + onAdd(pokemon: Pokemon): void { + pokemon.scene.queueMessage(i18next.t("moveTriggers:setUpShellTrap", { pokemonName: getPokemonNameWithAffix(pokemon) })); + } + + /** + * "Activates" the shell trap, causing the tag owner to move next. + * @param pokemon {@linkcode Pokemon} the owner of this tag + * @param lapseType {@linkcode BattlerTagLapseType} the type of functionality invoked in battle + * @returns `true` if invoked with the `CUSTOM` lapse type; `false` otherwise + */ + lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { + if (lapseType === BattlerTagLapseType.CUSTOM) { + const shellTrapPhaseIndex = pokemon.scene.phaseQueue.findIndex( + phase => phase instanceof MovePhase && phase.pokemon === pokemon + ); + const firstMovePhaseIndex = pokemon.scene.phaseQueue.findIndex( + phase => phase instanceof MovePhase + ); + + if (shellTrapPhaseIndex !== -1 && shellTrapPhaseIndex !== firstMovePhaseIndex) { + const shellTrapMovePhase = pokemon.scene.phaseQueue.splice(shellTrapPhaseIndex, 1)[0]; + pokemon.scene.prependToPhase(shellTrapMovePhase, MovePhase); + } + + this.activated = true; + return true; + } + return super.lapse(pokemon, lapseType); + } +} + export class TrappedTag extends BattlerTag { constructor(tagType: BattlerTagType, lapseType: BattlerTagLapseType, turnCount: number, sourceMove: Moves, sourceId: number) { super(tagType, lapseType, turnCount, sourceMove, sourceId); @@ -1785,6 +1829,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source return new RechargingTag(sourceMove); case BattlerTagType.BEAK_BLAST_CHARGING: return new BeakBlastChargingTag(); + case BattlerTagType.SHELL_TRAP: + return new ShellTrapTag(); case BattlerTagType.FLINCHED: return new FlinchedTag(sourceMove); case BattlerTagType.INTERRUPTED: diff --git a/src/data/move.ts b/src/data/move.ts index 7e3b4410727..28b7835639c 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -1,7 +1,7 @@ import { ChargeAnim, MoveChargeAnim, initMoveAnim, loadMoveAnimAssets } from "./battle-anims"; import { BattleEndPhase, MoveEndPhase, MovePhase, NewBattlePhase, PartyStatusCurePhase, PokemonHealPhase, StatChangePhase, SwitchPhase, SwitchSummonPhase } from "../phases"; import { BattleStat, getBattleStatName } from "./battle-stat"; -import { EncoreTag, GulpMissileTag, HelpingHandTag, SemiInvulnerableTag, StockpilingTag, TrappedTag, TypeBoostTag } from "./battler-tags"; +import { EncoreTag, GulpMissileTag, HelpingHandTag, SemiInvulnerableTag, ShellTrapTag, StockpilingTag, TrappedTag, TypeBoostTag } from "./battler-tags"; import { getPokemonNameWithAffix } from "../messages"; import Pokemon, { AttackMoveResult, EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove, TurnMove } from "../field/pokemon"; import { StatusEffect, getStatusEffectHealText, isNonVolatileStatusEffect, getNonVolatileStatusEffects} from "./status-effect"; @@ -1021,19 +1021,36 @@ export class MessageHeaderAttr extends MoveHeaderAttr { } } +/** + * Header attribute to add a battler tag to the user at the beginning of a turn. + * @see {@linkcode MoveHeaderAttr} + */ +export class AddBattlerTagHeaderAttr extends MoveHeaderAttr { + private tagType: BattlerTagType; + + constructor(tagType: BattlerTagType) { + super(); + this.tagType = tagType; + } + + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + user.addTag(this.tagType); + return true; + } +} + /** * Header attribute to implement the "charge phase" of Beak Blast at the * beginning of a turn. * @see {@link https://bulbapedia.bulbagarden.net/wiki/Beak_Blast_(move) | Beak Blast} * @see {@linkcode BeakBlastChargingTag} */ -export class BeakBlastHeaderAttr extends MoveHeaderAttr { +export class BeakBlastHeaderAttr extends AddBattlerTagHeaderAttr { /** Required to initialize Beak Blast's charge animation correctly */ public chargeAnim = ChargeAnim.BEAK_BLAST_CHARGING; - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - user.addTag(BattlerTagType.BEAK_BLAST_CHARGING); - return true; + constructor() { + super(BattlerTagType.BEAK_BLAST_CHARGING); } } @@ -8145,8 +8162,10 @@ export function initMoves() { .ignoresVirtual(), /* End Unused */ new AttackMove(Moves.SHELL_TRAP, Type.FIRE, MoveCategory.SPECIAL, 150, 100, 5, -1, -3, 7) + .attr(AddBattlerTagHeaderAttr, BattlerTagType.SHELL_TRAP) .target(MoveTarget.ALL_NEAR_ENEMIES) - .partial(), + // Fails if the user was not hit by a physical attack during the turn + .condition((user, target, move) => user.getTag(ShellTrapTag)?.activated === true), new AttackMove(Moves.FLEUR_CANNON, Type.FAIRY, MoveCategory.SPECIAL, 130, 90, 5, -1, 0, 7) .attr(StatChangeAttr, BattleStat.SPATK, -2, true), new AttackMove(Moves.PSYCHIC_FANGS, Type.PSYCHIC, MoveCategory.PHYSICAL, 85, 100, 10, -1, 0, 7) diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index 5aab5036caf..b133b442801 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -68,5 +68,6 @@ export enum BattlerTagType { IGNORE_DARK = "IGNORE_DARK", GULP_MISSILE_ARROKUDA = "GULP_MISSILE_ARROKUDA", GULP_MISSILE_PIKACHU = "GULP_MISSILE_PIKACHU", - BEAK_BLAST_CHARGING = "BEAK_BLAST_CHARGING" + BEAK_BLAST_CHARGING = "BEAK_BLAST_CHARGING", + SHELL_TRAP = "SHELL_TRAP" } diff --git a/src/locales/ca_ES/move-trigger.ts b/src/locales/ca_ES/move-trigger.ts index dc6028b116e..5f9c6c0cdc2 100644 --- a/src/locales/ca_ES/move-trigger.ts +++ b/src/locales/ca_ES/move-trigger.ts @@ -33,6 +33,7 @@ export const moveTriggers: SimpleTranslationEntries = { "isChargingPower": "{{pokemonName}} is absorbing power!", "burnedItselfOut": "{{pokemonName}} burned itself out!", "startedHeatingUpBeak": "{{pokemonName}} started\nheating up its beak!", + "setUpShellTrap": "{{pokemonName}} set a shell trap!", "isOverflowingWithSpacePower": "{{pokemonName}} is overflowing\nwith space power!", "usedUpAllElectricity": "{{pokemonName}} used up all its electricity!", "stoleItem": "{{pokemonName}} stole\n{{targetName}}'s {{itemName}}!", diff --git a/src/locales/de/move-trigger.ts b/src/locales/de/move-trigger.ts index 4af494dea3c..c3d063c4b3f 100644 --- a/src/locales/de/move-trigger.ts +++ b/src/locales/de/move-trigger.ts @@ -33,6 +33,7 @@ export const moveTriggers: SimpleTranslationEntries = { "isChargingPower": "{{pokemonName}} saugt Kraft in sich auf!", "burnedItselfOut": "{{pokemonName}} braucht sein Feuer komplett auf!", "startedHeatingUpBeak": "{{pokemonName}} erhitzt seinen Schnabel!", + "setUpShellTrap": "{{pokemonName}} hat eine Panzerfalle gelegt!", "isOverflowingWithSpacePower": "Kosmische Kräfte strömen aus {{pokemonName}}!", "usedUpAllElectricity": "{{pokemonName}} braucht seinen Strom komplett auf!", "stoleItem": "{{pokemonName}} hat {{targetName}} das Item {{itemName}} geklaut!", diff --git a/src/locales/en/move-trigger.ts b/src/locales/en/move-trigger.ts index dc6028b116e..5f9c6c0cdc2 100644 --- a/src/locales/en/move-trigger.ts +++ b/src/locales/en/move-trigger.ts @@ -33,6 +33,7 @@ export const moveTriggers: SimpleTranslationEntries = { "isChargingPower": "{{pokemonName}} is absorbing power!", "burnedItselfOut": "{{pokemonName}} burned itself out!", "startedHeatingUpBeak": "{{pokemonName}} started\nheating up its beak!", + "setUpShellTrap": "{{pokemonName}} set a shell trap!", "isOverflowingWithSpacePower": "{{pokemonName}} is overflowing\nwith space power!", "usedUpAllElectricity": "{{pokemonName}} used up all its electricity!", "stoleItem": "{{pokemonName}} stole\n{{targetName}}'s {{itemName}}!", diff --git a/src/locales/es/move-trigger.ts b/src/locales/es/move-trigger.ts index e9bd5523c71..cee5b58508f 100644 --- a/src/locales/es/move-trigger.ts +++ b/src/locales/es/move-trigger.ts @@ -30,12 +30,13 @@ export const moveTriggers: SimpleTranslationEntries = { "tookTargetIntoSky": "{{pokemonName}} took {{targetName}}\ninto the sky!", "becameCloakedInFreezingLight": "{{pokemonName}} became cloaked\nin a freezing light!", "becameCloakedInFreezingAir": "{{pokemonName}} became cloaked\nin freezing air!", - "isChargingPower": "{{pokemonName}} is absorbing power!", - "burnedItselfOut": "{{pokemonName}} burned itself out!", - "startedHeatingUpBeak": "{{pokemonName}} started\nheating up its beak!", - "isOverflowingWithSpacePower": "{{pokemonName}} is overflowing\nwith space power!", - "usedUpAllElectricity": "{{pokemonName}} used up all its electricity!", - "stoleItem": "{{pokemonName}} stole\n{{targetName}}'s {{itemName}}!", + "isChargingPower": "¡{{pokemonName}} está acumulando energía!", + "burnedItselfOut": "¡El fuego interior de {{pokemonName}} se ha extinguido!", + "startedHeatingUpBeak": "¡{{pokemonName}} empieza\na calentar su pico!", + "setUpShellTrap": "¡{{pokemonName}} ha activado la Coraza Trampa!", + "isOverflowingWithSpacePower": "¡{{pokemonName}} rebosa\nenergía cósmica!", + "usedUpAllElectricity": "¡{{pokemonName}} ha descargado toda su electricidad!", + "stoleItem": "¡{{pokemonName}} robó el objeto\n{{itemName}} de {{targetName}}!", "incineratedItem": "{{pokemonName}} incinerated\n{{targetName}}'s {{itemName}}!", "knockedOffItem": "{{pokemonName}} knocked off\n{{targetName}}'s {{itemName}}!", "tookMoveAttack": "{{pokemonName}} took\nthe {{moveName}} attack!", diff --git a/src/locales/fr/move-trigger.ts b/src/locales/fr/move-trigger.ts index 0bec4de6467..988db2e1959 100644 --- a/src/locales/fr/move-trigger.ts +++ b/src/locales/fr/move-trigger.ts @@ -33,6 +33,7 @@ export const moveTriggers: SimpleTranslationEntries = { "isChargingPower": "{{pokemonName}}\nconcentre son énergie !", "burnedItselfOut": "Le feu intérieur de {{pokemonName}}\ns’est entièrement consumé !", "startedHeatingUpBeak": "{{pokemonName}}\nfait chauffer son bec !", + "setUpShellTrap": "{{pokemonName}} déclenche\nle Carapiège !", "isOverflowingWithSpacePower": "La puissance du cosmos afflue dans le corps\nde {{pokemonName}} !", "usedUpAllElectricity": "{{pokemonName}}a utilisé\ntoute son électricité !", "stoleItem": "{{pokemonName}} vole\nl’objet {{itemName}} de {{targetName}} !", diff --git a/src/locales/it/move-trigger.ts b/src/locales/it/move-trigger.ts index ab17000a95d..198fc269785 100644 --- a/src/locales/it/move-trigger.ts +++ b/src/locales/it/move-trigger.ts @@ -33,6 +33,7 @@ export const moveTriggers: SimpleTranslationEntries = { "isChargingPower": "{{pokemonName}} accumula energia!", "burnedItselfOut": "Le fiamme di {{pokemonName}} si sono spente!", "startedHeatingUpBeak": "{{pokemonName}} inizia a\nscaldare il becco!", + "setUpShellTrap": "{{pokemonName}} ha preparato\nla Gusciotrappola!", "isOverflowingWithSpacePower": "La forza dell’universo pervade {{pokemonName}}!", "usedUpAllElectricity": "{{pokemonName}} ha usato tutta la sua elettricità!", "stoleItem": "{{pokemonName}} ruba\nil/lo/la {{itemName}} di {{targetName}}!", diff --git a/src/locales/ja/move-trigger.ts b/src/locales/ja/move-trigger.ts index dd741643888..e3a089db53b 100644 --- a/src/locales/ja/move-trigger.ts +++ b/src/locales/ja/move-trigger.ts @@ -33,6 +33,7 @@ export const moveTriggers: SimpleTranslationEntries = { "isChargingPower": "{{pokemonName}}は\nパワーを ためこんでいる!", "burnedItselfOut": "{{pokemonName}}の ほのうは\nもえつきた!", "startedHeatingUpBeak": "{{pokemonName}}は\nクチバシを かねつしはじめた!", + "setUpShellTrap": "{{pokemonName}}は\nトラップシェルを 仕掛けた!", "isOverflowingWithSpacePower": "{{pokemonName}}に\nうちゅうの ちからが あふれだす!", "usedUpAllElectricity": "{{pokemonName}}は\nでんきを つかいきった!", "stoleItem": "{{pokemonName}}は\n{{targetName}}の {{itemName}}を ぬすんだ!", diff --git a/src/locales/ko/move-trigger.ts b/src/locales/ko/move-trigger.ts index ec4edb8d2ca..cea60e25333 100644 --- a/src/locales/ko/move-trigger.ts +++ b/src/locales/ko/move-trigger.ts @@ -33,6 +33,7 @@ export const moveTriggers: SimpleTranslationEntries = { "isChargingPower": "{{pokemonName}}는(은)\n파워를 모으고 있다!", "burnedItselfOut": "{{pokemonName}}의 불꽃은 다 타 버렸다!", "startedHeatingUpBeak": "{{pokemonName}}는(은)\n부리를 가열하기 시작했다!", + "setUpShellTrap": "{{pokemonName}}[[는]]\n트랩셸을 설치했다!", "isOverflowingWithSpacePower": "{{pokemonName}}에게서\n우주의 힘이 넘쳐난다!", "usedUpAllElectricity": "{{pokemonName}}[[는]]\n전기를 다 써 버렸다!", "stoleItem": "{{pokemonName}}[[는]] {{targetName}}[[로]]부터\n{{itemName}}[[을]] 빼앗았다!", diff --git a/src/locales/pt_BR/move-trigger.ts b/src/locales/pt_BR/move-trigger.ts index 9dc151e12b9..042d539338e 100644 --- a/src/locales/pt_BR/move-trigger.ts +++ b/src/locales/pt_BR/move-trigger.ts @@ -33,6 +33,7 @@ export const moveTriggers: SimpleTranslationEntries = { "isChargingPower": "{{pokemonName}} está absorvendo energia!", "burnedItselfOut": "{{pokemonName}} apagou seu próprio fogo!", "startedHeatingUpBeak": "{{pokemonName}} começou\na esquentar seu bico!", + "setUpShellTrap": "{{pokemonName}} armou uma armadilha de carapaça!", "isOverflowingWithSpacePower": "{{pokemonName}} está sobrecarregado\ncom energia espacial!", "usedUpAllElectricity": "{{pokemonName}} usou toda a sua eletricidade!", "stoleItem": "{{pokemonName}} roubou/no(a) {{itemName}} de {{targetName}}!", diff --git a/src/locales/zh_CN/move-trigger.ts b/src/locales/zh_CN/move-trigger.ts index 334e66ee33a..3bbab276a87 100644 --- a/src/locales/zh_CN/move-trigger.ts +++ b/src/locales/zh_CN/move-trigger.ts @@ -33,6 +33,7 @@ export const moveTriggers: SimpleTranslationEntries = { "isChargingPower": "{{pokemonName}}\n正在积蓄力量!", "burnedItselfOut": "{{pokemonName}}的火焰燃尽了!", "startedHeatingUpBeak": "{{pokemonName}}\n开始给鸟嘴加热了!", + "setUpShellTrap": "{{pokemonName}}\n设置了陷阱甲壳!", "isOverflowingWithSpacePower": "{{pokemonName}}身上\n溢出了宇宙之力!", "usedUpAllElectricity": "{{pokemonName}}\n用尽电力了!", "stoleItem": "{{pokemonName}}从{{targetName}}那里\n夺取了{{itemName}}!", diff --git a/src/locales/zh_TW/move-trigger.ts b/src/locales/zh_TW/move-trigger.ts index 5342e08ed85..9eb7a38dfb3 100644 --- a/src/locales/zh_TW/move-trigger.ts +++ b/src/locales/zh_TW/move-trigger.ts @@ -33,6 +33,7 @@ export const moveTriggers: SimpleTranslationEntries = { "isChargingPower": "{{pokemonName}}\n正在積蓄力量!", "burnedItselfOut": "{{pokemonName}}的火焰燃盡了!", "startedHeatingUpBeak": "{{pokemonName}}\n開始給鳥嘴加熱了!", + "setUpShellTrap": "{{pokemonName}}\n設下了陷阱甲殼!", "isOverflowingWithSpacePower": "{{pokemonName}}湧起了宇宙的力量!", "usedUpAllElectricity": "{{pokemonName}}\n用盡了電力!", "stoleItem": "{{pokemonName}}从{{targetName}}那裏\n奪取了{{itemName}}!", diff --git a/src/phases.ts b/src/phases.ts index 5b66ee3e771..c9d2517fbef 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -1,7 +1,7 @@ import BattleScene, { bypassLogin } from "./battle-scene"; import { default as Pokemon, PlayerPokemon, EnemyPokemon, PokemonMove, MoveResult, DamageResult, FieldPosition, HitResult, TurnMove } from "./field/pokemon"; import * as Utils from "./utils"; -import { allMoves, applyMoveAttrs, BypassSleepAttr, ChargeAttr, applyFilteredMoveAttrs, HitsTagAttr, MissEffectAttr, MoveAttr, MoveEffectAttr, MoveFlags, MultiHitAttr, OverrideMoveEffectAttr, MoveTarget, getMoveTargets, MoveTargetSet, MoveEffectTrigger, CopyMoveAttr, AttackMove, SelfStatusMove, PreMoveMessageAttr, HealStatusEffectAttr, NoEffectAttr, BypassRedirectAttr, FixedDamageAttr, PostVictoryStatChangeAttr, ForceSwitchOutAttr, VariableTargetAttr, IncrementMovePriorityAttr, MoveHeaderAttr } from "./data/move"; +import { allMoves, applyMoveAttrs, BypassSleepAttr, ChargeAttr, applyFilteredMoveAttrs, HitsTagAttr, MissEffectAttr, MoveAttr, MoveEffectAttr, MoveFlags, MultiHitAttr, OverrideMoveEffectAttr, MoveTarget, getMoveTargets, MoveTargetSet, MoveEffectTrigger, CopyMoveAttr, AttackMove, SelfStatusMove, PreMoveMessageAttr, HealStatusEffectAttr, NoEffectAttr, BypassRedirectAttr, FixedDamageAttr, PostVictoryStatChangeAttr, ForceSwitchOutAttr, VariableTargetAttr, IncrementMovePriorityAttr, MoveHeaderAttr, MoveCategory } from "./data/move"; import { Mode } from "./ui/ui"; import { Command } from "./ui/command-ui-handler"; import { Stat } from "./data/pokemon-stat"; @@ -3191,6 +3191,9 @@ export class MoveEffectPhase extends PokemonPhase { return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult).then(() => { // If the invoked move is an enemy attack, apply the enemy's status effect-inflicting tags and tokens target.lapseTag(BattlerTagType.BEAK_BLAST_CHARGING); + if (move.category === MoveCategory.PHYSICAL && user.isPlayer() !== target.isPlayer()) { + target.lapseTag(BattlerTagType.SHELL_TRAP); + } if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) { user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target); } diff --git a/src/test/moves/shell_trap.test.ts b/src/test/moves/shell_trap.test.ts new file mode 100644 index 00000000000..b027541c252 --- /dev/null +++ b/src/test/moves/shell_trap.test.ts @@ -0,0 +1,163 @@ +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import GameManager from "#test/utils/gameManager"; +import { Moves } from "#app/enums/moves.js"; +import { Species } from "#app/enums/species.js"; +import { allMoves } from "#app/data/move.js"; +import { BattlerIndex } from "#app/battle.js"; +import { getMovePosition } from "../utils/gameManagerUtils"; +import { BerryPhase, MoveEndPhase, MovePhase } from "#app/phases.js"; +import { SPLASH_ONLY } from "../utils/testUtils"; +import { MoveResult } from "#app/field/pokemon.js"; + +const TIMEOUT = 20 * 1000; + +describe("Moves - Shell Trap", () => { + 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 + .battleType("double") + .moveset([Moves.SHELL_TRAP, Moves.SPLASH, Moves.BULLDOZE]) + .enemySpecies(Species.SNORLAX) + .enemyMoveset(Array(4).fill(Moves.RAZOR_LEAF)) + .startingLevel(100) + .enemyLevel(100); + + vi.spyOn(allMoves[Moves.RAZOR_LEAF], "accuracy", "get").mockReturnValue(100); + }); + + it( + "should activate after the user is hit by a physical attack", + async () => { + await game.startBattle([Species.CHARIZARD, Species.TURTONATOR]); + + const playerPokemon = game.scene.getPlayerField(); + const enemyPokemon = game.scene.getEnemyField(); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); + game.doAttack(getMovePosition(game.scene, 1, Moves.SHELL_TRAP)); + + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER, BattlerIndex.PLAYER_2]); + + await game.phaseInterceptor.to(MoveEndPhase); + + const movePhase = game.scene.getCurrentPhase(); + expect(movePhase instanceof MovePhase).toBeTruthy(); + expect((movePhase as MovePhase).pokemon).toBe(playerPokemon[1]); + + await game.phaseInterceptor.to(MoveEndPhase); + enemyPokemon.forEach(p => expect(p.hp).toBeLessThan(p.getMaxHp())); + }, TIMEOUT + ); + + it( + "should fail if the user is only hit by special attacks", + async () => { + game.override.enemyMoveset(Array(4).fill(Moves.SWIFT)); + + await game.startBattle([Species.CHARIZARD, Species.TURTONATOR]); + + const playerPokemon = game.scene.getPlayerField(); + const enemyPokemon = game.scene.getEnemyField(); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); + game.doAttack(getMovePosition(game.scene, 1, Moves.SHELL_TRAP)); + + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER, BattlerIndex.PLAYER_2]); + + await game.phaseInterceptor.to(MoveEndPhase); + + const movePhase = game.scene.getCurrentPhase(); + expect(movePhase instanceof MovePhase).toBeTruthy(); + expect((movePhase as MovePhase).pokemon).not.toBe(playerPokemon[1]); + + await game.phaseInterceptor.to(BerryPhase, false); + enemyPokemon.forEach(p => expect(p.hp).toBe(p.getMaxHp())); + }, TIMEOUT + ); + + it( + "should fail if the user isn't hit with any attack", + async () => { + game.override.enemyMoveset(SPLASH_ONLY); + + await game.startBattle([Species.CHARIZARD, Species.TURTONATOR]); + + const playerPokemon = game.scene.getPlayerField(); + const enemyPokemon = game.scene.getEnemyField(); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); + game.doAttack(getMovePosition(game.scene, 1, Moves.SHELL_TRAP)); + + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER, BattlerIndex.PLAYER_2]); + + await game.phaseInterceptor.to(MoveEndPhase); + + const movePhase = game.scene.getCurrentPhase(); + expect(movePhase instanceof MovePhase).toBeTruthy(); + expect((movePhase as MovePhase).pokemon).not.toBe(playerPokemon[1]); + + await game.phaseInterceptor.to(BerryPhase, false); + enemyPokemon.forEach(p => expect(p.hp).toBe(p.getMaxHp())); + }, TIMEOUT + ); + + it( + "should not activate from an ally's attack", + async () => { + game.override.enemyMoveset(SPLASH_ONLY); + + await game.startBattle([Species.BLASTOISE, Species.CHARIZARD]); + + const playerPokemon = game.scene.getPlayerField(); + const enemyPokemon = game.scene.getEnemyField(); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SHELL_TRAP)); + game.doAttack(getMovePosition(game.scene, 1, Moves.BULLDOZE)); + + await game.phaseInterceptor.to(MoveEndPhase); + + const movePhase = game.scene.getCurrentPhase(); + expect(movePhase instanceof MovePhase).toBeTruthy(); + expect((movePhase as MovePhase).pokemon).not.toBe(playerPokemon[1]); + + const enemyStartingHp = enemyPokemon.map(p => p.hp); + + await game.phaseInterceptor.to(BerryPhase, false); + enemyPokemon.forEach((p, i) => expect(p.hp).toBe(enemyStartingHp[i])); + }, TIMEOUT + ); + + it( + "should not activate from a subsequent physical attack", + async () => { + game.override.battleType("single"); + vi.spyOn(allMoves[Moves.RAZOR_LEAF], "priority", "get").mockReturnValue(-4); + + await game.startBattle([Species.CHARIZARD]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.doAttack(getMovePosition(game.scene, 0, Moves.SHELL_TRAP)); + + await game.phaseInterceptor.to(BerryPhase, false); + + expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); + }, TIMEOUT + ); +});