From 4c327e9e63537935e3401fdfe50c65d66bfbe889 Mon Sep 17 00:00:00 2001 From: "Adrian T." <68144167+torranx@users.noreply.github.com> Date: Mon, 30 Sep 2024 09:57:50 +0800 Subject: [PATCH] [Ability] Refactor Gulp Missile and make it trigger when Cramorant faints (#4428) * reimplement gulp missile * cleanup + docs * more cleanup * add override * update paths --- src/data/ability.ts | 57 ++----------- src/data/battler-tags.ts | 31 ++++++- src/test/abilities/gulp_missile.test.ts | 104 +++++++++++++----------- 3 files changed, 96 insertions(+), 96 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 6ec4ac0e02c..041d90ac4c0 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -4,7 +4,7 @@ import { Constructor } from "#app/utils"; import * as Utils from "../utils"; import { getPokemonNameWithAffix } from "../messages"; import { Weather, WeatherType } from "./weather"; -import { BattlerTag, GroundedTag, GulpMissileTag, SemiInvulnerableTag } from "./battler-tags"; +import { BattlerTag, GroundedTag } 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, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move"; @@ -536,53 +536,6 @@ 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, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean | Promise { - const battlerTag = pokemon.getTag(GulpMissileTag); - if (!battlerTag || move.category === MoveCategory.STATUS || pokemon.getTag(SemiInvulnerableTag)) { - return false; - } - - if (simulated) { - return true; - } - - 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 StatStageChangePhase(pokemon.scene, attacker.getBattlerIndex(), false, [ Stat.DEF ], -1)); - } else { - attacker.trySetStatus(StatusEffect.PARALYSIS, true, pokemon); - } - - pokemon.removeTag(battlerTag.tagType); - return true; - } -} - export class FieldPriorityMoveImmunityAbAttr extends PreDefendAbAttr { applyPreDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean { const attackPriority = new Utils.IntegerHolder(move.priority); @@ -5668,13 +5621,19 @@ export function initAbilities() { new Ability(Abilities.MIRROR_ARMOR, 8) .ignorable() .unimplemented(), + /** + * Right now, the logic is attached to Surf and Dive moves. Ideally, the post-defend/hit should be an + * ability attribute but the current implementation of move effects for BattlerTag does not support this- in the case + * where Cramorant is fainted. + * @see {@linkcode GulpMissileTagAttr} and {@linkcode GulpMissileTag} for Gulp Missile implementation + */ new Ability(Abilities.GULP_MISSILE, 8) .attr(UnsuppressableAbilityAbAttr) .attr(NoTransformAbilityAbAttr) .attr(NoFusionAbilityAbAttr) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) - .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 6758dc1fada..d8094f96368 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -2123,7 +2123,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); + } + + override 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/test/abilities/gulp_missile.test.ts b/src/test/abilities/gulp_missile.test.ts index ac0efd8e8d1..d981f009974 100644 --- a/src/test/abilities/gulp_missile.test.ts +++ b/src/test/abilities/gulp_missile.test.ts @@ -1,11 +1,7 @@ -import { BattlerTagType } from "#app/enums/battler-tag-type"; -import { StatusEffect } from "#app/enums/status-effect"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { StatusEffect } from "#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 GameManager from "#test/utils/gameManager"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; @@ -53,13 +49,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(); @@ -67,21 +63,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); @@ -96,51 +92,51 @@ 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); }); it("deals 1/4 of the attacker's maximum HP when hit by a damaging attack", async () => { - game.override.enemyMoveset([Moves.TACKLE]); - await game.startBattle([Species.CRAMORANT]); + game.override.enemyMoveset(Moves.TACKLE); + 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([Moves.TAIL_WHIP]); - await game.startBattle([Species.CRAMORANT]); + game.override.enemyMoveset(Moves.TAIL_WHIP); + 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); }); it("lowers attacker's DEF stat stage by 1 when hit in Gulping form", async () => { - game.override.enemyMoveset([Moves.TACKLE]); - await game.startBattle([Species.CRAMORANT]); + game.override.enemyMoveset(Moves.TACKLE); + await game.classicMode.startBattle([Species.CRAMORANT]); const cramorant = game.scene.getPlayerPokemon()!; const enemy = game.scene.getEnemyPokemon()!; @@ -149,12 +145,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); @@ -163,8 +159,8 @@ describe("Abilities - Gulp Missile", () => { }); it("paralyzes the enemy when hit in Gorging form", async () => { - game.override.enemyMoveset([Moves.TACKLE]); - await game.startBattle([Species.CRAMORANT]); + game.override.enemyMoveset(Moves.TACKLE); + await game.classicMode.startBattle([Species.CRAMORANT]); const cramorant = game.scene.getPlayerPokemon()!; const enemy = game.scene.getEnemyPokemon()!; @@ -173,12 +169,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); @@ -187,21 +183,21 @@ describe("Abilities - Gulp Missile", () => { }); it("does not activate the ability when underwater", async () => { - game.override.enemyMoveset([Moves.SURF]); - await game.startBattle([Species.CRAMORANT]); + game.override.enemyMoveset(Moves.SURF); + 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); }); it("prevents effect damage but inflicts secondary effect on attacker with Magic Guard", async () => { - game.override.enemyMoveset([Moves.TACKLE]).enemyAbility(Abilities.MAGIC_GUARD); - await game.startBattle([Species.CRAMORANT]); + game.override.enemyMoveset(Moves.TACKLE).enemyAbility(Abilities.MAGIC_GUARD); + await game.classicMode.startBattle([Species.CRAMORANT]); const cramorant = game.scene.getPlayerPokemon()!; const enemy = game.scene.getEnemyPokemon()!; @@ -209,13 +205,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); @@ -223,20 +219,36 @@ describe("Abilities - Gulp Missile", () => { expect(cramorant.formIndex).toBe(NORMAL_FORM); }); + it("activates on faint", async () => { + game.override.enemyMoveset(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([Moves.GASTRO_ACID]); - await game.startBattle([Species.CRAMORANT]); + game.override.enemyMoveset(Moves.GASTRO_ACID); + 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(); @@ -244,19 +256,19 @@ describe("Abilities - Gulp Missile", () => { }); it("cannot be swapped with another ability", async () => { - game.override.enemyMoveset([Moves.SKILL_SWAP]); - await game.startBattle([Species.CRAMORANT]); + game.override.enemyMoveset(Moves.SKILL_SWAP); + 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(); @@ -266,9 +278,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); });