[P2] Fixes party status cure moves only curing the player's pokemon, even when used by enemy pokemon (#3369)
* Fixes bug with Status Cure moves only curing player pokemon, refactors PartyStatusCureAttr, removes PartyStatusCurePhase * Adds check for user ID, since user always cures its own status regardless of ability * Adds unit tests for sparkly swirl * Merge and fix conflicts * Fix conflicts with SPLASH_ONLY * Fix failing sparkly swirl test due to splash_only * Adds unit tests for heal bell and aromatherapy * Update src/data/move.ts --------- Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>
This commit is contained in:
parent
54efd44497
commit
c58b5e943b
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue