[Bug] Fix HP rounding issues (#2968)
This commit is contained in:
parent
26c98f4afe
commit
a526403534
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
|
@ -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 () => {
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue