From 9875fcc59d5e5e5de9d685523e1ea8f5ba0fa8af Mon Sep 17 00:00:00 2001 From: "Adrian T." <68144167+torranx@users.noreply.github.com> Date: Wed, 7 Aug 2024 04:18:47 +0800 Subject: [PATCH] [Ability] Partially implement Gulp Missile (#3327) * implement gulp missile * add tests * change fly to dive * show ability when reverting to normal form * update ai score, tests * update score condition * adjust conditions, damage * add underwater test * update damage in test * partial commit --- src/data/ability.ts | 51 ++++- src/data/battler-tags.ts | 36 ++++ src/data/move.ts | 46 +++- src/data/pokemon-forms.ts | 6 + src/enums/battler-tag-type.ts | 4 +- src/test/abilities/gulp_missile.test.ts | 266 ++++++++++++++++++++++++ 6 files changed, 404 insertions(+), 5 deletions(-) create mode 100644 src/test/abilities/gulp_missile.test.ts diff --git a/src/data/ability.ts b/src/data/ability.ts index 9e77013ff13..b483998612e 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -6,7 +6,7 @@ import { BattleStat, getBattleStatName } from "./battle-stat"; import { MovePhase, PokemonHealPhase, ShowAbilityPhase, StatChangePhase } from "../phases"; import { getPokemonNameWithAffix } from "../messages"; import { Weather, WeatherType } from "./weather"; -import { BattlerTag, GroundedTag } from "./battler-tags"; +import { BattlerTag, GroundedTag, GulpMissileTag } from "./battler-tags"; import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect"; import { Gender } from "./gender"; import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, ChargeAttr, SacrificialAttr, SacrificialAttrOnHit } from "./move"; @@ -496,6 +496,49 @@ export class PostDefendAbAttr extends AbAttr { } } +/** + * Applies the effects of Gulp Missile when the user is hit by an attack. + * @extends PostDefendAbAttr + */ +export class PostDefendGulpMissileAbAttr extends PostDefendAbAttr { + constructor() { + super(true); + } + + /** + * Damages the attacker and triggers the secondary effect based on the form or the BattlerTagType. + * @param {Pokemon} pokemon - The defending Pokemon. + * @param passive - n/a + * @param {Pokemon} attacker - The attacking Pokemon. + * @param {Move} move - The move being used. + * @param {HitResult} hitResult - n/a + * @param {any[]} args - n/a + * @returns Whether the effects of the ability are applied. + */ + applyPostDefend(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean | Promise { + const battlerTag = pokemon.getTag(GulpMissileTag); + if (!battlerTag || move.category === MoveCategory.STATUS) { + return false; + } + + const cancelled = new Utils.BooleanHolder(false); + applyAbAttrs(BlockNonDirectDamageAbAttr, attacker, cancelled); + + if (!cancelled.value) { + attacker.damageAndUpdate(Math.max(1, Math.floor(attacker.getMaxHp() / 4)), HitResult.OTHER); + } + + if (battlerTag.tagType === BattlerTagType.GULP_MISSILE_ARROKUDA) { + pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, attacker.getBattlerIndex(), false, [ BattleStat.DEF ], -1)); + } else { + attacker.trySetStatus(StatusEffect.PARALYSIS, true, pokemon); + } + + pokemon.removeTag(battlerTag.tagType); + return true; + } +} + export class PostDefendDisguiseAbAttr extends PostDefendAbAttr { applyPostDefend(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { @@ -5087,7 +5130,11 @@ export function initAbilities() { .attr(UnsuppressableAbilityAbAttr) .attr(NoTransformAbilityAbAttr) .attr(NoFusionAbilityAbAttr) - .unimplemented(), + .attr(UncopiableAbilityAbAttr) + .attr(UnswappableAbilityAbAttr) + .attr(PostDefendGulpMissileAbAttr) + // Does not transform when Surf/Dive misses/is protected + .partial(), new Ability(Abilities.STALWART, 8) .attr(BlockRedirectAbAttr), new Ability(Abilities.STEAM_ENGINE, 8) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 94df265ff5e..d2f462d5a44 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1647,6 +1647,39 @@ export class StockpilingTag extends BattlerTag { } } +/** + * Battler tag for Gulp Missile used by Cramorant. + * @extends BattlerTag + */ +export class GulpMissileTag extends BattlerTag { + constructor(tagType: BattlerTagType, sourceMove: Moves) { + super(tagType, BattlerTagLapseType.CUSTOM, 0, sourceMove); + } + + /** + * Gulp Missile's initial form changes are triggered by using Surf and Dive. + * @param {Pokemon} pokemon The Pokemon with Gulp Missile ability. + * @returns Whether the BattlerTag can be added. + */ + canAdd(pokemon: Pokemon): boolean { + const isSurfOrDive = [ Moves.SURF, Moves.DIVE ].includes(this.sourceMove); + const isNormalForm = pokemon.formIndex === 0 && !pokemon.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA) && !pokemon.getTag(BattlerTagType.GULP_MISSILE_PIKACHU); + const isCramorant = pokemon.species.speciesId === Species.CRAMORANT; + + return isSurfOrDive && isNormalForm && isCramorant; + } + + onAdd(pokemon: Pokemon): void { + super.onAdd(pokemon); + pokemon.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeManualTrigger); + } + + onRemove(pokemon: Pokemon): void { + super.onRemove(pokemon); + pokemon.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeManualTrigger); + } +} + export function getBattlerTag(tagType: BattlerTagType, turnCount: number, sourceMove: Moves, sourceId: number): BattlerTag { switch (tagType) { case BattlerTagType.RECHARGING: @@ -1770,6 +1803,9 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source return new StockpilingTag(sourceMove); case BattlerTagType.OCTOLOCK: return new OctolockTag(sourceId); + case BattlerTagType.GULP_MISSILE_ARROKUDA: + case BattlerTagType.GULP_MISSILE_PIKACHU: + return new GulpMissileTag(tagType, sourceMove); case BattlerTagType.NONE: default: return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId); diff --git a/src/data/move.ts b/src/data/move.ts index 3d1e7820582..8ddc984e304 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, SwitchSummonPhase } from "../phases"; import { BattleStat, getBattleStatName } from "./battle-stat"; -import { EncoreTag, HelpingHandTag, SemiInvulnerableTag, StockpilingTag, TypeBoostTag } from "./battler-tags"; +import { EncoreTag, GulpMissileTag, HelpingHandTag, SemiInvulnerableTag, StockpilingTag, 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"; @@ -4300,6 +4300,46 @@ export class AddBattlerTagAttr extends MoveEffectAttr { } } +/** + * Adds the appropriate battler tag for Gulp Missile when Surf or Dive is used. + * @extends MoveEffectAttr + */ +export class GulpMissileTagAttr extends MoveEffectAttr { + constructor() { + super(true, MoveEffectTrigger.POST_APPLY); + } + + /** + * Adds BattlerTagType from GulpMissileTag based on the Pokemon's HP ratio. + * @param {Pokemon} user The Pokemon using the move. + * @param {Pokemon} target The Pokemon being targeted by the move. + * @param {Move} move The move being used. + * @param {any[]} args Additional arguments, if any. + * @returns Whether the BattlerTag is applied. + */ + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean | Promise { + if (!super.apply(user, target, move, args)) { + return false; + } + + if (user.hasAbility(Abilities.GULP_MISSILE) && user.species.speciesId === Species.CRAMORANT) { + if (user.getHpRatio() >= .5) { + user.addTag(BattlerTagType.GULP_MISSILE_ARROKUDA, 0, move.id); + } else { + user.addTag(BattlerTagType.GULP_MISSILE_PIKACHU, 0, move.id); + } + return true; + } + + return false; + } + + getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { + const isCramorant = user.hasAbility(Abilities.GULP_MISSILE) && user.species.speciesId === Species.CRAMORANT; + return isCramorant && !user.getTag(GulpMissileTag) ? 10 : 0; + } +} + export class CurseAttr extends MoveEffectAttr { apply(user: Pokemon, target: Pokemon, move:Move, args: any[]): boolean { @@ -6157,7 +6197,8 @@ export function initMoves() { new AttackMove(Moves.HYDRO_PUMP, Type.WATER, MoveCategory.SPECIAL, 110, 80, 5, -1, 0, 1), new AttackMove(Moves.SURF, Type.WATER, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 1) .target(MoveTarget.ALL_NEAR_OTHERS) - .attr(HitsTagAttr, BattlerTagType.UNDERWATER, true), + .attr(HitsTagAttr, BattlerTagType.UNDERWATER, true) + .attr(GulpMissileTagAttr), new AttackMove(Moves.ICE_BEAM, Type.ICE, MoveCategory.SPECIAL, 90, 100, 10, 10, 0, 1) .attr(StatusEffectAttr, StatusEffect.FREEZE), new AttackMove(Moves.BLIZZARD, Type.ICE, MoveCategory.SPECIAL, 110, 70, 5, 10, 0, 1) @@ -6822,6 +6863,7 @@ export function initMoves() { .partial(), new AttackMove(Moves.DIVE, Type.WATER, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 3) .attr(ChargeAttr, ChargeAnim.DIVE_CHARGING, i18next.t("moveTriggers:hidUnderwater", {pokemonName: "{USER}"}), BattlerTagType.UNDERWATER) + .attr(GulpMissileTagAttr) .ignoresVirtual(), new AttackMove(Moves.ARM_THRUST, Type.FIGHTING, MoveCategory.PHYSICAL, 15, 100, 20, -1, 0, 3) .attr(MultiHitAttr), diff --git a/src/data/pokemon-forms.ts b/src/data/pokemon-forms.ts index 93781063061..7e29ea994e9 100644 --- a/src/data/pokemon-forms.ts +++ b/src/data/pokemon-forms.ts @@ -828,6 +828,12 @@ export const pokemonFormChanges: PokemonFormChanges = { [Species.EISCUE]: [ new SpeciesFormChange(Species.EISCUE, "", "no-ice", new SpeciesFormChangeManualTrigger(), true), new SpeciesFormChange(Species.EISCUE, "no-ice", "", new SpeciesFormChangeManualTrigger(), true), + ], + [Species.CRAMORANT]: [ + new SpeciesFormChange(Species.CRAMORANT, "", "gulping", new SpeciesFormChangeManualTrigger, true, new SpeciesFormChangeCondition(p => p.getHpRatio() >= .5)), + new SpeciesFormChange(Species.CRAMORANT, "", "gorging", new SpeciesFormChangeManualTrigger, true, new SpeciesFormChangeCondition(p => p.getHpRatio() < .5)), + new SpeciesFormChange(Species.CRAMORANT, "gulping", "", new SpeciesFormChangeManualTrigger, true), + new SpeciesFormChange(Species.CRAMORANT, "gorging", "", new SpeciesFormChangeManualTrigger, true), ] }; diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index 52f6402861e..7ef73bef281 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -63,5 +63,7 @@ export enum BattlerTagType { ICE_FACE = "ICE_FACE", STOCKPILING = "STOCKPILING", RECEIVE_DOUBLE_DAMAGE = "RECEIVE_DOUBLE_DAMAGE", - ALWAYS_GET_HIT = "ALWAYS_GET_HIT" + ALWAYS_GET_HIT = "ALWAYS_GET_HIT", + GULP_MISSILE_ARROKUDA = "GULP_MISSILE_ARROKUDA", + GULP_MISSILE_PIKACHU = "GULP_MISSILE_PIKACHU" } diff --git a/src/test/abilities/gulp_missile.test.ts b/src/test/abilities/gulp_missile.test.ts new file mode 100644 index 00000000000..f9329017006 --- /dev/null +++ b/src/test/abilities/gulp_missile.test.ts @@ -0,0 +1,266 @@ +import { BattlerTagType } from "#app/enums/battler-tag-type.js"; +import { + MoveEndPhase, + TurnEndPhase, + TurnStartPhase, +} from "#app/phases"; +import GameManager from "#app/test/utils/gameManager"; +import { getMovePosition } from "#app/test/utils/gameManagerUtils"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { SPLASH_ONLY } from "../utils/testUtils"; +import { BattleStat } from "#app/data/battle-stat.js"; +import { StatusEffect } from "#app/enums/status-effect.js"; +import { GulpMissileTag } from "#app/data/battler-tags.js"; +import Pokemon from "#app/field/pokemon.js"; + +describe("Abilities - Gulp Missile", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + const NORMAL_FORM = 0; + const GULPING_FORM = 1; + const GORGING_FORM = 2; + + /** + * Gets the effect damage of Gulp Missile + * See Gulp Missile {@link https://bulbapedia.bulbagarden.net/wiki/Gulp_Missile_(Ability)} + * @param {Pokemon} pokemon The pokemon taking the effect damage. + * @returns The effect damage of Gulp Missile + */ + const getEffectDamage = (pokemon: Pokemon): number => { + return Math.max(1, Math.floor(pokemon.getMaxHp() * 1/4)); + }; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .battleType("single") + .moveset([Moves.SURF, Moves.DIVE, Moves.SPLASH]) + .enemySpecies(Species.SNORLAX) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(SPLASH_ONLY) + .enemyLevel(5); + }); + + it("changes to Gulping Form if HP is over half when Surf or Dive is used", async () => { + await game.startBattle([Species.CRAMORANT]); + const cramorant = game.scene.getPlayerPokemon(); + + game.doAttack(getMovePosition(game.scene, 0, Moves.DIVE)); + await game.toNextTurn(); + game.doAttack(getMovePosition(game.scene, 0, Moves.DIVE)); + await game.phaseInterceptor.to(MoveEndPhase); + + expect(cramorant.getHpRatio()).toBeGreaterThanOrEqual(.5); + expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined(); + expect(cramorant.formIndex).toBe(GULPING_FORM); + }); + + it("changes to Gorging Form if HP is under half when Surf or Dive is used", async () => { + await game.startBattle([Species.CRAMORANT]); + const cramorant = game.scene.getPlayerPokemon(); + + vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.49); + expect(cramorant.getHpRatio()).toBe(.49); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SURF)); + await game.phaseInterceptor.to(MoveEndPhase); + + expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_PIKACHU)).toBeDefined(); + expect(cramorant.formIndex).toBe(GORGING_FORM); + }); + + it("deals ΒΌ of the attacker's maximum HP when hit by a damaging attack", async () => { + game.override.enemyMoveset(Array(4).fill(Moves.TACKLE)); + await game.startBattle([Species.CRAMORANT]); + + const enemy = game.scene.getEnemyPokemon(); + vi.spyOn(enemy, "damageAndUpdate"); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SURF)); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(enemy.damageAndUpdate).toHaveReturnedWith(getEffectDamage(enemy)); + }); + + it("does not have any effect when hit by non-damaging attack", async () => { + game.override.enemyMoveset(Array(4).fill(Moves.TAIL_WHIP)); + await game.startBattle([Species.CRAMORANT]); + + const cramorant = game.scene.getPlayerPokemon(); + vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.55); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SURF)); + await game.phaseInterceptor.to(MoveEndPhase); + + expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined(); + expect(cramorant.formIndex).toBe(GULPING_FORM); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined(); + expect(cramorant.formIndex).toBe(GULPING_FORM); + }); + + it("lowers the attacker's Defense by 1 stage when hit in Gulping form", async () => { + game.override.enemyMoveset(Array(4).fill(Moves.TACKLE)); + await game.startBattle([Species.CRAMORANT]); + + const cramorant = game.scene.getPlayerPokemon(); + const enemy = game.scene.getEnemyPokemon(); + + vi.spyOn(enemy, "damageAndUpdate"); + vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.55); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SURF)); + await game.phaseInterceptor.to(MoveEndPhase); + + expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined(); + expect(cramorant.formIndex).toBe(GULPING_FORM); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(enemy.damageAndUpdate).toHaveReturnedWith(getEffectDamage(enemy)); + expect(enemy.summonData.battleStats[BattleStat.DEF]).toBe(-1); + expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeUndefined(); + expect(cramorant.formIndex).toBe(NORMAL_FORM); + }); + + it("paralyzes the enemy when hit in Gorging form", async () => { + game.override.enemyMoveset(Array(4).fill(Moves.TACKLE)); + await game.startBattle([Species.CRAMORANT]); + + const cramorant = game.scene.getPlayerPokemon(); + const enemy = game.scene.getEnemyPokemon(); + + vi.spyOn(enemy, "damageAndUpdate"); + vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.45); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SURF)); + await game.phaseInterceptor.to(MoveEndPhase); + + expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_PIKACHU)).toBeDefined(); + expect(cramorant.formIndex).toBe(GORGING_FORM); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(enemy.damageAndUpdate).toHaveReturnedWith(getEffectDamage(enemy)); + expect(enemy.status.effect).toBe(StatusEffect.PARALYSIS); + expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_PIKACHU)).toBeUndefined(); + expect(cramorant.formIndex).toBe(NORMAL_FORM); + }); + + it("does not activate the ability when underwater", async () => { + game.override + .enemyMoveset(Array(4).fill(Moves.SURF)) + .enemySpecies(Species.REGIELEKI) + .enemyAbility(Abilities.BALL_FETCH) + .enemyLevel(5); + await game.startBattle([Species.CRAMORANT]); + + const cramorant = game.scene.getPlayerPokemon(); + + game.doAttack(getMovePosition(game.scene, 0, Moves.DIVE)); + await game.toNextTurn(); + + // Turn 2 underwater, enemy moves first + game.doAttack(getMovePosition(game.scene, 0, Moves.DIVE)); + await game.phaseInterceptor.to(MoveEndPhase); + + expect(cramorant.formIndex).toBe(NORMAL_FORM); + expect(cramorant.getTag(GulpMissileTag)).toBeUndefined(); + + // Turn 2 Cramorant comes out and changes form + await game.phaseInterceptor.to(TurnEndPhase); + expect(cramorant.formIndex).not.toBe(NORMAL_FORM); + expect(cramorant.getTag(GulpMissileTag)).toBeDefined(); + }); + + it("prevents effect damage but inflicts secondary effect on attacker with Magic Guard", async () => { + game.override.enemyMoveset(Array(4).fill(Moves.TACKLE)).enemyAbility(Abilities.MAGIC_GUARD); + await game.startBattle([Species.CRAMORANT]); + + const cramorant = game.scene.getPlayerPokemon(); + const enemy = game.scene.getEnemyPokemon(); + + vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.55); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SURF)); + await game.phaseInterceptor.to(MoveEndPhase); + const enemyHpPreEffect = enemy.hp; + + expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined(); + expect(cramorant.formIndex).toBe(GULPING_FORM); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(enemy.hp).toBe(enemyHpPreEffect); + expect(enemy.summonData.battleStats[BattleStat.DEF]).toBe(-1); + expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeUndefined(); + expect(cramorant.formIndex).toBe(NORMAL_FORM); + }); + + it("cannot be suppressed", async () => { + game.override.enemyMoveset(Array(4).fill(Moves.GASTRO_ACID)); + await game.startBattle([Species.CRAMORANT]); + + const cramorant = game.scene.getPlayerPokemon(); + vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.55); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SURF)); + await game.phaseInterceptor.to(MoveEndPhase); + + expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined(); + expect(cramorant.formIndex).toBe(GULPING_FORM); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(cramorant.hasAbility(Abilities.GULP_MISSILE)).toBe(true); + expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined(); + expect(cramorant.formIndex).toBe(GULPING_FORM); + }); + + it("cannot be swapped with another ability", async () => { + game.override.enemyMoveset(Array(4).fill(Moves.SKILL_SWAP)); + await game.startBattle([Species.CRAMORANT]); + + const cramorant = game.scene.getPlayerPokemon(); + vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.55); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SURF)); + await game.phaseInterceptor.to(MoveEndPhase); + + expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined(); + expect(cramorant.formIndex).toBe(GULPING_FORM); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(cramorant.hasAbility(Abilities.GULP_MISSILE)).toBe(true); + expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined(); + expect(cramorant.formIndex).toBe(GULPING_FORM); + }); + + it("cannot be copied", async () => { + game.override.enemyAbility(Abilities.TRACE); + + await game.startBattle([Species.CRAMORANT]); + game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); + await game.phaseInterceptor.to(TurnStartPhase); + + expect(game.scene.getEnemyPokemon().hasAbility(Abilities.GULP_MISSILE)).toBe(false); + }); +});