Disable endless boss passives (#3451)

* fix strict null broken

* disable endless boss passives

* jsdocs on mock objects and move helper function to gameManager.ts

* Apply suggestions from flx's review

Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>

* fix broken test

* fix lint

---------

Co-authored-by: ImperialSympathizer <imperialsympathizer@gmail.com>
Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>
This commit is contained in:
ImperialSympathizer 2024-08-11 14:40:47 -04:00 committed by GitHub
parent b20314b45f
commit 988ec664e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 190 additions and 40 deletions

View File

@ -1082,8 +1082,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return true;
}
// Final boss does not have passive
if (this.scene.currentBattle?.battleSpec === BattleSpec.FINAL_BOSS && this instanceof EnemyPokemon) {
// Classic Final boss and Endless Minor/Major bosses do not have passive
const { currentBattle, gameMode } = this.scene;
const waveIndex = currentBattle?.waveIndex;
if (this instanceof EnemyPokemon &&
(currentBattle?.battleSpec === BattleSpec.FINAL_BOSS ||
gameMode.isEndlessMinorBoss(waveIndex) ||
gameMode.isEndlessMajorBoss(waveIndex))) {
return false;
}

View File

@ -0,0 +1,88 @@
import { Biome } from "#app/enums/biome";
import { Species } from "#app/enums/species";
import { GameModes } from "#app/game-mode";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import GameManager from "./utils/gameManager";
const EndlessBossWave = {
Minor: 250,
Major: 1000
};
describe("Endless Boss", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.startingBiome(Biome.END)
.disableCrits();
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
it(`should spawn a minor boss every ${EndlessBossWave.Minor} waves in END biome in Endless`, async () => {
game.override.startingWave(EndlessBossWave.Minor);
await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.ENDLESS);
expect(game.scene.currentBattle.waveIndex).toBe(EndlessBossWave.Minor);
expect(game.scene.arena.biomeType).toBe(Biome.END);
const eternatus = game.scene.getEnemyPokemon();
expect(eternatus?.species.speciesId).toBe(Species.ETERNATUS);
expect(eternatus?.hasPassive()).toBe(false);
expect(eternatus?.formIndex).toBe(0);
});
it(`should spawn a major boss every ${EndlessBossWave.Major} waves in END biome in Endless`, async () => {
game.override.startingWave(EndlessBossWave.Major);
await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.ENDLESS);
expect(game.scene.currentBattle.waveIndex).toBe(EndlessBossWave.Major);
expect(game.scene.arena.biomeType).toBe(Biome.END);
const eternatus = game.scene.getEnemyPokemon();
expect(eternatus?.species.speciesId).toBe(Species.ETERNATUS);
expect(eternatus?.hasPassive()).toBe(false);
expect(eternatus?.formIndex).toBe(1);
});
it(`should spawn a minor boss every ${EndlessBossWave.Minor} waves in END biome in Spliced Endless`, async () => {
game.override.startingWave(EndlessBossWave.Minor);
await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.SPLICED_ENDLESS);
expect(game.scene.currentBattle.waveIndex).toBe(EndlessBossWave.Minor);
expect(game.scene.arena.biomeType).toBe(Biome.END);
const eternatus = game.scene.getEnemyPokemon();
expect(eternatus?.species.speciesId).toBe(Species.ETERNATUS);
expect(eternatus?.hasPassive()).toBe(false);
expect(eternatus?.formIndex).toBe(0);
});
it(`should spawn a major boss every ${EndlessBossWave.Major} waves in END biome in Spliced Endless`, async () => {
game.override.startingWave(EndlessBossWave.Major);
await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.SPLICED_ENDLESS);
expect(game.scene.currentBattle.waveIndex).toBe(EndlessBossWave.Major);
expect(game.scene.arena.biomeType).toBe(Biome.END);
const eternatus = game.scene.getEnemyPokemon();
expect(eternatus?.species.speciesId).toBe(Species.ETERNATUS);
expect(eternatus?.hasPassive()).toBe(false);
expect(eternatus?.formIndex).toBe(1);
});
it(`should NOT spawn major or minor boss outside wave ${EndlessBossWave.Minor}s in END biome`, async () => {
game.override.startingWave(EndlessBossWave.Minor - 1);
await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.ENDLESS);
expect(game.scene.currentBattle.waveIndex).not.toBe(EndlessBossWave.Minor);
expect(game.scene.getEnemyPokemon()!.species.speciesId).not.toBe(Species.ETERNATUS);
});
});

