[Bug] Fix Ball Fetch activating on enemy Pokemon (#6777)

This commit is contained in:
Bertie690 2025-11-14 03:27:53 -05:00 committed by GitHub
parent bf68f59161
commit 4e080465b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 110 additions and 10 deletions

View File

@ -2707,6 +2707,7 @@ export class PostSummonAddArenaTagAbAttr extends PostSummonAbAttr {
private readonly turnCount: number;
private readonly side?: ArenaTagSide;
private readonly quiet?: boolean;
// TODO: This should not need to track the source ID in a tempvar
private sourceId: number;
constructor(showAbility: boolean, tagType: ArenaTagType, turnCount: number, side?: ArenaTagSide, quiet?: boolean) {
@ -2741,6 +2742,7 @@ export class PostSummonMessageAbAttr extends PostSummonAbAttr {
}
}
// TODO: This should be merged with message func
export class PostSummonUnnamedMessageAbAttr extends PostSummonAbAttr {
//Attr doesn't force pokemon name on the message
private readonly message: string;
@ -2811,13 +2813,13 @@ export class PostSummonStatStageChangeAbAttr extends PostSummonAbAttr {
private readonly selfTarget: boolean;
private readonly intimidate: boolean;
constructor(stats: readonly BattleStat[], stages: number, selfTarget?: boolean, intimidate?: boolean) {
constructor(stats: readonly BattleStat[], stages: number, selfTarget = false, intimidate = true) {
super(true);
this.stats = stats;
this.stages = stages;
this.selfTarget = !!selfTarget;
this.intimidate = !!intimidate;
this.selfTarget = selfTarget;
this.intimidate = intimidate;
}
override apply({ pokemon, simulated }: AbAttrBaseParams): void {
@ -5012,25 +5014,26 @@ export class PostTurnHurtIfSleepingAbAttr extends PostTurnAbAttr {
*/
export class FetchBallAbAttr extends PostTurnAbAttr {
override canApply({ simulated, pokemon }: AbAttrBaseParams): boolean {
return !simulated && globalScene.currentBattle.lastUsedPokeball != null && !!pokemon.isPlayer;
return !simulated && globalScene.currentBattle.lastUsedPokeball != null && pokemon.isPlayer();
}
/**
* Adds the last used Pokeball back into the player's inventory
*/
override apply({ pokemon }: AbAttrBaseParams): void {
const lastUsed = globalScene.currentBattle.lastUsedPokeball;
globalScene.pokeballCounts[lastUsed!]++;
const lastUsed = globalScene.currentBattle.lastUsedPokeball!;
globalScene.pokeballCounts[lastUsed]++;
globalScene.currentBattle.lastUsedPokeball = null;
globalScene.phaseManager.queueMessage(
i18next.t("abilityTriggers:fetchBall", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
pokeballName: getPokeballName(lastUsed!),
pokeballName: getPokeballName(lastUsed),
}),
);
}
}
// TODO: Remove this and just replace it with applying `PostSummonChangeTerrainAbAttr` again
export class PostBiomeChangeAbAttr extends AbAttr {
private declare readonly _: never;
}
@ -5055,6 +5058,7 @@ export class PostBiomeChangeWeatherChangeAbAttr extends PostBiomeChangeAbAttr {
}
}
// TODO: Remove this and just replace it with applying `PostSummonChangeTerrainAbAttr` again
/** @sealed */
export class PostBiomeChangeTerrainChangeAbAttr extends PostBiomeChangeAbAttr {
private readonly terrainType: TerrainType;

View File

@ -0,0 +1,92 @@
/*
* SPDX-FileCopyrightText: 2024-2025 Pagefault Games
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { getPokemonNameWithAffix } from "#app/messages";
import { getPokeballName } from "#data/pokeball";
import { AbilityId } from "#enums/ability-id";
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";
// NB: These tests pass when done locally, but we currently have no mechanism to make catches fail
// due to battle scene RNG overrides making ball shake checks always succeed.
//
// TODO: Enable suite once `AttemptCapturePhase` is made sane
describe.todo("Ability - Ball Fetch", () => {
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
.ability(AbilityId.BALL_FETCH)
.battleStyle("single")
.criticalHits(false)
.enemySpecies(SpeciesId.MAGIKARP)
.enemyMoveset(MoveId.SPLASH)
.startingLevel(100)
.enemyLevel(100);
});
it("should restore the user's first failed ball throw at end of turn", async () => {
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
const feebas = game.field.getPlayerPokemon();
const karp = game.field.getEnemyPokemon();
vi.spyOn(karp.species, "catchRate", "get").mockReturnValue(0);
game.doThrowPokeball(PokeballType.POKEBALL);
await game.toEndOfTurn(false);
expect(game.scene.pokeballCounts[PokeballType.POKEBALL]).toBe(4);
await game.toEndOfTurn();
expect(feebas).toHaveAbilityApplied(AbilityId.BALL_FETCH);
expect(game.scene.pokeballCounts[PokeballType.POKEBALL]).toBe(5);
expect(game).toHaveShownMessage(
i18next.t("abilityTriggers:fetchBall", {
pokemonNameWithAffix: getPokemonNameWithAffix(feebas),
pokeballName: getPokeballName(PokeballType.POKEBALL),
}),
);
});
it("should not work on enemies", async () => {
game.override.ability(AbilityId.AIR_LOCK).enemyAbility(AbilityId.BALL_FETCH);
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
const karp = game.field.getEnemyPokemon();
vi.spyOn(karp.species, "catchRate", "get").mockReturnValue(0);
game.doThrowPokeball(PokeballType.POKEBALL);
await game.toEndOfTurn(false);
expect(game.scene.pokeballCounts[PokeballType.POKEBALL]).toBe(4);
await game.toEndOfTurn();
// did nothing; still at 4 balls
expect(karp).not.toHaveAbilityApplied(AbilityId.BALL_FETCH);
expect(game.scene.pokeballCounts[PokeballType.POKEBALL]).toBe(4);
});
});

View File

@ -372,9 +372,13 @@ export class GameManager {
console.log("==================[New Turn]==================");
}
/** Transition to the {@linkcode TurnEndPhase | end of the current turn}. */
async toEndOfTurn() {
await this.phaseInterceptor.to("TurnEndPhase");
/**
* Transition to the {@linkcode TurnEndPhase | end of the current turn}.
* @param endTurn - Whether to run the `TurnEndPhase` or not; default `true`
* @returns A Promise that resolves once the current turn has ended.
*/
async toEndOfTurn(endTurn = true): Promise<void> {
await this.phaseInterceptor.to("TurnEndPhase", endTurn);
console.log("==================[End of Turn]==================");
}