[Bug] Fix final boss 2nd phase transition on status effect damage (#3226)

* fix final-boss phase change on status-effect dmg

- move final-boss 2nd phase check into battle-scene.
- call method at the end of damage phase
- call method at the end of PostTurnStatusEffect phase

* improve ui.getHandler types

* WIP: final_boss.test.ts

* add "should spawn Eternatus on wave 200 in END biome" test

* add more final_boss tests

couldn't cover the form change due to lack of support from current test framework
This commit is contained in:
flx-sta 2024-07-29 20:51:56 -07:00 committed by GitHub
parent c2b2cf08db
commit 7582eefabc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 144 additions and 33 deletions

View File

@ -1,6 +1,6 @@
import Phaser from "phaser";
import UI from "./ui/ui";
import { NextEncounterPhase, NewBiomeEncounterPhase, SelectBiomePhase, MessagePhase, TurnInitPhase, ReturnPhase, LevelCapPhase, ShowTrainerPhase, LoginPhase, MovePhase, TitlePhase, SwitchPhase } from "./phases";
import { NextEncounterPhase, NewBiomeEncounterPhase, SelectBiomePhase, MessagePhase, TurnInitPhase, ReturnPhase, LevelCapPhase, ShowTrainerPhase, LoginPhase, MovePhase, TitlePhase, SwitchPhase, SummonPhase, ToggleDoublePositionPhase } from "./phases";
import Pokemon, { PlayerPokemon, EnemyPokemon } from "./field/pokemon";
import PokemonSpecies, { PokemonSpeciesFilter, allSpecies, getPokemonSpecies } from "./data/pokemon-species";
import { Constructor } from "#app/utils";
@ -14,7 +14,7 @@ import { Arena, ArenaBase } from "./field/arena";
import { GameData } from "./system/game-data";
import { TextStyle, addTextObject, getTextColor } from "./ui/text";
import { allMoves } from "./data/move";
import { ModifierPoolType, getDefaultModifierTypeForTier, getEnemyModifierTypesForWave, getLuckString, getLuckTextTint, getModifierPoolForType, getPartyLuckValue } from "./modifier/modifier-type";
import { ModifierPoolType, getDefaultModifierTypeForTier, getEnemyModifierTypesForWave, getLuckString, getLuckTextTint, getModifierPoolForType, getModifierType, getPartyLuckValue, modifierTypes } from "./modifier/modifier-type";
import AbilityBar from "./ui/ability-bar";
import { BlockItemTheftAbAttr, DoubleBattleChanceAbAttr, IncrementMovePriorityAbAttr, PostBattleInitAbAttr, applyAbAttrs, applyPostBattleInitAbAttrs } from "./data/ability";
import { allAbilities } from "./data/ability";
@ -68,6 +68,7 @@ import { UiTheme } from "#enums/ui-theme";
import { TimedEventManager } from "#app/timed-event-manager.js";
import i18next from "i18next";
import {TrainerType} from "#enums/trainer-type";
import { battleSpecDialogue } from "./data/dialogue";
export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1";
@ -2619,4 +2620,33 @@ export default class BattleScene extends SceneBase {
};
(window as any).gameInfo = gameInfo;
}
/**
* Initialized the 2nd phase of the final boss (e.g. form-change for Eternatus)
* @param pokemon The (enemy) pokemon
*/
initFinalBossPhaseTwo(pokemon: Pokemon): void {
if (pokemon instanceof EnemyPokemon && pokemon.isBoss() && !pokemon.formIndex && pokemon.bossSegmentIndex < 1) {
this.fadeOutBgm(Utils.fixedInt(2000), false);
this.ui.showDialogue(battleSpecDialogue[BattleSpec.FINAL_BOSS].firstStageWin, pokemon.species.name, null, () => {
this.addEnemyModifier(getModifierType(modifierTypes.MINI_BLACK_HOLE).newModifier(pokemon) as PersistentModifier, false, true);
pokemon.generateAndPopulateMoveset(1);
this.setFieldScale(0.75);
this.triggerPokemonFormChange(pokemon, SpeciesFormChangeManualTrigger, false);
this.currentBattle.double = true;
const availablePartyMembers = this.getParty().filter((p) => p.isAllowedInBattle());
if (availablePartyMembers.length > 1) {
this.pushPhase(new ToggleDoublePositionPhase(this, true));
if (!availablePartyMembers[1].isOnField()) {
this.pushPhase(new SummonPhase(this, 1));
}
}
this.shiftPhase();
});
return;
}
this.shiftPhase();
}
}

