mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-06 15:39:27 +02:00
Added heal pulse tests and clarified rounding
This commit is contained in:
parent
90c9c71cd9
commit
95dbfe69a0
@ -1940,9 +1940,8 @@ export class AddSubstituteAttr extends MoveEffectAttr {
|
||||
}
|
||||
|
||||
/**
|
||||
* Heals the user or target by {@linkcode healRatio} depending on the value of {@linkcode selfTarget}
|
||||
* @extends MoveEffectAttr
|
||||
* @see {@linkcode apply}
|
||||
* Attribute to implement healing moves, such as {@linkcode MoveId.RECOVER} or {@linkcode MoveId.HEAL_PULSE}.
|
||||
* Heals the user or target of the move by a fixed amount relative to their maximum HP.
|
||||
*/
|
||||
export class HealAttr extends MoveEffectAttr {
|
||||
/** The percentage of {@linkcode Stat.HP} to heal; default `1` */
|
||||
@ -1952,7 +1951,8 @@ export class HealAttr extends MoveEffectAttr {
|
||||
|
||||
/**
|
||||
* Whether the move should fail if the target is at full HP.
|
||||
* @todo Remove post move failure rework
|
||||
* @defaultValue `true`
|
||||
* @todo Remove post move failure rework - this solely exists to prevent Lunar Blessing and co. from failing
|
||||
*/
|
||||
private failOnFullHp = true;
|
||||
|
||||
@ -1964,8 +1964,8 @@ export class HealAttr extends MoveEffectAttr {
|
||||
) {
|
||||
super(selfTarget);
|
||||
this.healRatio = healRatio;
|
||||
this.showAnim = showAnim
|
||||
this.failOnFullHp = failOnFullHp
|
||||
this.showAnim = showAnim;
|
||||
this.failOnFullHp = failOnFullHp;
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
@ -1973,7 +1973,7 @@ export class HealAttr extends MoveEffectAttr {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.addHealPhase(this.selfTarget ? user : target, this.healRatio);
|
||||
this.addHealPhase(this.selfTarget ? user : target);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -1981,11 +1981,12 @@ export class HealAttr extends MoveEffectAttr {
|
||||
* Creates a new {@linkcode PokemonHealPhase}.
|
||||
* This heals the target and shows the appropriate message.
|
||||
*/
|
||||
protected addHealPhase(target: Pokemon, healRatio: number) {
|
||||
globalScene.phaseManager.unshiftNew("PokemonHealPhase", target.getBattlerIndex(),
|
||||
toDmgValue(target.getMaxHp() * healRatio),
|
||||
protected addHealPhase(healedPokemon: Pokemon) {
|
||||
globalScene.phaseManager.unshiftNew("PokemonHealPhase", healedPokemon.getBattlerIndex(),
|
||||
// Healing moves round half UP hp healed
|
||||
Math.round(healedPokemon.getMaxHp() * this.healRatio),
|
||||
{
|
||||
message: i18next.t("moveTriggers:healHp", { pokemonName: getPokemonNameWithAffix(target) }),
|
||||
message: i18next.t("moveTriggers:healHp", { pokemonName: getPokemonNameWithAffix(healedPokemon) }),
|
||||
showFullHpMessage: true,
|
||||
skipAnim: !this.showAnim,
|
||||
}
|
||||
@ -2002,12 +2003,10 @@ export class HealAttr extends MoveEffectAttr {
|
||||
}
|
||||
|
||||
override getFailedText(user: Pokemon, target: Pokemon): string | undefined {
|
||||
const healedPokemon = (this.selfTarget ? user : target);
|
||||
if (healedPokemon.isFullHp()) {
|
||||
return i18next.t("battle:hpIsFull", {
|
||||
pokemonName: getPokemonNameWithAffix(healedPokemon),
|
||||
})
|
||||
}
|
||||
const healedPokemon = this.selfTarget ? user : target;
|
||||
return i18next.t("battle:hpIsFull", {
|
||||
pokemonName: getPokemonNameWithAffix(healedPokemon),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -179,7 +179,9 @@ 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.
|
||||
* @todo Prevent double rounding from callers
|
||||
* @remarks
|
||||
* The effect of Healing Charms are rounded down for parity with the closest mainline counterpart
|
||||
* (Big Root).
|
||||
*/
|
||||
private getHealAmount(): number {
|
||||
if (this.revive) {
|
||||
|
@ -3,6 +3,7 @@ import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { WeatherType } from "#enums/weather-type";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import { getEnumValues } from "#utils/enums";
|
||||
@ -11,7 +12,7 @@ import i18next from "i18next";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Moves - Recovery Moves - ", () => {
|
||||
describe("Moves - ", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
@ -31,118 +32,192 @@ describe("Moves - Recovery Moves - ", () => {
|
||||
.ability(AbilityId.MAGIC_GUARD) // prevents passive weather damage
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemySpecies(SpeciesId.CHANSEY)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH)
|
||||
.startingLevel(100)
|
||||
.enemyLevel(100);
|
||||
});
|
||||
|
||||
describe.each<{ name: string; move: MoveId }>([
|
||||
{ name: "Recover", move: MoveId.RECOVER },
|
||||
{ name: "Soft-Boiled", move: MoveId.SOFT_BOILED },
|
||||
{ name: "Milk Drink", move: MoveId.MILK_DRINK },
|
||||
{ name: "Slack Off", move: MoveId.SLACK_OFF },
|
||||
{ name: "Roost", move: MoveId.ROOST },
|
||||
{ name: "Weather-based Healing Moves", move: MoveId.SYNTHESIS },
|
||||
{ name: "Shore Up", move: MoveId.SHORE_UP },
|
||||
])("$name", ({ move }) => {
|
||||
it("should heal 50% of the user's maximum HP", async () => {
|
||||
describe("Self-Healing Moves -", () => {
|
||||
describe.each<{ name: string; move: MoveId }>([
|
||||
{ name: "Recover", move: MoveId.RECOVER },
|
||||
{ name: "Soft-Boiled", move: MoveId.SOFT_BOILED },
|
||||
{ name: "Milk Drink", move: MoveId.MILK_DRINK },
|
||||
{ name: "Slack Off", move: MoveId.SLACK_OFF },
|
||||
{ name: "Heal Order", move: MoveId.HEAL_ORDER },
|
||||
{ name: "Roost", move: MoveId.ROOST },
|
||||
{ name: "Weather-based Healing Moves", move: MoveId.SYNTHESIS },
|
||||
{ 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
|
||||
|
||||
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) }),
|
||||
);
|
||||
expect(chansey).toHaveHp(252); // 251 + 1
|
||||
});
|
||||
|
||||
it("should fail if 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(game.textInterceptor.logs).toContain(
|
||||
i18next.t("battle:hpIsFull", {
|
||||
pokemonName: getPokemonNameWithAffix(blissey),
|
||||
}),
|
||||
);
|
||||
expect(game.phaseInterceptor.log).not.toContain("PokemonHealPhase");
|
||||
expect(blissey).toHaveUsedMove({ move, result: MoveResult.FAIL });
|
||||
});
|
||||
});
|
||||
|
||||
describe("Weather-based Healing Moves", () => {
|
||||
it.each([
|
||||
{ name: "Harsh Sunlight", weather: WeatherType.SUNNY },
|
||||
{ name: "Extremely Harsh Sunlight", weather: WeatherType.HARSH_SUN },
|
||||
])("should heal 66% of the user's maximum HP under $name", async ({ weather }) => {
|
||||
game.override.weather(weather);
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
|
||||
const blissey = game.field.getPlayerPokemon();
|
||||
blissey.hp = 1;
|
||||
|
||||
game.move.use(MoveId.MOONLIGHT);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(blissey.getHpRatio()).toBeCloseTo(0.66, 1);
|
||||
});
|
||||
|
||||
const nonSunWTs = getEnumValues(WeatherType)
|
||||
.filter(
|
||||
wt => ![WeatherType.SUNNY, WeatherType.HARSH_SUN, WeatherType.NONE, WeatherType.STRONG_WINDS].includes(wt),
|
||||
)
|
||||
.map(wt => ({
|
||||
name: toTitleCase(WeatherType[wt]),
|
||||
weather: wt,
|
||||
}));
|
||||
|
||||
it.each(nonSunWTs)("should heal 25% of the user's maximum HP under $name", async ({ weather }) => {
|
||||
game.override.weather(weather);
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
|
||||
const blissey = game.field.getPlayerPokemon();
|
||||
blissey.hp = 1;
|
||||
|
||||
game.move.use(MoveId.MOONLIGHT);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(blissey.getHpRatio()).toBeCloseTo(0.25, 1);
|
||||
});
|
||||
|
||||
it("should heal 50% of the user's maximum HP under strong winds", async () => {
|
||||
game.override.ability(AbilityId.DELTA_STREAM);
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
|
||||
const blissey = game.field.getPlayerPokemon();
|
||||
blissey.hp = 1;
|
||||
|
||||
game.move.use(MoveId.MOONLIGHT);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(blissey.getHpRatio()).toBeCloseTo(0.5, 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Shore Up", () => {
|
||||
it("should heal 66% of the user's maximum HP in a sandstorm", async () => {
|
||||
game.override.weather(WeatherType.SANDSTORM);
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
|
||||
const blissey = game.field.getPlayerPokemon();
|
||||
blissey.hp = 1;
|
||||
|
||||
game.move.use(MoveId.SHORE_UP);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(blissey.getHpRatio()).toBeCloseTo(0.66, 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([
|
||||
{
|
||||
name: "Heal Pulse",
|
||||
move: MoveId.HEAL_PULSE,
|
||||
percent: 75,
|
||||
ability: AbilityId.MEGA_LAUNCHER,
|
||||
condText: "user has Mega Launcher",
|
||||
},
|
||||
{
|
||||
name: "Floral Healing",
|
||||
move: MoveId.FLORAL_HEALING,
|
||||
percent: 66,
|
||||
ability: AbilityId.GRASSY_SURGE,
|
||||
condText: "Grassy Terrain is active",
|
||||
},
|
||||
])("Target-Healing Moves - $name", ({ move, percent, ability }) => {
|
||||
it("should heal 50% of the target's maximum HP, rounded half up", async () => {
|
||||
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
|
||||
|
||||
game.move.use(move);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(blissey.getHpRatio()).toBeCloseTo(0.5, 1);
|
||||
expect(game.phaseInterceptor.log).toContain("PokemonHealPhase");
|
||||
expect(game.textInterceptor.logs).toContain(
|
||||
i18next.t("moveTriggers:healHp", { pokemonName: getPokemonNameWithAffix(blissey) }),
|
||||
);
|
||||
expect(blissey).toHaveHp(252); // 251 + 1
|
||||
});
|
||||
|
||||
it("should fail if used at full HP", async () => {
|
||||
it("should fail if 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.hp).toBe(blissey.getMaxHp());
|
||||
expect(blissey).toHaveFullHp();
|
||||
expect(game.textInterceptor.logs).toContain(
|
||||
i18next.t("battle:hpIsFull", {
|
||||
pokemonName: getPokemonNameWithAffix(blissey),
|
||||
}),
|
||||
);
|
||||
expect(game.phaseInterceptor.log).not.toContain("PokemonHealPhase");
|
||||
expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Weather-based Healing Moves", () => {
|
||||
it.each([
|
||||
{ name: "Harsh Sunlight", weather: WeatherType.SUNNY },
|
||||
{ name: "Extremely Harsh Sunlight", weather: WeatherType.HARSH_SUN },
|
||||
])("should heal 66% of the user's maximum HP under $name", async ({ weather }) => {
|
||||
game.override.weather(weather);
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
|
||||
const blissey = game.field.getPlayerPokemon();
|
||||
blissey.hp = 1;
|
||||
|
||||
game.move.use(MoveId.MOONLIGHT);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(blissey.getHpRatio()).toBeCloseTo(0.66, 1);
|
||||
expect(blissey).toHaveUsedMove({ move, result: MoveResult.FAIL });
|
||||
});
|
||||
|
||||
const nonSunWTs = getEnumValues(WeatherType)
|
||||
.filter(
|
||||
wt => ![WeatherType.SUNNY, WeatherType.HARSH_SUN, WeatherType.NONE, WeatherType.STRONG_WINDS].includes(wt),
|
||||
)
|
||||
.map(wt => ({
|
||||
name: toTitleCase(WeatherType[wt]),
|
||||
weather: wt,
|
||||
}));
|
||||
|
||||
it.each(nonSunWTs)("should heal 25% of the user's maximum HP under $name", async ({ weather }) => {
|
||||
game.override.weather(weather);
|
||||
it("should heal $percent% 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]);
|
||||
|
||||
const blissey = game.field.getPlayerPokemon();
|
||||
blissey.hp = 1;
|
||||
const chansey = game.field.getEnemyPokemon();
|
||||
chansey.hp = 1;
|
||||
|
||||
game.move.use(MoveId.MOONLIGHT);
|
||||
game.move.use(move);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(blissey.getHpRatio()).toBeCloseTo(0.25, 1);
|
||||
});
|
||||
|
||||
it("should heal 50% of the user's maximum HP under strong winds", async () => {
|
||||
game.override.ability(AbilityId.DELTA_STREAM);
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
|
||||
const blissey = game.field.getPlayerPokemon();
|
||||
blissey.hp = 1;
|
||||
|
||||
game.move.use(MoveId.MOONLIGHT);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(blissey.getHpRatio()).toBeCloseTo(0.5, 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Shore Up", () => {
|
||||
it("should heal 66% of the user's maximum HP in a sandstorm", async () => {
|
||||
game.override.weather(WeatherType.SANDSTORM);
|
||||
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
||||
|
||||
const blissey = game.field.getPlayerPokemon();
|
||||
blissey.hp = 1;
|
||||
|
||||
game.move.use(MoveId.SHORE_UP);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(blissey.getHpRatio()).toBeCloseTo(0.66, 1);
|
||||
expect(chansey).toHaveHp(Math.round((percent * chansey.getMaxHp()) / 100) + 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user