View File

@ -1,11 +1,8 @@
import { Biome } from "#app/enums/biome.js";
import { Species } from "#app/enums/species.js";
import { GameModes, getGameMode } from "#app/game-mode.js";
import { EncounterPhase, SelectStarterPhase } from "#app/phases.js";
import { Mode } from "#app/ui/ui.js";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import GameManager from "./utils/gameManager";
import { generateStarter } from "./utils/gameManagerUtils";
import { GameModes } from "#app/game-mode";
const FinalWave = {
Classic: 200,
@ -31,7 +28,7 @@ describe("Final Boss", () => {
});
it("should spawn Eternatus on wave 200 in END biome", async () => {
await runToFinalBossEncounter(game, [Species.BIDOOF]);
await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.CLASSIC);
expect(game.scene.currentBattle.waveIndex).toBe(FinalWave.Classic);
expect(game.scene.arena.biomeType).toBe(Biome.END);
@ -40,7 +37,7 @@ describe("Final Boss", () => {
it("should NOT spawn Eternatus before wave 200 in END biome", async () => {
game.override.startingWave(FinalWave.Classic - 1);
await runToFinalBossEncounter(game, [Species.BIDOOF]);
await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.CLASSIC);
expect(game.scene.currentBattle.waveIndex).not.toBe(FinalWave.Classic);
expect(game.scene.arena.biomeType).toBe(Biome.END);
@ -49,7 +46,7 @@ describe("Final Boss", () => {
it("should NOT spawn Eternatus outside of END biome", async () => {
game.override.startingBiome(Biome.FOREST);
await runToFinalBossEncounter(game, [Species.BIDOOF]);
await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.CLASSIC);
expect(game.scene.currentBattle.waveIndex).toBe(FinalWave.Classic);
expect(game.scene.arena.biomeType).not.toBe(Biome.END);
@ -57,7 +54,7 @@ describe("Final Boss", () => {
});
it("should not have passive enabled on Eternatus", async () => {
await runToFinalBossEncounter(game, [Species.BIDOOF]);
await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.CLASSIC);
const eternatus = game.scene.getEnemyPokemon();
expect(eternatus?.species.speciesId).toBe(Species.ETERNATUS);
@ -66,32 +63,3 @@ describe("Final Boss", () => {
it.todo("should change form on direct hit down to last boss fragment", () => {});
});
/**
* Helper function to run to the final boss encounter as it's a bit tricky due to extra dialogue
* @param game - The game manager
*/
async function runToFinalBossEncounter(game: GameManager, species: Species[]) {
console.log("===to final boss encounter===");
await game.runToTitle();
game.onNextPrompt("TitlePhase", Mode.TITLE, () => {
game.scene.gameMode = getGameMode(GameModes.CLASSIC);
const starters = generateStarter(game.scene, species);
const selectStarterPhase = new SelectStarterPhase(game.scene);
game.scene.pushPhase(new EncounterPhase(game.scene, false));
selectStarterPhase.initBattle(starters);
});
game.onNextPrompt("EncounterPhase", Mode.MESSAGE, async () => {
// This will skip all entry dialogue (I can't figure out a way to sequentially handle the 8 chained messages via 1 prompt handler)
game.setMode(Mode.MESSAGE);
const encounterPhase = game.scene.getCurrentPhase() as EncounterPhase;
// No need to end phase, this will do it for you
encounterPhase.doEncounterCommon(false);
});
await game.phaseInterceptor.to(EncounterPhase, true);
console.log("===finished run to final boss encounter===");
}

View File

@ -141,6 +141,38 @@ export default class GameManager {
}
}
/**
* Helper function to run to the final boss encounter as it's a bit tricky due to extra dialogue
* Also handles Major/Minor bosses from endless modes
* @param game - The game manager
* @param species
* @param mode
*/
async runToFinalBossEncounter(game: GameManager, species: Species[], mode: GameModes) {
console.log("===to final boss encounter===");
await game.runToTitle();
game.onNextPrompt("TitlePhase", Mode.TITLE, () => {
game.scene.gameMode = getGameMode(mode);
const starters = generateStarter(game.scene, species);
const selectStarterPhase = new SelectStarterPhase(game.scene);
game.scene.pushPhase(new EncounterPhase(game.scene, false));
selectStarterPhase.initBattle(starters);
});
game.onNextPrompt("EncounterPhase", Mode.MESSAGE, async () => {
// This will skip all entry dialogue (I can't figure out a way to sequentially handle the 8 chained messages via 1 prompt handler)
game.setMode(Mode.MESSAGE);
const encounterPhase = game.scene.getCurrentPhase() as EncounterPhase;
// No need to end phase, this will do it for you
encounterPhase.doEncounterCommon(false);
});
await game.phaseInterceptor.to(EncounterPhase, true);
console.log("===finished run to final boss encounter===");
}
/**
* Transitions to the start of a battle.
* @param species - Optional array of species to start the battle with.

View File

@ -89,6 +89,7 @@ export default class GameWrapper {
frames: {},
});
Pokemon.prototype.enableMask = () => null;
Pokemon.prototype.updateFusionPalette = () => null;
}
setScene(scene: BattleScene) {
@ -128,7 +129,9 @@ export default class GameWrapper {
manager: {
game: this.game,
},
destroy: () => null,
setVolume: () => null,
stop: () => null,
stopByKey: () => null,
on: (evt, callback) => callback(),
key: "",
@ -202,6 +205,7 @@ export default class GameWrapper {
};
const mockTextureManager = new MockTextureManager(this.scene);
this.scene.add = mockTextureManager.add;
this.scene.textures = mockTextureManager;
this.scene.sys.displayList = this.scene.add.displayList;
this.scene.sys.updateList = new UpdateList(this.scene);
this.scene.systems = this.scene.sys;

View File

@ -6,8 +6,11 @@ import MockImage from "#test/utils/mocks/mocksContainer/mockImage";
import MockText from "#test/utils/mocks/mocksContainer/mockText";
import MockPolygon from "#test/utils/mocks/mocksContainer/mockPolygon";
import { MockGameObject } from "./mockGameObject";
import MockTexture from "#test/utils/mocks/mocksContainer/mockTexture";
/**
* Stub class for Phaser.Textures.TextureManager
*/
export default class MockTextureManager {
private textures: Map<string, any>;
private scene;
@ -54,6 +57,14 @@ export default class MockTextureManager {
// }
}
/**
* Returns a mock texture
* @param key
*/
get(key) {
return new MockTexture(this, key, null);
}
rectangle(x, y, width, height, fillColor) {
const rectangle = new MockRectangle(this, x, y, width, height, fillColor);
this.list.push(rectangle);

View File

@ -0,0 +1,42 @@
import { MockGameObject } from "../mockGameObject";
import MockTextureManager from "#test/utils/mocks/mockTextureManager";
/**
* Stub for Phaser.Textures.Texture object
* Just mocks the function calls and data required for use in tests
*/
export default class MockTexture implements MockGameObject {
public manager: MockTextureManager;
public key: string;
public source;
public frames: object;
public firstFrame: string;
constructor(manager, key: string, source) {
this.manager = manager;
this.key = key;
this.source = source;
const mockFrame = {
width: 100,
height: 100,
cutX: 0,
cutY: 0
};
this.frames = {
firstFrame: mockFrame,
0: mockFrame,
1: mockFrame,
2: mockFrame,
3: mockFrame,
4: mockFrame
};
this.firstFrame = "firstFrame";
}
/** Mocks the function call that gets an HTMLImageElement, see {@link Pokemon.updateFusionPalette} */
getSourceImage() {
return null;
}
}