diff --git a/src/data/move.ts b/src/data/move.ts index 5a8ad208467..7a6f08a5372 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -1881,8 +1881,14 @@ export class FlameBurstAttr extends MoveEffectAttr { } export class SacrificialFullRestoreAttr extends SacrificialAttr { - constructor() { + protected restorePP: boolean; + protected moveMessage: string; + + constructor(restorePP: boolean, moveMessage: string) { super(); + + this.restorePP = restorePP; + this.moveMessage = moveMessage; } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { @@ -1893,8 +1899,19 @@ export class SacrificialFullRestoreAttr extends SacrificialAttr { // We don't know which party member will be chosen, so pick the highest max HP in the party const maxPartyMemberHp = user.scene.getPlayerParty().map(p => p.getMaxHp()).reduce((maxHp: integer, hp: integer) => Math.max(hp, maxHp), 0); - user.scene.pushPhase(new PokemonHealPhase(user.scene, user.getBattlerIndex(), - maxPartyMemberHp, i18next.t("moveTriggers:sacrificialFullRestore", { pokemonName: getPokemonNameWithAffix(user) }), true, false, false, true), true); + user.scene.pushPhase( + new PokemonHealPhase( + user.scene, + user.getBattlerIndex(), + maxPartyMemberHp, + i18next.t(this.moveMessage, { pokemonName: getPokemonNameWithAffix(user) }), + true, + false, + false, + true, + false, + this.restorePP), + true); return true; } @@ -9002,7 +9019,7 @@ export function initMoves() { .attr(GyroBallPowerAttr) .ballBombMove(), new SelfStatusMove(Moves.HEALING_WISH, Type.PSYCHIC, -1, 10, -1, 0, 4) - .attr(SacrificialFullRestoreAttr) + .attr(SacrificialFullRestoreAttr, false, "moveTriggers:sacrificialFullRestore") .triageMove(), new AttackMove(Moves.BRINE, Type.WATER, MoveCategory.SPECIAL, 65, 100, 10, -1, 0, 4) .attr(MovePowerMultiplierAttr, (user, target, move) => target.getHpRatio() < 0.5 ? 2 : 1), @@ -9279,10 +9296,9 @@ export function initMoves() { new AttackMove(Moves.SPACIAL_REND, Type.DRAGON, MoveCategory.SPECIAL, 100, 95, 5, -1, 0, 4) .attr(HighCritAttr), new SelfStatusMove(Moves.LUNAR_DANCE, Type.PSYCHIC, -1, 10, -1, 0, 4) - .attr(SacrificialAttrOnHit) + .attr(SacrificialFullRestoreAttr, true, "moveTriggers:lunarDanceRestore") .danceMove() - .triageMove() - .unimplemented(), + .triageMove(), new AttackMove(Moves.CRUSH_GRIP, Type.NORMAL, MoveCategory.PHYSICAL, -1, 100, 5, -1, 0, 4) .attr(OpponentHighHpPowerAttr, 120), new AttackMove(Moves.MAGMA_STORM, Type.FIRE, MoveCategory.SPECIAL, 100, 75, 5, -1, 0, 4) diff --git a/src/phases/pokemon-heal-phase.ts b/src/phases/pokemon-heal-phase.ts index dc0bd235bb5..c95b92e3b64 100644 --- a/src/phases/pokemon-heal-phase.ts +++ b/src/phases/pokemon-heal-phase.ts @@ -21,8 +21,9 @@ export class PokemonHealPhase extends CommonAnimPhase { private revive: boolean; private healStatus: boolean; private preventFullHeal: boolean; + private fullRestorePP: boolean; - constructor(scene: BattleScene, battlerIndex: BattlerIndex, hpHealed: integer, message: string | null, showFullHpMessage: boolean, skipAnim: boolean = false, revive: boolean = false, healStatus: boolean = false, preventFullHeal: boolean = false) { + constructor(scene: BattleScene, battlerIndex: BattlerIndex, hpHealed: integer, message: string | null, showFullHpMessage: boolean, skipAnim: boolean = false, revive: boolean = false, healStatus: boolean = false, preventFullHeal: boolean = false, fullRestorePP: boolean = false) { super(scene, battlerIndex, undefined, CommonAnim.HEALTH_UP); this.hpHealed = hpHealed; @@ -32,6 +33,7 @@ export class PokemonHealPhase extends CommonAnimPhase { this.revive = revive; this.healStatus = healStatus; this.preventFullHeal = preventFullHeal; + this.fullRestorePP = fullRestorePP; } start() { @@ -86,6 +88,13 @@ export class PokemonHealPhase extends CommonAnimPhase { lastStatusEffect = pokemon.status.effect; pokemon.resetStatus(); } + if (this.fullRestorePP) { + for (const move of this.getPokemon().getMoveset()) { + if (move) { + move.ppUsed = 0; + } + } + } pokemon.updateInfo().then(() => super.end()); } else if (this.healStatus && !this.revive && pokemon.status) { lastStatusEffect = pokemon.status.effect; diff --git a/src/test/moves/lunar_dance.test.ts b/src/test/moves/lunar_dance.test.ts new file mode 100644 index 00000000000..603247298ac --- /dev/null +++ b/src/test/moves/lunar_dance.test.ts @@ -0,0 +1,77 @@ +import { StatusEffect } from "#app/enums/status-effect"; +import { CommandPhase } from "#app/phases/command-phase"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, it, expect } from "vitest"; + +describe("Moves - Lunar Dance", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .statusEffect(StatusEffect.BURN) + .battleType("double") + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should full restore HP, PP and status of switched in pokemon, then fail second use because no remaining backup pokemon in party", async () => { + await game.classicMode.startBattle([ Species.BULBASAUR, Species.ODDISH, Species.RATTATA ]); + + const [ bulbasaur, oddish, rattata ] = game.scene.getPlayerParty(); + game.move.changeMoveset(bulbasaur, [ Moves.LUNAR_DANCE, Moves.SPLASH ]); + game.move.changeMoveset(oddish, [ Moves.LUNAR_DANCE, Moves.SPLASH ]); + game.move.changeMoveset(rattata, [ Moves.LUNAR_DANCE, Moves.SPLASH ]); + + game.move.select(Moves.SPLASH, 0); + game.move.select(Moves.SPLASH, 1); + await game.phaseInterceptor.to(CommandPhase); + await game.toNextTurn(); + + // Bulbasaur should still be burned and have used a PP for splash and not at max hp + expect(bulbasaur.status?.effect).toBe(StatusEffect.BURN); + expect(bulbasaur.moveset[1]?.ppUsed).toBe(1); + expect(bulbasaur.hp).toBeLessThan(bulbasaur.getMaxHp()); + + // Switch out Bulbasaur for Rattata so we can swtich bulbasaur back in with lunar dance + game.doSwitchPokemon(2); + game.move.select(Moves.SPLASH, 1); + await game.phaseInterceptor.to(CommandPhase); + await game.toNextTurn(); + + game.move.select(Moves.SPLASH, 0); + game.move.select(Moves.LUNAR_DANCE); + game.doSelectPartyPokemon(2); + await game.phaseInterceptor.to("SwitchPhase", false); + await game.toNextTurn(); + + // Bulbasaur should NOT have any status and have full PP for splash and be at max hp + expect(bulbasaur.status?.effect).toBeUndefined(); + expect(bulbasaur.moveset[1]?.ppUsed).toBe(0); + expect(bulbasaur.isFullHp()).toBe(true); + + game.move.select(Moves.SPLASH, 0); + game.move.select(Moves.LUNAR_DANCE); + await game.phaseInterceptor.to(CommandPhase); + await game.toNextTurn(); + + // Using Lunar dance again should fail because nothing in party and rattata should be alive + expect(rattata.status?.effect).toBe(StatusEffect.BURN); + expect(rattata.hp).toBeLessThan(rattata.getMaxHp()); + }); +});