View File

@ -5,7 +5,7 @@ import { allMoves, applyMoveAttrs, BypassSleepAttr, ChargeAttr, applyFilteredMov
import { Mode } from "./ui/ui";
import { Command } from "./ui/command-ui-handler";
import { Stat } from "./data/pokemon-stat";
import { BerryModifier, ContactHeldItemTransferChanceModifier, EnemyAttackStatusEffectChanceModifier, EnemyPersistentModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, HealingBoosterModifier, HitHealModifier, LapsingPersistentModifier, MapModifier, Modifier, MultipleParticipantExpBonusModifier, PersistentModifier, PokemonExpBoosterModifier, PokemonHeldItemModifier, PokemonInstantReviveModifier, SwitchEffectTransferModifier, TurnHealModifier, TurnHeldItemTransferModifier, MoneyMultiplierModifier, MoneyInterestModifier, IvScannerModifier, LapsingPokemonHeldItemModifier, PokemonMultiHitModifier, overrideModifiers, overrideHeldItems, BypassSpeedChanceModifier, TurnStatusEffectModifier, PokemonResetNegativeStatStageModifier } from "./modifier/modifier";
import { BerryModifier, ContactHeldItemTransferChanceModifier, EnemyAttackStatusEffectChanceModifier, EnemyPersistentModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, HealingBoosterModifier, HitHealModifier, LapsingPersistentModifier, MapModifier, Modifier, MultipleParticipantExpBonusModifier, PokemonExpBoosterModifier, PokemonHeldItemModifier, PokemonInstantReviveModifier, SwitchEffectTransferModifier, TurnHealModifier, TurnHeldItemTransferModifier, MoneyMultiplierModifier, MoneyInterestModifier, IvScannerModifier, LapsingPokemonHeldItemModifier, PokemonMultiHitModifier, overrideModifiers, overrideHeldItems, BypassSpeedChanceModifier, TurnStatusEffectModifier, PokemonResetNegativeStatStageModifier } from "./modifier/modifier";
import PartyUiHandler, { PartyOption, PartyUiMode } from "./ui/party-ui-handler";
import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor, PokeballType } from "./data/pokeball";
import { CommonAnim, CommonBattleAnim, MoveAnim, initMoveAnim, loadMoveAnimAssets } from "./data/battle-anims";
@ -37,7 +37,7 @@ import { vouchers } from "./system/voucher";
import { clientSessionId, loggedInUser, updateUserInfo } from "./account";
import { SessionSaveData } from "./system/game-data";
import { addPokeballCaptureStars, addPokeballOpenParticles } from "./field/anims";
import { SpeciesFormChangeActiveTrigger, SpeciesFormChangeManualTrigger, SpeciesFormChangeMoveLearnedTrigger, SpeciesFormChangePostMoveTrigger, SpeciesFormChangePreMoveTrigger } from "./data/pokemon-forms";
import { SpeciesFormChangeActiveTrigger, SpeciesFormChangeMoveLearnedTrigger, SpeciesFormChangePostMoveTrigger, SpeciesFormChangePreMoveTrigger } from "./data/pokemon-forms";
import { battleSpecDialogue, getCharVariantFromDialogue, miscDialogue } from "./data/dialogue";
import ModifierSelectUiHandler, { SHOP_OPTIONS_ROW_LIMIT } from "./ui/modifier-select-ui-handler";
import { SettingKeys } from "./system/settings/settings";
@ -3598,6 +3598,14 @@ export class PostTurnStatusEffectPhase extends PokemonPhase {
this.end();
}
}
override end() {
if (this.scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) {
this.scene.initFinalBossPhaseTwo(this.getPokemon());
} else {
super.end();
}
}
}
export class MessagePhase extends Phase {
@ -3705,34 +3713,12 @@ export class DamagePhase extends PokemonPhase {
}
}
end() {
switch (this.scene.currentBattle.battleSpec) {
case BattleSpec.FINAL_BOSS:
const pokemon = this.getPokemon();
if (pokemon instanceof EnemyPokemon && pokemon.isBoss() && !pokemon.formIndex && pokemon.bossSegmentIndex < 1) {
this.scene.fadeOutBgm(Utils.fixedInt(2000), false);
this.scene.ui.showDialogue(battleSpecDialogue[BattleSpec.FINAL_BOSS].firstStageWin, pokemon.species.name, null, () => {
this.scene.addEnemyModifier(getModifierType(modifierTypes.MINI_BLACK_HOLE).newModifier(pokemon) as PersistentModifier, false, true);
pokemon.generateAndPopulateMoveset(1);
this.scene.setFieldScale(0.75);
this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeManualTrigger, false);
this.scene.currentBattle.double = true;
const availablePartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle());
if (availablePartyMembers.length > 1) {
this.scene.pushPhase(new ToggleDoublePositionPhase(this.scene, true));
if (!availablePartyMembers[1].isOnField()) {
this.scene.pushPhase(new SummonPhase(this.scene, 1));
}
}
override end() {
if (this.scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) {
this.scene.initFinalBossPhaseTwo(this.getPokemon());
} else {
super.end();
});
return;
}
break;
}
super.end();
}
}

