From 90c9c71cd9be5a96154e5649a82282fb9727ced8 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Tue, 5 Aug 2025 12:53:10 -0400 Subject: [PATCH] Cleaned up `pokemonHealPhase` + wrapped inside an object --- src/data/abilities/ability.ts | 64 ++++--- src/data/battler-tags.ts | 34 ++-- src/data/berry.ts | 10 +- src/data/moves/move.ts | 60 +++--- src/data/positional-tags/positional-tag.ts | 9 +- src/modifier/modifier.ts | 73 ++++---- src/phases/pokemon-heal-phase.ts | 207 +++++++++++++-------- src/phases/quiet-form-change-phase.ts | 15 +- src/phases/turn-end-phase.ts | 10 +- 9 files changed, 276 insertions(+), 206 deletions(-) diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 2f57df4a551..d21219ffb4e 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -748,16 +748,12 @@ export class TypeImmunityHealAbAttr extends TypeImmunityAbAttr { const { pokemon, cancelled, simulated, passive } = params; if (!pokemon.isFullHp() && !simulated) { const abilityName = (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name; - globalScene.phaseManager.unshiftNew( - "PokemonHealPhase", - pokemon.getBattlerIndex(), - toDmgValue(pokemon.getMaxHp() / 4), - i18next.t("abilityTriggers:typeImmunityHeal", { + globalScene.phaseManager.unshiftNew("PokemonHealPhase", pokemon.getBattlerIndex(), pokemon.getMaxHp() / 4, { + message: i18next.t("abilityTriggers:typeImmunityHeal", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName, }), - true, - ); + }); cancelled.value = true; // Suppresses "No Effect" message } } @@ -2830,12 +2826,13 @@ export class PostSummonAllyHealAbAttr extends PostSummonAbAttr { "PokemonHealPhase", target.getBattlerIndex(), toDmgValue(pokemon.getMaxHp() / this.healRatio), - i18next.t("abilityTriggers:postSummonAllyHeal", { - pokemonNameWithAffix: getPokemonNameWithAffix(target), - pokemonName: pokemon.name, - }), - true, - !this.showAnim, + { + message: i18next.t("abilityTriggers:postSummonAllyHeal", { + pokemonNameWithAffix: getPokemonNameWithAffix(target), + pokemonName: pokemon.name, + }), + skipAnim: !this.showAnim, + }, ); } } @@ -4476,11 +4473,12 @@ export class PostWeatherLapseHealAbAttr extends PostWeatherLapseAbAttr { "PokemonHealPhase", pokemon.getBattlerIndex(), toDmgValue(pokemon.getMaxHp() / (16 / this.healFactor)), - i18next.t("abilityTriggers:postWeatherLapseHeal", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - abilityName, - }), - true, + { + message: i18next.t("abilityTriggers:postWeatherLapseHeal", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), + abilityName, + }), + }, ); } } @@ -4595,8 +4593,12 @@ export class PostTurnStatusHealAbAttr extends PostTurnAbAttr { "PokemonHealPhase", pokemon.getBattlerIndex(), toDmgValue(pokemon.getMaxHp() / 8), - i18next.t("abilityTriggers:poisonHeal", { pokemonName: getPokemonNameWithAffix(pokemon), abilityName }), - true, + { + message: i18next.t("abilityTriggers:poisonHeal", { + pokemonName: getPokemonNameWithAffix(pokemon), + abilityName, + }), + }, ); } } @@ -4843,11 +4845,12 @@ export class PostTurnHealAbAttr extends PostTurnAbAttr { "PokemonHealPhase", pokemon.getBattlerIndex(), toDmgValue(pokemon.getMaxHp() / 16), - i18next.t("abilityTriggers:postTurnHeal", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - abilityName, - }), - true, + { + message: i18next.t("abilityTriggers:postTurnHeal", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), + abilityName, + }), + }, ); } } @@ -5224,11 +5227,12 @@ export class HealFromBerryUseAbAttr extends AbAttr { "PokemonHealPhase", pokemon.getBattlerIndex(), toDmgValue(pokemon.getMaxHp() * this.healPercent), - i18next.t("abilityTriggers:healFromBerryUse", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - abilityName, - }), - true, + { + message: i18next.t("abilityTriggers:healFromBerryUse", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), + abilityName, + }), + }, ); } } diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 455beec6901..2c9193b3647 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1076,18 +1076,16 @@ export class SeedTag extends SerializableBattlerTag { ); // Damage the target and restore our HP (or take damage in the case of liquid ooze) + // TODO: Liquid ooze should queue a damage anim phase directly const damage = pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8), { result: HitResult.INDIRECT }); const reverseDrain = pokemon.hasAbilityWithAttr("ReverseDrainAbAttr", false); - globalScene.phaseManager.unshiftNew( - "PokemonHealPhase", - source.getBattlerIndex(), - reverseDrain ? -damage : damage, - i18next.t(reverseDrain ? "battlerTags:seededLapseShed" : "battlerTags:seededLapse", { + globalScene.phaseManager.unshiftNew("PokemonHealPhase", source.getBattlerIndex(), reverseDrain ? -damage : damage, { + message: i18next.t(reverseDrain ? "battlerTags:seededLapseShed" : "battlerTags:seededLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), }), - false, - true, - ); + showFullHpMessage: false, + skipAnim: true, + }); return true; } @@ -1382,10 +1380,11 @@ export class IngrainTag extends TrappedTag { "PokemonHealPhase", pokemon.getBattlerIndex(), toDmgValue(pokemon.getMaxHp() / 16), - i18next.t("battlerTags:ingrainLapse", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - }), - true, + { + message: i18next.t("battlerTags:ingrainLapse", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), + }), + }, ); } @@ -1455,11 +1454,12 @@ export class AquaRingTag extends SerializableBattlerTag { "PokemonHealPhase", pokemon.getBattlerIndex(), toDmgValue(pokemon.getMaxHp() / 16), - i18next.t("battlerTags:aquaRingLapse", { - moveName: this.getMoveName(), - pokemonName: getPokemonNameWithAffix(pokemon), - }), - true, + { + message: i18next.t("battlerTags:aquaRingLapse", { + moveName: this.getMoveName(), + pokemonName: getPokemonNameWithAffix(pokemon), + }), + }, ); } diff --git a/src/data/berry.ts b/src/data/berry.ts index 61235b75e21..056bd7f7397 100644 --- a/src/data/berry.ts +++ b/src/data/berry.ts @@ -73,16 +73,12 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { { const hpHealed = new NumberHolder(toDmgValue(consumer.getMaxHp() / 4)); applyAbAttrs("DoubleBerryEffectAbAttr", { pokemon: consumer, effectValue: hpHealed }); - globalScene.phaseManager.unshiftNew( - "PokemonHealPhase", - consumer.getBattlerIndex(), - hpHealed.value, - i18next.t("battle:hpHealBerry", { + globalScene.phaseManager.unshiftNew("PokemonHealPhase", consumer.getBattlerIndex(), hpHealed.value, { + message: i18next.t("battle:hpHealBerry", { pokemonNameWithAffix: getPokemonNameWithAffix(consumer), berryName: getBerryName(berryType), }), - true, - ); + }); } break; case BerryType.LUM: diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 912ba1c4d1d..0b4e8291026 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -1983,7 +1983,13 @@ export class HealAttr extends MoveEffectAttr { */ protected addHealPhase(target: Pokemon, healRatio: number) { globalScene.phaseManager.unshiftNew("PokemonHealPhase", target.getBattlerIndex(), - toDmgValue(target.getMaxHp() * healRatio), i18next.t("moveTriggers:healHp", { pokemonName: getPokemonNameWithAffix(target) }), true, !this.showAnim); + toDmgValue(target.getMaxHp() * healRatio), + { + message: i18next.t("moveTriggers:healHp", { pokemonName: getPokemonNameWithAffix(target) }), + showFullHpMessage: true, + skipAnim: !this.showAnim, + } + ); } override getTargetBenefitScore(user: Pokemon, target: Pokemon, _move: Move): number { @@ -2175,16 +2181,19 @@ export class SacrificialFullRestoreAttr extends SacrificialAttr { const pm = globalScene.phaseManager; pm.pushPhase( - pm.create("PokemonHealPhase", + pm.create( + "PokemonHealPhase", user.getBattlerIndex(), maxPartyMemberHp, - i18next.t(this.moveMessage, { pokemonName: getPokemonNameWithAffix(user) }), - true, - false, - false, - true, - this.restorePP), - true); + { + message: i18next.t(this.moveMessage, { pokemonName: getPokemonNameWithAffix(user) }), + showFullHpMessage: false, + skipAnim: true, + healStatus: true, + fullRestorePP: this.restorePP, + } + ), + true); return true; } @@ -2280,7 +2289,9 @@ export class HitHealAttr extends MoveEffectAttr { message = ""; } } - globalScene.phaseManager.unshiftNew("PokemonHealPhase", user.getBattlerIndex(), healAmount, message, false, true); + globalScene.phaseManager.unshiftNew("PokemonHealPhase", user.getBattlerIndex(), healAmount, + {message, showFullHpMessage: false, skipAnim: true} + ); return true; } @@ -4313,7 +4324,8 @@ export class PunishmentPowerAttr extends VariablePowerAttr { } export class PresentPowerAttr extends VariablePowerAttr { - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + apply(user: Pokemon, target: Pokemon, move: Move, args: [NumberHolder]): boolean { + const power = args[0] /** * If this move is multi-hit, and this attribute is applied to any hit * other than the first, this move cannot result in a heal. @@ -4322,17 +4334,21 @@ export class PresentPowerAttr extends VariablePowerAttr { const powerSeed = randSeedInt(firstHit ? 100 : 80); if (powerSeed <= 40) { - (args[0] as NumberHolder).value = 40; - } else if (40 < powerSeed && powerSeed <= 70) { - (args[0] as NumberHolder).value = 80; - } else if (70 < powerSeed && powerSeed <= 80) { - (args[0] as NumberHolder).value = 120; - } else if (80 < powerSeed && powerSeed <= 100) { - // If this move is multi-hit, disable all other hits + power.value = 40; + } else if (powerSeed <= 70) { + power.value = 80; + } else if (powerSeed <= 80) { + power.value = 120; + } else if (powerSeed <= 100) { + // Disable all other hits and heal the target for 25% max HP user.turnData.hitCount = 1; user.turnData.hitsLeft = 1; - globalScene.phaseManager.unshiftNew("PokemonHealPhase", target.getBattlerIndex(), - toDmgValue(target.getMaxHp() / 4), i18next.t("moveTriggers:regainedHealth", { pokemonName: getPokemonNameWithAffix(target) }), true); + globalScene.phaseManager.unshiftNew( + "PokemonHealPhase", + target.getBattlerIndex(), + toDmgValue(target.getMaxHp() / 4), + {message: i18next.t("moveTriggers:regainedHealth", { pokemonName: getPokemonNameWithAffix(target) })} + ) } return true; @@ -5853,8 +5869,8 @@ export class ProtectAttr extends AddBattlerTagAttr { for (const turnMove of user.getLastXMoves(-1).slice()) { if ( // Quick & Wide guard increment the Protect counter without using it for fail chance - !(allMoves[turnMove.move].hasAttr("ProtectAttr") || - [MoveId.QUICK_GUARD, MoveId.WIDE_GUARD].includes(turnMove.move)) || + !(allMoves[turnMove.move].hasAttr("ProtectAttr") || + [MoveId.QUICK_GUARD, MoveId.WIDE_GUARD].includes(turnMove.move)) || turnMove.result !== MoveResult.SUCCESS ) { break; diff --git a/src/data/positional-tags/positional-tag.ts b/src/data/positional-tags/positional-tag.ts index 77ca6f0e9eb..2c5e270a65f 100644 --- a/src/data/positional-tags/positional-tag.ts +++ b/src/data/positional-tags/positional-tag.ts @@ -155,13 +155,12 @@ export class WishTag extends PositionalTag implements WishArgs { public override trigger(): void { // TODO: Rename this locales key - wish shows a message on REMOVAL, not addition - globalScene.phaseManager.queueMessage( - i18next.t("arenaTag:wishTagOnAdd", { + // TODO: What messages does Wish show when healing a Pokemon at full HP? + globalScene.phaseManager.unshiftNew("PokemonHealPhase", this.targetIndex, this.healHp, { + message: i18next.t("arenaTag:wishTagOnAdd", { pokemonNameWithAffix: this.pokemonName, }), - ); - - globalScene.phaseManager.unshiftNew("PokemonHealPhase", this.targetIndex, this.healHp, null, true, false); + }); } public override shouldTrigger(): boolean { diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index b31bee7fc69..ecacea47e33 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -1674,11 +1674,12 @@ export class TurnHealModifier extends PokemonHeldItemModifier { "PokemonHealPhase", pokemon.getBattlerIndex(), toDmgValue(pokemon.getMaxHp() / 16) * this.stackCount, - i18next.t("modifier:turnHealApply", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - typeName: this.type.name, - }), - true, + { + message: i18next.t("modifier:turnHealApply", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), + typeName: this.type.name, + }), + }, ); return true; } @@ -1766,16 +1767,16 @@ export class HitHealModifier extends PokemonHeldItemModifier { */ override apply(pokemon: Pokemon): boolean { if (pokemon.turnData.totalDamageDealt && !pokemon.isFullHp()) { - // TODO: this shouldn't be undefined AFAIK globalScene.phaseManager.unshiftNew( "PokemonHealPhase", pokemon.getBattlerIndex(), - toDmgValue(pokemon.turnData.totalDamageDealt / 8) * this.stackCount, - i18next.t("modifier:hitHealApply", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - typeName: this.type.name, - }), - true, + toDmgValue((pokemon.turnData.totalDamageDealt * this.stackCount) / 8), + { + message: i18next.t("modifier:hitHealApply", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), + typeName: this.type.name, + }), + }, ); } @@ -1934,20 +1935,22 @@ export class PokemonInstantReviveModifier extends PokemonHeldItemModifier { */ override apply(pokemon: Pokemon): boolean { // Restore the Pokemon to half HP + // TODO: This should not use a phase to revive pokemon globalScene.phaseManager.unshiftNew( "PokemonHealPhase", pokemon.getBattlerIndex(), toDmgValue(pokemon.getMaxHp() / 2), - i18next.t("modifier:pokemonInstantReviveApply", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - typeName: this.type.name, - }), - false, - false, - true, + { + message: i18next.t("modifier:pokemonInstantReviveApply", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), + typeName: this.type.name, + }), + revive: true, + }, ); // 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 +3552,24 @@ 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 + globalScene.phaseManager.unshiftNew( + "PokemonHealPhase", + enemyPokemon.getBattlerIndex(), + (enemyPokemon.getMaxHp() * this.stackCount * this.healPercent) / 100, + { + message: i18next.t("modifier:enemyTurnHealApply", { + pokemonNameWithAffix: getPokemonNameWithAffix(enemyPokemon), + }), + preventFullHeal: true, + }, + ); + + return true; } getMaxStackCount(): number { diff --git a/src/phases/pokemon-heal-phase.ts b/src/phases/pokemon-heal-phase.ts index fa6a3222466..bf7fb3e9453 100644 --- a/src/phases/pokemon-heal-phase.ts +++ b/src/phases/pokemon-heal-phase.ts @@ -1,39 +1,81 @@ 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"; -import { NumberHolder } from "#utils/common"; +import { NumberHolder, toDmgValue } from "#utils/common"; import i18next from "i18next"; export class PokemonHealPhase extends CommonAnimPhase { public readonly phaseName = "PokemonHealPhase"; + + /** The base amount of HP to heal. */ private hpHealed: number; - private message: string | null; + /** + * The message to display upon healing the target, or `undefined` to show no message. + * Will be overridden by the full HP message if {@linkcode showFullHpMessage} is set to `true` + */ + private message: string | undefined; + /** + * Whether to show a failure message upon healing a Pokemon already at full HP. + * @defaultValue `true` + */ private showFullHpMessage: boolean; + /** + * Whether to skip showing the healing animation. + * @defaultValue `false` + */ private skipAnim: boolean; + /** + * Whether to revive the affected Pokemon in addition to healing. + * Revives will not be affected by any Healing Charms. + * @todo Remove post modifier rework as revives will not be using phases to heal stuff + * @defaultValue `false` + */ private revive: boolean; + /** + * Whether to heal the affected Pokemon's status condition. + * @todo This should not be the healing phase's job + * @defaultValue `false` + */ private healStatus: boolean; + /** + * Whether to prevent fully healing affected Pokemon, leaving them 1 HP below full. + * @defaultValue `false` + */ private preventFullHeal: boolean; + /** + * Whether to fully restore PP upon healing. + * @todo This should not be the healing phase's job + * @defaultValue `false` + */ private fullRestorePP: boolean; constructor( battlerIndex: BattlerIndex, hpHealed: number, - message: string | null, - showFullHpMessage: boolean, - skipAnim = false, - revive = false, - healStatus = false, - preventFullHeal = false, - fullRestorePP = false, + { + message, + showFullHpMessage = true, + skipAnim = false, + revive = false, + healStatus = false, + preventFullHeal = false, + fullRestorePP = false, + }: { + message?: string; + showFullHpMessage?: boolean; + skipAnim?: boolean; + revive?: boolean; + healStatus?: boolean; + preventFullHeal?: boolean; + fullRestorePP?: boolean; + } = {}, ) { super(battlerIndex, undefined, CommonAnim.HEALTH_UP); @@ -47,89 +89,108 @@ export class PokemonHealPhase extends CommonAnimPhase { this.fullRestorePP = fullRestorePP; } - start() { - if (!this.skipAnim && (this.revive || this.getPokemon().hp) && !this.getPokemon().isFullHp()) { + override start() { + // Only play animation if not skipped and target is at full HP + if (!this.skipAnim && !this.getPokemon().isFullHp()) { super.start(); - } else { - this.end(); } + + this.heal(); + + super.end(); } - end() { + private heal() { const pokemon = this.getPokemon(); - if (!pokemon.isOnField() || (!this.revive && !pokemon.isActive())) { - return super.end(); + // Prevent healing off-field pokemon unless via revives + // TODO: Revival effects shouldn't use this phase + if (!this.revive && !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)); - this.message = null; - 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; - } - } - } - pokemon.updateInfo().then(() => super.end()); - } else if (this.healStatus && !this.revive && pokemon.status) { - lastStatusEffect = pokemon.status.effect; + + this.doHealPokemon(); + + // Cure status as applicable + // TODO: This should not be the job of the healing phase + if (this.healStatus && pokemon.status) { + this.message = 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), + } + + // Restore PP. + // TODO: This should not be the job of the healing phase + if (this.fullRestorePP) { + pokemon.getMoveset().forEach(m => { + m.ppUsed = 0; }); } + // Show message, update info boxes and then wrap up. if (this.message) { globalScene.phaseManager.queueMessage(this.message); } + pokemon.updateInfo().then(() => super.end()); + } - if (this.healStatus && lastStatusEffect && !hasMessage) { - globalScene.phaseManager.queueMessage( - getStatusEffectHealText(lastStatusEffect, getPokemonNameWithAffix(pokemon)), - ); + /** + * Heal the Pokemon affected by this Phase. + */ + private doHealPokemon(): void { + const pokemon = this.getPokemon()!; + + // If we would heal the user past full HP, don't. + if (this.hpHealed > 0 && pokemon.isFullHp()) { + if (this.showFullHpMessage) { + this.message = i18next.t("battle:hpIsFull", { + pokemonName: getPokemonNameWithAffix(pokemon), + }); + } + return; } - if (!healOrDamage && !lastStatusEffect) { - super.end(); + const healAmount = this.getHealAmount(); + + if (healAmount < 0) { + // If Liquid Ooze is active, damage the user for the healing amount, then return. + // TODO: Consider refactoring liquid ooze to not use a heal phase to do damage + pokemon.damageAndUpdate(-healAmount, { result: HitResult.INDIRECT }); + return; + } + + // Heal the pokemon, then show damage numbers and validate achievements. + 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); } } + + /** + * Calculate the amount of HP to be healed during this Phase. + * @returns The updated healing amount, rounded down and capped at the Pokemon's maximum HP. + * @todo Prevent double rounding from callers + */ + private getHealAmount(): number { + if (this.revive) { + return toDmgValue(this.hpHealed); + } + + // Apply the effect of healing charms for non-revival items before rounding down and capping at max HP + // (or 1 below max for healing tokens). + // Liquid Ooze damage (being negative) remains uncapped as normal. + const healMulti = new NumberHolder(1); + globalScene.applyModifiers(HealingBoosterModifier, this.player, healMulti); + return toDmgValue(Math.min(this.hpHealed * healMulti.value, this.getPokemon().getMaxHp() - +this.preventFullHeal)); + } } diff --git a/src/phases/quiet-form-change-phase.ts b/src/phases/quiet-form-change-phase.ts index ef53b16cc56..52eb128de45 100644 --- a/src/phases/quiet-form-change-phase.ts +++ b/src/phases/quiet-form-change-phase.ts @@ -154,16 +154,11 @@ export class QuietFormChangePhase extends BattlePhase { this.pokemon.findAndRemoveTags(t => t.tagType === BattlerTagType.AUTOTOMIZED); if (globalScene?.currentBattle.battleSpec === BattleSpec.FINAL_BOSS && this.pokemon.isEnemy()) { globalScene.playBgm(); - globalScene.phaseManager.unshiftNew( - "PokemonHealPhase", - this.pokemon.getBattlerIndex(), - this.pokemon.getMaxHp(), - null, - false, - false, - false, - true, - ); + globalScene.phaseManager.unshiftNew("PokemonHealPhase", this.pokemon.getBattlerIndex(), this.pokemon.getMaxHp(), { + showFullHpMessage: false, + healStatus: true, + fullRestorePP: true, + }); this.pokemon.findAndRemoveTags(() => true); this.pokemon.bossSegments = 5; this.pokemon.bossSegmentIndex = 4; diff --git a/src/phases/turn-end-phase.ts b/src/phases/turn-end-phase.ts index 463f26e73a2..1a994acca49 100644 --- a/src/phases/turn-end-phase.ts +++ b/src/phases/turn-end-phase.ts @@ -35,15 +35,11 @@ export class TurnEndPhase extends FieldPhase { globalScene.applyModifiers(TurnHealModifier, pokemon.isPlayer(), pokemon); if (globalScene.arena.terrain?.terrainType === TerrainType.GRASSY && pokemon.isGrounded()) { - globalScene.phaseManager.unshiftNew( - "PokemonHealPhase", - pokemon.getBattlerIndex(), - Math.max(pokemon.getMaxHp() >> 4, 1), - i18next.t("battle:turnEndHpRestore", { + globalScene.phaseManager.unshiftNew("PokemonHealPhase", pokemon.getBattlerIndex(), pokemon.getMaxHp() / 16, { + message: i18next.t("battle:turnEndHpRestore", { pokemonName: getPokemonNameWithAffix(pokemon), }), - true, - ); + }); } if (!pokemon.isPlayer()) {