From 93c7ab79352b52ec4ecbd4690aec7d635604294c Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Fri, 1 Aug 2025 18:00:21 -0400 Subject: [PATCH] Refactored healing phase + removed `preventFullHp` param --- src/data/moves/move.ts | 1 - src/field/pokemon.ts | 10 +-- src/modifier/modifier.ts | 36 ++++----- src/phases/pokemon-heal-phase.ts | 123 +++++++++++++++---------------- 4 files changed, 79 insertions(+), 91 deletions(-) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 6d232e7c8f4..f307bea7897 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -2183,7 +2183,6 @@ export class SacrificialFullRestoreAttr extends SacrificialAttr { false, false, true, - false, this.restorePP), true); diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 2c9329a95ef..d7730ff0cd2 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -24,11 +24,11 @@ import { NoCritTag, WeakenMoveScreenTag } from "#data/arena-tag"; import { AutotomizedTag, BattlerTag, + type BattlerTagTypeMap, CritBoostTag, EncoreTag, ExposedTag, GroundedTag, - type GrudgeTag, getBattlerTag, HighestStatBoostTag, MoveRestrictionBattlerTag, @@ -4240,14 +4240,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return false; } - /**@overload */ - getTag(tagType: BattlerTagType.GRUDGE): GrudgeTag | undefined; - /** @overload */ - getTag(tagType: BattlerTagType.SUBSTITUTE): SubstituteTag | undefined; - - /** @overload */ - getTag(tagType: BattlerTagType): BattlerTag | undefined; + getTag(tagType: T): BattlerTagTypeMap[T] | undefined; /** @overload */ getTag(tagType: Constructor): T | undefined; diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index b31bee7fc69..2cae080d339 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -1933,7 +1933,6 @@ export class PokemonInstantReviveModifier extends PokemonHeldItemModifier { * @returns always `true` */ override apply(pokemon: Pokemon): boolean { - // Restore the Pokemon to half HP globalScene.phaseManager.unshiftNew( "PokemonHealPhase", pokemon.getBattlerIndex(), @@ -1948,6 +1947,7 @@ export class PokemonInstantReviveModifier extends PokemonHeldItemModifier { ); // Remove the Pokemon's FAINT status + // TODO: Remove call to `resetStatus` once StatusEffect.FAINT is canned pokemon.resetStatus(true, false, true, false); // Reapply Commander on the Pokemon's side of the field, if applicable @@ -3549,24 +3549,26 @@ export class EnemyTurnHealModifier extends EnemyPersistentModifier { * @returns `true` if the {@linkcode Pokemon} was healed */ override apply(enemyPokemon: Pokemon): boolean { - if (!enemyPokemon.isFullHp()) { - globalScene.phaseManager.unshiftNew( - "PokemonHealPhase", - enemyPokemon.getBattlerIndex(), - Math.max(Math.floor(enemyPokemon.getMaxHp() / (100 / this.healPercent)) * this.stackCount, 1), - i18next.t("modifier:enemyTurnHealApply", { - pokemonNameWithAffix: getPokemonNameWithAffix(enemyPokemon), - }), - true, - false, - false, - false, - true, - ); - return true; + if (enemyPokemon.isFullHp()) { + return false; } - return false; + // Prevent healing to full from healing tokens + const healAmt = Math.min( + enemyPokemon.getMaxHp() - 1, + toDmgValue((enemyPokemon.getMaxHp() * this.stackCount * this.healPercent) / 100), + ); + globalScene.phaseManager.unshiftNew( + "PokemonHealPhase", + enemyPokemon.getBattlerIndex(), + healAmt, + i18next.t("modifier:enemyTurnHealApply", { + pokemonNameWithAffix: getPokemonNameWithAffix(enemyPokemon), + }), + true, + ); + + return true; } getMaxStackCount(): number { diff --git a/src/phases/pokemon-heal-phase.ts b/src/phases/pokemon-heal-phase.ts index 573df9c2da9..e9914cf377b 100644 --- a/src/phases/pokemon-heal-phase.ts +++ b/src/phases/pokemon-heal-phase.ts @@ -1,12 +1,10 @@ import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; -import type { HealBlockTag } from "#data/battler-tags"; import { getStatusEffectHealText } from "#data/status-effect"; import type { BattlerIndex } from "#enums/battler-index"; import { BattlerTagType } from "#enums/battler-tag-type"; import { HitResult } from "#enums/hit-result"; import { CommonAnim } from "#enums/move-anims-common"; -import { StatusEffect } from "#enums/status-effect"; import { HealingBoosterModifier } from "#modifiers/modifier"; import { CommonAnimPhase } from "#phases/common-anim-phase"; import { HealAchv } from "#system/achv"; @@ -15,13 +13,16 @@ import i18next from "i18next"; export class PokemonHealPhase extends CommonAnimPhase { public readonly phaseName = "PokemonHealPhase"; + + /** The base amount of HP to heal. */ private hpHealed: number; + /** The message to display upon healing the target, or `null` to show no message. */ private message: string | null; + /** Whether to show a message and quit out early upon healing a Pokemon already at full hp. */ private showFullHpMessage: boolean; private skipAnim: boolean; private revive: boolean; private healStatus: boolean; - private preventFullHeal: boolean; private fullRestorePP: boolean; constructor( @@ -32,7 +33,6 @@ export class PokemonHealPhase extends CommonAnimPhase { skipAnim = false, revive = false, healStatus = false, - preventFullHeal = false, fullRestorePP = false, ) { super(battlerIndex, undefined, CommonAnim.HEALTH_UP); @@ -43,12 +43,11 @@ export class PokemonHealPhase extends CommonAnimPhase { this.skipAnim = skipAnim; this.revive = revive; this.healStatus = healStatus; - this.preventFullHeal = preventFullHeal; this.fullRestorePP = fullRestorePP; } start() { - if (!this.skipAnim && (this.revive || this.getPokemon().hp) && !this.getPokemon().isFullHp()) { + if (!this.skipAnim && !this.getPokemon().isFullHp()) { super.start(); } else { this.end(); @@ -58,78 +57,72 @@ export class PokemonHealPhase extends CommonAnimPhase { end() { const pokemon = this.getPokemon(); - if (!pokemon.isOnField() || (!this.revive && !pokemon.isActive())) { - return super.end(); + // Prevent healing off-field pokemon + if (!pokemon.isActive(true)) { + super.end(); + return; } - const hasMessage = !!this.message; - const healOrDamage = !pokemon.isFullHp() || this.hpHealed < 0; - const healBlock = pokemon.getTag(BattlerTagType.HEAL_BLOCK) as HealBlockTag; - let lastStatusEffect = StatusEffect.NONE; - + // Check for heal block, ending the phase early if healing was prevented + const healBlock = pokemon.getTag(BattlerTagType.HEAL_BLOCK); if (healBlock && this.hpHealed > 0) { globalScene.phaseManager.queueMessage(healBlock.onActivation(pokemon)); - return super.end(); + super.end(); + return; } - if (healOrDamage) { - const hpRestoreMultiplier = new NumberHolder(1); - if (!this.revive) { - globalScene.applyModifiers(HealingBoosterModifier, this.player, hpRestoreMultiplier); - } - const healAmount = new NumberHolder(Math.floor(this.hpHealed * hpRestoreMultiplier.value)); - if (healAmount.value < 0) { - pokemon.damageAndUpdate(healAmount.value * -1, { result: HitResult.INDIRECT }); - healAmount.value = 0; - } - // Prevent healing to full if specified (in case of healing tokens so Sturdy doesn't cause a softlock) - if (this.preventFullHeal && pokemon.hp + healAmount.value >= pokemon.getMaxHp()) { - healAmount.value = pokemon.getMaxHp() - pokemon.hp - 1; - } - healAmount.value = pokemon.heal(healAmount.value); - if (healAmount.value) { - globalScene.damageNumberHandler.add(pokemon, healAmount.value, HitResult.HEAL); - } - if (pokemon.isPlayer()) { - globalScene.validateAchvs(HealAchv, healAmount); - if (healAmount.value > globalScene.gameData.gameStats.highestHeal) { - globalScene.gameData.gameStats.highestHeal = healAmount.value; - } - } - if (this.healStatus && !this.revive && pokemon.status) { - lastStatusEffect = pokemon.status.effect; - pokemon.resetStatus(); - } - if (this.fullRestorePP) { - for (const move of this.getPokemon().getMoveset()) { - if (move) { - move.ppUsed = 0; - } - } + // If we would heal the user past full HP, don't. + if (this.hpHealed >= 0 && pokemon.isFullHp()) { + if (this.showFullHpMessage) { + globalScene.phaseManager.queueMessage( + i18next.t("battle:hpIsFull", { + pokemonName: getPokemonNameWithAffix(pokemon), + }), + ); } + super.end(); + return; + } + + // Apply the effect of healing charms for non-revival items + const hpRestoreMultiplier = new NumberHolder(1); + globalScene.applyModifiers(HealingBoosterModifier, this.player, hpRestoreMultiplier); + let healAmount = Math.floor(this.hpHealed * hpRestoreMultiplier.value); + + // If Liquid Ooze is active, damage the user for the healing amount, then return. + // TODO: Refactor liquid ooze to not use a heal phase to do damage + if (healAmount < 0) { + pokemon.damageAndUpdate(-healAmount, { result: HitResult.INDIRECT }); pokemon.updateInfo().then(() => super.end()); - } else if (this.healStatus && !this.revive && pokemon.status) { - lastStatusEffect = pokemon.status.effect; + return; + } + + // Heal the pokemon (capping at max HP), then show damage numbers and + // do achievement validation + healAmount = pokemon.heal(healAmount); + globalScene.damageNumberHandler.add(pokemon, healAmount, HitResult.HEAL); + if (pokemon.isPlayer()) { + globalScene.validateAchvs(HealAchv, healAmount); + globalScene.gameData.gameStats.highestHeal = Math.max(globalScene.gameData.gameStats.highestHeal, healAmount); + } + + // Cure status as applicable + // TODO: This should not be the job of the healing phase + if (this.healStatus && pokemon.status) { + globalScene.phaseManager.queueMessage( + getStatusEffectHealText(pokemon.status.effect, getPokemonNameWithAffix(pokemon)), + ); pokemon.resetStatus(); - pokemon.updateInfo().then(() => super.end()); - } else if (this.showFullHpMessage) { - this.message = i18next.t("battle:hpIsFull", { - pokemonName: getPokemonNameWithAffix(pokemon), - }); } + this.showMessage(); + + pokemon.updateInfo().then(() => super.end()); + } + + private showMessage(): void { if (this.message) { globalScene.phaseManager.queueMessage(this.message); } - - if (this.healStatus && lastStatusEffect && !hasMessage) { - globalScene.phaseManager.queueMessage( - getStatusEffectHealText(lastStatusEffect, getPokemonNameWithAffix(pokemon)), - ); - } - - if (!healOrDamage && !lastStatusEffect) { - super.end(); - } } }