diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 70195d6a152..498bccec7c5 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -1172,43 +1172,45 @@ export class MoveImmunityStatStageChangeAbAttr extends MoveImmunityAbAttr { * @see {@linkcode applyPostDefend} */ export class ReverseDrainAbAttr extends PostDefendAbAttr { + private attacker: Pokemon; override canApplyPostDefend( - _pokemon: Pokemon, - _passive: boolean, - _simulated: boolean, - _attacker: Pokemon, - move: Move, - _hitResult: HitResult | null, - _args: any[], - ): boolean { - return move.hasAttr("HitHealAttr"); - } - - /** - * Determines if a damage and draining move was used to check if this ability should stop the healing. - * Examples include: Absorb, Draining Kiss, Bitter Blade, etc. - * Also displays a message to show this ability was activated. - * @param _pokemon {@linkcode Pokemon} with this ability - * @param _passive N/A - * @param attacker {@linkcode Pokemon} that is attacking this Pokemon - * @param _move {@linkcode PokemonMove} that is being used - * @param _hitResult N/A - * @param _args N/A - */ - override applyPostDefend( _pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, - _move: Move, - _hitResult: HitResult, + move: Move, + _hitResult: HitResult | null, + _args: any[], + ): boolean { + const cancelled = new BooleanHolder(false); + applyAbAttrs("BlockNonDirectDamageAbAttr", attacker, cancelled, simulated); + this.attacker = attacker; + return !cancelled.value && move.hasAttr("HitHealAttr"); + } + + override applyPostDefend( + pokemon: Pokemon, + _passive: boolean, + _simulated: boolean, + attacker: Pokemon, + move: Move, + _hitResult: HitResult | null, _args: any[], ): void { - if (!simulated) { - globalScene.phaseManager.queueMessage( - i18next.t("abilityTriggers:reverseDrain", { pokemonNameWithAffix: getPokemonNameWithAffix(attacker) }), - ); - } + const damageAmount = move.getAttrs<"HitHealAttr">("HitHealAttr")[0].getHealAmount(attacker, pokemon); + pokemon.turnData.damageTaken += damageAmount; + globalScene.phaseManager.unshiftNew( + "PokemonHealPhase", + attacker.getBattlerIndex(), + -damageAmount, + null, + false, + true, + ); + } + + public override getTriggerMessage(_pokemon: Pokemon, _abilityName: string, ..._args: any[]): string | null { + return i18next.t("abilityTriggers:reverseDrain", { pokemonNameWithAffix: getPokemonNameWithAffix(this.attacker) }); } } diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index f61e8debc9f..875a93eb315 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -2254,28 +2254,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; } @@ -2294,6 +2284,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..da9f861ad2c --- /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/testUtils/gameManager"; +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.scene.getPlayerPokemon()?.isFullHp()).toBe(false); + }); + + 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.scene.getPlayerPokemon()?.isFullHp()).toBe(true); + }); + + 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.scene.getPlayerPokemon()?.isFullHp()).toBe(true); + }); +});