mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-07-04 15:32:18 +02:00
Combined all force switching moves into 1 large test file + added more tests for failures
This commit is contained in:
parent
10472452f8
commit
4c8b320705
@ -1,131 +0,0 @@
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import { Stat } from "#enums/stat";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Moves - Baton Pass", () => {
|
||||
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
|
||||
.battleStyle("single")
|
||||
.enemySpecies(Species.MAGIKARP)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.moveset([Moves.BATON_PASS, Moves.NASTY_PLOT, Moves.SPLASH])
|
||||
.ability(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
.disableCrits();
|
||||
});
|
||||
|
||||
it("transfers all stat stages when player uses it", async () => {
|
||||
// arrange
|
||||
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]);
|
||||
|
||||
// round 1 - buff
|
||||
game.move.select(Moves.NASTY_PLOT);
|
||||
await game.toNextTurn();
|
||||
|
||||
let playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
expect(playerPokemon.getStatStage(Stat.SPATK)).toEqual(2);
|
||||
|
||||
// round 2 - baton pass
|
||||
game.move.select(Moves.BATON_PASS);
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
// assert
|
||||
playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
expect(playerPokemon.species.speciesId).toEqual(Species.SHUCKLE);
|
||||
expect(playerPokemon.getStatStage(Stat.SPATK)).toEqual(2);
|
||||
}, 20000);
|
||||
|
||||
it("passes stat stage buffs when AI uses it", async () => {
|
||||
// arrange
|
||||
game.override.startingWave(5).enemyMoveset(new Array(4).fill([Moves.NASTY_PLOT]));
|
||||
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]);
|
||||
|
||||
// round 1 - ai buffs
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
// round 2 - baton pass
|
||||
game.scene.getEnemyPokemon()!.hp = 100;
|
||||
game.override.enemyMoveset([Moves.BATON_PASS]);
|
||||
// Force moveset to update mid-battle
|
||||
// TODO: replace with enemy ai control function when it's added
|
||||
game.scene.getEnemyParty()[0].getMoveset();
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.phaseInterceptor.to("PostSummonPhase", false);
|
||||
|
||||
// assert
|
||||
// check buffs are still there
|
||||
expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.SPATK)).toEqual(2);
|
||||
// confirm that a switch actually happened. can't use species because I
|
||||
// can't find a way to override trainer parties with more than 1 pokemon species
|
||||
expect(game.scene.getEnemyPokemon()!.hp).not.toEqual(100);
|
||||
expect(game.phaseInterceptor.log.slice(-4)).toEqual([
|
||||
"MoveEffectPhase",
|
||||
"SwitchSummonPhase",
|
||||
"SummonPhase",
|
||||
"PostSummonPhase",
|
||||
]);
|
||||
}, 20000);
|
||||
|
||||
it("doesn't transfer effects that aren't transferrable", async () => {
|
||||
game.override.enemyMoveset([Moves.SALT_CURE]);
|
||||
await game.classicMode.startBattle([Species.PIKACHU, Species.FEEBAS]);
|
||||
|
||||
const [player1, player2] = game.scene.getPlayerParty();
|
||||
|
||||
game.move.select(Moves.BATON_PASS);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
expect(player1.findTag(t => t.tagType === BattlerTagType.SALT_CURED)).toBeTruthy();
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(player2.findTag(t => t.tagType === BattlerTagType.SALT_CURED)).toBeUndefined();
|
||||
}, 20000);
|
||||
|
||||
it("doesn't allow binding effects from the user to persist", async () => {
|
||||
game.override.moveset([Moves.FIRE_SPIN, Moves.BATON_PASS]);
|
||||
|
||||
await game.classicMode.startBattle([Species.MAGIKARP, Species.FEEBAS]);
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.FIRE_SPIN);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.move.forceHit();
|
||||
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemy.getTag(BattlerTagType.FIRE_SPIN)).toBeDefined();
|
||||
|
||||
game.move.select(Moves.BATON_PASS);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemy.getTag(BattlerTagType.FIRE_SPIN)).toBeUndefined();
|
||||
});
|
||||
});
|
@ -1,340 +0,0 @@
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { allMoves } from "#app/data/moves/move";
|
||||
import { Challenges } from "#enums/challenges";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { BattleType } from "#enums/battle-type";
|
||||
import { TrainerSlot } from "#enums/trainer-slot";
|
||||
import { TrainerType } from "#enums/trainer-type";
|
||||
import { splitArray } from "#app/utils/common";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
|
||||
describe("Moves - Dragon Tail", () => {
|
||||
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
|
||||
.battleStyle("single")
|
||||
.moveset([Moves.DRAGON_TAIL, Moves.SPLASH, Moves.FLAMETHROWER])
|
||||
.enemySpecies(Species.WAILORD)
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
.startingLevel(5)
|
||||
.enemyLevel(5);
|
||||
|
||||
vi.spyOn(allMoves[Moves.DRAGON_TAIL], "accuracy", "get").mockReturnValue(100);
|
||||
});
|
||||
|
||||
it("should cause opponent to flee, display ability, and not crash", async () => {
|
||||
game.override.enemyAbility(Abilities.ROUGH_SKIN);
|
||||
await game.classicMode.startBattle([Species.DRATINI]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.DRAGON_TAIL);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
const isVisible = enemyPokemon.visible;
|
||||
const hasFled = enemyPokemon.switchOutStatus;
|
||||
expect(isVisible).toBe(false);
|
||||
expect(hasFled).toBe(true);
|
||||
expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp());
|
||||
});
|
||||
|
||||
it("should proceed without crashing in a double battle", async () => {
|
||||
game.override.battleStyle("double").enemyMoveset(Moves.SPLASH).enemyAbility(Abilities.ROUGH_SKIN);
|
||||
await game.classicMode.startBattle([Species.DRATINI, Species.DRATINI, Species.WAILORD, Species.WAILORD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerParty()[0]!;
|
||||
|
||||
const enemyLeadPokemon = game.scene.getEnemyParty()[0]!;
|
||||
const enemySecPokemon = game.scene.getEnemyParty()[1]!;
|
||||
|
||||
game.move.select(Moves.DRAGON_TAIL, 0, BattlerIndex.ENEMY);
|
||||
game.move.select(Moves.SPLASH, 1);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
const isVisibleLead = enemyLeadPokemon.visible;
|
||||
const hasFledLead = enemyLeadPokemon.switchOutStatus;
|
||||
const isVisibleSec = enemySecPokemon.visible;
|
||||
const hasFledSec = enemySecPokemon.switchOutStatus;
|
||||
expect(!isVisibleLead && hasFledLead && isVisibleSec && !hasFledSec).toBe(true);
|
||||
expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp());
|
||||
|
||||
// second turn
|
||||
game.move.select(Moves.FLAMETHROWER, 0, BattlerIndex.ENEMY_2);
|
||||
game.move.select(Moves.SPLASH, 1);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
expect(enemySecPokemon.hp).toBeLessThan(enemySecPokemon.getMaxHp());
|
||||
});
|
||||
|
||||
it("should force trainers to switch randomly without selecting from a partner's party", async () => {
|
||||
game.override
|
||||
.battleStyle("double")
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
.enemyAbility(Abilities.STURDY)
|
||||
.battleType(BattleType.TRAINER)
|
||||
.randomTrainer({ trainerType: TrainerType.TATE, alwaysDouble: true })
|
||||
.enemySpecies(0);
|
||||
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRANITAR]);
|
||||
|
||||
// Grab each trainer's pokemon based on species name
|
||||
const [tateParty, lizaParty] = splitArray(
|
||||
game.scene.getEnemyParty(),
|
||||
pkmn => pkmn.trainerSlot === TrainerSlot.TRAINER,
|
||||
).map(a => a.map(p => p.species.name));
|
||||
|
||||
expect(tateParty).not.toEqual(lizaParty);
|
||||
|
||||
// Force enemy trainers to switch to the first mon available.
|
||||
// Due to how enemy trainer parties are laid out, this prevents false positives
|
||||
// as Tate's pokemon are placed immediately before Liza's corresponding members.
|
||||
vi.fn(Phaser.Math.RND.integerInRange).mockImplementation(min => min);
|
||||
|
||||
// Spy on the function responsible for making informed switches
|
||||
const choiceSwitchSpy = vi.spyOn(game.scene.currentBattle.trainer!, "getNextSummonIndex");
|
||||
|
||||
game.move.select(Moves.DRAGON_TAIL, BattlerIndex.PLAYER, BattlerIndex.ENEMY_2);
|
||||
game.move.select(Moves.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
const [tatePartyNew, lizaPartyNew] = splitArray(
|
||||
game.scene.getEnemyParty(),
|
||||
pkmn => pkmn.trainerSlot === TrainerSlot.TRAINER,
|
||||
).map(a => a.map(p => p.species.name));
|
||||
|
||||
// Forced switch move should have switched Liza's Pokemon with another one of her own at random
|
||||
expect(tatePartyNew).toEqual(tateParty);
|
||||
expect(lizaPartyNew).not.toEqual(lizaParty);
|
||||
expect(choiceSwitchSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should redirect targets upon opponent flee", async () => {
|
||||
game.override.battleStyle("double").enemyMoveset(Moves.SPLASH).enemyAbility(Abilities.ROUGH_SKIN);
|
||||
await game.classicMode.startBattle([Species.DRATINI, Species.DRATINI, Species.WAILORD, Species.WAILORD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerParty()[0]!;
|
||||
const secPokemon = game.scene.getPlayerParty()[1]!;
|
||||
|
||||
const enemyLeadPokemon = game.scene.getEnemyParty()[0]!;
|
||||
const enemySecPokemon = game.scene.getEnemyParty()[1]!;
|
||||
|
||||
game.move.select(Moves.DRAGON_TAIL, 0, BattlerIndex.ENEMY);
|
||||
// target the same pokemon, second move should be redirected after first flees
|
||||
game.move.select(Moves.DRAGON_TAIL, 1, BattlerIndex.ENEMY);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
const isVisibleLead = enemyLeadPokemon.visible;
|
||||
const hasFledLead = enemyLeadPokemon.switchOutStatus;
|
||||
const isVisibleSec = enemySecPokemon.visible;
|
||||
const hasFledSec = enemySecPokemon.switchOutStatus;
|
||||
expect(!isVisibleLead && hasFledLead && !isVisibleSec && hasFledSec).toBe(true);
|
||||
expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp());
|
||||
expect(secPokemon.hp).toBeLessThan(secPokemon.getMaxHp());
|
||||
expect(enemyLeadPokemon.hp).toBeLessThan(enemyLeadPokemon.getMaxHp());
|
||||
expect(enemySecPokemon.hp).toBeLessThan(enemySecPokemon.getMaxHp());
|
||||
});
|
||||
|
||||
it("should not switch out a target with suction cups, unless the user has Mold Breaker", async () => {
|
||||
game.override.enemyAbility(Abilities.SUCTION_CUPS);
|
||||
await game.classicMode.startBattle([Species.REGIELEKI]);
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.DRAGON_TAIL);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(enemy.isOnField()).toBe(true);
|
||||
expect(enemy.isFullHp()).toBe(false);
|
||||
|
||||
// Turn 2: Mold Breaker should ignore switch blocking ability and switch out the target
|
||||
game.override.ability(Abilities.MOLD_BREAKER);
|
||||
enemy.hp = enemy.getMaxHp();
|
||||
|
||||
game.move.select(Moves.DRAGON_TAIL);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(enemy.isOnField()).toBe(false);
|
||||
expect(enemy.isFullHp()).toBe(false);
|
||||
});
|
||||
|
||||
it("should not switch out a Commanded Dondozo", async () => {
|
||||
game.override.battleStyle("double").enemySpecies(Species.DONDOZO);
|
||||
await game.classicMode.startBattle([Species.REGIELEKI]);
|
||||
|
||||
// pretend dondozo 2 commanded dondozo 1 (silly I know, but it works)
|
||||
const [dondozo1, dondozo2] = game.scene.getEnemyField();
|
||||
dondozo1.addTag(BattlerTagType.COMMANDED, 1, Moves.NONE, dondozo2.id);
|
||||
|
||||
game.move.select(Moves.DRAGON_TAIL);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(dondozo1.isOnField()).toBe(true);
|
||||
expect(dondozo1.isFullHp()).toBe(false);
|
||||
});
|
||||
|
||||
it("should force a switch upon fainting an opponent normally", async () => {
|
||||
game.override.startingWave(5).startingLevel(1000); // To make sure Dragon Tail KO's the opponent
|
||||
await game.classicMode.startBattle([Species.DRATINI]);
|
||||
|
||||
game.move.select(Moves.DRAGON_TAIL);
|
||||
|
||||
await game.toNextTurn();
|
||||
|
||||
// Make sure the enemy switched to a healthy Pokemon
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
expect(enemy).toBeDefined();
|
||||
expect(enemy.isFullHp()).toBe(true);
|
||||
|
||||
// Make sure the enemy has a fainted Pokemon in their party and not on the field
|
||||
const faintedEnemy = game.scene.getEnemyParty().find(p => !p.isAllowedInBattle());
|
||||
expect(faintedEnemy).toBeDefined();
|
||||
expect(game.scene.getEnemyField().length).toBe(1);
|
||||
});
|
||||
|
||||
it("should neither switch nor softlock when activating an opponent's reviver seed", async () => {
|
||||
game.override
|
||||
.battleType(BattleType.TRAINER)
|
||||
.enemyHeldItems([{ name: "REVIVER_SEED" }])
|
||||
.startingLevel(1000); // make sure Dragon Tail KO's the opponent
|
||||
await game.classicMode.startBattle([Species.DRATINI]);
|
||||
|
||||
const [wailord1, wailord2] = game.scene.getEnemyParty()!;
|
||||
expect(wailord1).toBeDefined();
|
||||
expect(wailord2).toBeDefined();
|
||||
|
||||
game.move.select(Moves.DRAGON_TAIL);
|
||||
await game.toNextTurn();
|
||||
|
||||
// Wailord should have consumed the reviver seed and stayed on field
|
||||
expect(wailord1.isOnField()).toBe(true);
|
||||
expect(wailord1.getHpRatio()).toBeCloseTo(0.5);
|
||||
expect(wailord1.getHeldItems()).toHaveLength(0);
|
||||
expect(wailord2.isOnField()).toBe(false);
|
||||
});
|
||||
|
||||
it("should neither switch nor softlock when activating a player's reviver seed", async () => {
|
||||
game.override
|
||||
.startingHeldItems([{ name: "REVIVER_SEED" }])
|
||||
.enemyMoveset(Moves.DRAGON_TAIL)
|
||||
.enemyLevel(1000); // make sure Dragon Tail KO's the player
|
||||
await game.classicMode.startBattle([Species.BLISSEY, Species.BULBASAUR]);
|
||||
|
||||
const [blissey, bulbasaur] = game.scene.getPlayerParty();
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
// dratini should have consumed the reviver seed and stayed on field
|
||||
expect(blissey.isOnField()).toBe(true);
|
||||
expect(blissey.getHpRatio()).toBeCloseTo(0.5);
|
||||
expect(blissey.getHeldItems()).toHaveLength(0);
|
||||
expect(bulbasaur.isOnField()).toBe(false);
|
||||
});
|
||||
|
||||
it("should force switches to a random off-field pokemon", async () => {
|
||||
game.override.enemyMoveset(Moves.DRAGON_TAIL).startingLevel(100).enemyLevel(1);
|
||||
await game.classicMode.startBattle([Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE]);
|
||||
|
||||
const [bulbasaur, charmander, squirtle] = game.scene.getPlayerParty();
|
||||
|
||||
// Turn 1: Mock an RNG call that calls for switching to 1st backup Pokemon (Charmander)
|
||||
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((_range, min = 0) => {
|
||||
return min;
|
||||
});
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.DRAGON_TAIL);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(bulbasaur.isOnField()).toBe(false);
|
||||
expect(charmander.isOnField()).toBe(true);
|
||||
expect(squirtle.isOnField()).toBe(false);
|
||||
expect(bulbasaur.getInverseHp()).toBeGreaterThan(0);
|
||||
|
||||
// Turn 2: Mock an RNG call that calls for switching to 2nd backup Pokemon (Squirtle)
|
||||
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((_range, min = 0) => {
|
||||
return min + 1;
|
||||
});
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(bulbasaur.isOnField()).toBe(false);
|
||||
expect(charmander.isOnField()).toBe(false);
|
||||
expect(squirtle.isOnField()).toBe(true);
|
||||
expect(charmander.getInverseHp()).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should not force a switch to a fainted or challenge-ineligible Pokemon", async () => {
|
||||
game.override.enemyMoveset(Moves.DRAGON_TAIL).startingLevel(100).enemyLevel(1);
|
||||
// Mono-Water challenge, Eevee is ineligible
|
||||
game.challengeMode.addChallenge(Challenges.SINGLE_TYPE, PokemonType.WATER + 1, 0);
|
||||
await game.challengeMode.startBattle([Species.LAPRAS, Species.EEVEE, Species.TOXAPEX, Species.PRIMARINA]);
|
||||
|
||||
const [lapras, eevee, toxapex, primarina] = game.scene.getPlayerParty();
|
||||
expect(toxapex).toBeDefined();
|
||||
toxapex.hp = 0;
|
||||
|
||||
// Mock an RNG call to switch to the first eligible pokemon.
|
||||
// Eevee is ineligible and Toxapex is fainted, so it should proc on Primarina instead
|
||||
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((_range, min = 0) => {
|
||||
return min;
|
||||
});
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(lapras.isOnField()).toBe(false);
|
||||
expect(eevee.isOnField()).toBe(false);
|
||||
expect(toxapex.isOnField()).toBe(false);
|
||||
expect(primarina.isOnField()).toBe(true);
|
||||
expect(lapras.getInverseHp()).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should deal damage without switching if there are no available backup Pokemon to switch into", async () => {
|
||||
game.override.enemyMoveset(Moves.DRAGON_TAIL).battleStyle("double").startingLevel(100).enemyLevel(1);
|
||||
// Mono-Water challenge
|
||||
game.challengeMode.addChallenge(Challenges.SINGLE_TYPE, PokemonType.WATER + 1, 0);
|
||||
await game.challengeMode.startBattle([Species.LAPRAS, Species.KYOGRE, Species.EEVEE, Species.CLOYSTER]);
|
||||
|
||||
const [lapras, kyogre, eevee, cloyster] = game.scene.getPlayerParty();
|
||||
expect(cloyster).toBeDefined();
|
||||
|
||||
game.move.select(Moves.SPLASH, BattlerIndex.PLAYER);
|
||||
game.move.select(Moves.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.forceEnemyMove(Moves.DRAGON_TAIL, BattlerIndex.PLAYER);
|
||||
await game.forceEnemyMove(Moves.DRAGON_TAIL, BattlerIndex.PLAYER_2);
|
||||
await game.killPokemon(kyogre);
|
||||
game.doSelectPartyPokemon(3);
|
||||
await game.toNextTurn();
|
||||
|
||||
// Eevee is ineligble due to challenge and kyogre is fainted, leaving no backup pokemon able to switch in
|
||||
expect(lapras.isOnField()).toBe(true);
|
||||
expect(kyogre.isOnField()).toBe(false);
|
||||
expect(eevee.isOnField()).toBe(false);
|
||||
expect(cloyster.isOnField()).toBe(true);
|
||||
expect(lapras.getInverseHp()).toBeGreaterThan(0);
|
||||
expect(kyogre.getInverseHp()).toBeGreaterThan(0);
|
||||
expect(game.scene.getBackupPartyMemberIndices(true)).toHaveLength(0);
|
||||
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
|
||||
});
|
||||
});
|
718
test/moves/force-switch.test.ts
Normal file
718
test/moves/force-switch.test.ts
Normal file
@ -0,0 +1,718 @@
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { Challenges } from "#enums/challenges";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { BattleType } from "#enums/battle-type";
|
||||
import { TrainerSlot } from "#enums/trainer-slot";
|
||||
import { TrainerType } from "#enums/trainer-type";
|
||||
import { splitArray } from "#app/utils/common";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { MoveResult } from "#app/field/pokemon";
|
||||
import { SubstituteTag } from "#app/data/battler-tags";
|
||||
|
||||
describe("Moves - Switching Moves", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
describe("Target Switch Moves", () => {
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.battleStyle("single")
|
||||
.ability(Abilities.NO_GUARD)
|
||||
.moveset([Moves.DRAGON_TAIL, Moves.SPLASH, Moves.FLAMETHROWER])
|
||||
.enemySpecies(Species.WAILORD)
|
||||
.enemyMoveset(Moves.SPLASH);
|
||||
});
|
||||
|
||||
it("should force switches to a random off-field pokemon", async () => {
|
||||
game.override.enemyMoveset(Moves.DRAGON_TAIL).startingLevel(100).enemyLevel(1);
|
||||
await game.classicMode.startBattle([Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE]);
|
||||
|
||||
const [bulbasaur, charmander, squirtle] = game.scene.getPlayerParty();
|
||||
|
||||
// Turn 1: Mock an RNG call that calls for switching to 1st backup Pokemon (Charmander)
|
||||
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((_range, min = 0) => {
|
||||
return min;
|
||||
});
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.DRAGON_TAIL);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(bulbasaur.isOnField()).toBe(false);
|
||||
expect(charmander.isOnField()).toBe(true);
|
||||
expect(squirtle.isOnField()).toBe(false);
|
||||
expect(bulbasaur.getInverseHp()).toBeGreaterThan(0);
|
||||
|
||||
// Turn 2: Mock an RNG call that calls for switching to 2nd backup Pokemon (Squirtle)
|
||||
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((_range, min = 0) => {
|
||||
return min + 1;
|
||||
});
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(bulbasaur.isOnField()).toBe(false);
|
||||
expect(charmander.isOnField()).toBe(false);
|
||||
expect(squirtle.isOnField()).toBe(true);
|
||||
expect(charmander.getInverseHp()).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should force trainers to switch randomly without selecting from a partner's party", async () => {
|
||||
game.override
|
||||
.battleStyle("double")
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
.enemyAbility(Abilities.STURDY)
|
||||
.battleType(BattleType.TRAINER)
|
||||
.randomTrainer({ trainerType: TrainerType.TATE, alwaysDouble: true })
|
||||
.enemySpecies(0);
|
||||
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRANITAR]);
|
||||
|
||||
// Grab each trainer's pokemon based on species name
|
||||
const [tateParty, lizaParty] = splitArray(
|
||||
game.scene.getEnemyParty(),
|
||||
pkmn => pkmn.trainerSlot === TrainerSlot.TRAINER,
|
||||
).map(a => a.map(p => p.species.name));
|
||||
|
||||
expect(tateParty).not.toEqual(lizaParty);
|
||||
|
||||
// Force enemy trainers to switch to the first mon available.
|
||||
// Due to how enemy trainer parties are laid out, this prevents false positives
|
||||
// as Tate's pokemon are placed immediately before Liza's corresponding members.
|
||||
vi.fn(Phaser.Math.RND.integerInRange).mockImplementation(min => min);
|
||||
|
||||
// Spy on the function responsible for making informed switches
|
||||
const choiceSwitchSpy = vi.spyOn(game.scene.currentBattle.trainer!, "getNextSummonIndex");
|
||||
|
||||
game.move.select(Moves.DRAGON_TAIL, BattlerIndex.PLAYER, BattlerIndex.ENEMY_2);
|
||||
game.move.select(Moves.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
const [tatePartyNew, lizaPartyNew] = splitArray(
|
||||
game.scene.getEnemyParty(),
|
||||
pkmn => pkmn.trainerSlot === TrainerSlot.TRAINER,
|
||||
).map(a => a.map(p => p.species.name));
|
||||
|
||||
// Forced switch move should have switched Liza's Pokemon with another one of her own at random
|
||||
expect(tatePartyNew).toEqual(tateParty);
|
||||
expect(lizaPartyNew).not.toEqual(lizaParty);
|
||||
expect(choiceSwitchSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should force wild Pokemon to flee and redirect moves accordingly", async () => {
|
||||
game.override.battleStyle("double").enemyMoveset(Moves.SPLASH).enemyAbility(Abilities.ROUGH_SKIN);
|
||||
await game.classicMode.startBattle([Species.DRATINI, Species.DRATINI, Species.WAILORD, Species.WAILORD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerParty()[0]!;
|
||||
const secPokemon = game.scene.getPlayerParty()[1]!;
|
||||
|
||||
const enemyLeadPokemon = game.scene.getEnemyParty()[0]!;
|
||||
const enemySecPokemon = game.scene.getEnemyParty()[1]!;
|
||||
|
||||
game.move.select(Moves.DRAGON_TAIL, 0, BattlerIndex.ENEMY);
|
||||
// target the same pokemon, second move should be redirected after first flees
|
||||
game.move.select(Moves.DRAGON_TAIL, 1, BattlerIndex.ENEMY);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
const isVisibleLead = enemyLeadPokemon.visible;
|
||||
const hasFledLead = enemyLeadPokemon.switchOutStatus;
|
||||
const isVisibleSec = enemySecPokemon.visible;
|
||||
const hasFledSec = enemySecPokemon.switchOutStatus;
|
||||
expect(!isVisibleLead && hasFledLead && !isVisibleSec && hasFledSec).toBe(true);
|
||||
expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp());
|
||||
expect(secPokemon.hp).toBeLessThan(secPokemon.getMaxHp());
|
||||
expect(enemyLeadPokemon.hp).toBeLessThan(enemyLeadPokemon.getMaxHp());
|
||||
expect(enemySecPokemon.hp).toBeLessThan(enemySecPokemon.getMaxHp());
|
||||
});
|
||||
|
||||
it("should not switch out a target with suction cups, unless the user has Mold Breaker", async () => {
|
||||
game.override.enemyAbility(Abilities.SUCTION_CUPS);
|
||||
await game.classicMode.startBattle([Species.REGIELEKI]);
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.DRAGON_TAIL);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(enemy.isOnField()).toBe(true);
|
||||
expect(enemy.isFullHp()).toBe(false);
|
||||
|
||||
// Turn 2: Mold Breaker should ignore switch blocking ability and switch out the target
|
||||
game.override.ability(Abilities.MOLD_BREAKER);
|
||||
enemy.hp = enemy.getMaxHp();
|
||||
|
||||
game.move.select(Moves.DRAGON_TAIL);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(enemy.isOnField()).toBe(false);
|
||||
expect(enemy.isFullHp()).toBe(false);
|
||||
});
|
||||
|
||||
it("should not switch out a Commanded Dondozo", async () => {
|
||||
game.override.battleStyle("double").enemySpecies(Species.DONDOZO);
|
||||
await game.classicMode.startBattle([Species.REGIELEKI]);
|
||||
|
||||
// pretend dondozo 2 commanded dondozo 1 (silly I know, but it works)
|
||||
const [dondozo1, dondozo2] = game.scene.getEnemyField();
|
||||
dondozo1.addTag(BattlerTagType.COMMANDED, 1, Moves.NONE, dondozo2.id);
|
||||
|
||||
game.move.select(Moves.DRAGON_TAIL);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(dondozo1.isOnField()).toBe(true);
|
||||
expect(dondozo1.isFullHp()).toBe(false);
|
||||
});
|
||||
|
||||
it("should force a switch upon fainting an opponent normally", async () => {
|
||||
game.override.startingWave(5).startingLevel(1000); // To make sure Dragon Tail KO's the opponent
|
||||
await game.classicMode.startBattle([Species.DRATINI]);
|
||||
|
||||
game.move.select(Moves.DRAGON_TAIL);
|
||||
|
||||
await game.toNextTurn();
|
||||
|
||||
// Make sure the enemy switched to a healthy Pokemon
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
expect(enemy).toBeDefined();
|
||||
expect(enemy.isFullHp()).toBe(true);
|
||||
|
||||
// Make sure the enemy has a fainted Pokemon in their party and not on the field
|
||||
const faintedEnemy = game.scene.getEnemyParty().find(p => !p.isAllowedInBattle());
|
||||
expect(faintedEnemy).toBeDefined();
|
||||
expect(game.scene.getEnemyField().length).toBe(1);
|
||||
});
|
||||
|
||||
it("should neither switch nor softlock when activating an opponent's reviver seed", async () => {
|
||||
game.override
|
||||
.battleType(BattleType.TRAINER)
|
||||
.enemyHeldItems([{ name: "REVIVER_SEED" }])
|
||||
.startingLevel(1000); // make sure Dragon Tail KO's the opponent
|
||||
await game.classicMode.startBattle([Species.DRATINI]);
|
||||
|
||||
const [wailord1, wailord2] = game.scene.getEnemyParty()!;
|
||||
expect(wailord1).toBeDefined();
|
||||
expect(wailord2).toBeDefined();
|
||||
|
||||
game.move.select(Moves.DRAGON_TAIL);
|
||||
await game.toNextTurn();
|
||||
|
||||
// Wailord should have consumed the reviver seed and stayed on field
|
||||
expect(wailord1.isOnField()).toBe(true);
|
||||
expect(wailord1.getHpRatio()).toBeCloseTo(0.5);
|
||||
expect(wailord1.getHeldItems()).toHaveLength(0);
|
||||
expect(wailord2.isOnField()).toBe(false);
|
||||
});
|
||||
|
||||
it("should neither switch nor softlock when activating a player's reviver seed", async () => {
|
||||
game.override
|
||||
.startingHeldItems([{ name: "REVIVER_SEED" }])
|
||||
.enemyMoveset(Moves.DRAGON_TAIL)
|
||||
.enemyLevel(1000); // make sure Dragon Tail KO's the player
|
||||
await game.classicMode.startBattle([Species.BLISSEY, Species.BULBASAUR]);
|
||||
|
||||
const [blissey, bulbasaur] = game.scene.getPlayerParty();
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
// dratini should have consumed the reviver seed and stayed on field
|
||||
expect(blissey.isOnField()).toBe(true);
|
||||
expect(blissey.getHpRatio()).toBeCloseTo(0.5);
|
||||
expect(blissey.getHeldItems()).toHaveLength(0);
|
||||
expect(bulbasaur.isOnField()).toBe(false);
|
||||
});
|
||||
|
||||
it("should not force a switch to a fainted or challenge-ineligible Pokemon", async () => {
|
||||
game.override.enemyMoveset(Moves.DRAGON_TAIL).startingLevel(100).enemyLevel(1);
|
||||
// Mono-Water challenge, Eevee is ineligible
|
||||
game.challengeMode.addChallenge(Challenges.SINGLE_TYPE, PokemonType.WATER + 1, 0);
|
||||
await game.challengeMode.startBattle([Species.LAPRAS, Species.EEVEE, Species.TOXAPEX, Species.PRIMARINA]);
|
||||
|
||||
const [lapras, eevee, toxapex, primarina] = game.scene.getPlayerParty();
|
||||
expect(toxapex).toBeDefined();
|
||||
toxapex.hp = 0;
|
||||
|
||||
// Mock an RNG call to switch to the first eligible pokemon.
|
||||
// Eevee is ineligible and Toxapex is fainted, so it should proc on Primarina instead
|
||||
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((_range, min = 0) => {
|
||||
return min;
|
||||
});
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(lapras.isOnField()).toBe(false);
|
||||
expect(eevee.isOnField()).toBe(false);
|
||||
expect(toxapex.isOnField()).toBe(false);
|
||||
expect(primarina.isOnField()).toBe(true);
|
||||
expect(lapras.getInverseHp()).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// TODO: This is not implemented yet (needs locales)
|
||||
it.todo.each<{ name: string; move: Moves }>([
|
||||
{ name: "Whirlwind", move: Moves.WHIRLWIND },
|
||||
{ name: "Roar", move: Moves.ROAR },
|
||||
{ name: "Dragon Tail", move: Moves.DRAGON_TAIL },
|
||||
{ name: "Circle Throw", move: Moves.CIRCLE_THROW },
|
||||
])("should display custom text for forced switch outs", async ({ move }) => {
|
||||
game.override.moveset(move).battleType(BattleType.TRAINER);
|
||||
await game.classicMode.startBattle([Species.BLISSEY, Species.BULBASAUR]);
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
game.move.select(move);
|
||||
await game.toNextTurn();
|
||||
|
||||
const newEnemy = game.scene.getEnemyPokemon()!;
|
||||
expect(newEnemy).not.toBe(enemy);
|
||||
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
|
||||
expect(game.textInterceptor.logs).not.toContain(
|
||||
i18next.t("battle:trainerGo", {
|
||||
trainerName: game.scene.currentBattle.trainer?.getName(newEnemy.trainerSlot),
|
||||
pokemonName: newEnemy.getNameToRender(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Self-Switch Attack Moves", () => {
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.battleStyle("single")
|
||||
.enemySpecies(Species.GENGAR)
|
||||
.moveset(Moves.U_TURN)
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
.disableCrits();
|
||||
});
|
||||
|
||||
it("triggers rough skin on the u-turn user before a new pokemon is switched in", async () => {
|
||||
game.override.enemyAbility(Abilities.ROUGH_SKIN);
|
||||
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]);
|
||||
|
||||
const raichu = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(Moves.U_TURN);
|
||||
game.doSelectPartyPokemon(1);
|
||||
// advance to the phase for picking party members to send out
|
||||
await game.phaseInterceptor.to("SwitchPhase", false);
|
||||
|
||||
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
expect(player).toBe(raichu);
|
||||
expect(player.isFullHp()).toBe(false);
|
||||
expect(game.scene.getEnemyPokemon()!.waveData.abilityRevealed).toBe(true); // proxy for asserting ability activated
|
||||
});
|
||||
|
||||
it("still forces a switch if u-turn KO's the opponent", async () => {
|
||||
game.override.startingLevel(1000);
|
||||
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]);
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
// KO the opponent with U-Turn
|
||||
game.move.select(Moves.U_TURN);
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(enemy.isFainted()).toBe(true);
|
||||
|
||||
// Check that U-Turn forced a switch
|
||||
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
|
||||
expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.SHUCKLE);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Failure Checks", () => {
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override.battleStyle("single").enemySpecies(Species.GENGAR).disableCrits().enemyAbility(Abilities.STURDY);
|
||||
});
|
||||
|
||||
it.each<{ name: string; move: Moves }>([
|
||||
{ name: "U-Turn", move: Moves.U_TURN },
|
||||
{ name: "Flip Turn", move: Moves.FLIP_TURN },
|
||||
{ name: "Volt Switch", move: Moves.VOLT_SWITCH },
|
||||
{ name: "Baton Pass", move: Moves.BATON_PASS },
|
||||
{ name: "Shed Tail", move: Moves.SHED_TAIL },
|
||||
{ name: "Parting Shot", move: Moves.PARTING_SHOT },
|
||||
])("$name should not allow wild pokemon to flee", async ({ move }) => {
|
||||
game.override.moveset(Moves.SPLASH).enemyMoveset(move);
|
||||
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]);
|
||||
|
||||
// reset species override so we get a different species
|
||||
game.override.enemySpecies(Species.ARBOK);
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
game.doSelectPartyPokemon(1);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
expect(player.species.speciesId).toBe(Species.SHUCKLE);
|
||||
expect(player.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
|
||||
expect(game.phaseInterceptor.log).not.toContain("BattleEndPhase");
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
expect(enemy.switchOutStatus).toBe(false);
|
||||
expect(enemy.species.speciesId).toBe(Species.GENGAR);
|
||||
});
|
||||
|
||||
it.each<{ name: string; move: Moves }>([
|
||||
{ name: "Teleport", move: Moves.TELEPORT },
|
||||
{ name: "Whirlwind", move: Moves.WHIRLWIND },
|
||||
{ name: "Roar", move: Moves.ROAR },
|
||||
{ name: "Dragon Tail", move: Moves.DRAGON_TAIL },
|
||||
{ name: "Circle Throw", move: Moves.CIRCLE_THROW },
|
||||
])("$name should allow wild pokemon to flee", async ({ move }) => {
|
||||
game.override.moveset(move).enemyMoveset(move);
|
||||
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]);
|
||||
|
||||
const gengar = game.scene.getEnemyPokemon();
|
||||
game.move.select(move);
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(game.phaseInterceptor.log).not.toContain("BattleEndPhase");
|
||||
expect(game.scene.getEnemyPokemon()).toBe(gengar);
|
||||
});
|
||||
|
||||
it.each<{ name: string; move?: Moves; enemyMove?: Moves }>([
|
||||
{ name: "U-Turn", move: Moves.U_TURN },
|
||||
{ name: "Flip Turn", move: Moves.FLIP_TURN },
|
||||
{ name: "Volt Switch", move: Moves.VOLT_SWITCH },
|
||||
// TODO: Enable once Parting shot is fixed
|
||||
// {name: "Parting Shot", move: Moves.PARTING_SHOT},
|
||||
{ name: "Dragon Tail", enemyMove: Moves.DRAGON_TAIL },
|
||||
{ name: "Circle Throw", enemyMove: Moves.CIRCLE_THROW },
|
||||
])(
|
||||
"$name should not fail if no valid switch out target is found",
|
||||
async ({ move = Moves.SPLASH, enemyMove = Moves.SPLASH }) => {
|
||||
game.override.moveset(move).enemyMoveset(enemyMove);
|
||||
await game.classicMode.startBattle([Species.RAICHU]);
|
||||
|
||||
game.move.select(move);
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
|
||||
expect(game.scene.getPlayerPokemon()?.getLastXMoves()[0].result).toBe(MoveResult.MISS);
|
||||
},
|
||||
);
|
||||
|
||||
it.each<{ name: string; move?: Moves; enemyMove?: Moves }>([
|
||||
{ name: "Teleport", move: Moves.TELEPORT },
|
||||
{ name: "Baton Pass", move: Moves.BATON_PASS },
|
||||
{ name: "Shed Tail", move: Moves.SHED_TAIL },
|
||||
{ name: "Roar", enemyMove: Moves.ROAR },
|
||||
{ name: "Whirlwind", enemyMove: Moves.WHIRLWIND },
|
||||
])(
|
||||
"$name should fail if no valid switch out target is found",
|
||||
async ({ move = Moves.SPLASH, enemyMove = Moves.SPLASH }) => {
|
||||
game.override.moveset(move).enemyMoveset(enemyMove);
|
||||
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]);
|
||||
|
||||
// reset species override so we get a different species
|
||||
game.override.enemySpecies(Species.ARBOK);
|
||||
|
||||
game.move.select(move);
|
||||
game.doSelectPartyPokemon(1);
|
||||
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(game.phaseInterceptor.log).not.toContain("BattleEndPhase");
|
||||
expect(game.scene.getEnemyPokemon()!.species.speciesId).toBe(Species.GENGAR);
|
||||
},
|
||||
);
|
||||
|
||||
describe("Baton Pass", () => {
|
||||
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
|
||||
.battleStyle("single")
|
||||
.enemySpecies(Species.MAGIKARP)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.moveset([Moves.BATON_PASS, Moves.NASTY_PLOT, Moves.SPLASH, Moves.SUBSTITUTE])
|
||||
.ability(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
.disableCrits();
|
||||
});
|
||||
|
||||
it("should pass the user's stat stages and BattlerTags to an ally", async () => {
|
||||
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]);
|
||||
|
||||
game.move.select(Moves.NASTY_PLOT);
|
||||
await game.toNextTurn();
|
||||
|
||||
const [raichu, shuckle] = game.scene.getPlayerParty();
|
||||
expect(raichu.getStatStage(Stat.SPATK)).toEqual(2);
|
||||
|
||||
game.move.select(Moves.SUBSTITUTE);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(raichu.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined();
|
||||
|
||||
game.move.select(Moves.BATON_PASS);
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(game.scene.getPlayerPokemon()).toBe(shuckle);
|
||||
expect(shuckle.getStatStage(Stat.SPATK)).toEqual(2);
|
||||
expect(shuckle.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined();
|
||||
});
|
||||
|
||||
it("should pass stat stages when used by enemy trainers", async () => {
|
||||
game.override.battleType(BattleType.TRAINER).enemyMoveset([Moves.NASTY_PLOT, Moves.BATON_PASS]);
|
||||
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]);
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
// round 1 - ai buffs
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.NASTY_PLOT);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.BATON_PASS);
|
||||
await game.toNextTurn();
|
||||
|
||||
// check buffs are still there
|
||||
const newEnemy = game.scene.getEnemyPokemon()!;
|
||||
expect(newEnemy).not.toBe(enemy);
|
||||
expect(newEnemy.getStatStage(Stat.SPATK)).toBe(2);
|
||||
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
|
||||
});
|
||||
|
||||
it("should not transfer non-transferrable effects", async () => {
|
||||
game.override.enemyMoveset([Moves.SALT_CURE]);
|
||||
await game.classicMode.startBattle([Species.PIKACHU, Species.FEEBAS]);
|
||||
|
||||
const [player1, player2] = game.scene.getPlayerParty();
|
||||
|
||||
game.move.select(Moves.BATON_PASS);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
|
||||
// enemy salt cure
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
expect(player1.getTag(BattlerTagType.SALT_CURED)).toBeDefined();
|
||||
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(player1.isOnField()).toBe(false);
|
||||
expect(player2.isOnField()).toBe(true);
|
||||
expect(player2.getTag(BattlerTagType.SALT_CURED)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("removes the user's binding effects", async () => {
|
||||
game.override.moveset([Moves.FIRE_SPIN, Moves.BATON_PASS]);
|
||||
|
||||
await game.classicMode.startBattle([Species.MAGIKARP, Species.FEEBAS]);
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.FIRE_SPIN);
|
||||
await game.move.forceHit();
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemy.getTag(BattlerTagType.FIRE_SPIN)).toBeDefined();
|
||||
|
||||
game.move.select(Moves.BATON_PASS);
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemy.getTag(BattlerTagType.FIRE_SPIN)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Baton Pass", () => {
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.battleStyle("single")
|
||||
.enemySpecies(Species.MAGIKARP)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.moveset([Moves.BATON_PASS, Moves.NASTY_PLOT, Moves.SPLASH, Moves.SUBSTITUTE])
|
||||
.ability(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
.disableCrits();
|
||||
});
|
||||
|
||||
it("should pass the user's stat stages and BattlerTags to an ally", async () => {
|
||||
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]);
|
||||
|
||||
game.move.select(Moves.NASTY_PLOT);
|
||||
await game.toNextTurn();
|
||||
|
||||
const [raichu, shuckle] = game.scene.getPlayerParty();
|
||||
expect(raichu.getStatStage(Stat.SPATK)).toEqual(2);
|
||||
|
||||
game.move.select(Moves.SUBSTITUTE);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(raichu.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined();
|
||||
|
||||
game.move.select(Moves.BATON_PASS);
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(game.scene.getPlayerPokemon()).toBe(shuckle);
|
||||
expect(shuckle.getStatStage(Stat.SPATK)).toEqual(2);
|
||||
expect(shuckle.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined();
|
||||
});
|
||||
|
||||
it("should pass stat stages when used by enemy trainers", async () => {
|
||||
game.override.battleType(BattleType.TRAINER).enemyMoveset([Moves.NASTY_PLOT, Moves.BATON_PASS]);
|
||||
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]);
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
// round 1 - ai buffs
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.NASTY_PLOT);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.BATON_PASS);
|
||||
await game.toNextTurn();
|
||||
|
||||
// check buffs are still there
|
||||
const newEnemy = game.scene.getEnemyPokemon()!;
|
||||
expect(newEnemy).not.toBe(enemy);
|
||||
expect(newEnemy.getStatStage(Stat.SPATK)).toBe(2);
|
||||
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
|
||||
});
|
||||
|
||||
it("should not transfer non-transferrable effects", async () => {
|
||||
game.override.enemyMoveset([Moves.SALT_CURE]);
|
||||
await game.classicMode.startBattle([Species.PIKACHU, Species.FEEBAS]);
|
||||
|
||||
const [player1, player2] = game.scene.getPlayerParty();
|
||||
|
||||
game.move.select(Moves.BATON_PASS);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
|
||||
// enemy salt cure
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
expect(player1.getTag(BattlerTagType.SALT_CURED)).toBeDefined();
|
||||
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(player1.isOnField()).toBe(false);
|
||||
expect(player2.isOnField()).toBe(true);
|
||||
expect(player2.getTag(BattlerTagType.SALT_CURED)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("removes the user's binding effects", async () => {
|
||||
game.override.moveset([Moves.FIRE_SPIN, Moves.BATON_PASS]);
|
||||
|
||||
await game.classicMode.startBattle([Species.MAGIKARP, Species.FEEBAS]);
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.FIRE_SPIN);
|
||||
await game.move.forceHit();
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemy.getTag(BattlerTagType.FIRE_SPIN)).toBeDefined();
|
||||
|
||||
game.move.select(Moves.BATON_PASS);
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemy.getTag(BattlerTagType.FIRE_SPIN)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Shed Tail", () => {
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset(Moves.SHED_TAIL)
|
||||
.battleStyle("single")
|
||||
.enemySpecies(Species.SNORLAX)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(Moves.SPLASH);
|
||||
});
|
||||
|
||||
it("should consume 50% of the user's max HP (rounded up) to transfer a 25% HP Substitute doll", async () => {
|
||||
await game.classicMode.startBattle([Species.MAGIKARP, Species.FEEBAS]);
|
||||
|
||||
const magikarp = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(Moves.SHED_TAIL);
|
||||
game.doSelectPartyPokemon(1);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase", false);
|
||||
|
||||
const feebas = game.scene.getPlayerPokemon()!;
|
||||
expect(feebas).not.toBe(magikarp);
|
||||
expect(feebas.hp).toBe(feebas.getMaxHp());
|
||||
|
||||
const substituteTag = feebas.getTag(SubstituteTag)!;
|
||||
expect(substituteTag).toBeDefined();
|
||||
|
||||
// Note: Altered the test to be consistent with the correct HP cost :yipeevee_static:
|
||||
expect(magikarp.getInverseHp()).toBe(Math.ceil(magikarp.getMaxHp() / 2));
|
||||
expect(substituteTag.hp).toBe(Math.ceil(magikarp.getMaxHp() / 4));
|
||||
});
|
||||
|
||||
it("should fail if user's HP is insufficient", async () => {
|
||||
await game.classicMode.startBattle([Species.MAGIKARP, Species.FEEBAS]);
|
||||
|
||||
const magikarp = game.scene.getPlayerPokemon()!;
|
||||
magikarp.hp = Math.floor(magikarp.getMaxHp() / 2 - 1);
|
||||
|
||||
game.move.select(Moves.SHED_TAIL);
|
||||
await game.phaseInterceptor.to("TurnEndPhase", false);
|
||||
|
||||
expect(magikarp.isOnField()).toBe(true);
|
||||
expect(magikarp.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
expect(magikarp.hp).toBe(magikarp.getMaxHp() / 2 - 1);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,68 +0,0 @@
|
||||
import { SubstituteTag } from "#app/data/battler-tags";
|
||||
import { MoveResult } from "#app/field/pokemon";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, it, expect } from "vitest";
|
||||
|
||||
describe("Moves - Shed Tail", () => {
|
||||
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.SHED_TAIL])
|
||||
.battleStyle("single")
|
||||
.enemySpecies(Species.SNORLAX)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(Moves.SPLASH);
|
||||
});
|
||||
|
||||
it("transfers a Substitute doll to the switched in Pokemon", async () => {
|
||||
await game.classicMode.startBattle([Species.MAGIKARP, Species.FEEBAS]);
|
||||
|
||||
const magikarp = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(Moves.SHED_TAIL);
|
||||
game.doSelectPartyPokemon(1);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase", false);
|
||||
|
||||
const feebas = game.scene.getPlayerPokemon()!;
|
||||
const substituteTag = feebas.getTag(SubstituteTag);
|
||||
|
||||
expect(feebas).not.toBe(magikarp);
|
||||
expect(feebas.hp).toBe(feebas.getMaxHp());
|
||||
// Note: Altered the test to be consistent with the correct HP cost :yipeevee_static:
|
||||
expect(magikarp.hp).toBe(Math.floor(magikarp.getMaxHp() / 2));
|
||||
expect(substituteTag).toBeDefined();
|
||||
expect(substituteTag?.hp).toBe(Math.floor(magikarp.getMaxHp() / 4));
|
||||
});
|
||||
|
||||
it("should fail if no ally is available to switch in", async () => {
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
|
||||
const magikarp = game.scene.getPlayerPokemon()!;
|
||||
expect(game.scene.getPlayerParty().length).toBe(1);
|
||||
|
||||
game.move.select(Moves.SHED_TAIL);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase", false);
|
||||
|
||||
expect(magikarp.isOnField()).toBeTruthy();
|
||||
expect(magikarp.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
});
|
||||
});
|
@ -1,106 +0,0 @@
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("Moves - U-turn", () => {
|
||||
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
|
||||
.battleStyle("single")
|
||||
.enemySpecies(Species.GENGAR)
|
||||
.startingLevel(90)
|
||||
.startingWave(97)
|
||||
.moveset([Moves.U_TURN])
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
.disableCrits();
|
||||
});
|
||||
|
||||
it("triggers regenerator a single time when a regenerator user switches out with u-turn", async () => {
|
||||
// arrange
|
||||
const playerHp = 1;
|
||||
game.override.ability(Abilities.REGENERATOR);
|
||||
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]);
|
||||
game.scene.getPlayerPokemon()!.hp = playerHp;
|
||||
|
||||
// act
|
||||
game.move.select(Moves.U_TURN);
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
// assert
|
||||
expect(game.scene.getPlayerParty()[1].hp).toEqual(
|
||||
Math.floor(game.scene.getPlayerParty()[1].getMaxHp() * 0.33 + playerHp),
|
||||
);
|
||||
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
|
||||
expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.SHUCKLE);
|
||||
}, 20000);
|
||||
|
||||
it("triggers rough skin on the u-turn user before a new pokemon is switched in", async () => {
|
||||
// arrange
|
||||
game.override.enemyAbility(Abilities.ROUGH_SKIN);
|
||||
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]);
|
||||
|
||||
// act
|
||||
game.move.select(Moves.U_TURN);
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.phaseInterceptor.to("SwitchPhase", false);
|
||||
|
||||
// assert
|
||||
const playerPkm = game.scene.getPlayerPokemon()!;
|
||||
expect(playerPkm.hp).not.toEqual(playerPkm.getMaxHp());
|
||||
expect(game.scene.getEnemyPokemon()!.waveData.abilityRevealed).toBe(true); // proxy for asserting ability activated
|
||||
expect(playerPkm.species.speciesId).toEqual(Species.RAICHU);
|
||||
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
|
||||
}, 20000);
|
||||
|
||||
it("triggers contact abilities on the u-turn user (eg poison point) before a new pokemon is switched in", async () => {
|
||||
// arrange
|
||||
game.override.enemyAbility(Abilities.POISON_POINT);
|
||||
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]);
|
||||
vi.spyOn(game.scene.getEnemyPokemon()!, "randBattleSeedInt").mockReturnValue(0);
|
||||
|
||||
// act
|
||||
game.move.select(Moves.U_TURN);
|
||||
await game.phaseInterceptor.to("SwitchPhase", false);
|
||||
|
||||
// assert
|
||||
const playerPkm = game.scene.getPlayerPokemon()!;
|
||||
expect(playerPkm.status?.effect).toEqual(StatusEffect.POISON);
|
||||
expect(playerPkm.species.speciesId).toEqual(Species.RAICHU);
|
||||
expect(game.scene.getEnemyPokemon()!.waveData.abilityRevealed).toBe(true); // proxy for asserting ability activated
|
||||
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
|
||||
}, 20000);
|
||||
|
||||
it("still forces a switch if u-turn KO's the opponent", async () => {
|
||||
game.override.startingLevel(1000); // Ensure that U-Turn KO's the opponent
|
||||
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]);
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
// KO the opponent with U-Turn
|
||||
game.move.select(Moves.U_TURN);
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(enemy.isFainted()).toBe(true);
|
||||
|
||||
// Check that U-Turn forced a switch
|
||||
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
|
||||
expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.SHUCKLE);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user