mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-06-21 00:52:47 +02:00
383 lines
15 KiB
TypeScript
383 lines
15 KiB
TypeScript
import { BattlerIndex } from "#enums/battler-index";
|
|
import { allMoves } from "#app/data/data-lists";
|
|
import { getTerrainName, TerrainType } from "#app/data/terrain";
|
|
import { MoveResult } from "#enums/move-result";
|
|
import { getPokemonNameWithAffix } from "#app/messages";
|
|
import { capitalizeFirstLetter, randSeedInt, toDmgValue } from "#app/utils/common";
|
|
import { AbilityId } from "#enums/ability-id";
|
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
|
import { MoveId } from "#enums/move-id";
|
|
import { PokemonType } from "#enums/pokemon-type";
|
|
import { SpeciesId } from "#enums/species-id";
|
|
import { Stat } from "#enums/stat";
|
|
import { StatusEffect } from "#enums/status-effect";
|
|
import GameManager from "#test/testUtils/gameManager";
|
|
import i18next from "i18next";
|
|
import Phaser from "phaser";
|
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
describe("Terrain -", () => {
|
|
let phaserGame: Phaser.Game;
|
|
let game: GameManager;
|
|
|
|
beforeAll(() => {
|
|
phaserGame = new Phaser.Game({
|
|
type: Phaser.HEADLESS,
|
|
});
|
|
});
|
|
|
|
describe.each<{ terrain: string; type: PokemonType; ability: AbilityId; move: MoveId }>([
|
|
{ terrain: "Electric", type: PokemonType.ELECTRIC, ability: AbilityId.ELECTRIC_SURGE, move: MoveId.THUNDERBOLT },
|
|
{ terrain: "Psychic", type: PokemonType.PSYCHIC, ability: AbilityId.PSYCHIC_SURGE, move: MoveId.PSYCHIC },
|
|
{ terrain: "Grassy", type: PokemonType.GRASS, ability: AbilityId.GRASSY_SURGE, move: MoveId.GIGA_DRAIN },
|
|
])("Move Power Boosts - $terrain Terrain", ({ type, ability, move }) => {
|
|
afterEach(() => {
|
|
game.phaseInterceptor.restoreOg();
|
|
});
|
|
|
|
beforeEach(() => {
|
|
game = new GameManager(phaserGame);
|
|
game.override
|
|
.battleStyle("single")
|
|
.criticalHits(false)
|
|
.enemySpecies(SpeciesId.SNOM)
|
|
.enemyAbility(AbilityId.STURDY)
|
|
.ability(ability)
|
|
.passiveAbility(AbilityId.NO_GUARD)
|
|
.enemyPassiveAbility(AbilityId.LEVITATE);
|
|
});
|
|
|
|
const typeStr = capitalizeFirstLetter(PokemonType[type].toLowerCase());
|
|
|
|
it(`should boost grounded uses of ${typeStr}-type moves by 1.3x, even against ungrounded targets`, async () => {
|
|
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
|
|
|
const powerSpy = vi.spyOn(allMoves[move], "calculateBattlePower");
|
|
game.move.use(move);
|
|
await game.move.forceEnemyMove(move);
|
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
|
await game.toEndOfTurn();
|
|
|
|
// Player grounded attack got boosted while enemy ungrounded attack didn't
|
|
expect(powerSpy).toHaveLastReturnedWith(allMoves[move].power * 1.3);
|
|
expect(powerSpy).toHaveNthReturnedWith(1, allMoves[move].power);
|
|
});
|
|
|
|
it(`should change Terrain Pulse into a ${typeStr}-type move and double its base power`, async () => {
|
|
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
|
|
|
const powerSpy = vi.spyOn(allMoves[MoveId.TERRAIN_PULSE], "calculateBattlePower");
|
|
const playerTypeSpy = vi.spyOn(game.field.getPlayerPokemon(), "getMoveType");
|
|
const enemyTypeSpy = vi.spyOn(game.field.getEnemyPokemon(), "getMoveType");
|
|
|
|
game.move.use(MoveId.TERRAIN_PULSE);
|
|
await game.move.forceEnemyMove(MoveId.TERRAIN_PULSE);
|
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
|
await game.toEndOfTurn();
|
|
|
|
// player grounded terrain pulse was boosted & type converted; enemy ungrounded one wasn't
|
|
expect(powerSpy).toHaveLastReturnedWith(allMoves[move].power * 2.6); // 2 * 1.3
|
|
expect(playerTypeSpy).toHaveLastReturnedWith(type);
|
|
expect(powerSpy).toHaveNthReturnedWith(1, allMoves[move].power);
|
|
expect(enemyTypeSpy).toHaveNthReturnedWith(1, allMoves[MoveId.TERRAIN_PULSE].type);
|
|
});
|
|
});
|
|
|
|
describe("Grassy Terrain", () => {
|
|
afterEach(() => {
|
|
game.phaseInterceptor.restoreOg();
|
|
});
|
|
|
|
beforeEach(() => {
|
|
game = new GameManager(phaserGame);
|
|
game.override
|
|
.battleStyle("single")
|
|
.criticalHits(false)
|
|
.enemyLevel(100)
|
|
.enemySpecies(SpeciesId.SHUCKLE)
|
|
.enemyAbility(AbilityId.STURDY)
|
|
.enemyMoveset(MoveId.GRASSY_TERRAIN)
|
|
.ability(AbilityId.NO_GUARD);
|
|
});
|
|
|
|
it("should heal all grounded, non-semi-invulnerable pokemon for 1/16th max HP at end of turn", async () => {
|
|
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
|
|
|
// shuckle is grounded, pidgeot isn't
|
|
const blissey = game.field.getPlayerPokemon();
|
|
blissey.hp = toDmgValue(blissey.getMaxHp() / 2);
|
|
expect(blissey.getHpRatio()).toBeCloseTo(0.5, 1);
|
|
|
|
const shuckle = game.field.getEnemyPokemon();
|
|
game.field.mockAbility(shuckle, AbilityId.LEVITATE);
|
|
shuckle.hp = toDmgValue(shuckle.getMaxHp() / 2);
|
|
expect(shuckle.getHpRatio()).toBeCloseTo(0.5, 1);
|
|
|
|
game.move.use(MoveId.SPLASH);
|
|
await game.toNextTurn();
|
|
|
|
expect(game.phaseInterceptor.log).toContain("PokemonHealPhase");
|
|
expect(blissey.getHpRatio()).toBeCloseTo(0.5625, 1);
|
|
expect(shuckle.getHpRatio()).toBeCloseTo(0.5, 1);
|
|
|
|
game.move.use(MoveId.DIG);
|
|
await game.toNextTurn();
|
|
|
|
// shuckle is airborne and blissey is semi-invulnerable, so nobody gets healed
|
|
expect(blissey.getHpRatio()).toBeCloseTo(0.5625, 1);
|
|
expect(shuckle.getHpRatio()).toBeCloseTo(0.5, 1);
|
|
});
|
|
|
|
it.each<{ name: string; move: MoveId; basePower?: number }>([
|
|
{ name: "Bulldoze", move: MoveId.BULLDOZE },
|
|
{ name: "Earthquake", move: MoveId.EARTHQUAKE },
|
|
{ name: "Magnitude", move: MoveId.MAGNITUDE, basePower: 150 }, // magnitude 10
|
|
])(
|
|
"should halve $name's base power against grounded, on-field targets",
|
|
async ({ move, basePower = allMoves[move].power }) => {
|
|
await game.classicMode.startBattle([SpeciesId.TAUROS]);
|
|
// force high rolls for guaranteed magnitude 10s
|
|
vi.fn(randSeedInt).mockReturnValue(100);
|
|
|
|
const powerSpy = vi.spyOn(allMoves[move], "calculateBattlePower");
|
|
|
|
// Turn 1: attack before grassy terrain set up; 1x
|
|
game.move.use(move);
|
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
|
await game.toNextTurn();
|
|
|
|
expect(powerSpy).toHaveLastReturnedWith(basePower);
|
|
|
|
// Turn 2: attack with grassy terrain already active; 0.5x
|
|
game.move.use(move);
|
|
await game.toNextTurn();
|
|
|
|
expect(powerSpy).toHaveLastReturnedWith(basePower / 2);
|
|
|
|
// Turn 3: Give enemy levitate to make ungrounded and attack; 1x
|
|
game.field.mockAbility(game.field.getEnemyPokemon(), AbilityId.LEVITATE);
|
|
game.move.use(move);
|
|
await game.toNextTurn();
|
|
|
|
expect(powerSpy).toHaveLastReturnedWith(basePower);
|
|
|
|
// Turn 4: Remove levitate and make enemy semi-invulnerable; 1x
|
|
game.field.mockAbility(game.field.getEnemyPokemon(), AbilityId.BALL_FETCH);
|
|
game.move.use(move);
|
|
await game.move.forceEnemyMove(MoveId.FLY);
|
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
|
await game.toNextTurn();
|
|
|
|
expect(powerSpy).toHaveLastReturnedWith(basePower);
|
|
},
|
|
);
|
|
});
|
|
|
|
// TODO: Enable suites after terrain-fail-msg branch is merged
|
|
describe.skip("Electric Terrain", () => {
|
|
afterEach(() => {
|
|
game.phaseInterceptor.restoreOg();
|
|
});
|
|
|
|
beforeEach(() => {
|
|
game = new GameManager(phaserGame);
|
|
game.override
|
|
.battleStyle("single")
|
|
.criticalHits(false)
|
|
.enemyLevel(100)
|
|
.enemySpecies(SpeciesId.SHUCKLE)
|
|
.enemyAbility(AbilityId.ELECTRIC_SURGE)
|
|
.ability(AbilityId.NO_GUARD);
|
|
});
|
|
|
|
it("should prevent all grounded pokemon from being put to sleep", async () => {
|
|
await game.classicMode.startBattle([SpeciesId.PIDGEOT]);
|
|
|
|
game.move.use(MoveId.SPORE);
|
|
await game.move.forceEnemyMove(MoveId.SPORE);
|
|
await game.toEndOfTurn();
|
|
|
|
const pidgeot = game.field.getPlayerPokemon();
|
|
const shuckle = game.field.getEnemyPokemon();
|
|
expect(pidgeot.status?.effect).toBeUndefined();
|
|
expect(shuckle.status?.effect).toBe(StatusEffect.SLEEP);
|
|
// TODO: These don't work due to how move failures are propagated
|
|
expect(pidgeot.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
|
expect(shuckle.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
|
|
|
expect(game.textInterceptor.logs).not.toContain(
|
|
i18next.t("terrain:defaultBlockMessage", {
|
|
pokemonNameWithAffix: getPokemonNameWithAffix(shuckle),
|
|
terrainName: getTerrainName(TerrainType.ELECTRIC),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("should prevent attack moves from applying sleep without showing text/failing move", async () => {
|
|
vi.spyOn(allMoves[MoveId.RELIC_SONG], "chance", "get").mockReturnValue(100);
|
|
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
|
|
|
const shuckle = game.field.getEnemyPokemon();
|
|
const statusSpy = vi.spyOn(shuckle, "canSetStatus");
|
|
|
|
game.move.use(MoveId.RELIC_SONG);
|
|
await game.move.forceEnemyMove(MoveId.SPLASH);
|
|
await game.toEndOfTurn();
|
|
|
|
const blissey = game.field.getPlayerPokemon();
|
|
expect(shuckle.status?.effect).toBeUndefined();
|
|
expect(statusSpy).toHaveLastReturnedWith(false);
|
|
expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
|
|
|
expect(game.textInterceptor.logs).not.toContain(
|
|
i18next.t("terrain:defaultBlockMessage", {
|
|
pokemonNameWithAffix: getPokemonNameWithAffix(shuckle),
|
|
terrainName: getTerrainName(TerrainType.ELECTRIC),
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe.skip("Misty Terrain", () => {
|
|
afterEach(() => {
|
|
game.phaseInterceptor.restoreOg();
|
|
});
|
|
|
|
beforeEach(() => {
|
|
game = new GameManager(phaserGame);
|
|
game.override
|
|
.battleStyle("single")
|
|
.criticalHits(false)
|
|
.enemyLevel(100)
|
|
.enemySpecies(SpeciesId.SHUCKLE)
|
|
.enemyAbility(AbilityId.MISTY_SURGE)
|
|
.ability(AbilityId.NO_GUARD);
|
|
});
|
|
|
|
it("should prevent all grounded pokemon from being statused or confused", async () => {
|
|
game.override.confusionActivation(false); // prevent self hits from ruining things
|
|
await game.classicMode.startBattle([SpeciesId.PIDGEOT]);
|
|
|
|
// shuckle is grounded, pidgeot isn't
|
|
game.move.use(MoveId.TOXIC);
|
|
await game.move.forceEnemyMove(MoveId.TOXIC);
|
|
await game.toNextTurn();
|
|
|
|
const pidgeot = game.field.getPlayerPokemon();
|
|
const shuckle = game.field.getEnemyPokemon();
|
|
expect(pidgeot.status?.effect).toBeUndefined();
|
|
expect(shuckle.status?.effect).toBe(StatusEffect.TOXIC);
|
|
// TODO: These don't work due to how move failures are propagated
|
|
expect(pidgeot.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
|
expect(shuckle.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
|
|
|
expect(game.textInterceptor.logs).toContain(
|
|
i18next.t("terrain:mistyBlockMessage", {
|
|
pokemonNameWithAffix: getPokemonNameWithAffix(shuckle),
|
|
}),
|
|
);
|
|
game.textInterceptor.logs = [];
|
|
|
|
game.move.use(MoveId.CONFUSE_RAY);
|
|
await game.move.forceEnemyMove(MoveId.CONFUSE_RAY);
|
|
await game.toNextTurn();
|
|
|
|
expect(pidgeot.getTag(BattlerTagType.CONFUSED)).toBeUndefined();
|
|
expect(shuckle.getTag(BattlerTagType.CONFUSED)).toBeDefined();
|
|
});
|
|
|
|
it.each<{ status: string; move: MoveId }>([
|
|
{ status: "Sleep", move: MoveId.RELIC_SONG },
|
|
{ status: "Burn", move: MoveId.SACRED_FIRE },
|
|
{ status: "Freeze", move: MoveId.ICE_BEAM },
|
|
{ status: "Paralysis", move: MoveId.NUZZLE },
|
|
{ status: "Poison", move: MoveId.SLUDGE_BOMB },
|
|
{ status: "Toxic", move: MoveId.MALIGNANT_CHAIN },
|
|
{ status: "Confusion", move: MoveId.MAGICAL_TORQUE },
|
|
])("should prevent attack moves from applying $name without showing text/failing move", async ({ move }) => {
|
|
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
|
vi.spyOn(allMoves[move], "chance", "get").mockReturnValue(100);
|
|
|
|
game.move.use(move);
|
|
await game.move.forceEnemyMove(MoveId.SPLASH);
|
|
await game.toEndOfTurn();
|
|
|
|
const blissey = game.field.getPlayerPokemon();
|
|
const shuckle = game.field.getEnemyPokemon();
|
|
expect(shuckle.status?.effect).toBeUndefined();
|
|
expect(shuckle.getTag(BattlerTagType.CONFUSED)).toBeUndefined();
|
|
expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
|
|
|
expect(game.textInterceptor.logs).not.toContain(
|
|
i18next.t("terrain:mistyBlockMessage", {
|
|
pokemonNameWithAffix: getPokemonNameWithAffix(shuckle),
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Psychic Terrain", () => {
|
|
afterEach(() => {
|
|
game.phaseInterceptor.restoreOg();
|
|
});
|
|
|
|
beforeEach(() => {
|
|
game = new GameManager(phaserGame);
|
|
game.override
|
|
.battleStyle("single")
|
|
.criticalHits(false)
|
|
.enemyLevel(100)
|
|
.enemySpecies(SpeciesId.SHUCKLE)
|
|
.enemyAbility(AbilityId.PSYCHIC_SURGE)
|
|
.enemyPassiveAbility(AbilityId.PRANKSTER)
|
|
.ability(AbilityId.NO_GUARD)
|
|
.passiveAbility(AbilityId.GALE_WINGS);
|
|
});
|
|
|
|
it("should block all opponent-targeted priority moves", async () => {
|
|
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
|
|
|
// normal + priority moves
|
|
game.move.use(MoveId.FAKE_OUT);
|
|
await game.move.forceEnemyMove(MoveId.FOLLOW_ME);
|
|
await game.toEndOfTurn();
|
|
|
|
const blissey = game.field.getPlayerPokemon();
|
|
const shuckle = game.field.getEnemyPokemon();
|
|
expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
|
expect(shuckle.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
|
expect(game.textInterceptor.logs).toContain(
|
|
i18next.t("terrain:defaultBlockMessage", {
|
|
pokemonNameWithAffix: getPokemonNameWithAffix(shuckle),
|
|
terrainName: getTerrainName(TerrainType.PSYCHIC),
|
|
}),
|
|
);
|
|
|
|
// moves that only become priority via ability
|
|
blissey.hp = 1;
|
|
game.move.use(MoveId.ROOST);
|
|
await game.move.forceEnemyMove(MoveId.TICKLE);
|
|
await game.toEndOfTurn();
|
|
|
|
expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
|
expect(shuckle.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
|
});
|
|
|
|
// TODO: This needs to be fixed
|
|
it.todo("should not block enemy-targeting spread moves, even if they become priority", async () => {
|
|
await game.classicMode.startBattle([SpeciesId.BLISSEY]);
|
|
|
|
game.move.use(MoveId.AIR_CUTTER);
|
|
await game.move.forceEnemyMove(MoveId.SWEET_SCENT);
|
|
await game.toEndOfTurn();
|
|
|
|
const blissey = game.field.getPlayerPokemon();
|
|
const shuckle = game.field.getEnemyPokemon();
|
|
expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
|
expect(shuckle.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
|
expect(shuckle.hp).toBeLessThan(shuckle.getMaxHp());
|
|
expect(blissey.getStatStage(Stat.EVA)).toBe(-2);
|
|
});
|
|
});
|
|
});
|