diff --git a/src/data/ability.ts b/src/data/ability.ts index 10aba1f030e..e3dbb84dedd 100755 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -5545,7 +5545,8 @@ export function initAbilities() { .attr(NoFusionAbilityAbAttr) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) - .attr(PostDefendGulpMissileAbAttr), + // .attr(PostDefendGulpMissileAbAttr) + .bypassFaint(), 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 ddb85600c18..d56839651ae 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -30,6 +30,7 @@ export enum BattlerTagLapseType { AFTER_MOVE, MOVE_EFFECT, TURN_END, + HIT, CUSTOM } @@ -1917,7 +1918,36 @@ export class StockpilingTag extends BattlerTag { */ export class GulpMissileTag extends BattlerTag { constructor(tagType: BattlerTagType, sourceMove: Moves) { - super(tagType, BattlerTagLapseType.CUSTOM, 0, sourceMove); + super(tagType, BattlerTagLapseType.HIT, 0, sourceMove); + } + + lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { + if (pokemon.getTag(BattlerTagType.UNDERWATER)) { + return true; + } + + const moveEffectPhase = pokemon.scene.getCurrentPhase(); + if (moveEffectPhase instanceof MoveEffectPhase) { + const attacker = moveEffectPhase.getUserPokemon(); + + if (!attacker) { + 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 (this.tagType === BattlerTagType.GULP_MISSILE_ARROKUDA) { + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, attacker.getBattlerIndex(), false, [ Stat.DEF ], -1)); + } else { + attacker.trySetStatus(StatusEffect.PARALYSIS, true, pokemon); + } + } + return false; } /** diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 863b0f41d2c..1e48729bc1d 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2370,6 +2370,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const destinyTag = this.getTag(BattlerTagType.DESTINY_BOND); if (damage.value) { + this.lapseTags(BattlerTagLapseType.HIT); + if (this.isFullHp()) { applyPreDefendAbAttrs(PreDefendFullHpEndureAbAttr, this, source, move, cancelled, false, damage); } else if (!this.isPlayer() && damage.value >= this.hp) { diff --git a/src/test/abilities/gulp_missile.test.ts b/src/test/abilities/gulp_missile.test.ts index 286c3af1c56..01fff75224d 100644 --- a/src/test/abilities/gulp_missile.test.ts +++ b/src/test/abilities/gulp_missile.test.ts @@ -1,10 +1,6 @@ import { BattlerTagType } from "#app/enums/battler-tag-type"; import { StatusEffect } from "#app/enums/status-effect"; import Pokemon from "#app/field/pokemon"; -import { BerryPhase } from "#app/phases/berry-phase"; -import { MoveEndPhase } from "#app/phases/move-end-phase"; -import { TurnEndPhase } from "#app/phases/turn-end-phase"; -import { TurnStartPhase } from "#app/phases/turn-start-phase"; import GameManager from "#app/test/utils/gameManager"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; @@ -54,13 +50,13 @@ describe("Abilities - Gulp Missile", () => { }); it("changes to Gulping Form if HP is over half when Surf or Dive is used", async () => { - await game.startBattle([Species.CRAMORANT]); + await game.classicMode.startBattle([Species.CRAMORANT]); const cramorant = game.scene.getPlayerPokemon()!; game.move.select(Moves.DIVE); await game.toNextTurn(); game.move.select(Moves.DIVE); - await game.phaseInterceptor.to(MoveEndPhase); + await game.phaseInterceptor.to("MoveEndPhase"); expect(cramorant.getHpRatio()).toBeGreaterThanOrEqual(.5); expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined(); @@ -68,21 +64,21 @@ describe("Abilities - Gulp Missile", () => { }); it("changes to Gorging Form if HP is under half when Surf or Dive is used", async () => { - await game.startBattle([Species.CRAMORANT]); + await game.classicMode.startBattle([Species.CRAMORANT]); const cramorant = game.scene.getPlayerPokemon()!; vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.49); expect(cramorant.getHpRatio()).toBe(.49); game.move.select(Moves.SURF); - await game.phaseInterceptor.to(MoveEndPhase); + await game.phaseInterceptor.to("MoveEndPhase"); expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_PIKACHU)).toBeDefined(); expect(cramorant.formIndex).toBe(GORGING_FORM); }); it("changes to base form when switched out after Surf or Dive is used", async () => { - await game.startBattle([Species.CRAMORANT, Species.MAGIKARP]); + await game.classicMode.startBattle([Species.CRAMORANT, Species.MAGIKARP]); const cramorant = game.scene.getPlayerPokemon()!; game.move.select(Moves.SURF); @@ -97,11 +93,11 @@ describe("Abilities - Gulp Missile", () => { }); it("changes form during Dive's charge turn", async () => { - await game.startBattle([Species.CRAMORANT]); + await game.classicMode.startBattle([Species.CRAMORANT]); const cramorant = game.scene.getPlayerPokemon()!; game.move.select(Moves.DIVE); - await game.phaseInterceptor.to(MoveEndPhase); + await game.phaseInterceptor.to("MoveEndPhase"); expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined(); expect(cramorant.formIndex).toBe(GULPING_FORM); @@ -109,31 +105,31 @@ describe("Abilities - Gulp Missile", () => { it("deals 1/4 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]); + await game.classicMode.startBattle([Species.CRAMORANT]); const enemy = game.scene.getEnemyPokemon()!; vi.spyOn(enemy, "damageAndUpdate"); game.move.select(Moves.SURF); - await game.phaseInterceptor.to(TurnEndPhase); + 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]); + await game.classicMode.startBattle([Species.CRAMORANT]); const cramorant = game.scene.getPlayerPokemon()!; vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.55); game.move.select(Moves.SURF); - await game.phaseInterceptor.to(MoveEndPhase); + await game.phaseInterceptor.to("MoveEndPhase"); expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined(); expect(cramorant.formIndex).toBe(GULPING_FORM); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined(); expect(cramorant.formIndex).toBe(GULPING_FORM); @@ -141,7 +137,7 @@ describe("Abilities - Gulp Missile", () => { it("lowers attacker's DEF stat stage by 1 when hit in Gulping form", async () => { game.override.enemyMoveset(Array(4).fill(Moves.TACKLE)); - await game.startBattle([Species.CRAMORANT]); + await game.classicMode.startBattle([Species.CRAMORANT]); const cramorant = game.scene.getPlayerPokemon()!; const enemy = game.scene.getEnemyPokemon()!; @@ -150,12 +146,12 @@ describe("Abilities - Gulp Missile", () => { vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.55); game.move.select(Moves.SURF); - await game.phaseInterceptor.to(MoveEndPhase); + await game.phaseInterceptor.to("MoveEndPhase"); expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined(); expect(cramorant.formIndex).toBe(GULPING_FORM); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); expect(enemy.damageAndUpdate).toHaveReturnedWith(getEffectDamage(enemy)); expect(enemy.getStatStage(Stat.DEF)).toBe(-1); @@ -165,7 +161,7 @@ describe("Abilities - Gulp Missile", () => { it("paralyzes the enemy when hit in Gorging form", async () => { game.override.enemyMoveset(Array(4).fill(Moves.TACKLE)); - await game.startBattle([Species.CRAMORANT]); + await game.classicMode.startBattle([Species.CRAMORANT]); const cramorant = game.scene.getPlayerPokemon()!; const enemy = game.scene.getEnemyPokemon()!; @@ -174,12 +170,12 @@ describe("Abilities - Gulp Missile", () => { vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.45); game.move.select(Moves.SURF); - await game.phaseInterceptor.to(MoveEndPhase); + await game.phaseInterceptor.to("MoveEndPhase"); expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_PIKACHU)).toBeDefined(); expect(cramorant.formIndex).toBe(GORGING_FORM); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); expect(enemy.damageAndUpdate).toHaveReturnedWith(getEffectDamage(enemy)); expect(enemy.status?.effect).toBe(StatusEffect.PARALYSIS); @@ -189,12 +185,12 @@ describe("Abilities - Gulp Missile", () => { it("does not activate the ability when underwater", async () => { game.override.enemyMoveset(Array(4).fill(Moves.SURF)); - await game.startBattle([Species.CRAMORANT]); + await game.classicMode.startBattle([Species.CRAMORANT]); const cramorant = game.scene.getPlayerPokemon()!; game.move.select(Moves.DIVE); - await game.phaseInterceptor.to(BerryPhase, false); + await game.phaseInterceptor.to("BerryPhase", false); expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined(); expect(cramorant.formIndex).toBe(GULPING_FORM); @@ -202,7 +198,7 @@ describe("Abilities - Gulp Missile", () => { 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]); + await game.classicMode.startBattle([Species.CRAMORANT]); const cramorant = game.scene.getPlayerPokemon()!; const enemy = game.scene.getEnemyPokemon()!; @@ -210,13 +206,13 @@ describe("Abilities - Gulp Missile", () => { vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.55); game.move.select(Moves.SURF); - await game.phaseInterceptor.to(MoveEndPhase); + 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); + await game.phaseInterceptor.to("TurnEndPhase"); expect(enemy.hp).toBe(enemyHpPreEffect); expect(enemy.getStatStage(Stat.DEF)).toBe(-1); @@ -224,20 +220,36 @@ describe("Abilities - Gulp Missile", () => { expect(cramorant.formIndex).toBe(NORMAL_FORM); }); + it("activates on faint", async () => { + game.override.enemyMoveset(Array(4).fill(Moves.THUNDERBOLT)); + await game.classicMode.startBattle([Species.CRAMORANT]); + + const cramorant = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.SURF); + await game.phaseInterceptor.to("FaintPhase"); + + expect(cramorant.hp).toBe(0); + expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeUndefined(); + expect(cramorant.formIndex).toBe(NORMAL_FORM); + expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.DEF)).toBe(-1); + }); + + it("cannot be suppressed", async () => { game.override.enemyMoveset(Array(4).fill(Moves.GASTRO_ACID)); - await game.startBattle([Species.CRAMORANT]); + await game.classicMode.startBattle([Species.CRAMORANT]); const cramorant = game.scene.getPlayerPokemon()!; vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.55); game.move.select(Moves.SURF); - await game.phaseInterceptor.to(MoveEndPhase); + await game.phaseInterceptor.to("MoveEndPhase"); expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined(); expect(cramorant.formIndex).toBe(GULPING_FORM); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); expect(cramorant.hasAbility(Abilities.GULP_MISSILE)).toBe(true); expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined(); @@ -246,18 +258,18 @@ describe("Abilities - Gulp Missile", () => { it("cannot be swapped with another ability", async () => { game.override.enemyMoveset(Array(4).fill(Moves.SKILL_SWAP)); - await game.startBattle([Species.CRAMORANT]); + await game.classicMode.startBattle([Species.CRAMORANT]); const cramorant = game.scene.getPlayerPokemon()!; vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.55); game.move.select(Moves.SURF); - await game.phaseInterceptor.to(MoveEndPhase); + await game.phaseInterceptor.to("MoveEndPhase"); expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined(); expect(cramorant.formIndex).toBe(GULPING_FORM); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); expect(cramorant.hasAbility(Abilities.GULP_MISSILE)).toBe(true); expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined(); @@ -267,9 +279,9 @@ describe("Abilities - Gulp Missile", () => { it("cannot be copied", async () => { game.override.enemyAbility(Abilities.TRACE); - await game.startBattle([Species.CRAMORANT]); + await game.classicMode.startBattle([Species.CRAMORANT]); game.move.select(Moves.SPLASH); - await game.phaseInterceptor.to(TurnStartPhase); + await game.phaseInterceptor.to("TurnStartPhase"); expect(game.scene.getEnemyPokemon()?.hasAbility(Abilities.GULP_MISSILE)).toBe(false); });