diff --git a/src/data/ability.ts b/src/data/ability.ts index e3e40620b8e..40461f72e97 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -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 { - 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 { 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) diff --git a/src/data/move.ts b/src/data/move.ts index 19b2b77c5d8..2d70fd3dece 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -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) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index ba88fa9e18a..0f34c4bc3ff 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -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); diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 2c69c13a11b..5dfad42ad1d 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -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; diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 9443f524039..9515bd2bf6b 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -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)); diff --git a/src/phases.ts b/src/phases.ts index 84fd04f9352..8dc3d8661a3 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -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) { diff --git a/src/test/abilities/ice_face.test.ts b/src/test/abilities/ice_face.test.ts index 676cff66a02..1b00ef748eb 100644 --- a/src/test/abilities/ice_face.test.ts +++ b/src/test/abilities/ice_face.test.ts @@ -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); diff --git a/src/test/abilities/libero.test.ts b/src/test/abilities/libero.test.ts index 03ade6a3020..015e6a44e24 100644 --- a/src/test/abilities/libero.test.ts +++ b/src/test/abilities/libero.test.ts @@ -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, diff --git a/src/test/abilities/protean.test.ts b/src/test/abilities/protean.test.ts index cb15d591a1f..6255a2bc2b2 100644 --- a/src/test/abilities/protean.test.ts +++ b/src/test/abilities/protean.test.ts @@ -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, diff --git a/src/test/abilities/sand_veil.test.ts b/src/test/abilities/sand_veil.test.ts index c4733cd20bd..ea75027101a 100644 --- a/src/test/abilities/sand_veil.test.ts +++ b/src/test/abilities/sand_veil.test.ts @@ -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 ); diff --git a/src/test/abilities/sturdy.test.ts b/src/test/abilities/sturdy.test.ts index 4eee99556d4..0cfa45ef843 100644 --- a/src/test/abilities/sturdy.test.ts +++ b/src/test/abilities/sturdy.test.ts @@ -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 ); diff --git a/src/test/abilities/wind_rider.test.ts b/src/test/abilities/wind_rider.test.ts index c805ce5684c..d578c5a22dc 100644 --- a/src/test/abilities/wind_rider.test.ts +++ b/src/test/abilities/wind_rider.test.ts @@ -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)); diff --git a/src/test/items/leftovers.test.ts b/src/test/items/leftovers.test.ts new file mode 100644 index 00000000000..dff5ba4ee3b --- /dev/null +++ b/src/test/items/leftovers.test.ts @@ -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); +}); diff --git a/src/test/moves/gastro_acid.test.ts b/src/test/moves/gastro_acid.test.ts index 810870438d0..c8f6ab6cc6a 100644 --- a/src/test/moves/gastro_acid.test.ts +++ b/src/test/moves/gastro_acid.test.ts @@ -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 () => { diff --git a/src/test/moves/purify.test.ts b/src/test/moves/purify.test.ts index 7658ea7dc58..282d33f85f9 100644 --- a/src/test/moves/purify.test.ts +++ b/src/test/moves/purify.test.ts @@ -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 );