Added tests for failing catches

This commit is contained in:
Wlowscha 2025-08-17 10:40:43 +02:00
parent b32bb090d4
commit 6c22ab9483
No known key found for this signature in database
GPG Key ID: 3C8F1AD330565D04
3 changed files with 263 additions and 7 deletions

View File

@ -82,6 +82,14 @@ export class GameMode implements GameModeConfig {
return this.challenges.some(c => c.id === challenge && c.value !== 0);
}
/**
* Helper function to see if a GameMode has any challenges, needed in tests
* @returns true if the game mode has at least one challenge
*/
hasAnyChallenges(): boolean {
return this.challenges.length > 0;
}
/**
* Helper function to see if the game mode is using fresh start
* @returns true if a fresh start challenge is being applied

View File

@ -390,12 +390,12 @@ export class CommandPhase extends FieldPhase {
const isClassicFinalBoss = globalScene.gameMode.isBattleClassicFinalBoss(globalScene.currentBattle.waveIndex);
const isEndlessMinorBoss = globalScene.gameMode.isEndlessMinorBoss(globalScene.currentBattle.waveIndex);
const isFullFreshStart = globalScene.gameMode.isFullFreshStartChallenge();
const someUncaughtSpeciesOnField = globalScene
.getEnemyField()
.some(p => p.isActive() && !dexData[p.species.speciesId].caughtAttr);
const missingMultipleStarters =
gameData.getStarterCount(d => !!d.caughtAttr) < Object.keys(speciesStarterCosts).length - 1;
if (biomeType === BiomeId.END) {
if (
(isClassic && !isClassicFinalBoss && someUncaughtSpeciesOnField) ||
@ -410,7 +410,7 @@ export class CommandPhase extends FieldPhase {
(isEndless && isEndlessMinorBoss) ||
isDaily
) {
// Uncatchable final boss in classic and endless
// Uncatchable final boss in classic, endless and daily
this.queueShowText("battle:noPokeballForceFinalBoss");
} else {
return true;
@ -446,10 +446,8 @@ export class CommandPhase extends FieldPhase {
return false;
}
// Restricts use of Master Ball against final boss in challenges
const restrictMasterBall =
globalScene.gameMode.isChallenge &&
globalScene.gameMode.isBattleClassicFinalBoss(globalScene.currentBattle.waveIndex);
const isChallengeActive = globalScene.gameMode.hasAnyChallenges();
const isFinalBoss = globalScene.gameMode.isBattleClassicFinalBoss(globalScene.currentBattle.waveIndex);
const numBallTypes = 5;
if (cursor < numBallTypes) {
@ -460,10 +458,16 @@ export class CommandPhase extends FieldPhase {
// TODO: Decouple this hardcoded exception for wonder guard and just check the target...
!targetPokemon?.hasAbility(AbilityId.WONDER_GUARD, false, true)
) {
if (restrictMasterBall) {
// When facing the final boss, it must be weakened unless a Master Ball is used AND no challenges are active.
// The message is customized for the final boss.
if (
isFinalBoss &&
(cursor < PokeballType.MASTER_BALL || (cursor === PokeballType.MASTER_BALL && isChallengeActive))
) {
this.queueShowText("battle:noPokeballForceFinalBossCatchable");
return false;
}
// When facing any other boss, Master Ball can always be used, and we use the standard message.
if (cursor < PokeballType.MASTER_BALL) {
this.queueShowText("battle:noPokeballStrong");
return false;

244
test/field/catching.test.ts Normal file
View File

@ -0,0 +1,244 @@
import { pokerogueApi } from "#api/pokerogue-api";
import { BiomeId } from "#enums/biome-id";
import { Challenges } from "#enums/challenges";
import { GameModes } from "#enums/game-modes";
import { MoveId } from "#enums/move-id";
import { PokeballType } from "#enums/pokeball";
import { SpeciesId } from "#enums/species-id";
import { GameManager } from "#test/test-utils/game-manager";
import i18next from "i18next";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
/**
* Helper function to run tests on cactching mons
*
* @remarks
* - Starts a run on the desired game mode, then attempts to throw a ball
* - If still in the command phase (meaning the ball did not catch) uses a move to proceed
* - If expecting success, checks that party length has increased by 1
* - Otherwise, checks that {@link i18next} has been called on the requested error key
*
* @param game {@link GameManager} The game manager from the test
* @param ball {@link PokeballType} The type of pokéball to be used for the catch attempt
* @param expectedResult {@link string} Either "success", if the enemy should be caught, or the expected locales error key
* @param mode One between "classic", "daily", and "challenge", defaults to "classic".
*/
async function runPokeballTest(
game: GameManager,
ball: PokeballType,
expectedResult: string,
mode: "classic" | "daily" | "challenge" = "classic",
) {
if (mode === "classic") {
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
} else if (mode === "daily") {
// Have to do it this way because daily run is weird...
await game.runToFinalBossEncounter([SpeciesId.MAGIKARP], GameModes.DAILY);
} else if (mode === "challenge") {
await game.challengeMode.startBattle([SpeciesId.MAGIKARP]);
}
const partyLength = game.scene.getPlayerParty().length;
game.scene.pokeballCounts[ball] = 1;
const tSpy = vi.spyOn(i18next, "t").mockImplementation((key: string) => key);
game.doThrowPokeball(ball);
// If still in the command phase due to ball failing, use a move to go on
if (game.isCurrentPhase("CommandPhase")) {
game.move.select(MoveId.SPLASH);
}
await game.toEndOfTurn();
if (expectedResult === "success") {
// Check that a mon has been caught by noticing that party length has increased
expect(game.scene.getPlayerParty()).toHaveLength(partyLength + 1);
} else {
expect(tSpy).toHaveBeenCalledWith(expectedResult);
}
}
describe("Throwing balls in classic", () => {
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
.startingWave(199)
.startingBiome(BiomeId.END)
.battleStyle("single")
.moveset([MoveId.SPLASH])
.enemyMoveset([MoveId.SPLASH])
.startingLevel(9999);
});
it("throwing ball at paradox mon", async () => {
await runPokeballTest(game, PokeballType.MASTER_BALL, "battle:noPokeballForce");
});
it("throwing ball at two paradox mons", async () => {
game.override.battleStyle("double");
await runPokeballTest(game, PokeballType.MASTER_BALL, "battle:noPokeballMulti");
});
it("throwing ball at previously caught paradox mon", async () => {
await game.importData("./test/test-utils/saves/everything.prsv");
await runPokeballTest(game, PokeballType.MASTER_BALL, "success");
});
it("throwing ball at final boss", async () => {
game.override.startingWave(200);
await runPokeballTest(game, PokeballType.MASTER_BALL, "battle:noPokeballForceFinalBoss");
});
it("throwing rogue ball at final boss with full dex", async () => {
await game.importData("./test/test-utils/saves/everything.prsv");
game.override.startingWave(200);
await runPokeballTest(game, PokeballType.ROGUE_BALL, "battle:noPokeballForceFinalBossCatchable");
});
it("throwing master ball at final boss with full dex", async () => {
await game.importData("./test/test-utils/saves/everything.prsv");
game.override.startingWave(200);
await runPokeballTest(game, PokeballType.MASTER_BALL, "success");
});
});
describe("Throwing balls in fresh start challenge", () => {
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.challengeMode.addChallenge(Challenges.FRESH_START, 2, 1);
game.override
.startingWave(199)
.startingBiome(BiomeId.END)
.battleStyle("single")
.moveset([MoveId.SPLASH])
.enemyMoveset([MoveId.SPLASH])
.startingLevel(9999);
});
// Tests should give the same result as a normal classic run, except for the last one
it("throwing ball at paradox mon", async () => {
await runPokeballTest(game, PokeballType.MASTER_BALL, "battle:noPokeballForce", "challenge");
});
it("throwing ball at previously caught paradox mon", async () => {
await game.importData("./test/test-utils/saves/everything.prsv");
await runPokeballTest(game, PokeballType.MASTER_BALL, "success", "challenge");
});
it("throwing ball at final boss", async () => {
game.override.startingWave(200);
await runPokeballTest(game, PokeballType.MASTER_BALL, "battle:noPokeballForceFinalBoss", "challenge");
});
it("throwing rogue ball at final boss with full dex", async () => {
await game.importData("./test/test-utils/saves/everything.prsv");
game.override.startingWave(200);
await runPokeballTest(game, PokeballType.ROGUE_BALL, "battle:noPokeballForceFinalBossCatchable", "challenge");
});
// If a challenge is active, even if the dex is complete we still need to weaken the final boss to master ball it
it("throwing ball at final boss with full dex", async () => {
await game.importData("./test/test-utils/saves/everything.prsv");
game.override.startingWave(200);
await runPokeballTest(game, PokeballType.MASTER_BALL, "battle:noPokeballForceFinalBossCatchable", "challenge");
});
});
describe("Throwing balls in full fresh start challenge", () => {
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.challengeMode.addChallenge(Challenges.FRESH_START, 1, 1);
game.override
.startingWave(199)
.startingBiome(BiomeId.END)
.battleStyle("single")
.moveset([MoveId.SPLASH])
.enemyMoveset([MoveId.SPLASH])
.startingLevel(9999);
});
// Paradox and final boss can NEVER be caught in the full fresh start challenge
it("throwing ball at previously caught paradox mon", async () => {
await game.importData("./test/test-utils/saves/everything.prsv");
await runPokeballTest(game, PokeballType.MASTER_BALL, "battle:noPokeballForce", "challenge");
});
it("throwing ball at final boss with full dex", async () => {
await game.importData("./test/test-utils/saves/everything.prsv");
game.override.startingWave(200);
await runPokeballTest(game, PokeballType.MASTER_BALL, "battle:noPokeballForceFinalBoss", "challenge");
});
});
describe("Throwing balls in daily run", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
vi.spyOn(pokerogueApi.daily, "getSeed").mockResolvedValue("test-seed");
game.override
.startingWave(50)
.startingBiome(BiomeId.END)
.battleStyle("single")
.moveset([MoveId.SPLASH])
.enemyMoveset([MoveId.SPLASH])
.startingLevel(9999);
});
it("throwing ball at daily run boss", async () => {
await runPokeballTest(game, PokeballType.MASTER_BALL, "battle:noPokeballForceFinalBoss", "daily");
});
});