pokerogue/test/abilities/protean-libero.test.ts
2025-06-17 14:37:43 -04:00

272 lines
9.9 KiB
TypeScript

import { allMoves } from "#app/data/data-lists";
import { PokemonType } from "#enums/pokemon-type";
import type { PlayerPokemon } from "#app/field/pokemon";
import { MoveResult } from "#enums/move-result";
import { AbilityId } from "#enums/ability-id";
import { BattlerTagType } from "#enums/battler-tag-type";
import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id";
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 { BattlerIndex } from "#enums/battler-index";
describe("Abilities - Protean/Libero", () => {
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")
.ability(AbilityId.PROTEAN)
.startingLevel(100)
.moveset([MoveId.CURSE, MoveId.DIG, MoveId.SPLASH])
.enemySpecies(SpeciesId.RATTATA)
.enemyMoveset(MoveId.SPLASH);
});
/**
* Assert that the protean/libero ability triggered to change the user's type to
* the type of its most recently used move.
* Takes into account type overrides from effects.
* @param pokemon - The {@linkcode PlayerPokemon} being checked.
* @remarks
* This will clear the given Pokemon's `abilitiesApplied` set after being called to allow for easier multi-turn testing.
*/
function expectTypeChange(pokemon: PlayerPokemon) {
expect(pokemon.waveData.abilitiesApplied).toContainEqual(expect.toBeOneOf([AbilityId.PROTEAN, AbilityId.LIBERO]));
const lastMove = allMoves[pokemon.getLastXMoves()[0].move]!;
const pokemonTypes = pokemon.getTypes().map(pt => PokemonType[pt]);
const moveType = PokemonType[pokemon.getMoveType(lastMove)];
expect(pokemonTypes).toEqual([moveType]);
pokemon.waveData.abilitiesApplied.clear();
}
/**
* Assert that the protean/libero ability did NOT trigger to change the user's type to
* the type of its most recently used move.
* Takes into account type overrides from effects.
* @param pokemon - The {@linkcode PlayerPokemon} being checked.
* @remarks
* This will clear the given Pokemon's `abilitiesApplied` set after being called to allow for easier multi-turn testing.
*/
function expectNoTypeChange(pokemon: PlayerPokemon) {
expect(pokemon.waveData.abilitiesApplied).not.toContainEqual(
expect.toBeOneOf([AbilityId.PROTEAN, AbilityId.LIBERO]),
);
const lastMove = allMoves[pokemon.getLastXMoves()[0].move]!;
const pokemonTypes = pokemon.getTypes().map(pt => PokemonType[pt]);
const moveType = PokemonType[pokemon.getMoveType(lastMove, true)];
expect(pokemonTypes).not.toEqual([moveType]);
pokemon.waveData.abilitiesApplied.clear();
}
it.each([
{ name: "Protean", ability: AbilityId.PROTEAN },
{ name: "Libero", ability: AbilityId.PROTEAN },
])("$name should change the user's type to the type of the move being used", async ({ ability }) => {
game.override.ability(ability);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.field.getPlayerPokemon();
game.move.use(MoveId.SPLASH);
await game.toEndOfTurn();
expectTypeChange(leadPokemon);
});
// Test for Gen9+ functionality, we are using previous funcionality
it.skip("should apply only once per switch in", async () => {
game.override.moveset([MoveId.SPLASH, MoveId.AGILITY]);
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.BULBASAUR]);
const bulbasaur = game.field.getPlayerPokemon();
game.move.select(MoveId.SPLASH);
await game.toEndOfTurn();
expectTypeChange(bulbasaur);
game.move.select(MoveId.AGILITY);
await game.toEndOfTurn();
expectNoTypeChange(bulbasaur);
// switch out and back in
game.doSwitchPokemon(1);
await game.toNextTurn();
game.doSwitchPokemon(1);
await game.toNextTurn();
expect(bulbasaur.isOnField()).toBe(true);
game.move.select(MoveId.SPLASH);
await game.toEndOfTurn();
expectTypeChange(bulbasaur);
});
it.each<{ category: string; move?: MoveId; passive?: AbilityId; enemyMove?: MoveId }>([
{ category: "Variable type Moves'", move: MoveId.WEATHER_BALL, passive: AbilityId.DROUGHT },
{ category: "Type Change Abilities'", passive: AbilityId.REFRIGERATE },
{ category: "Move-calling Moves'", move: MoveId.NATURE_POWER, passive: AbilityId.PSYCHIC_SURGE },
{ category: "Ion Deluge's", enemyMove: MoveId.ION_DELUGE },
{ category: "Electrify's", enemyMove: MoveId.ELECTRIFY },
])(
"should respect $category final type",
async ({ move = MoveId.TACKLE, passive = AbilityId.NONE, enemyMove = MoveId.SPLASH }) => {
game.override.passiveAbility(passive);
await game.classicMode.startBattle([SpeciesId.LINOONE]); // Pure normal type for move overrides
const linoone = game.field.getPlayerPokemon();
game.move.use(move);
await game.move.forceEnemyMove(enemyMove);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("BerryPhase"); // NB: berry phase = turn end tags stay = tests happy
expectTypeChange(linoone);
},
);
it.each<{ cause: string; move?: MoveId; passive?: AbilityId; enemyMove?: MoveId }>([
{ cause: "misses", move: MoveId.FOCUS_BLAST },
{ cause: "is protected against", enemyMove: MoveId.PROTECT },
{ cause: "is ineffective", move: MoveId.EARTHQUAKE },
{ cause: "matches only one of its types", move: MoveId.NIGHT_SLASH },
{ cause: "is blocked by terrain", move: MoveId.SHADOW_SNEAK, passive: AbilityId.PSYCHIC_SURGE },
])(
"should still trigger if the user's move $cause",
async ({ move = MoveId.TACKLE, passive = AbilityId.NONE, enemyMove = MoveId.SPLASH }) => {
game.override.passiveAbility(passive).enemySpecies(SpeciesId.SKARMORY);
await game.classicMode.startBattle([SpeciesId.MEOWSCARADA]);
// FOCUS MISS IS REAL CHAT
vi.spyOn(allMoves[MoveId.FOCUS_BLAST], "accuracy", "get").mockReturnValue(0);
const meow = game.field.getPlayerPokemon();
game.move.use(move);
await game.move.forceEnemyMove(enemyMove);
await game.toEndOfTurn();
expectTypeChange(meow);
},
);
it.each<{ cause: string; move?: MoveId; tera?: boolean; passive?: AbilityId }>([
{ cause: "user is terastallized to any type", tera: true },
{ cause: "user uses Struggle", move: MoveId.STRUGGLE },
{ cause: "the user's move is blocked by weather", move: MoveId.FIRE_BLAST, passive: AbilityId.PRIMORDIAL_SEA },
{ cause: "the user's move fails", move: MoveId.BURN_UP },
])("should not apply if $cause", async ({ move = MoveId.TACKLE, tera = false, passive = AbilityId.NONE }) => {
game.override.enemyPassiveAbility(passive);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const karp = game.field.getPlayerPokemon();
karp.teraType = PokemonType.STEEL;
game.move.use(move, BattlerIndex.PLAYER, undefined, tera);
await game.toEndOfTurn();
expectNoTypeChange(karp);
});
it("should not apply if user is already the move's type", async () => {
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const karp = game.field.getPlayerPokemon();
game.move.use(MoveId.WATERFALL);
await game.toEndOfTurn();
expect(karp.waveData.abilitiesApplied.size).toBe(0);
expect(karp.getTypes()).toEqual([allMoves[MoveId.WATERFALL].type]);
});
it.each<{ moveName: string; move: MoveId }>([
{ moveName: "Roar", move: MoveId.ROAR },
{ moveName: "Whirlwind", move: MoveId.WHIRLWIND },
{ moveName: "Forest's Curse", move: MoveId.FORESTS_CURSE },
{ moveName: "Trick-or-Treat", move: MoveId.TRICK_OR_TREAT },
])("should still apply if the user's $moveName fails", async ({ move }) => {
game.override.battleType(BattleType.TRAINER).enemySpecies(SpeciesId.TREVENANT); // ghost/grass makes both moves fail
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.field.getPlayerPokemon();
game.move.use(move);
// KO all off-field opponents for Whirlwind and co.
for (const enemyMon of game.scene.getEnemyParty()) {
if (!enemyMon.isActive()) {
enemyMon.hp = 0;
}
}
await game.toEndOfTurn();
expectTypeChange(leadPokemon);
});
it("should trigger on the first turn of charging moves", async () => {
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const karp = game.field.getPlayerPokemon();
game.move.select(MoveId.DIG);
await game.toEndOfTurn();
expectTypeChange(karp);
await game.toEndOfTurn();
expect(karp.waveData.abilitiesApplied).not.toContain(AbilityId.PROTEAN);
});
it("should cause the user to cast Ghost-type Curse on itself", async () => {
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const karp = game.field.getPlayerPokemon();
expect(karp.isOfType(PokemonType.GHOST)).toBe(false);
game.move.select(MoveId.CURSE);
await game.toEndOfTurn();
expectTypeChange(karp);
expect(karp.getHpRatio(true)).toBeCloseTo(0.25);
expect(karp.getTag(BattlerTagType.CURSED)).toBeDefined();
});
it("should not trigger during Focus Punch's start-of-turn message or being interrupted", async () => {
game.override.moveset(MoveId.FOCUS_PUNCH).enemyMoveset(MoveId.ABSORB);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const karp = game.field.getPlayerPokemon();
expect(karp.isOfType(PokemonType.FIGHTING)).toBe(false);
game.move.select(MoveId.FOCUS_PUNCH);
await game.phaseInterceptor.to("MessagePhase");
expect(karp.isOfType(PokemonType.FIGHTING)).toBe(false);
await game.toEndOfTurn();
expectNoTypeChange(karp);
expect(karp.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
});
});