View File

@ -0,0 +1,89 @@
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";
const FinalWave = {
Classic: 200,
};
describe("Final Boss", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override.startingWave(FinalWave.Classic).startingBiome(Biome.END).disableCrits();
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
it("should spawn Eternatus on wave 200 in END biome", async () => {
await runToFinalBossEncounter(game, [Species.BIDOOF]);
expect(game.scene.currentBattle.waveIndex).toBe(FinalWave.Classic);
expect(game.scene.arena.biomeType).toBe(Biome.END);
expect(game.scene.getEnemyPokemon().species.speciesId).toBe(Species.ETERNATUS);
});
it("should NOT spawn Eternatus before wave 200 in END biome", async () => {
game.override.startingWave(FinalWave.Classic - 1);
await runToFinalBossEncounter(game, [Species.BIDOOF]);
expect(game.scene.currentBattle.waveIndex).not.toBe(FinalWave.Classic);
expect(game.scene.arena.biomeType).toBe(Biome.END);
expect(game.scene.getEnemyPokemon().species.speciesId).not.toBe(Species.ETERNATUS);
});
it("should NOT spawn Eternatus outside of END biome", async () => {
game.override.startingBiome(Biome.FOREST);
await runToFinalBossEncounter(game, [Species.BIDOOF]);
expect(game.scene.currentBattle.waveIndex).toBe(FinalWave.Classic);
expect(game.scene.arena.biomeType).not.toBe(Biome.END);
expect(game.scene.getEnemyPokemon().species.speciesId).not.toBe(Species.ETERNATUS);
});
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

@ -11,6 +11,11 @@ export default class TextInterceptor {
this.logs.push(text);
}
showDialogue(text: string, name: string, delay?: integer, callback?: Function, callbackDelay?: integer, promptDelay?: integer): void {
console.log(name, text);
this.logs.push(name, text);
}
getLatestMessage(): string {
return this.logs.pop();
}

View File

@ -31,6 +31,7 @@ export default class MockSprite {
};
this.anims = {
pause: () => null,
stop: () => null,
};
}

View File

@ -235,8 +235,8 @@ export default class UI extends Phaser.GameObjects.Container {
(this.scene as BattleScene).uiContainer.add(this.tooltipContainer);
}
getHandler(): UiHandler {
return this.handlers[this.mode];
getHandler<H extends UiHandler = UiHandler>(): H {
return this.handlers[this.mode] as H;
}
getMessageHandler(): BattleMessageUiHandler {