mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-06-20 16:42:45 +02:00
* Extract Mode enum out of UI and into its own file Reduces circular imports from 909 to 773 * Move around utility files Reduces cyclical dependencies from 773 to 765 * Remove starterColors and bypassLogin from battle-scene Reduces cyclical dependencies from 765 to 623 * Fix test runner error * Update import for bypassLogin in test * Update mocks for utils in tests * Fix broken tests * Update selectWithTera override * Update path for utils in ab-attr.ts * Update path for utils in ability-class.ts * Fix utils import path in healer.test.ts
653 lines
24 KiB
TypeScript
653 lines
24 KiB
TypeScript
import { BattlerTagLapseType } from "#app/data/battler-tags";
|
|
import type { OptionPhaseCallback } from "#app/data/mystery-encounters/mystery-encounter-option";
|
|
import type MysteryEncounterOption from "#app/data/mystery-encounters/mystery-encounter-option";
|
|
import { SeenEncounterData } from "#app/data/mystery-encounters/mystery-encounter-save-data";
|
|
import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
|
|
import { CheckSwitchPhase } from "#app/phases/check-switch-phase";
|
|
import { GameOverPhase } from "#app/phases/game-over-phase";
|
|
import { NewBattlePhase } from "#app/phases/new-battle-phase";
|
|
import { PostTurnStatusEffectPhase } from "#app/phases/post-turn-status-effect-phase";
|
|
import { ReturnPhase } from "#app/phases/return-phase";
|
|
import { ScanIvsPhase } from "#app/phases/scan-ivs-phase";
|
|
import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
|
|
import { SummonPhase } from "#app/phases/summon-phase";
|
|
import { SwitchPhase } from "#app/phases/switch-phase";
|
|
import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase";
|
|
import { BattleSpec } from "#enums/battle-spec";
|
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
|
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
|
|
import { SwitchType } from "#enums/switch-type";
|
|
import i18next from "i18next";
|
|
import { globalScene } from "#app/global-scene";
|
|
import { getCharVariantFromDialogue } from "../data/dialogue";
|
|
import type { OptionSelectSettings } from "../data/mystery-encounters/utils/encounter-phase-utils";
|
|
import { transitionMysteryEncounterIntroVisuals } from "../data/mystery-encounters/utils/encounter-phase-utils";
|
|
import { TrainerSlot } from "#enums/trainer-slot";
|
|
import { IvScannerModifier } from "../modifier/modifier";
|
|
import { Phase } from "../phase";
|
|
import { UiMode } from "#enums/ui-mode";
|
|
import { isNullOrUndefined, randSeedItem } from "#app/utils/common";
|
|
|
|
/**
|
|
* Will handle (in order):
|
|
* - Clearing of phase queues to enter the Mystery Encounter game state
|
|
* - Management of session data related to MEs
|
|
* - Initialization of ME option select menu and UI
|
|
* - Execute {@linkcode MysteryEncounter.onPreOptionPhase} logic if it exists for the selected option
|
|
* - Display any `OptionTextDisplay.selected` type dialogue that is set in the {@linkcode MysteryEncounterDialogue} dialogue tree for selected option
|
|
* - Queuing of the {@linkcode MysteryEncounterOptionSelectedPhase}
|
|
*/
|
|
export class MysteryEncounterPhase extends Phase {
|
|
private readonly FIRST_DIALOGUE_PROMPT_DELAY = 300;
|
|
optionSelectSettings?: OptionSelectSettings;
|
|
|
|
/**
|
|
* Mostly useful for having repeated queries during a single encounter, where the queries and options may differ each time
|
|
* @param optionSelectSettings allows overriding the typical options of an encounter with new ones
|
|
*/
|
|
constructor(optionSelectSettings?: OptionSelectSettings) {
|
|
super();
|
|
this.optionSelectSettings = optionSelectSettings;
|
|
}
|
|
|
|
/**
|
|
* Updates seed offset, sets seen encounter session data, sets UI mode
|
|
*/
|
|
start() {
|
|
super.start();
|
|
|
|
// Clears out queued phases that are part of standard battle
|
|
globalScene.clearPhaseQueue();
|
|
globalScene.clearPhaseQueueSplice();
|
|
|
|
const encounter = globalScene.currentBattle.mysteryEncounter!;
|
|
encounter.updateSeedOffset();
|
|
|
|
if (!this.optionSelectSettings) {
|
|
// Sets flag that ME was encountered, only if this is not a followup option select phase
|
|
// Can be used in later MEs to check for requirements to spawn, run history, etc.
|
|
globalScene.mysteryEncounterSaveData.encounteredEvents.push(
|
|
new SeenEncounterData(encounter.encounterType, encounter.encounterTier, globalScene.currentBattle.waveIndex),
|
|
);
|
|
}
|
|
|
|
// Initiates encounter dialogue window and option select
|
|
globalScene.ui.setMode(UiMode.MYSTERY_ENCOUNTER, this.optionSelectSettings);
|
|
}
|
|
|
|
/**
|
|
* Triggers after a player selects an option for the encounter
|
|
* @param option
|
|
* @param index
|
|
*/
|
|
handleOptionSelect(option: MysteryEncounterOption, index: number): boolean {
|
|
// Set option selected flag
|
|
globalScene.currentBattle.mysteryEncounter!.selectedOption = option;
|
|
|
|
if (!this.optionSelectSettings) {
|
|
// Saves the selected option in the ME save data, only if this is not a followup option select phase
|
|
// Can be used for analytics purposes to track what options are popular on certain encounters
|
|
const encounterSaveData =
|
|
globalScene.mysteryEncounterSaveData.encounteredEvents[
|
|
globalScene.mysteryEncounterSaveData.encounteredEvents.length - 1
|
|
];
|
|
if (encounterSaveData.type === globalScene.currentBattle.mysteryEncounter?.encounterType) {
|
|
encounterSaveData.selectedOption = index;
|
|
}
|
|
}
|
|
|
|
if (!option.onOptionPhase) {
|
|
return false;
|
|
}
|
|
|
|
// Populate dialogue tokens for option requirements
|
|
globalScene.currentBattle.mysteryEncounter!.populateDialogueTokensFromRequirements();
|
|
|
|
if (option.onPreOptionPhase) {
|
|
globalScene.executeWithSeedOffset(async () => {
|
|
return await option.onPreOptionPhase!().then(result => {
|
|
if (isNullOrUndefined(result) || result) {
|
|
this.continueEncounter();
|
|
}
|
|
});
|
|
}, globalScene.currentBattle.mysteryEncounter?.getSeedOffset());
|
|
} else {
|
|
this.continueEncounter();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Queues {@linkcode MysteryEncounterOptionSelectedPhase}, displays option.selected dialogue and ends phase
|
|
*/
|
|
continueEncounter() {
|
|
const endDialogueAndContinueEncounter = () => {
|
|
globalScene.pushPhase(new MysteryEncounterOptionSelectedPhase());
|
|
this.end();
|
|
};
|
|
|
|
const optionSelectDialogue = globalScene.currentBattle?.mysteryEncounter?.selectedOption?.dialogue;
|
|
if (optionSelectDialogue?.selected && optionSelectDialogue.selected.length > 0) {
|
|
// Handle intermediate dialogue (between player selection event and the onOptionSelect logic)
|
|
globalScene.ui.setMode(UiMode.MESSAGE);
|
|
const selectedDialogue = optionSelectDialogue.selected;
|
|
let i = 0;
|
|
const showNextDialogue = () => {
|
|
const nextAction = i === selectedDialogue.length - 1 ? endDialogueAndContinueEncounter : showNextDialogue;
|
|
const dialogue = selectedDialogue[i];
|
|
let title: string | null = null;
|
|
const text: string | null = getEncounterText(dialogue.text);
|
|
if (dialogue.speaker) {
|
|
title = getEncounterText(dialogue.speaker);
|
|
}
|
|
|
|
i++;
|
|
if (title) {
|
|
globalScene.ui.showDialogue(
|
|
text ?? "",
|
|
title,
|
|
null,
|
|
nextAction,
|
|
0,
|
|
i === 1 ? this.FIRST_DIALOGUE_PROMPT_DELAY : 0,
|
|
);
|
|
} else {
|
|
globalScene.ui.showText(text ?? "", null, nextAction, i === 1 ? this.FIRST_DIALOGUE_PROMPT_DELAY : 0, true);
|
|
}
|
|
};
|
|
|
|
showNextDialogue();
|
|
} else {
|
|
endDialogueAndContinueEncounter();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ends phase
|
|
*/
|
|
end() {
|
|
globalScene.ui.setMode(UiMode.MESSAGE).then(() => super.end());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Will handle (in order):
|
|
* - Execute {@linkcode MysteryEncounter.onOptionSelect} logic if it exists for the selected option
|
|
*
|
|
* It is important to point out that no phases are directly queued by any logic within this phase
|
|
* Any phase that is meant to follow this one MUST be queued via the onOptionSelect() logic of the selected option
|
|
*/
|
|
export class MysteryEncounterOptionSelectedPhase extends Phase {
|
|
onOptionSelect: OptionPhaseCallback;
|
|
|
|
constructor() {
|
|
super();
|
|
this.onOptionSelect = globalScene.currentBattle.mysteryEncounter!.selectedOption!.onOptionPhase;
|
|
}
|
|
|
|
/**
|
|
* Will handle (in order):
|
|
* - Execute {@linkcode MysteryEncounter.onOptionSelect} logic if it exists for the selected option
|
|
*
|
|
* It is important to point out that no phases are directly queued by any logic within this phase.
|
|
* Any phase that is meant to follow this one MUST be queued via the {@linkcode MysteryEncounter.onOptionSelect} logic of the selected option.
|
|
*/
|
|
start() {
|
|
super.start();
|
|
if (globalScene.currentBattle.mysteryEncounter?.autoHideIntroVisuals) {
|
|
transitionMysteryEncounterIntroVisuals().then(() => {
|
|
globalScene.executeWithSeedOffset(() => {
|
|
this.onOptionSelect().finally(() => {
|
|
this.end();
|
|
});
|
|
}, globalScene.currentBattle.mysteryEncounter?.getSeedOffset() * 500);
|
|
});
|
|
} else {
|
|
globalScene.executeWithSeedOffset(() => {
|
|
this.onOptionSelect().finally(() => {
|
|
this.end();
|
|
});
|
|
}, globalScene.currentBattle.mysteryEncounter?.getSeedOffset() * 500);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Runs at the beginning of an Encounter's battle
|
|
* Will clean up any residual flinches, Endure, etc. that are left over from {@linkcode MysteryEncounter.startOfBattleEffects}
|
|
* Will also handle Game Overs, switches, etc. that could happen from {@linkcode handleMysteryEncounterBattleStartEffects}
|
|
* See {@linkcode TurnEndPhase} for more details
|
|
*/
|
|
export class MysteryEncounterBattleStartCleanupPhase extends Phase {
|
|
/**
|
|
* Cleans up `TURN_END` tags, any {@linkcode PostTurnStatusEffectPhase}s, checks for Pokemon switches, then continues
|
|
*/
|
|
start() {
|
|
super.start();
|
|
|
|
// Lapse any residual flinches/endures but ignore all other turn-end battle tags
|
|
const includedLapseTags = [BattlerTagType.FLINCHED, BattlerTagType.ENDURING];
|
|
const field = globalScene.getField(true).filter(p => p.summonData);
|
|
field.forEach(pokemon => {
|
|
const tags = pokemon.summonData.tags;
|
|
tags
|
|
.filter(
|
|
t =>
|
|
includedLapseTags.includes(t.tagType) &&
|
|
t.lapseTypes.includes(BattlerTagLapseType.TURN_END) &&
|
|
!t.lapse(pokemon, BattlerTagLapseType.TURN_END),
|
|
)
|
|
.forEach(t => {
|
|
t.onRemove(pokemon);
|
|
tags.splice(tags.indexOf(t), 1);
|
|
});
|
|
});
|
|
|
|
// Remove any status tick phases
|
|
while (globalScene.findPhase(p => p instanceof PostTurnStatusEffectPhase)) {
|
|
globalScene.tryRemovePhase(p => p instanceof PostTurnStatusEffectPhase);
|
|
}
|
|
|
|
// The total number of Pokemon in the player's party that can legally fight
|
|
const legalPlayerPokemon = globalScene.getPokemonAllowedInBattle();
|
|
// The total number of legal player Pokemon that aren't currently on the field
|
|
const legalPlayerPartyPokemon = legalPlayerPokemon.filter(p => !p.isActive(true));
|
|
if (!legalPlayerPokemon.length) {
|
|
globalScene.unshiftPhase(new GameOverPhase());
|
|
return this.end();
|
|
}
|
|
|
|
// Check for any KOd player mons and switch
|
|
// For each fainted mon on the field, if there is a legal replacement, summon it
|
|
const playerField = globalScene.getPlayerField();
|
|
playerField.forEach((pokemon, i) => {
|
|
if (!pokemon.isAllowedInBattle() && legalPlayerPartyPokemon.length > i) {
|
|
globalScene.unshiftPhase(new SwitchPhase(SwitchType.SWITCH, i, true, false));
|
|
}
|
|
});
|
|
|
|
// THEN, if is a double battle, and player only has 1 summoned pokemon, center pokemon on field
|
|
if (globalScene.currentBattle.double && legalPlayerPokemon.length === 1 && legalPlayerPartyPokemon.length === 0) {
|
|
globalScene.unshiftPhase(new ToggleDoublePositionPhase(true));
|
|
}
|
|
|
|
this.end();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Will handle (in order):
|
|
* - Setting BGM
|
|
* - Showing intro dialogue for an enemy trainer or wild Pokemon
|
|
* - Sliding in the visuals for enemy trainer or wild Pokemon, as well as handling summoning animations
|
|
* - Queue the {@linkcode SummonPhase}s, {@linkcode PostSummonPhase}s, etc., required to initialize the phase queue for a battle
|
|
*/
|
|
export class MysteryEncounterBattlePhase extends Phase {
|
|
disableSwitch: boolean;
|
|
|
|
constructor(disableSwitch = false) {
|
|
super();
|
|
this.disableSwitch = disableSwitch;
|
|
}
|
|
|
|
/**
|
|
* Sets up a ME battle
|
|
*/
|
|
start() {
|
|
super.start();
|
|
|
|
this.doMysteryEncounterBattle();
|
|
}
|
|
|
|
/**
|
|
* Gets intro battle message for new battle
|
|
* @private
|
|
*/
|
|
private getBattleMessage(): string {
|
|
const enemyField = globalScene.getEnemyField();
|
|
const encounterMode = globalScene.currentBattle.mysteryEncounter!.encounterMode;
|
|
|
|
if (globalScene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) {
|
|
return i18next.t("battle:bossAppeared", { bossName: enemyField[0].name });
|
|
}
|
|
|
|
if (encounterMode === MysteryEncounterMode.TRAINER_BATTLE) {
|
|
if (globalScene.currentBattle.double) {
|
|
return i18next.t("battle:trainerAppearedDouble", {
|
|
trainerName: globalScene.currentBattle.trainer?.getName(TrainerSlot.NONE, true),
|
|
});
|
|
}
|
|
return i18next.t("battle:trainerAppeared", {
|
|
trainerName: globalScene.currentBattle.trainer?.getName(TrainerSlot.NONE, true),
|
|
});
|
|
}
|
|
|
|
return enemyField.length === 1
|
|
? i18next.t("battle:singleWildAppeared", {
|
|
pokemonName: enemyField[0].name,
|
|
})
|
|
: i18next.t("battle:multiWildAppeared", {
|
|
pokemonName1: enemyField[0].name,
|
|
pokemonName2: enemyField[1].name,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Queues {@linkcode SummonPhase}s for the new battle, and handles trainer animations/dialogue if it's a Trainer battle
|
|
* @private
|
|
*/
|
|
private doMysteryEncounterBattle() {
|
|
const encounterMode = globalScene.currentBattle.mysteryEncounter!.encounterMode;
|
|
if (encounterMode === MysteryEncounterMode.WILD_BATTLE || encounterMode === MysteryEncounterMode.BOSS_BATTLE) {
|
|
// Summons the wild/boss Pokemon
|
|
if (encounterMode === MysteryEncounterMode.BOSS_BATTLE) {
|
|
globalScene.playBgm();
|
|
}
|
|
const availablePartyMembers = globalScene.getEnemyParty().filter(p => !p.isFainted()).length;
|
|
globalScene.unshiftPhase(new SummonPhase(0, false));
|
|
if (globalScene.currentBattle.double && availablePartyMembers > 1) {
|
|
globalScene.unshiftPhase(new SummonPhase(1, false));
|
|
}
|
|
|
|
if (!globalScene.currentBattle.mysteryEncounter?.hideBattleIntroMessage) {
|
|
globalScene.ui.showText(this.getBattleMessage(), null, () => this.endBattleSetup(), 0);
|
|
} else {
|
|
this.endBattleSetup();
|
|
}
|
|
} else if (encounterMode === MysteryEncounterMode.TRAINER_BATTLE) {
|
|
this.showEnemyTrainer();
|
|
const doSummon = () => {
|
|
globalScene.currentBattle.started = true;
|
|
globalScene.playBgm();
|
|
globalScene.pbTray.showPbTray(globalScene.getPlayerParty());
|
|
globalScene.pbTrayEnemy.showPbTray(globalScene.getEnemyParty());
|
|
const doTrainerSummon = () => {
|
|
this.hideEnemyTrainer();
|
|
const availablePartyMembers = globalScene.getEnemyParty().filter(p => !p.isFainted()).length;
|
|
globalScene.unshiftPhase(new SummonPhase(0, false));
|
|
if (globalScene.currentBattle.double && availablePartyMembers > 1) {
|
|
globalScene.unshiftPhase(new SummonPhase(1, false));
|
|
}
|
|
this.endBattleSetup();
|
|
};
|
|
if (!globalScene.currentBattle.mysteryEncounter?.hideBattleIntroMessage) {
|
|
globalScene.ui.showText(this.getBattleMessage(), null, doTrainerSummon, 1000, true);
|
|
} else {
|
|
doTrainerSummon();
|
|
}
|
|
};
|
|
|
|
const encounterMessages = globalScene.currentBattle.trainer?.getEncounterMessages();
|
|
|
|
if (!encounterMessages || !encounterMessages.length) {
|
|
doSummon();
|
|
} else {
|
|
const trainer = globalScene.currentBattle.trainer;
|
|
let message: string;
|
|
globalScene.executeWithSeedOffset(
|
|
() => (message = randSeedItem(encounterMessages)),
|
|
globalScene.currentBattle.mysteryEncounter?.getSeedOffset(),
|
|
);
|
|
message = message!; // tell TS compiler it's defined now
|
|
const showDialogueAndSummon = () => {
|
|
globalScene.ui.showDialogue(message, trainer?.getName(TrainerSlot.NONE, true), null, () => {
|
|
globalScene.charSprite.hide().then(() => globalScene.hideFieldOverlay(250).then(() => doSummon()));
|
|
});
|
|
};
|
|
if (globalScene.currentBattle.trainer?.config.hasCharSprite && !globalScene.ui.shouldSkipDialogue(message)) {
|
|
globalScene
|
|
.showFieldOverlay(500)
|
|
.then(() =>
|
|
globalScene.charSprite
|
|
.showCharacter(trainer?.getKey()!, getCharVariantFromDialogue(encounterMessages[0]))
|
|
.then(() => showDialogueAndSummon()),
|
|
); // TODO: is this bang correct?
|
|
} else {
|
|
showDialogueAndSummon();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initiate {@linkcode SummonPhase}s, {@linkcode ScanIvsPhase}, {@linkcode PostSummonPhase}s, etc.
|
|
* @private
|
|
*/
|
|
private endBattleSetup() {
|
|
const enemyField = globalScene.getEnemyField();
|
|
const encounterMode = globalScene.currentBattle.mysteryEncounter!.encounterMode;
|
|
|
|
// PostSummon and ShinySparkle phases are handled by SummonPhase
|
|
|
|
if (encounterMode !== MysteryEncounterMode.TRAINER_BATTLE) {
|
|
const ivScannerModifier = globalScene.findModifier(m => m instanceof IvScannerModifier);
|
|
if (ivScannerModifier) {
|
|
enemyField.map(p => globalScene.pushPhase(new ScanIvsPhase(p.getBattlerIndex())));
|
|
}
|
|
}
|
|
|
|
const availablePartyMembers = globalScene.getPlayerParty().filter(p => p.isAllowedInBattle());
|
|
|
|
if (!availablePartyMembers[0].isOnField()) {
|
|
globalScene.pushPhase(new SummonPhase(0));
|
|
}
|
|
|
|
if (globalScene.currentBattle.double) {
|
|
if (availablePartyMembers.length > 1) {
|
|
globalScene.pushPhase(new ToggleDoublePositionPhase(true));
|
|
if (!availablePartyMembers[1].isOnField()) {
|
|
globalScene.pushPhase(new SummonPhase(1));
|
|
}
|
|
}
|
|
} else {
|
|
if (availablePartyMembers.length > 1 && availablePartyMembers[1].isOnField()) {
|
|
globalScene.getPlayerField().forEach(pokemon => pokemon.lapseTag(BattlerTagType.COMMANDED));
|
|
globalScene.pushPhase(new ReturnPhase(1));
|
|
}
|
|
globalScene.pushPhase(new ToggleDoublePositionPhase(false));
|
|
}
|
|
|
|
if (encounterMode !== MysteryEncounterMode.TRAINER_BATTLE && !this.disableSwitch) {
|
|
const minPartySize = globalScene.currentBattle.double ? 2 : 1;
|
|
if (availablePartyMembers.length > minPartySize) {
|
|
globalScene.pushPhase(new CheckSwitchPhase(0, globalScene.currentBattle.double));
|
|
if (globalScene.currentBattle.double) {
|
|
globalScene.pushPhase(new CheckSwitchPhase(1, globalScene.currentBattle.double));
|
|
}
|
|
}
|
|
}
|
|
|
|
this.end();
|
|
}
|
|
|
|
/**
|
|
* Ease in enemy trainer
|
|
* @private
|
|
*/
|
|
private showEnemyTrainer(): void {
|
|
// Show enemy trainer
|
|
const trainer = globalScene.currentBattle.trainer;
|
|
if (!trainer) {
|
|
return;
|
|
}
|
|
trainer.alpha = 0;
|
|
trainer.x += 16;
|
|
trainer.y -= 16;
|
|
trainer.setVisible(true);
|
|
globalScene.tweens.add({
|
|
targets: trainer,
|
|
x: "-=16",
|
|
y: "+=16",
|
|
alpha: 1,
|
|
ease: "Sine.easeInOut",
|
|
duration: 750,
|
|
onComplete: () => {
|
|
trainer.untint(100, "Sine.easeOut");
|
|
trainer.playAnim();
|
|
},
|
|
});
|
|
}
|
|
|
|
private hideEnemyTrainer(): void {
|
|
globalScene.tweens.add({
|
|
targets: globalScene.currentBattle.trainer,
|
|
x: "+=16",
|
|
y: "-=16",
|
|
alpha: 0,
|
|
ease: "Sine.easeInOut",
|
|
duration: 750,
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Will handle (in order):
|
|
* - doContinueEncounter() callback for continuous encounters with back-to-back battles (this should push/shift its own phases as needed)
|
|
*
|
|
* OR
|
|
*
|
|
* - Any encounter reward logic that is set within {@linkcode MysteryEncounter.doEncounterExp}
|
|
* - Any encounter reward logic that is set within {@linkcode MysteryEncounter.doEncounterRewards}
|
|
* - Otherwise, can add a no-reward-item shop with only Potions, etc. if addHealPhase is true
|
|
* - Queuing of the {@linkcode PostMysteryEncounterPhase}
|
|
*/
|
|
export class MysteryEncounterRewardsPhase extends Phase {
|
|
addHealPhase: boolean;
|
|
|
|
constructor(addHealPhase = false) {
|
|
super();
|
|
this.addHealPhase = addHealPhase;
|
|
}
|
|
|
|
/**
|
|
* Runs {@linkcode MysteryEncounter.doContinueEncounter} and ends phase, OR {@linkcode MysteryEncounter.onRewards} then continues encounter
|
|
*/
|
|
start() {
|
|
super.start();
|
|
const encounter = globalScene.currentBattle.mysteryEncounter!;
|
|
|
|
if (encounter.doContinueEncounter) {
|
|
encounter.doContinueEncounter().then(() => {
|
|
this.end();
|
|
});
|
|
} else {
|
|
globalScene.executeWithSeedOffset(() => {
|
|
if (encounter.onRewards) {
|
|
encounter.onRewards().then(() => {
|
|
this.doEncounterRewardsAndContinue();
|
|
});
|
|
} else {
|
|
this.doEncounterRewardsAndContinue();
|
|
}
|
|
// Do not use ME's seedOffset for rewards, these should always be consistent with waveIndex (once per wave)
|
|
}, globalScene.currentBattle.waveIndex * 1000);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Queues encounter EXP and rewards phases, {@linkcode PostMysteryEncounterPhase}, and ends phase
|
|
*/
|
|
doEncounterRewardsAndContinue() {
|
|
const encounter = globalScene.currentBattle.mysteryEncounter!;
|
|
|
|
if (encounter.doEncounterExp) {
|
|
encounter.doEncounterExp();
|
|
}
|
|
|
|
if (encounter.doEncounterRewards) {
|
|
encounter.doEncounterRewards();
|
|
} else if (this.addHealPhase) {
|
|
globalScene.tryRemovePhase(p => p instanceof SelectModifierPhase);
|
|
globalScene.unshiftPhase(
|
|
new SelectModifierPhase(0, undefined, {
|
|
fillRemaining: false,
|
|
rerollMultiplier: -1,
|
|
}),
|
|
);
|
|
}
|
|
|
|
globalScene.pushPhase(new PostMysteryEncounterPhase());
|
|
this.end();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Will handle (in order):
|
|
* - {@linkcode MysteryEncounter.onPostOptionSelect} logic (based on an option that was selected)
|
|
* - Showing any outro dialogue messages
|
|
* - Cleanup of any leftover intro visuals
|
|
* - Queuing of the next wave
|
|
*/
|
|
export class PostMysteryEncounterPhase extends Phase {
|
|
private readonly FIRST_DIALOGUE_PROMPT_DELAY = 750;
|
|
onPostOptionSelect?: OptionPhaseCallback;
|
|
|
|
constructor() {
|
|
super();
|
|
this.onPostOptionSelect = globalScene.currentBattle.mysteryEncounter?.selectedOption?.onPostOptionPhase;
|
|
}
|
|
|
|
/**
|
|
* Runs {@linkcode MysteryEncounter.onPostOptionSelect} then continues encounter
|
|
*/
|
|
start() {
|
|
super.start();
|
|
|
|
if (this.onPostOptionSelect) {
|
|
globalScene.executeWithSeedOffset(async () => {
|
|
return await this.onPostOptionSelect!().then(result => {
|
|
if (isNullOrUndefined(result) || result) {
|
|
this.continueEncounter();
|
|
}
|
|
});
|
|
}, globalScene.currentBattle.mysteryEncounter?.getSeedOffset() * 2000);
|
|
} else {
|
|
this.continueEncounter();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Queues {@linkcode NewBattlePhase}, plays outro dialogue and ends phase
|
|
*/
|
|
continueEncounter() {
|
|
const endPhase = () => {
|
|
globalScene.pushPhase(new NewBattlePhase());
|
|
this.end();
|
|
};
|
|
|
|
const outroDialogue = globalScene.currentBattle?.mysteryEncounter?.dialogue?.outro;
|
|
if (outroDialogue && outroDialogue.length > 0) {
|
|
let i = 0;
|
|
const showNextDialogue = () => {
|
|
const nextAction = i === outroDialogue.length - 1 ? endPhase : showNextDialogue;
|
|
const dialogue = outroDialogue[i];
|
|
let title: string | null = null;
|
|
const text: string | null = getEncounterText(dialogue.text);
|
|
if (dialogue.speaker) {
|
|
title = getEncounterText(dialogue.speaker);
|
|
}
|
|
|
|
i++;
|
|
globalScene.ui.setMode(UiMode.MESSAGE);
|
|
if (title) {
|
|
globalScene.ui.showDialogue(
|
|
text ?? "",
|
|
title,
|
|
null,
|
|
nextAction,
|
|
0,
|
|
i === 1 ? this.FIRST_DIALOGUE_PROMPT_DELAY : 0,
|
|
);
|
|
} else {
|
|
globalScene.ui.showText(text ?? "", null, nextAction, i === 1 ? this.FIRST_DIALOGUE_PROMPT_DELAY : 0, true);
|
|
}
|
|
};
|
|
|
|
showNextDialogue();
|
|
} else {
|
|
endPhase();
|
|
}
|
|
}
|
|
}
|