From 7ce2aa3ed9f608707e3f47266fa53cd3b9b600c1 Mon Sep 17 00:00:00 2001 From: Dean <69436131+emdeann@users.noreply.github.com> Date: Mon, 6 Oct 2025 09:00:08 -0700 Subject: [PATCH] [Bug] Liquid Ooze can now properly be suppressed (#5535) * Fix applying even when suppressed * Rewrite move/ability effects * Fix using defender instead of attacker when applying magic guard * Add test * Unchange move effect phase * Kev fixes Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Fix liquid ooze test * Fix hithealattr apply method * Fix test * Move checks to canApply --------- Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/abilities/ability.ts | 28 ++++++++++---- src/data/moves/move.ts | 26 +++++-------- test/abilities/liquid_ooze.test.ts | 62 ++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 23 deletions(-) create mode 100644 test/abilities/liquid_ooze.test.ts diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index be508648b71..536175e6450 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -1082,8 +1082,10 @@ export class PostDefendAbAttr extends AbAttr { /** Class for abilities that make drain moves deal damage to user instead of healing them. */ export class ReverseDrainAbAttr extends PostDefendAbAttr { - override canApply({ move }: PostMoveInteractionAbAttrParams): boolean { - return move.hasAttr("HitHealAttr"); + override canApply({ move, opponent, simulated }: PostMoveInteractionAbAttrParams): boolean { + const cancelled = new BooleanHolder(false); + applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon: opponent, cancelled, simulated }); + return !cancelled.value && move.hasAttr("HitHealAttr"); } /** @@ -1091,12 +1093,24 @@ export class ReverseDrainAbAttr extends PostDefendAbAttr { * Examples include: Absorb, Draining Kiss, Bitter Blade, etc. * Also displays a message to show this ability was activated. */ - override apply({ simulated, opponent: attacker }: PostMoveInteractionAbAttrParams): void { - if (!simulated) { - globalScene.phaseManager.queueMessage( - i18next.t("abilityTriggers:reverseDrain", { pokemonNameWithAffix: getPokemonNameWithAffix(attacker) }), - ); + override apply({ move, simulated, opponent, pokemon }: PostMoveInteractionAbAttrParams): void { + if (simulated) { + return; } + const damageAmount = move.getAttrs<"HitHealAttr">("HitHealAttr")[0].getHealAmount(opponent, pokemon); + pokemon.turnData.damageTaken += damageAmount; + globalScene.phaseManager.unshiftNew( + "PokemonHealPhase", + opponent.getBattlerIndex(), + -damageAmount, + null, + false, + true, + ); + } + + public override getTriggerMessage({ opponent }: PostMoveInteractionAbAttrParams): string | null { + return i18next.t("abilityTriggers:reverseDrain", { pokemonNameWithAffix: getPokemonNameWithAffix(opponent) }); } } diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 4735c694673..d33ccd4e8fa 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -2546,28 +2546,18 @@ export class HitHealAttr extends MoveEffectAttr { * @returns true if the function succeeds */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - let healAmount = 0; + if (target.hasAbilityWithAttr("ReverseDrainAbAttr")) { + return false; + } + + const healAmount = this.getHealAmount(user, target); let message = ""; - const reverseDrain = target.hasAbilityWithAttr("ReverseDrainAbAttr", false); if (this.healStat !== null) { - // Strength Sap formula - healAmount = target.getEffectiveStat(this.healStat); message = i18next.t("battle:drainMessage", { pokemonName: getPokemonNameWithAffix(target) }); } else { - // Default healing formula used by draining moves like Absorb, Draining Kiss, Bitter Blade, etc. - healAmount = toDmgValue(user.turnData.singleHitDamageDealt * this.healRatio); message = i18next.t("battle:regainHealth", { pokemonName: getPokemonNameWithAffix(user) }); } - if (reverseDrain) { - if (user.hasAbilityWithAttr("BlockNonDirectDamageAbAttr")) { - healAmount = 0; - message = ""; - } else { - user.turnData.damageTaken += healAmount; - healAmount = healAmount * -1; - message = ""; - } - } + globalScene.phaseManager.unshiftNew("PokemonHealPhase", user.getBattlerIndex(), healAmount, message, false, true); return true; } @@ -2586,6 +2576,10 @@ export class HitHealAttr extends MoveEffectAttr { } return Math.floor(Math.max((1 - user.getHpRatio()) - 0.33, 0) * (move.power / 4)); } + + public getHealAmount(user: Pokemon, target: Pokemon): number { + return (this.healStat) ? target.getEffectiveStat(this.healStat) : toDmgValue(user.turnData.singleHitDamageDealt * this.healRatio); + } } /** diff --git a/test/abilities/liquid_ooze.test.ts b/test/abilities/liquid_ooze.test.ts new file mode 100644 index 00000000000..1d04ac5389a --- /dev/null +++ b/test/abilities/liquid_ooze.test.ts @@ -0,0 +1,62 @@ +import { AbilityId } from "#enums/ability-id"; +import { MoveId } from "#enums/move-id"; +import { SpeciesId } from "#enums/species-id"; +import { GameManager } from "#test/test-utils/game-manager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Abilities - Liquid Ooze", () => { + 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 + .moveset([MoveId.SPLASH, MoveId.GIGA_DRAIN]) + .ability(AbilityId.BALL_FETCH) + .battleStyle("single") + .enemyLevel(20) + .enemySpecies(SpeciesId.MAGIKARP) + .enemyAbility(AbilityId.LIQUID_OOZE) + .enemyMoveset(MoveId.SPLASH); + }); + + it("should drain the attacker's HP after a draining move", async () => { + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + game.move.select(MoveId.GIGA_DRAIN); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.field.getPlayerPokemon()).not.toHaveFullHp(); + }); + + it("should not drain the attacker's HP if it ignores indirect damage", async () => { + game.override.ability(AbilityId.MAGIC_GUARD); + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + game.move.select(MoveId.GIGA_DRAIN); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.field.getPlayerPokemon()).toHaveFullHp(); + }); + + it("should not apply if suppressed", async () => { + game.override.ability(AbilityId.NEUTRALIZING_GAS); + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + game.move.select(MoveId.GIGA_DRAIN); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.field.getPlayerPokemon()).toHaveFullHp(); + }); +});