mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-07 07:59:26 +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;
|
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 {
|
export interface ModifyMoveEffectChanceAbAttrParams extends AbAttrBaseParams {
|
||||||
/** The move being used by the attacker */
|
/** The move being used by the attacker */
|
||||||
move: Move;
|
move: Move;
|
||||||
@ -1682,7 +1727,7 @@ export class MoveTypeChangeAbAttr extends PreAttackAbAttr {
|
|||||||
*/
|
*/
|
||||||
override canApply({ pokemon, opponent: target, move }: MoveTypeChangeAbAttrParams): boolean {
|
override canApply({ pokemon, opponent: target, move }: MoveTypeChangeAbAttrParams): boolean {
|
||||||
return (
|
return (
|
||||||
(!this.condition || this.condition(pokemon, target, move)) &&
|
(this.condition?.(pokemon, target, move) ?? true) &&
|
||||||
!noAbilityTypeOverrideMoves.has(move.id) &&
|
!noAbilityTypeOverrideMoves.has(move.id) &&
|
||||||
!(
|
!(
|
||||||
pokemon.isTerastallized &&
|
pokemon.isTerastallized &&
|
||||||
@ -6558,6 +6603,7 @@ const AbilityAttrs = Object.freeze({
|
|||||||
PostDefendMoveDisableAbAttr,
|
PostDefendMoveDisableAbAttr,
|
||||||
PostStatStageChangeStatStageChangeAbAttr,
|
PostStatStageChangeStatStageChangeAbAttr,
|
||||||
PreAttackAbAttr,
|
PreAttackAbAttr,
|
||||||
|
MoveHealBoostAbAttr,
|
||||||
MoveEffectChanceMultiplierAbAttr,
|
MoveEffectChanceMultiplierAbAttr,
|
||||||
IgnoreMoveEffectsAbAttr,
|
IgnoreMoveEffectsAbAttr,
|
||||||
VariableMovePowerAbAttr,
|
VariableMovePowerAbAttr,
|
||||||
@ -7333,7 +7379,9 @@ export function initAbilities() {
|
|||||||
.attr(PostSummonUserFieldRemoveStatusEffectAbAttr, StatusEffect.SLEEP)
|
.attr(PostSummonUserFieldRemoveStatusEffectAbAttr, StatusEffect.SLEEP)
|
||||||
.attr(UserFieldBattlerTagImmunityAbAttr, BattlerTagType.DROWSY)
|
.attr(UserFieldBattlerTagImmunityAbAttr, BattlerTagType.DROWSY)
|
||||||
.ignorable()
|
.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)
|
new Ability(AbilityId.STANCE_CHANGE, 6)
|
||||||
.attr(NoFusionAbilityAbAttr)
|
.attr(NoFusionAbilityAbAttr)
|
||||||
.uncopiable()
|
.uncopiable()
|
||||||
@ -7342,7 +7390,8 @@ export function initAbilities() {
|
|||||||
new Ability(AbilityId.GALE_WINGS, 6)
|
new Ability(AbilityId.GALE_WINGS, 6)
|
||||||
.attr(ChangeMovePriorityAbAttr, (pokemon, move) => pokemon.isFullHp() && pokemon.getMoveType(move) === PokemonType.FLYING, 1),
|
.attr(ChangeMovePriorityAbAttr, (pokemon, move) => pokemon.isFullHp() && pokemon.getMoveType(move) === PokemonType.FLYING, 1),
|
||||||
new Ability(AbilityId.MEGA_LAUNCHER, 6)
|
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)
|
new Ability(AbilityId.GRASS_PELT, 6)
|
||||||
.conditionalAttr(getTerrainCondition(TerrainType.GRASSY), StatMultiplierAbAttr, Stat.DEF, 1.5)
|
.conditionalAttr(getTerrainCondition(TerrainType.GRASSY), StatMultiplierAbAttr, Stat.DEF, 1.5)
|
||||||
.ignorable(),
|
.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.
|
* Heals the user or target of the move by a fixed amount relative to their maximum HP.
|
||||||
*/
|
*/
|
||||||
export class HealAttr extends MoveEffectAttr {
|
export class HealAttr extends MoveEffectAttr {
|
||||||
@ -1968,11 +1968,22 @@ export class HealAttr extends MoveEffectAttr {
|
|||||||
this.failOnFullHp = failOnFullHp;
|
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)) {
|
if (!super.apply(user, target, move, args)) {
|
||||||
return false;
|
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);
|
this.addHealPhase(this.selfTarget ? user : target);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -1983,7 +1994,8 @@ export class HealAttr extends MoveEffectAttr {
|
|||||||
*/
|
*/
|
||||||
protected addHealPhase(healedPokemon: Pokemon) {
|
protected addHealPhase(healedPokemon: Pokemon) {
|
||||||
globalScene.phaseManager.unshiftNew("PokemonHealPhase", healedPokemon.getBattlerIndex(),
|
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),
|
Math.round(healedPokemon.getMaxHp() * this.healRatio),
|
||||||
{
|
{
|
||||||
message: i18next.t("moveTriggers:healHp", { pokemonName: getPokemonNameWithAffix(healedPokemon) }),
|
message: i18next.t("moveTriggers:healHp", { pokemonName: getPokemonNameWithAffix(healedPokemon) }),
|
||||||
@ -2012,7 +2024,7 @@ export class HealAttr extends MoveEffectAttr {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Attribute for moves with variable healing amounts.
|
* 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:
|
* Used for:
|
||||||
* - {@linkcode MoveId.MOONLIGHT} and variants
|
* - {@linkcode MoveId.MOONLIGHT} and variants
|
||||||
@ -2023,7 +2035,7 @@ export class HealAttr extends MoveEffectAttr {
|
|||||||
export class VariableHealAttr extends HealAttr {
|
export class VariableHealAttr extends HealAttr {
|
||||||
constructor(
|
constructor(
|
||||||
/** A function yielding the amount of HP to heal. */
|
/** 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,
|
showAnim = false,
|
||||||
selfTarget = true,
|
selfTarget = true,
|
||||||
) {
|
) {
|
||||||
@ -10571,7 +10583,7 @@ export function initMoves() {
|
|||||||
.attr(StatStageChangeAttr, [ Stat.SPD ], -1, true)
|
.attr(StatStageChangeAttr, [ Stat.SPD ], -1, true)
|
||||||
.punchingMove(),
|
.punchingMove(),
|
||||||
new StatusMove(MoveId.FLORAL_HEALING, PokemonType.FAIRY, -1, 10, -1, 0, 7)
|
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()
|
.triageMove()
|
||||||
.reflectable(),
|
.reflectable(),
|
||||||
new AttackMove(MoveId.HIGH_HORSEPOWER, PokemonType.GROUND, MoveCategory.PHYSICAL, 95, 95, 10, -1, 0, 7),
|
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.
|
* 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
|
* @remarks
|
||||||
* The effect of Healing Charms are rounded down for parity with the closest mainline counterpart
|
* The effect of Healing Charms are rounded down for parity with the closest mainline counterpart
|
||||||
* (Big Root).
|
* (i.e. Big Root).
|
||||||
*/
|
*/
|
||||||
private getHealAmount(): number {
|
private getHealAmount(): number {
|
||||||
if (this.revive) {
|
if (this.revive) {
|
||||||
|
@ -51,32 +51,30 @@ describe("Moves - ", () => {
|
|||||||
{ name: "Shore Up", move: MoveId.SHORE_UP },
|
{ name: "Shore Up", move: MoveId.SHORE_UP },
|
||||||
])("$name", ({ move }) => {
|
])("$name", ({ move }) => {
|
||||||
it("should heal 50% of the user's maximum HP, rounded half up", async () => {
|
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]);
|
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||||
|
|
||||||
const chansey = game.field.getEnemyPokemon();
|
const blissey = game.field.getPlayerPokemon();
|
||||||
chansey.hp = 1;
|
blissey.hp = 1;
|
||||||
chansey.setStat(Stat.HP, 501); // half is 250.5, rounded half up to 251
|
blissey.setStat(Stat.HP, 501); // half is 250.5, rounded half up to 251
|
||||||
|
|
||||||
game.move.use(move);
|
game.move.use(move);
|
||||||
await game.toEndOfTurn();
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
expect(game.phaseInterceptor.log).toContain("PokemonHealPhase");
|
expect(game.phaseInterceptor.log).toContain("PokemonHealPhase");
|
||||||
expect(game.textInterceptor.logs).toContain(
|
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]);
|
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||||
|
|
||||||
game.move.use(move);
|
game.move.use(move);
|
||||||
await game.toEndOfTurn();
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
const blissey = game.field.getPlayerPokemon();
|
const blissey = game.field.getPlayerPokemon();
|
||||||
const chansey = game.field.getEnemyPokemon();
|
expect(blissey).toHaveFullHp();
|
||||||
expect(chansey).toHaveFullHp();
|
|
||||||
expect(game.textInterceptor.logs).toContain(
|
expect(game.textInterceptor.logs).toContain(
|
||||||
i18next.t("battle:hpIsFull", {
|
i18next.t("battle:hpIsFull", {
|
||||||
pokemonName: getPokemonNameWithAffix(blissey),
|
pokemonName: getPokemonNameWithAffix(blissey),
|
||||||
@ -160,53 +158,55 @@ describe("Moves - ", () => {
|
|||||||
{
|
{
|
||||||
name: "Heal Pulse",
|
name: "Heal Pulse",
|
||||||
move: MoveId.HEAL_PULSE,
|
move: MoveId.HEAL_PULSE,
|
||||||
percent: 75,
|
percent: 3 / 4,
|
||||||
ability: AbilityId.MEGA_LAUNCHER,
|
ability: AbilityId.MEGA_LAUNCHER,
|
||||||
condText: "user has Mega Launcher",
|
condText: "user has Mega Launcher",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Floral Healing",
|
name: "Floral Healing",
|
||||||
move: MoveId.FLORAL_HEALING,
|
move: MoveId.FLORAL_HEALING,
|
||||||
percent: 66,
|
percent: 2 / 3,
|
||||||
ability: AbilityId.GRASSY_SURGE,
|
ability: AbilityId.GRASSY_SURGE,
|
||||||
condText: "Grassy Terrain is active",
|
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 () => {
|
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]);
|
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||||
|
|
||||||
const blissey = game.field.getPlayerPokemon();
|
const chansey = game.field.getEnemyPokemon();
|
||||||
blissey.hp = 1;
|
chansey.hp = 1;
|
||||||
blissey.setStat(Stat.HP, 501); // half is 250.5, rounded half up to 251
|
chansey.setStat(Stat.HP, 501); // half is 250.5, rounded half up to 251
|
||||||
|
|
||||||
game.move.use(move);
|
game.move.use(move);
|
||||||
await game.toEndOfTurn();
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
expect(game.phaseInterceptor.log).toContain("PokemonHealPhase");
|
expect(game.phaseInterceptor.log).toContain("PokemonHealPhase");
|
||||||
expect(game.textInterceptor.logs).toContain(
|
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]);
|
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||||
|
|
||||||
game.move.use(move);
|
game.move.use(move);
|
||||||
await game.toEndOfTurn();
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
const blissey = game.field.getPlayerPokemon();
|
const blissey = game.field.getPlayerPokemon();
|
||||||
expect(blissey).toHaveFullHp();
|
const chansey = game.field.getEnemyPokemon();
|
||||||
|
expect(chansey).toHaveFullHp();
|
||||||
expect(game.textInterceptor.logs).toContain(
|
expect(game.textInterceptor.logs).toContain(
|
||||||
i18next.t("battle:hpIsFull", {
|
i18next.t("battle:hpIsFull", {
|
||||||
pokemonName: getPokemonNameWithAffix(blissey),
|
pokemonName: getPokemonNameWithAffix(chansey),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(game.phaseInterceptor.log).not.toContain("PokemonHealPhase");
|
expect(game.phaseInterceptor.log).not.toContain("PokemonHealPhase");
|
||||||
expect(blissey).toHaveUsedMove({ move, result: MoveResult.FAIL });
|
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
|
// prevents passive turn heal from grassy terrain
|
||||||
game.override.ability(ability).enemyAbility(AbilityId.LEVITATE);
|
game.override.ability(ability).enemyAbility(AbilityId.LEVITATE);
|
||||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||||
@ -217,7 +217,7 @@ describe("Moves - ", () => {
|
|||||||
game.move.use(move);
|
game.move.use(move);
|
||||||
await game.toEndOfTurn();
|
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