diff --git a/src/data/move.ts b/src/data/move.ts index 13ab84e8898c..d547cd565248 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -25,7 +25,6 @@ import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { MoveUsedEvent } from "#app/events/battle-scene"; import { BATTLE_STATS, type BattleStat, EFFECTIVE_STATS, type EffectiveStat, getStatKey, Stat } from "#app/enums/stat"; -import { PartyStatusCurePhase } from "#app/phases/party-status-cure-phase"; import { BattleEndPhase } from "#app/phases/battle-end-phase"; import { MoveEndPhase } from "#app/phases/move-end-phase"; import { MovePhase } from "#app/phases/move-phase"; @@ -34,6 +33,7 @@ import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { SwitchPhase } from "#app/phases/switch-phase"; import { SwitchSummonPhase } from "#app/phases/switch-summon-phase"; +import { ShowAbilityPhase } from "#app/phases/show-ability-phase"; import { SpeciesFormChangeRevertWeatherFormTrigger } from "./pokemon-forms"; import { GameMode } from "#app/game-mode"; import { applyChallenges, ChallengeType } from "./challenge"; @@ -1585,12 +1585,31 @@ export class PartyStatusCureAttr extends MoveEffectAttr { if (!this.canApply(user, target, move, args)) { return false; } - this.addPartyCurePhase(user); + const partyPokemon = user.isPlayer() ? user.scene.getParty() : user.scene.getEnemyParty(); + partyPokemon.forEach(p => this.cureStatus(p, user.id)); + + if (this.message) { + user.scene.queueMessage(this.message); + } + return true; } - addPartyCurePhase(user: Pokemon) { - user.scene.unshiftPhase(new PartyStatusCurePhase(user.scene, user, this.message, this.abilityCondition)); + /** + * Tries to cure the status of the given {@linkcode Pokemon} + * @param pokemon The {@linkcode Pokemon} to cure. + * @param userId The ID of the (move) {@linkcode Pokemon | user}. + */ + public cureStatus(pokemon: Pokemon, userId: number) { + if (!pokemon.isOnField() || pokemon.id === userId) { // user always cures its own status, regardless of ability + pokemon.resetStatus(false); + pokemon.updateInfo(); + } else if (!pokemon.hasAbility(this.abilityCondition)) { + pokemon.resetStatus(); + pokemon.updateInfo(); + } else { + pokemon.scene.unshiftPhase(new ShowAbilityPhase(pokemon.scene, pokemon.id, pokemon.getPassiveAbility()?.id === this.abilityCondition)); + } } } diff --git a/src/phases/party-status-cure-phase.ts b/src/phases/party-status-cure-phase.ts deleted file mode 100644 index e4903c7fc1fd..000000000000 --- a/src/phases/party-status-cure-phase.ts +++ /dev/null @@ -1,48 +0,0 @@ -import BattleScene from "#app/battle-scene"; -import { Abilities } from "#app/enums/abilities"; -import Pokemon from "#app/field/pokemon"; -import { BattlePhase } from "./battle-phase"; -import { ShowAbilityPhase } from "./show-ability-phase"; - -/** - * Cures the party of all non-volatile status conditions, shows a message - * @param {BattleScene} scene The current scene - * @param {Pokemon} user The user of the move that cures the party - * @param {string} message The message that should be displayed - * @param {Abilities} abilityCondition Pokemon with this ability will not be affected ie. Soundproof - */ -export class PartyStatusCurePhase extends BattlePhase { - private user: Pokemon; - private message: string; - private abilityCondition: Abilities; - - constructor(scene: BattleScene, user: Pokemon, message: string, abilityCondition: Abilities) { - super(scene); - - this.user = user; - this.message = message; - this.abilityCondition = abilityCondition; - } - - start() { - super.start(); - for (const pokemon of this.scene.getParty()) { - if (!pokemon.isOnField() || pokemon === this.user) { - pokemon.resetStatus(false); - pokemon.updateInfo(true); - } else { - if (!pokemon.hasAbility(this.abilityCondition)) { - pokemon.resetStatus(); - pokemon.updateInfo(true); - } else { - // Manually show ability bar, since we're not hooked into the targeting system - pokemon.scene.unshiftPhase(new ShowAbilityPhase(pokemon.scene, pokemon.id, pokemon.getPassiveAbility()?.id === this.abilityCondition)); - } - } - } - if (this.message) { - this.scene.queueMessage(this.message); - } - this.end(); - } -} diff --git a/src/test/moves/aromatherapy.test.ts b/src/test/moves/aromatherapy.test.ts new file mode 100644 index 000000000000..acc2e9c5fae5 --- /dev/null +++ b/src/test/moves/aromatherapy.test.ts @@ -0,0 +1,101 @@ +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, vi } from "vitest"; + +describe("Moves - Aromatherapy", () => { + 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 + .moveset([Moves.AROMATHERAPY, Moves.SPLASH]) + .statusEffect(StatusEffect.BURN) + .battleType("double") + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should cure status effect of the user, its ally, and all party pokemon", async () => { + await game.classicMode.startBattle([Species.RATTATA, Species.RATTATA, Species.RATTATA]); + const [leftPlayer, rightPlayer, partyPokemon] = game.scene.getParty(); + + vi.spyOn(leftPlayer, "resetStatus"); + vi.spyOn(rightPlayer, "resetStatus"); + vi.spyOn(partyPokemon, "resetStatus"); + + game.move.select(Moves.AROMATHERAPY, 0); + await game.phaseInterceptor.to(CommandPhase); + game.move.select(Moves.SPLASH, 1); + await game.toNextTurn(); + + expect(leftPlayer.resetStatus).toHaveBeenCalledOnce(); + expect(rightPlayer.resetStatus).toHaveBeenCalledOnce(); + expect(partyPokemon.resetStatus).toHaveBeenCalledOnce(); + + expect(leftPlayer.status?.effect).toBeUndefined(); + expect(rightPlayer.status?.effect).toBeUndefined(); + expect(partyPokemon.status?.effect).toBeUndefined(); + }); + + it("should not cure status effect of the target/target's allies", async () => { + game.override.enemyStatusEffect(StatusEffect.BURN); + await game.classicMode.startBattle([Species.RATTATA, Species.RATTATA]); + const [leftOpp, rightOpp] = game.scene.getEnemyField(); + + vi.spyOn(leftOpp, "resetStatus"); + vi.spyOn(rightOpp, "resetStatus"); + + game.move.select(Moves.AROMATHERAPY, 0); + await game.phaseInterceptor.to(CommandPhase); + game.move.select(Moves.SPLASH, 1); + await game.toNextTurn(); + + expect(leftOpp.resetStatus).toHaveBeenCalledTimes(0); + expect(rightOpp.resetStatus).toHaveBeenCalledTimes(0); + + expect(leftOpp.status?.effect).toBeTruthy(); + expect(rightOpp.status?.effect).toBeTruthy(); + + expect(leftOpp.status?.effect).toBe(StatusEffect.BURN); + expect(rightOpp.status?.effect).toBe(StatusEffect.BURN); + }); + + it("should not cure status effect of allies ON FIELD with Sap Sipper, should still cure allies in party", async () => { + game.override.ability(Abilities.SAP_SIPPER); + await game.classicMode.startBattle([Species.RATTATA, Species.RATTATA, Species.RATTATA]); + const [leftPlayer, rightPlayer, partyPokemon] = game.scene.getParty(); + + vi.spyOn(leftPlayer, "resetStatus"); + vi.spyOn(rightPlayer, "resetStatus"); + vi.spyOn(partyPokemon, "resetStatus"); + + game.move.select(Moves.AROMATHERAPY, 0); + await game.phaseInterceptor.to(CommandPhase); + game.move.select(Moves.SPLASH, 1); + await game.toNextTurn(); + + expect(leftPlayer.resetStatus).toHaveBeenCalledOnce(); + expect(rightPlayer.resetStatus).toHaveBeenCalledTimes(0); + expect(partyPokemon.resetStatus).toHaveBeenCalledOnce(); + + expect(leftPlayer.status?.effect).toBeUndefined(); + expect(rightPlayer.status?.effect).toBe(StatusEffect.BURN); + expect(partyPokemon.status?.effect).toBeUndefined(); + }); +}); diff --git a/src/test/moves/heal_bell.test.ts b/src/test/moves/heal_bell.test.ts new file mode 100644 index 000000000000..1421809cf6c6 --- /dev/null +++ b/src/test/moves/heal_bell.test.ts @@ -0,0 +1,101 @@ +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, vi } from "vitest"; + +describe("Moves - Heal Bell", () => { + 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 + .moveset([Moves.HEAL_BELL, Moves.SPLASH]) + .statusEffect(StatusEffect.BURN) + .battleType("double") + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should cure status effect of the user, its ally, and all party pokemon", async () => { + await game.classicMode.startBattle([Species.RATTATA, Species.RATTATA, Species.RATTATA]); + const [leftPlayer, rightPlayer, partyPokemon] = game.scene.getParty(); + + vi.spyOn(leftPlayer, "resetStatus"); + vi.spyOn(rightPlayer, "resetStatus"); + vi.spyOn(partyPokemon, "resetStatus"); + + game.move.select(Moves.HEAL_BELL, 0); + await game.phaseInterceptor.to(CommandPhase); + game.move.select(Moves.SPLASH, 1); + await game.toNextTurn(); + + expect(leftPlayer.resetStatus).toHaveBeenCalledOnce(); + expect(rightPlayer.resetStatus).toHaveBeenCalledOnce(); + expect(partyPokemon.resetStatus).toHaveBeenCalledOnce(); + + expect(leftPlayer.status?.effect).toBeUndefined(); + expect(rightPlayer.status?.effect).toBeUndefined(); + expect(partyPokemon.status?.effect).toBeUndefined(); + }); + + it("should not cure status effect of the target/target's allies", async () => { + game.override.enemyStatusEffect(StatusEffect.BURN); + await game.classicMode.startBattle([Species.RATTATA, Species.RATTATA]); + const [leftOpp, rightOpp] = game.scene.getEnemyField(); + + vi.spyOn(leftOpp, "resetStatus"); + vi.spyOn(rightOpp, "resetStatus"); + + game.move.select(Moves.HEAL_BELL, 0); + await game.phaseInterceptor.to(CommandPhase); + game.move.select(Moves.SPLASH, 1); + await game.toNextTurn(); + + expect(leftOpp.resetStatus).toHaveBeenCalledTimes(0); + expect(rightOpp.resetStatus).toHaveBeenCalledTimes(0); + + expect(leftOpp.status?.effect).toBeTruthy(); + expect(rightOpp.status?.effect).toBeTruthy(); + + expect(leftOpp.status?.effect).toBe(StatusEffect.BURN); + expect(rightOpp.status?.effect).toBe(StatusEffect.BURN); + }); + + it("should not cure status effect of allies ON FIELD with Soundproof, should still cure allies in party", async () => { + game.override.ability(Abilities.SOUNDPROOF); + await game.classicMode.startBattle([Species.RATTATA, Species.RATTATA, Species.RATTATA]); + const [leftPlayer, rightPlayer, partyPokemon] = game.scene.getParty(); + + vi.spyOn(leftPlayer, "resetStatus"); + vi.spyOn(rightPlayer, "resetStatus"); + vi.spyOn(partyPokemon, "resetStatus"); + + game.move.select(Moves.HEAL_BELL, 0); + await game.phaseInterceptor.to(CommandPhase); + game.move.select(Moves.SPLASH, 1); + await game.toNextTurn(); + + expect(leftPlayer.resetStatus).toHaveBeenCalledOnce(); + expect(rightPlayer.resetStatus).toHaveBeenCalledTimes(0); + expect(partyPokemon.resetStatus).toHaveBeenCalledOnce(); + + expect(leftPlayer.status?.effect).toBeUndefined(); + expect(rightPlayer.status?.effect).toBe(StatusEffect.BURN); + expect(partyPokemon.status?.effect).toBeUndefined(); + }); +}); diff --git a/src/test/moves/sparkly_swirl.test.ts b/src/test/moves/sparkly_swirl.test.ts new file mode 100644 index 000000000000..83c154e57e7e --- /dev/null +++ b/src/test/moves/sparkly_swirl.test.ts @@ -0,0 +1,86 @@ +import { allMoves } from "#app/data/move"; +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, expect, it, vi } from "vitest"; + +describe("Moves - Sparkly Swirl", () => { + 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 + .enemySpecies(Species.SHUCKLE) + .enemyLevel(100) + .enemyMoveset(Moves.SPLASH) + .enemyAbility(Abilities.BALL_FETCH) + .moveset([Moves.SPARKLY_SWIRL, Moves.SPLASH]) + .ability(Abilities.BALL_FETCH); + + vi.spyOn(allMoves[Moves.SPARKLY_SWIRL], "accuracy", "get").mockReturnValue(100); + }); + + it("should cure status effect of the user, its ally, and all party pokemon", async () => { + game.override + .battleType("double") + .statusEffect(StatusEffect.BURN); + await game.classicMode.startBattle([Species.RATTATA, Species.RATTATA, Species.RATTATA]); + const [leftPlayer, rightPlayer, partyPokemon] = game.scene.getParty(); + const leftOpp = game.scene.getEnemyPokemon()!; + + vi.spyOn(leftPlayer, "resetStatus"); + vi.spyOn(rightPlayer, "resetStatus"); + vi.spyOn(partyPokemon, "resetStatus"); + + game.move.select(Moves.SPARKLY_SWIRL, 0, leftOpp.getBattlerIndex()); + await game.phaseInterceptor.to(CommandPhase); + game.move.select(Moves.SPLASH, 1); + await game.toNextTurn(); + + expect(leftPlayer.resetStatus).toHaveBeenCalledOnce(); + expect(rightPlayer.resetStatus).toHaveBeenCalledOnce(); + expect(partyPokemon.resetStatus).toHaveBeenCalledOnce(); + + expect(leftPlayer.status?.effect).toBeUndefined(); + expect(rightPlayer.status?.effect).toBeUndefined(); + expect(partyPokemon.status?.effect).toBeUndefined(); + }); + + it("should not cure status effect of the target/target's allies", async () => { + game.override + .battleType("double") + .enemyStatusEffect(StatusEffect.BURN); + await game.classicMode.startBattle([Species.RATTATA, Species.RATTATA]); + const [leftOpp, rightOpp] = game.scene.getEnemyField(); + + vi.spyOn(leftOpp, "resetStatus"); + vi.spyOn(rightOpp, "resetStatus"); + + game.move.select(Moves.SPARKLY_SWIRL, 0, leftOpp.getBattlerIndex()); + await game.phaseInterceptor.to(CommandPhase); + game.move.select(Moves.SPLASH, 1); + await game.toNextTurn(); + + expect(leftOpp.resetStatus).toHaveBeenCalledTimes(0); + expect(rightOpp.resetStatus).toHaveBeenCalledTimes(0); + + expect(leftOpp.status?.effect).toBeTruthy(); + expect(rightOpp.status?.effect).toBeTruthy(); + + expect(leftOpp.status?.effect).toBe(StatusEffect.BURN); + expect(rightOpp.status?.effect).toBe(StatusEffect.BURN); + }); +});