[Bug] Fix HP rounding issues (#2968)

This commit is contained in:
EmberCM 2024-07-24 13:05:26 -05:00 committed by GitHub
parent 26c98f4afe
commit a526403534
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 99 additions and 32 deletions

View File

@ -266,7 +266,7 @@ export class PreDefendFormChangeAbAttr extends PreDefendAbAttr {
}
export class PreDefendFullHpEndureAbAttr extends PreDefendAbAttr {
applyPreDefend(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean {
if (pokemon.hp === pokemon.getMaxHp() &&
if (pokemon.isFullHp() &&
pokemon.getMaxHp() > 1 && //Checks if pokemon has wonder_guard (which forces 1hp)
(args[0] as Utils.NumberHolder).value >= pokemon.hp) { //Damage >= hp
return pokemon.addTag(BattlerTagType.STURDY, 1);
@ -400,7 +400,7 @@ export class TypeImmunityHealAbAttr extends TypeImmunityAbAttr {
const ret = super.applyPreDefend(pokemon, passive, attacker, move, cancelled, args);
if (ret) {
if (pokemon.getHpRatio() < 1) {
if (!pokemon.isFullHp()) {
const simulated = args.length > 1 && args[1];
if (!simulated) {
const abilityName = (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name;
@ -2283,7 +2283,7 @@ export class PreSwitchOutClearWeatherAbAttr extends PreSwitchOutAbAttr {
export class PreSwitchOutHealAbAttr extends PreSwitchOutAbAttr {
applyPreSwitchOut(pokemon: Pokemon, passive: boolean, args: any[]): boolean | Promise<boolean> {
if (pokemon.getHpRatio() < 1 ) {
if (!pokemon.isFullHp()) {
const healAmount = Math.floor(pokemon.getMaxHp() * 0.33);
pokemon.heal(healAmount);
pokemon.updateInfo();
@ -2840,7 +2840,7 @@ export class PostWeatherLapseHealAbAttr extends PostWeatherLapseAbAttr {
}
applyPostWeatherLapse(pokemon: Pokemon, passive: boolean, weather: Weather, args: any[]): boolean {
if (pokemon.getHpRatio() < 1) {
if (!pokemon.isFullHp()) {
const scene = pokemon.scene;
const abilityName = (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name;
scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.getBattlerIndex(),
@ -2934,7 +2934,7 @@ export class PostTurnStatusHealAbAttr extends PostTurnAbAttr {
*/
applyPostTurn(pokemon: Pokemon, passive: boolean, args: any[]): boolean | Promise<boolean> {
if (this.effects.includes(pokemon.status?.effect)) {
if (pokemon.getMaxHp() !== pokemon.hp) {
if (!pokemon.isFullHp()) {
const scene = pokemon.scene;
const abilityName = (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name;
scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.getBattlerIndex(),
@ -3091,7 +3091,7 @@ export class PostTurnStatChangeAbAttr extends PostTurnAbAttr {
export class PostTurnHealAbAttr extends PostTurnAbAttr {
applyPostTurn(pokemon: Pokemon, passive: boolean, args: any[]): boolean {
if (pokemon.getHpRatio() < 1) {
if (!pokemon.isFullHp()) {
const scene = pokemon.scene;
const abilityName = (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name;
scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.getBattlerIndex(),
@ -4598,7 +4598,7 @@ export function initAbilities() {
.attr(WeightMultiplierAbAttr, 0.5)
.ignorable(),
new Ability(Abilities.MULTISCALE, 5)
.attr(ReceivedMoveDamageMultiplierAbAttr,(target, user, move) => target.getHpRatio() === 1, 0.5)
.attr(ReceivedMoveDamageMultiplierAbAttr,(target, user, move) => target.isFullHp(), 0.5)
.ignorable(),
new Ability(Abilities.TOXIC_BOOST, 5)
.attr(MovePowerBoostAbAttr, (user, target, move) => move.category === MoveCategory.PHYSICAL && (user.status?.effect === StatusEffect.POISON || user.status?.effect === StatusEffect.TOXIC), 1.5),
@ -4729,7 +4729,7 @@ export function initAbilities() {
.attr(UnsuppressableAbilityAbAttr)
.attr(NoFusionAbilityAbAttr),
new Ability(Abilities.GALE_WINGS, 6)
.attr(IncrementMovePriorityAbAttr, (pokemon, move) => pokemon.getHpRatio() === 1 && move.type === Type.FLYING),
.attr(IncrementMovePriorityAbAttr, (pokemon, move) => pokemon.isFullHp() && move.type === Type.FLYING),
new Ability(Abilities.MEGA_LAUNCHER, 6)
.attr(MovePowerBoostAbAttr, (user, target, move) => move.hasFlag(MoveFlags.PULSE_MOVE), 1.5),
new Ability(Abilities.GRASS_PELT, 6)
@ -4934,7 +4934,7 @@ export function initAbilities() {
new Ability(Abilities.FULL_METAL_BODY, 7)
.attr(ProtectStatAbAttr),
new Ability(Abilities.SHADOW_SHIELD, 7)
.attr(ReceivedMoveDamageMultiplierAbAttr,(target, user, move) => target.getHpRatio() === 1, 0.5),
.attr(ReceivedMoveDamageMultiplierAbAttr,(target, user, move) => target.isFullHp(), 0.5),
new Ability(Abilities.PRISM_ARMOR, 7)
.attr(ReceivedMoveDamageMultiplierAbAttr,(target, user, move) => target.getAttackTypeEffectiveness(move.type, user) >= 2, 0.75),
new Ability(Abilities.NEUROFORCE, 7)

View File

@ -6306,7 +6306,7 @@ export function initMoves() {
new SelfStatusMove(Moves.REST, Type.PSYCHIC, -1, 5, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.SLEEP, true, 3, true)
.attr(HealAttr, 1, true)
.condition((user, target, move) => user.getHpRatio() < 1 && user.canSetStatus(StatusEffect.SLEEP, true, true))
.condition((user, target, move) => !user.isFullHp() && user.canSetStatus(StatusEffect.SLEEP, true, true))
.triageMove(),
new AttackMove(Moves.ROCK_SLIDE, Type.ROCK, MoveCategory.PHYSICAL, 75, 90, 10, 30, 0, 1)
.attr(FlinchAttr)

View File

@ -804,6 +804,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.setNature(nature);
}
isFullHp(): boolean {
return this.hp >= this.getMaxHp();
}
getMaxHp(): integer {
return this.getStat(Stat.HP);
}
@ -2046,7 +2050,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const destinyTag = this.getTag(BattlerTagType.DESTINY_BOND);
if (damage.value) {
if (this.getHpRatio() === 1) {
if (this.isFullHp()) {
applyPreDefendAbAttrs(PreDefendFullHpEndureAbAttr, this, source, move, cancelled, damage);
} else if (!this.isPlayer() && damage.value >= this.hp) {
this.scene.applyModifiers(EnemyEndureChanceModifier, false, this);

View File

@ -235,7 +235,7 @@ export class PokemonHpRestoreModifierType extends PokemonModifierType {
constructor(localeKey: string, iconImage: string, restorePoints: integer, restorePercent: integer, healStatus: boolean = false, newModifierFunc?: NewModifierFunc, selectFilter?: PokemonSelectFilter, group?: string) {
super(localeKey, iconImage, newModifierFunc || ((_type, args) => new Modifiers.PokemonHpRestoreModifier(this, (args[0] as PlayerPokemon).id, this.restorePoints, this.restorePercent, this.healStatus, false)),
selectFilter || ((pokemon: PlayerPokemon) => {
if (!pokemon.hp || (pokemon.hp >= pokemon.getMaxHp() && (!this.healStatus || (!pokemon.status && !pokemon.getTag(BattlerTagType.CONFUSED))))) {
if (!pokemon.hp || (pokemon.isFullHp() && (!this.healStatus || (!pokemon.status && !pokemon.getTag(BattlerTagType.CONFUSED))))) {
return PartyUiHandler.NoEffectMessage;
}
return null;

View File

@ -1151,7 +1151,7 @@ export class TurnHealModifier extends PokemonHeldItemModifier {
apply(args: any[]): boolean {
const pokemon = args[0] as Pokemon;
if (pokemon.getHpRatio() < 1) {
if (!pokemon.isFullHp()) {
const scene = pokemon.scene;
scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.getBattlerIndex(),
Math.max(Math.floor(pokemon.getMaxHp() / 16) * this.stackCount, 1), i18next.t("modifier:turnHealApply", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), typeName: this.type.name }), true));
@ -1242,7 +1242,7 @@ export class HitHealModifier extends PokemonHeldItemModifier {
apply(args: any[]): boolean {
const pokemon = args[0] as Pokemon;
if (pokemon.turnData.damageDealt && pokemon.getHpRatio() < 1) {
if (pokemon.turnData.damageDealt && !pokemon.isFullHp()) {
const scene = pokemon.scene;
scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.getBattlerIndex(),
Math.max(Math.floor(pokemon.turnData.damageDealt / 8) * this.stackCount, 1), i18next.t("modifier:hitHealApply", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), typeName: this.type.name }), true));
@ -2520,7 +2520,7 @@ export class EnemyTurnHealModifier extends EnemyPersistentModifier {
apply(args: any[]): boolean {
const pokemon = args[0] as Pokemon;
if (pokemon.getHpRatio() < 1) {
if (!pokemon.isFullHp()) {
const scene = pokemon.scene;
scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.getBattlerIndex(),
Math.max(Math.floor(pokemon.getMaxHp() / (100 / this.healPercent)) * this.stackCount, 1), i18next.t("modifier:enemyTurnHealApply", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), true, false, false, false, true));

View File

@ -4715,7 +4715,7 @@ export class PokemonHealPhase extends CommonAnimPhase {
}
start() {
if (!this.skipAnim && (this.revive || this.getPokemon().hp) && this.getPokemon().getHpRatio() < 1) {
if (!this.skipAnim && (this.revive || this.getPokemon().hp) && !this.getPokemon().isFullHp()) {
super.start();
} else {
this.end();
@ -4730,10 +4730,8 @@ export class PokemonHealPhase extends CommonAnimPhase {
return;
}
const fullHp = pokemon.getHpRatio() >= 1;
const hasMessage = !!this.message;
const healOrDamage = (!fullHp || this.hpHealed < 0);
const healOrDamage = (!pokemon.isFullHp() || this.hpHealed < 0);
let lastStatusEffect = StatusEffect.NONE;
if (healOrDamage) {

View File

@ -48,7 +48,7 @@ describe("Abilities - Ice Face", () => {
const eiscue = game.scene.getEnemyPokemon();
expect(eiscue.hp).equals(eiscue.getMaxHp());
expect(eiscue.isFullHp()).toBe(true);
expect(eiscue.formIndex).toBe(noiceForm);
expect(eiscue.getTag(BattlerTagType.ICE_FACE)).toBe(undefined);
});
@ -65,7 +65,7 @@ describe("Abilities - Ice Face", () => {
// First hit
await game.phaseInterceptor.to(MoveEffectPhase);
expect(eiscue.hp).equals(eiscue.getMaxHp());
expect(eiscue.isFullHp()).toBe(true);
expect(eiscue.formIndex).toBe(icefaceForm);
expect(eiscue.getTag(BattlerTagType.ICE_FACE)).toBeUndefined();
@ -120,7 +120,7 @@ describe("Abilities - Ice Face", () => {
const eiscue = game.scene.getEnemyPokemon();
expect(eiscue.hp).equals(eiscue.getMaxHp());
expect(eiscue.isFullHp()).toBe(true);
expect(eiscue.formIndex).toBe(noiceForm);
expect(eiscue.getTag(BattlerTagType.ICE_FACE)).toBe(undefined);
@ -143,7 +143,7 @@ describe("Abilities - Ice Face", () => {
expect(eiscue.getTag(BattlerTagType.ICE_FACE)).toBe(undefined);
expect(eiscue.formIndex).toBe(noiceForm);
expect(eiscue.hp).equals(eiscue.getMaxHp());
expect(eiscue.isFullHp()).toBe(true);
await game.toNextTurn();
game.doSwitchPokemon(1);
@ -189,7 +189,7 @@ describe("Abilities - Ice Face", () => {
expect(eiscue.getTag(BattlerTagType.ICE_FACE)).toBe(undefined);
expect(eiscue.formIndex).toBe(noiceForm);
expect(eiscue.hp).equals(eiscue.getMaxHp());
expect(eiscue.isFullHp()).toBe(true);
await game.toNextTurn();
game.doSwitchPokemon(1);

View File

@ -197,7 +197,7 @@ describe("Abilities - Protean", () => {
await game.phaseInterceptor.to(TurnEndPhase);
const enemyPokemon = game.scene.getEnemyPokemon();
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
expect(enemyPokemon.isFullHp()).toBe(true);
testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.TACKLE);
},
TIMEOUT,

View File

@ -197,7 +197,7 @@ describe("Abilities - Protean", () => {
await game.phaseInterceptor.to(TurnEndPhase);
const enemyPokemon = game.scene.getEnemyPokemon();
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
expect(enemyPokemon.isFullHp()).toBe(true);
testPokemonTypeMatchesDefaultMoveType(leadPokemon, Moves.TACKLE);
},
TIMEOUT,

View File

@ -76,7 +76,7 @@ describe("Abilities - Sand Veil", () => {
await game.phaseInterceptor.to(MoveEndPhase, false);
expect(leadPokemon[0].hp).toBe(leadPokemon[0].getMaxHp());
expect(leadPokemon[0].isFullHp()).toBe(true);
expect(leadPokemon[1].hp).toBeLessThan(leadPokemon[1].getMaxHp());
}, TIMEOUT
);

View File

@ -77,7 +77,7 @@ describe("Abilities - Sturdy", () => {
await game.phaseInterceptor.to(MoveEndPhase);
const enemyPokemon: EnemyPokemon = game.scene.getEnemyParty()[0];
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
expect(enemyPokemon.isFullHp()).toBe(true);
},
TIMEOUT
);

View File

@ -44,7 +44,7 @@ describe("Abilities - Wind Rider", () => {
await game.phaseInterceptor.to(TurnEndPhase);
expect(shiftry.hp).equals(shiftry.getMaxHp());
expect(shiftry.isFullHp()).toBe(true);
expect(shiftry.summonData.battleStats[BattleStat.ATK]).toBe(1);
});
@ -108,7 +108,7 @@ describe("Abilities - Wind Rider", () => {
const shiftry = game.scene.getPlayerPokemon();
expect(shiftry.summonData.battleStats[BattleStat.ATK]).toBe(0);
expect(shiftry.hp).equals(shiftry.getMaxHp());
expect(shiftry.isFullHp()).toBe(true);
game.doAttack(getMovePosition(game.scene, 0, Moves.SANDSTORM));

View File

@ -0,0 +1,65 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import Phaser from "phaser";
import GameManager from "#app/test/utils/gameManager";
import * as overrides from "#app/overrides";
import { DamagePhase, TurnEndPhase } from "#app/phases";
import { getMovePosition } from "#app/test/utils/gameManagerUtils";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
describe("Items - Leftovers", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(2000);
vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.UNNERVE);
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH]);
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.SHUCKLE);
vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.UNNERVE);
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]);
vi.spyOn(overrides, "STARTING_HELD_ITEMS_OVERRIDE", "get").mockReturnValue([{name: "LEFTOVERS", count: 1}]);
});
it("leftovers works", async() => {
await game.startBattle([Species.ARCANINE]);
// Make sure leftovers are there
expect(game.scene.modifiers[0].type.id).toBe("LEFTOVERS");
const leadPokemon = game.scene.getPlayerPokemon();
expect(leadPokemon).toBeDefined();
const enemyPokemon = game.scene.getEnemyPokemon();
expect(enemyPokemon).toBeDefined();
// We should have full hp
expect(leadPokemon.isFullHp()).toBe(true);
game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH));
// We should have less hp after the attack
await game.phaseInterceptor.to(DamagePhase, false);
expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp());
const leadHpAfterDamage = leadPokemon.hp;
// Check if leftovers heal us
await game.phaseInterceptor.to(TurnEndPhase);
expect(leadPokemon.hp).toBeGreaterThan(leadHpAfterDamage);
}, 20000);
});

View File

@ -68,7 +68,7 @@ describe("Moves - Gastro Acid", () => {
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemyField[0].hp).toBeLessThan(enemyField[0].getMaxHp());
expect(enemyField[1].hp).toBe(enemyField[1].getMaxHp());
expect(enemyField[1].isFullHp()).toBe(true);
}, TIMEOUT);
it("fails if used on an enemy with an already-suppressed ability", async () => {

View File

@ -55,7 +55,7 @@ describe("Moves - Purify", () => {
await game.phaseInterceptor.to(MoveEndPhase);
expect(enemyPokemon.status).toBe(undefined);
expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp());
expect(playerPokemon.isFullHp()).toBe(true);
},
TIMEOUT
);