mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-06 15:39:27 +02:00
Added MoveHealBoostAbAttr
+ implemented Healing Pulse boost
This commit is contained in:
parent
95dbfe69a0
commit
89536fafda
@ -1541,6 +1541,51 @@ export abstract class PreAttackAbAttr extends AbAttr {
|
||||
private declare readonly _: never;
|
||||
}
|
||||
|
||||
export interface MoveHealBoostAbAttrParams extends AugmentMoveInteractionAbAttrParams {
|
||||
/** The base amount of HP being healed, as a fraction of the recipient's maximum HP. */
|
||||
healRatio: NumberHolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ability attribute to boost the healing potency of the user's moves.
|
||||
* Used by {@linkcode AbilityId.MEGA_LAUNCHER} to implement Heal Pulse boosting.
|
||||
*/
|
||||
export class MoveHealBoostAbAttr extends AbAttr {
|
||||
/**
|
||||
* The amount to boost the healing by, as a multiplier of the base amount.
|
||||
*/
|
||||
private healMulti: number;
|
||||
/**
|
||||
* A lambda function determining whether to boost the heal amount.
|
||||
* The ability will not be applied if this evaluates to `false`.
|
||||
*/
|
||||
// TODO: Use a `MoveConditionFunc` maybe?
|
||||
private boostCondition: (user: Pokemon, target: Pokemon, move: Move) => boolean;
|
||||
|
||||
constructor(
|
||||
boostCondition: (user: Pokemon, target: Pokemon, move: Move) => boolean,
|
||||
healMulti: number,
|
||||
showAbility = false,
|
||||
) {
|
||||
super(showAbility);
|
||||
|
||||
if (healMulti === 1) {
|
||||
throw new Error("Calling `MoveHealBoostAbAttr` with a multiplier of 1 is useless!");
|
||||
}
|
||||
|
||||
this.healMulti = healMulti;
|
||||
this.boostCondition = boostCondition;
|
||||
}
|
||||
|
||||
override canApply({ pokemon: user, opponent: target, move }: MoveHealBoostAbAttrParams): boolean {
|
||||
return this.boostCondition?.(user, target, move) ?? true;
|
||||
}
|
||||
|
||||
override apply({ healRatio }: MoveHealBoostAbAttrParams): void {
|
||||
healRatio.value *= this.healMulti;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ModifyMoveEffectChanceAbAttrParams extends AbAttrBaseParams {
|
||||
/** The move being used by the attacker */
|
||||
move: Move;
|
||||
@ -1682,7 +1727,7 @@ export class MoveTypeChangeAbAttr extends PreAttackAbAttr {
|
||||
*/
|
||||
override canApply({ pokemon, opponent: target, move }: MoveTypeChangeAbAttrParams): boolean {
|
||||
return (
|
||||
(!this.condition || this.condition(pokemon, target, move)) &&
|
||||
(this.condition?.(pokemon, target, move) ?? true) &&
|
||||
!noAbilityTypeOverrideMoves.has(move.id) &&
|
||||
!(
|
||||
pokemon.isTerastallized &&
|
||||
@ -6558,6 +6603,7 @@ const AbilityAttrs = Object.freeze({
|
||||
PostDefendMoveDisableAbAttr,
|
||||
PostStatStageChangeStatStageChangeAbAttr,
|
||||
PreAttackAbAttr,
|
||||
MoveHealBoostAbAttr,
|
||||
MoveEffectChanceMultiplierAbAttr,
|
||||
IgnoreMoveEffectsAbAttr,
|
||||
VariableMovePowerAbAttr,
|
||||
@ -7333,7 +7379,9 @@ export function initAbilities() {
|
||||
.attr(PostSummonUserFieldRemoveStatusEffectAbAttr, StatusEffect.SLEEP)
|
||||
.attr(UserFieldBattlerTagImmunityAbAttr, BattlerTagType.DROWSY)
|
||||
.ignorable()
|
||||
.partial(), // Mold Breaker ally should not be affected by Sweet Veil
|
||||
// Mold Breaker ally should not be affected by Sweet Veil
|
||||
// TODO: Review this
|
||||
.partial(),
|
||||
new Ability(AbilityId.STANCE_CHANGE, 6)
|
||||
.attr(NoFusionAbilityAbAttr)
|
||||
.uncopiable()
|
||||
@ -7342,7 +7390,8 @@ export function initAbilities() {
|
||||
new Ability(AbilityId.GALE_WINGS, 6)
|
||||
.attr(ChangeMovePriorityAbAttr, (pokemon, move) => pokemon.isFullHp() && pokemon.getMoveType(move) === PokemonType.FLYING, 1),
|
||||
new Ability(AbilityId.MEGA_LAUNCHER, 6)
|
||||
.attr(MovePowerBoostAbAttr, (_user, _target, move) => move.hasFlag(MoveFlags.PULSE_MOVE), 1.5),
|
||||
.attr(MovePowerBoostAbAttr, (_user, _target, move) => move.hasFlag(MoveFlags.PULSE_MOVE), 1.5)
|
||||
.attr(MoveHealBoostAbAttr, (_user, _target, move) => move.hasFlag(MoveFlags.PULSE_MOVE), 1.5),
|
||||
new Ability(AbilityId.GRASS_PELT, 6)
|
||||
.conditionalAttr(getTerrainCondition(TerrainType.GRASSY), StatMultiplierAbAttr, Stat.DEF, 1.5)
|
||||
.ignorable(),
|
||||
|
@ -1940,7 +1940,7 @@ export class AddSubstituteAttr extends MoveEffectAttr {
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute to implement healing moves, such as {@linkcode MoveId.RECOVER} or {@linkcode MoveId.HEAL_PULSE}.
|
||||
* Attribute to implement healing moves, such as {@linkcode MoveId.RECOVER} or {@linkcode MoveId.SOFT_BOILED}.
|
||||
* Heals the user or target of the move by a fixed amount relative to their maximum HP.
|
||||
*/
|
||||
export class HealAttr extends MoveEffectAttr {
|
||||
@ -1968,11 +1968,22 @@ export class HealAttr extends MoveEffectAttr {
|
||||
this.failOnFullHp = failOnFullHp;
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (!super.apply(user, target, move, args)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Apply any boosts to healing amounts (i.e. Heal Pulse + Mega Launcher).
|
||||
const hp = new NumberHolder(this.healRatio)
|
||||
applyAbAttrs("MoveHealBoostAbAttr", {
|
||||
pokemon: user,
|
||||
opponent: target,
|
||||
move,
|
||||
healRatio: hp
|
||||
})
|
||||
this.healRatio = hp.value;
|
||||
|
||||
|
||||
this.addHealPhase(this.selfTarget ? user : target);
|
||||
return true;
|
||||
}
|
||||
@ -1983,7 +1994,8 @@ export class HealAttr extends MoveEffectAttr {
|
||||
*/
|
||||
protected addHealPhase(healedPokemon: Pokemon) {
|
||||
globalScene.phaseManager.unshiftNew("PokemonHealPhase", healedPokemon.getBattlerIndex(),
|
||||
// Healing moves round half UP hp healed
|
||||
// Healing moves round half UP the hp healed
|
||||
// (unlike most other sources which round down)
|
||||
Math.round(healedPokemon.getMaxHp() * this.healRatio),
|
||||
{
|
||||
message: i18next.t("moveTriggers:healHp", { pokemonName: getPokemonNameWithAffix(healedPokemon) }),
|
||||
@ -2012,7 +2024,7 @@ export class HealAttr extends MoveEffectAttr {
|
||||
|
||||
/**
|
||||
* Attribute for moves with variable healing amounts.
|
||||
* Heals the target by an amount depending on the return value of {@linkcode healFunc}.
|
||||
* Heals the user/target by an amount depending on the return value of {@linkcode healFunc}.
|
||||
*
|
||||
* Used for:
|
||||
* - {@linkcode MoveId.MOONLIGHT} and variants
|
||||
@ -2023,7 +2035,7 @@ export class HealAttr extends MoveEffectAttr {
|
||||
export class VariableHealAttr extends HealAttr {
|
||||
constructor(
|
||||
/** A function yielding the amount of HP to heal. */
|
||||
private healFunc: (user: Pokemon, target: Pokemon, _move: Move) => number,
|
||||
private healFunc: (user: Pokemon, target: Pokemon, move: Move) => number,
|
||||
showAnim = false,
|
||||
selfTarget = true,
|
||||
) {
|
||||
@ -10571,7 +10583,7 @@ export function initMoves() {
|
||||
.attr(StatStageChangeAttr, [ Stat.SPD ], -1, true)
|
||||
.punchingMove(),
|
||||
new StatusMove(MoveId.FLORAL_HEALING, PokemonType.FAIRY, -1, 10, -1, 0, 7)
|
||||
.attr(VariableHealAttr, () => globalScene.arena.terrain?.terrainType === TerrainType.GRASSY ? 2 / 3 : 1 / 2, true, false)
|
||||
.attr(VariableHealAttr, () => globalScene.arena.getTerrainType() === TerrainType.GRASSY ? 2 / 3 : 1 / 2, true, false)
|
||||
.triageMove()
|
||||
.reflectable(),
|
||||
new AttackMove(MoveId.HIGH_HORSEPOWER, PokemonType.GROUND, MoveCategory.PHYSICAL, 95, 95, 10, -1, 0, 7),
|
||||
|
@ -178,10 +178,10 @@ export class PokemonHealPhase extends CommonAnimPhase {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @returns The updated healing amount post-modifications, capped at the Pokemon's maximum HP.
|
||||
* @remarks
|
||||
* The effect of Healing Charms are rounded down for parity with the closest mainline counterpart
|
||||
* (Big Root).
|
||||
* (i.e. Big Root).
|
||||
*/
|
||||
private getHealAmount(): number {
|
||||
if (this.revive) {
|
||||
|
@ -51,32 +51,30 @@ describe("Moves - ", () => {
|
||||
{ name: "Shore Up", move: MoveId.SHORE_UP },
|
||||
])("$name", ({ move }) => {
|
||||
it("should heal 50% of the user's maximum HP, rounded half up", async () => {
|
||||
// NB: Shore Up and co. round down in mainline, but we keep them the same as others for consistency's sake
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
|
||||
const chansey = game.field.getEnemyPokemon();
|
||||
chansey.hp = 1;
|
||||
chansey.setStat(Stat.HP, 501); // half is 250.5, rounded half up to 251
|
||||
const blissey = game.field.getPlayerPokemon();
|
||||
blissey.hp = 1;
|
||||
blissey.setStat(Stat.HP, 501); // half is 250.5, rounded half up to 251
|
||||
|
||||
game.move.use(move);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(game.phaseInterceptor.log).toContain("PokemonHealPhase");
|
||||
expect(game.textInterceptor.logs).toContain(
|
||||
i18next.t("moveTriggers:healHp", { pokemonName: getPokemonNameWithAffix(chansey) }),
|
||||
i18next.t("moveTriggers:healHp", { pokemonName: getPokemonNameWithAffix(blissey) }),
|
||||
);
|
||||
expect(chansey).toHaveHp(252); // 251 + 1
|
||||
expect(blissey).toHaveHp(252); // 251 + 1
|
||||
});
|
||||
|
||||
it("should fail if user is at full HP", async () => {
|
||||
it("should fail if the user is at full HP", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
|
||||
game.move.use(move);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
const blissey = game.field.getPlayerPokemon();
|
||||
const chansey = game.field.getEnemyPokemon();
|
||||
expect(chansey).toHaveFullHp();
|
||||
expect(blissey).toHaveFullHp();
|
||||
expect(game.textInterceptor.logs).toContain(
|
||||
i18next.t("battle:hpIsFull", {
|
||||
pokemonName: getPokemonNameWithAffix(blissey),
|
||||
@ -160,53 +158,55 @@ describe("Moves - ", () => {
|
||||
{
|
||||
name: "Heal Pulse",
|
||||
move: MoveId.HEAL_PULSE,
|
||||
percent: 75,
|
||||
percent: 3 / 4,
|
||||
ability: AbilityId.MEGA_LAUNCHER,
|
||||
condText: "user has Mega Launcher",
|
||||
},
|
||||
{
|
||||
name: "Floral Healing",
|
||||
move: MoveId.FLORAL_HEALING,
|
||||
percent: 66,
|
||||
percent: 2 / 3,
|
||||
ability: AbilityId.GRASSY_SURGE,
|
||||
condText: "Grassy Terrain is active",
|
||||
},
|
||||
])("Target-Healing Moves - $name", ({ move, percent, ability }) => {
|
||||
])("Target-Healing Moves - $name", ({ move, percent, ability, condText }) => {
|
||||
it("should heal 50% of the target's maximum HP, rounded half up", async () => {
|
||||
// NB: Shore Up and co. round down in mainline, but we keep them the same as others for consistency's sake
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
|
||||
const blissey = game.field.getPlayerPokemon();
|
||||
blissey.hp = 1;
|
||||
blissey.setStat(Stat.HP, 501); // half is 250.5, rounded half up to 251
|
||||
const chansey = game.field.getEnemyPokemon();
|
||||
chansey.hp = 1;
|
||||
chansey.setStat(Stat.HP, 501); // half is 250.5, rounded half up to 251
|
||||
|
||||
game.move.use(move);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(game.phaseInterceptor.log).toContain("PokemonHealPhase");
|
||||
expect(game.textInterceptor.logs).toContain(
|
||||
i18next.t("moveTriggers:healHp", { pokemonName: getPokemonNameWithAffix(blissey) }),
|
||||
i18next.t("moveTriggers:healHp", { pokemonName: getPokemonNameWithAffix(chansey) }),
|
||||
);
|
||||
expect(blissey).toHaveHp(252); // 251 + 1
|
||||
expect(chansey).toHaveHp(252); // 251 + 1
|
||||
});
|
||||
|
||||
it("should fail if target is at full HP", async () => {
|
||||
it("should fail if the target is at full HP", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
|
||||
game.move.use(move);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
const blissey = game.field.getPlayerPokemon();
|
||||
expect(blissey).toHaveFullHp();
|
||||
const chansey = game.field.getEnemyPokemon();
|
||||
expect(chansey).toHaveFullHp();
|
||||
expect(game.textInterceptor.logs).toContain(
|
||||
i18next.t("battle:hpIsFull", {
|
||||
pokemonName: getPokemonNameWithAffix(blissey),
|
||||
pokemonName: getPokemonNameWithAffix(chansey),
|
||||
}),
|
||||
);
|
||||
expect(game.phaseInterceptor.log).not.toContain("PokemonHealPhase");
|
||||
expect(blissey).toHaveUsedMove({ move, result: MoveResult.FAIL });
|
||||
});
|
||||
|
||||
it("should heal $percent% of the target's maximum HP if $condText", async () => {
|
||||
it(`should heal ${(percent * 100).toPrecision(2)}% of the target's maximum HP if ${condText}`, async () => {
|
||||
// prevents passive turn heal from grassy terrain
|
||||
game.override.ability(ability).enemyAbility(AbilityId.LEVITATE);
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
@ -217,7 +217,7 @@ describe("Moves - ", () => {
|
||||
game.move.use(move);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(chansey).toHaveHp(Math.round((percent * chansey.getMaxHp()) / 100) + 1);
|
||||
expect(chansey).toHaveHp(Math.round(percent * chansey.getMaxHp()) + 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user