Skip to content

Commit

Permalink
[P2] Fixes party status cure moves only curing the player's pokemon, …
Browse files Browse the repository at this point in the history
…even when used by enemy pokemon (pagefaultgames#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 <[email protected]>
  • Loading branch information
schmidtc1 and flx-sta authored Oct 3, 2024
1 parent 54efd44 commit c58b5e9
Show file tree
Hide file tree
Showing 5 changed files with 311 additions and 52 deletions.
27 changes: 23 additions & 4 deletions src/data/move.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -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));
}
}
}

Expand Down
48 changes: 0 additions & 48 deletions src/phases/party-status-cure-phase.ts

This file was deleted.

101 changes: 101 additions & 0 deletions src/test/moves/aromatherapy.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
101 changes: 101 additions & 0 deletions src/test/moves/heal_bell.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
86 changes: 86 additions & 0 deletions src/test/moves/sparkly_swirl.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});

0 comments on commit c58b5e9

Please sign in to comment.