[Balance] End of turn triggers won't occur when you end a biome (#6169)

* [Balance] End of turn triggers won't occur when you end a biome

* Add tests

* Move phase manipulation logic into `PhaseManager`

* Rename "biome end" to "interlude"

* Rename `TurnEndPhase#endOfBiome` to `upcomingInterlude`

---------

Co-authored-by: Wlowscha <54003515+Wlowscha@users.noreply.github.com>
This commit is contained in:
NightKev 2025-07-29 15:50:57 -07:00 committed by GitHub
parent ede2a947ca
commit 10b9cfcdb0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 104 additions and 6 deletions

View File

@ -9,6 +9,7 @@ import { AttemptCapturePhase } from "#phases/attempt-capture-phase";
import { AttemptRunPhase } from "#phases/attempt-run-phase";
import { BattleEndPhase } from "#phases/battle-end-phase";
import { BerryPhase } from "#phases/berry-phase";
import { CheckInterludePhase } from "#phases/check-interlude-phase";
import { CheckStatusEffectPhase } from "#phases/check-status-effect-phase";
import { CheckSwitchPhase } from "#phases/check-switch-phase";
import { CommandPhase } from "#phases/command-phase";
@ -121,6 +122,7 @@ const PHASES = Object.freeze({
AttemptRunPhase,
BattleEndPhase,
BerryPhase,
CheckInterludePhase,
CheckStatusEffectPhase,
CheckSwitchPhase,
CommandPhase,
@ -665,4 +667,15 @@ export class PhaseManager {
): void {
this.startDynamicPhase(this.create(phase, ...args));
}
/** Prevents end of turn effects from triggering when transitioning to a new biome on a X0 wave */
public onInterlude(): void {
const phasesToRemove = ["WeatherEffectPhase", "BerryPhase", "CheckStatusEffectPhase"];
this.phaseQueue = this.phaseQueue.filter(p => !phasesToRemove.includes(p.phaseName));
const turnEndPhase = this.findPhase<TurnEndPhase>(p => p.phaseName === "TurnEndPhase");
if (turnEndPhase) {
turnEndPhase.upcomingInterlude = true;
}
}
}

View File

@ -0,0 +1,18 @@
import { globalScene } from "#app/global-scene";
import { Phase } from "#app/phase";
export class CheckInterludePhase extends Phase {
public override readonly phaseName = "CheckInterludePhase";
public override start(): void {
super.start();
const { phaseManager } = globalScene;
const { waveIndex } = globalScene.currentBattle;
if (waveIndex % 10 === 0 && globalScene.getEnemyParty().every(p => p.isFainted())) {
phaseManager.onInterlude();
}
this.end();
}
}

View File

@ -18,6 +18,8 @@ import i18next from "i18next";
export class TurnEndPhase extends FieldPhase {
public readonly phaseName = "TurnEndPhase";
public upcomingInterlude = false;
start() {
super.start();
@ -59,9 +61,11 @@ export class TurnEndPhase extends FieldPhase {
pokemon.tempSummonData.waveTurnCount++;
};
this.executeForAll(handlePokemon);
if (!this.upcomingInterlude) {
this.executeForAll(handlePokemon);
globalScene.arena.lapseTags();
globalScene.arena.lapseTags();
}
if (globalScene.arena.weather && !globalScene.arena.weather.lapse()) {
globalScene.arena.trySetWeather(WeatherType.NONE);

View File

@ -218,6 +218,7 @@ export class TurnStartPhase extends FieldPhase {
break;
}
}
phaseManager.pushNew("CheckInterludePhase");
phaseManager.pushNew("WeatherEffectPhase");
phaseManager.pushNew("BerryPhase");
@ -227,10 +228,10 @@ export class TurnStartPhase extends FieldPhase {
phaseManager.pushNew("TurnEndPhase");
/**
* this.end() will call shiftPhase(), which dumps everything from PrependQueue (aka everything that is unshifted()) to the front
* of the queue and dequeues to start the next phase
* this is important since stuff like SwitchSummon, AttemptRun, AttemptCapture Phases break the "flow" and should take precedence
/*
* `this.end()` will call `PhaseManager#shiftPhase()`, which dumps everything from `phaseQueuePrepend`
* (aka everything that is queued via `unshift()`) to the front of the queue and dequeues to start the next phase.
* This is important since stuff like `SwitchSummonPhase`, `AttemptRunPhase`, and `AttemptCapturePhase` break the "flow" and should take precedence
*/
this.end();
}

View File

@ -0,0 +1,62 @@
import { AbilityId } from "#enums/ability-id";
import { BerryType } from "#enums/berry-type";
import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id";
import { WeatherType } from "#enums/weather-type";
import { GameManager } from "#test/test-utils/game-manager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Check Biome End Phase", () => {
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
.enemySpecies(SpeciesId.MAGIKARP)
.enemyMoveset(MoveId.SPLASH)
.enemyAbility(AbilityId.BALL_FETCH)
.ability(AbilityId.BALL_FETCH)
.startingLevel(100);
});
it("should not trigger end of turn effects when defeating the final pokemon of a biome in classic", async () => {
game.override
.startingWave(10)
.weather(WeatherType.SANDSTORM)
.startingHeldItems([{ name: "BERRY", type: BerryType.SITRUS }]);
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
const player = game.field.getPlayerPokemon();
player.hp = 1;
game.move.use(MoveId.EXTREME_SPEED);
await game.toEndOfTurn();
expect(player.hp).toBe(1);
});
it("should not prevent end of turn effects when transitioning waves within a biome", async () => {
game.override.weather(WeatherType.SANDSTORM);
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
const player = game.field.getPlayerPokemon();
game.move.use(MoveId.EXTREME_SPEED);
await game.toEndOfTurn();
expect(player.hp).toBeLessThan(player.getMaxHp());
});
});