480 lines
17 KiB
TypeScript
480 lines
17 KiB
TypeScript
import i18next from "i18next";
|
|
import BattleScene from "../battle-scene";
|
|
import { Phase } from "../phase";
|
|
import { Mode } from "../ui/ui";
|
|
import { transitionMysteryEncounterIntroVisuals, OptionSelectSettings } from "../data/mystery-encounters/utils/encounter-phase-utils";
|
|
import { CheckSwitchPhase, NewBattlePhase, ReturnPhase, ScanIvsPhase, SelectModifierPhase, SummonPhase, ToggleDoublePositionPhase } from "../phases";
|
|
import MysteryEncounterOption, { OptionPhaseCallback } from "../data/mystery-encounters/mystery-encounter-option";
|
|
import { MysteryEncounterVariant } from "../data/mystery-encounters/mystery-encounter";
|
|
import { getCharVariantFromDialogue } from "../data/dialogue";
|
|
import { TrainerSlot } from "../data/trainer-config";
|
|
import { BattleSpec } from "#enums/battle-spec";
|
|
import { Tutorial, handleTutorial } from "../tutorial";
|
|
import { IvScannerModifier } from "../modifier/modifier";
|
|
import * as Utils from "../utils";
|
|
import { isNullOrUndefined } from "../utils";
|
|
import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
|
|
import { BattlerTagLapseType } from "#app/data/battler-tags";
|
|
|
|
/**
|
|
* 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 onPreOptionPhase() logic if it exists for the selected option
|
|
* - Display any OptionTextDisplay.selected type dialogue that is set in the MysteryEncounterDialogue dialogue tree for selected option
|
|
* - Queuing of the MysteryEncounterOptionSelectedPhase
|
|
*/
|
|
export class MysteryEncounterPhase extends Phase {
|
|
optionSelectSettings: OptionSelectSettings;
|
|
|
|
private FIRST_DIALOGUE_PROMPT_DELAY = 300;
|
|
|
|
/**
|
|
*
|
|
* @param scene
|
|
* @param optionSelectSettings - allows overriding the typical options of an encounter with new ones
|
|
* Mostly useful for having repeated queries during a single encounter, where the queries and options may differ each time
|
|
*/
|
|
constructor(scene: BattleScene, optionSelectSettings?: OptionSelectSettings) {
|
|
super(scene);
|
|
this.optionSelectSettings = optionSelectSettings;
|
|
}
|
|
|
|
start() {
|
|
super.start();
|
|
|
|
// Clears out queued phases that are part of standard battle
|
|
this.scene.clearPhaseQueue();
|
|
this.scene.clearPhaseQueueSplice();
|
|
|
|
this.scene.currentBattle.mysteryEncounter.updateSeedOffset(this.scene);
|
|
|
|
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, etc.
|
|
this.scene.mysteryEncounterData.encounteredEvents.push([this.scene.currentBattle.mysteryEncounter.encounterType, this.scene.currentBattle.mysteryEncounter.encounterTier]);
|
|
}
|
|
|
|
// Initiates encounter dialogue window and option select
|
|
this.scene.ui.setMode(Mode.MYSTERY_ENCOUNTER, this.optionSelectSettings);
|
|
}
|
|
|
|
handleOptionSelect(option: MysteryEncounterOption, index: number): boolean {
|
|
// Set option selected flag
|
|
this.scene.currentBattle.mysteryEncounter.selectedOption = option;
|
|
|
|
if (!option.onOptionPhase) {
|
|
return false;
|
|
}
|
|
|
|
// Populate dialogue tokens for option requirements
|
|
this.scene.currentBattle.mysteryEncounter.populateDialogueTokensFromRequirements(this.scene);
|
|
|
|
if (option.onPreOptionPhase) {
|
|
this.scene.executeWithSeedOffset(async () => {
|
|
return await option.onPreOptionPhase(this.scene)
|
|
.then((result) => {
|
|
if (isNullOrUndefined(result) || result) {
|
|
this.continueEncounter();
|
|
}
|
|
});
|
|
}, this.scene.currentBattle.mysteryEncounter.getSeedOffset());
|
|
} else {
|
|
this.continueEncounter();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
continueEncounter() {
|
|
const endDialogueAndContinueEncounter = () => {
|
|
this.scene.pushPhase(new MysteryEncounterOptionSelectedPhase(this.scene));
|
|
this.end();
|
|
};
|
|
|
|
const optionSelectDialogue = this.scene.currentBattle?.mysteryEncounter?.selectedOption?.dialogue;
|
|
if (optionSelectDialogue?.selected?.length > 0) {
|
|
// Handle intermediate dialogue (between player selection event and the onOptionSelect logic)
|
|
this.scene.ui.setMode(Mode.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;
|
|
const text: string = getEncounterText(this.scene, dialogue.text);
|
|
if (dialogue.speaker) {
|
|
title = getEncounterText(this.scene, dialogue.speaker);
|
|
}
|
|
|
|
if (title) {
|
|
this.scene.ui.showDialogue(text, title, null, nextAction, 0, i === 0 ? this.FIRST_DIALOGUE_PROMPT_DELAY : 0);
|
|
} else {
|
|
this.scene.ui.showText(text, null, nextAction, i === 0 ? this.FIRST_DIALOGUE_PROMPT_DELAY : 0, true);
|
|
}
|
|
i++;
|
|
};
|
|
|
|
showNextDialogue();
|
|
} else {
|
|
endDialogueAndContinueEncounter();
|
|
}
|
|
}
|
|
|
|
cancel() {
|
|
this.end();
|
|
}
|
|
|
|
end() {
|
|
this.scene.ui.setMode(Mode.MESSAGE).then(() => super.end());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Will handle (in order):
|
|
* - Execute 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(scene: BattleScene) {
|
|
super(scene);
|
|
this.onOptionSelect = this.scene.currentBattle.mysteryEncounter.selectedOption.onOptionPhase;
|
|
}
|
|
|
|
start() {
|
|
super.start();
|
|
if (this.scene.currentBattle.mysteryEncounter.autoHideIntroVisuals) {
|
|
transitionMysteryEncounterIntroVisuals(this.scene).then(() => {
|
|
this.scene.executeWithSeedOffset(() => {
|
|
this.onOptionSelect(this.scene).finally(() => {
|
|
this.end();
|
|
});
|
|
}, this.scene.currentBattle.mysteryEncounter.getSeedOffset());
|
|
});
|
|
} else {
|
|
this.scene.executeWithSeedOffset(() => {
|
|
this.onOptionSelect(this.scene).finally(() => {
|
|
this.end();
|
|
});
|
|
}, this.scene.currentBattle.mysteryEncounter.getSeedOffset());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Runs at the beginning of an Encounter's battle
|
|
* Will cleanup any residual flinches, Endure, etc. that are left over from startOfBattleEffects
|
|
* See [TurnEndPhase](../phases.ts) for more details
|
|
*/
|
|
export class MysteryEncounterBattleStartCleanupPhase extends Phase {
|
|
constructor(scene: BattleScene) {
|
|
super(scene);
|
|
}
|
|
|
|
start() {
|
|
super.start();
|
|
|
|
const field = this.scene.getField(true).filter(p => p.summonData);
|
|
field.forEach(pokemon => {
|
|
pokemon.lapseTags(BattlerTagLapseType.TURN_END);
|
|
});
|
|
|
|
super.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 SummonPhases, PostSummonPhases, etc., required to initialize the phase queue for a battle
|
|
*/
|
|
export class MysteryEncounterBattlePhase extends Phase {
|
|
disableSwitch: boolean;
|
|
|
|
constructor(scene: BattleScene, disableSwitch = false) {
|
|
super(scene);
|
|
this.disableSwitch = disableSwitch;
|
|
}
|
|
|
|
start() {
|
|
super.start();
|
|
|
|
this.doMysteryEncounterBattle(this.scene);
|
|
}
|
|
|
|
getBattleMessage(scene: BattleScene): string {
|
|
const enemyField = scene.getEnemyField();
|
|
const encounterVariant = scene.currentBattle.mysteryEncounter.encounterVariant;
|
|
|
|
if (scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) {
|
|
return i18next.t("battle:bossAppeared", { bossName: enemyField[0].name });
|
|
}
|
|
|
|
if (encounterVariant === MysteryEncounterVariant.TRAINER_BATTLE) {
|
|
if (scene.currentBattle.double) {
|
|
return i18next.t("battle:trainerAppearedDouble", { trainerName: scene.currentBattle.trainer.getName(TrainerSlot.NONE, true) });
|
|
|
|
} else {
|
|
return i18next.t("battle:trainerAppeared", { trainerName: scene.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 });
|
|
}
|
|
|
|
doMysteryEncounterBattle(scene: BattleScene) {
|
|
const encounterVariant = scene.currentBattle.mysteryEncounter.encounterVariant;
|
|
if (encounterVariant === MysteryEncounterVariant.WILD_BATTLE || encounterVariant === MysteryEncounterVariant.BOSS_BATTLE) {
|
|
// Summons the wild/boss Pokemon
|
|
if (encounterVariant === MysteryEncounterVariant.BOSS_BATTLE) {
|
|
scene.playBgm(undefined);
|
|
}
|
|
const availablePartyMembers = scene.getEnemyParty().filter(p => !p.isFainted()).length;
|
|
scene.unshiftPhase(new SummonPhase(scene, 0, false));
|
|
if (scene.currentBattle.double && availablePartyMembers > 1) {
|
|
scene.unshiftPhase(new SummonPhase(scene, 1, false));
|
|
}
|
|
|
|
if (!scene.currentBattle.mysteryEncounter.hideBattleIntroMessage) {
|
|
scene.ui.showText(this.getBattleMessage(scene), null, () => this.endBattleSetup(scene), 0);
|
|
} else {
|
|
this.endBattleSetup(scene);
|
|
}
|
|
} else if (encounterVariant === MysteryEncounterVariant.TRAINER_BATTLE) {
|
|
this.showEnemyTrainer();
|
|
const doSummon = () => {
|
|
scene.currentBattle.started = true;
|
|
scene.playBgm(undefined);
|
|
scene.pbTray.showPbTray(scene.getParty());
|
|
scene.pbTrayEnemy.showPbTray(scene.getEnemyParty());
|
|
const doTrainerSummon = () => {
|
|
this.hideEnemyTrainer();
|
|
const availablePartyMembers = scene.getEnemyParty().filter(p => !p.isFainted()).length;
|
|
scene.unshiftPhase(new SummonPhase(scene, 0, false));
|
|
if (scene.currentBattle.double && availablePartyMembers > 1) {
|
|
scene.unshiftPhase(new SummonPhase(scene, 1, false));
|
|
}
|
|
this.endBattleSetup(scene);
|
|
};
|
|
if (!scene.currentBattle.mysteryEncounter.hideBattleIntroMessage) {
|
|
scene.ui.showText(this.getBattleMessage(scene), null, doTrainerSummon, 1000, true);
|
|
} else {
|
|
doTrainerSummon();
|
|
}
|
|
};
|
|
|
|
const encounterMessages = scene.currentBattle.trainer.getEncounterMessages();
|
|
|
|
if (!encounterMessages?.length) {
|
|
doSummon();
|
|
} else {
|
|
const trainer = this.scene.currentBattle.trainer;
|
|
let message: string;
|
|
scene.executeWithSeedOffset(() => message = Utils.randSeedItem(encounterMessages), this.scene.currentBattle.mysteryEncounter.getSeedOffset());
|
|
|
|
const showDialogueAndSummon = () => {
|
|
scene.ui.showDialogue(message, trainer.getName(TrainerSlot.NONE, true), null, () => {
|
|
scene.charSprite.hide().then(() => scene.hideFieldOverlay(250).then(() => doSummon()));
|
|
});
|
|
};
|
|
if (scene.currentBattle.trainer.config.hasCharSprite && !scene.ui.shouldSkipDialogue(message)) {
|
|
scene.showFieldOverlay(500).then(() => scene.charSprite.showCharacter(trainer.getKey(), getCharVariantFromDialogue(encounterMessages[0])).then(() => showDialogueAndSummon()));
|
|
} else {
|
|
showDialogueAndSummon();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
endBattleSetup(scene: BattleScene) {
|
|
const enemyField = scene.getEnemyField();
|
|
const encounterVariant = scene.currentBattle.mysteryEncounter.encounterVariant;
|
|
|
|
// PostSummon and ShinySparkle phases are handled by SummonPhase
|
|
|
|
if (encounterVariant !== MysteryEncounterVariant.TRAINER_BATTLE) {
|
|
const ivScannerModifier = this.scene.findModifier(m => m instanceof IvScannerModifier);
|
|
if (ivScannerModifier) {
|
|
enemyField.map(p => this.scene.pushPhase(new ScanIvsPhase(this.scene, p.getBattlerIndex(), Math.min(ivScannerModifier.getStackCount() * 2, 6))));
|
|
}
|
|
}
|
|
|
|
const availablePartyMembers = scene.getParty().filter(p => !p.isFainted());
|
|
|
|
if (!availablePartyMembers[0].isOnField()) {
|
|
scene.pushPhase(new SummonPhase(scene, 0));
|
|
}
|
|
|
|
if (scene.currentBattle.double) {
|
|
if (availablePartyMembers.length > 1) {
|
|
scene.pushPhase(new ToggleDoublePositionPhase(scene, true));
|
|
if (!availablePartyMembers[1].isOnField()) {
|
|
scene.pushPhase(new SummonPhase(scene, 1));
|
|
}
|
|
}
|
|
} else {
|
|
if (availablePartyMembers.length > 1 && availablePartyMembers[1].isOnField()) {
|
|
scene.pushPhase(new ReturnPhase(scene, 1));
|
|
}
|
|
scene.pushPhase(new ToggleDoublePositionPhase(scene, false));
|
|
}
|
|
|
|
if (encounterVariant !== MysteryEncounterVariant.TRAINER_BATTLE && !this.disableSwitch) {
|
|
const minPartySize = scene.currentBattle.double ? 2 : 1;
|
|
if (availablePartyMembers.length > minPartySize) {
|
|
scene.pushPhase(new CheckSwitchPhase(scene, 0, scene.currentBattle.double));
|
|
if (scene.currentBattle.double) {
|
|
scene.pushPhase(new CheckSwitchPhase(scene, 1, scene.currentBattle.double));
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: remove?
|
|
handleTutorial(this.scene, Tutorial.Access_Menu).then(() => super.end());
|
|
}
|
|
|
|
showEnemyTrainer(): void {
|
|
// Show enemy trainer
|
|
const trainer = this.scene.currentBattle.trainer;
|
|
trainer.alpha = 0;
|
|
trainer.x += 16;
|
|
trainer.y -= 16;
|
|
trainer.setVisible(true);
|
|
this.scene.tweens.add({
|
|
targets: trainer,
|
|
x: "-=16",
|
|
y: "+=16",
|
|
alpha: 1,
|
|
ease: "Sine.easeInOut",
|
|
duration: 750,
|
|
onComplete: () => {
|
|
trainer.untint(100, "Sine.easeOut");
|
|
trainer.playAnim();
|
|
}
|
|
});
|
|
}
|
|
|
|
hideEnemyTrainer(): void {
|
|
this.scene.tweens.add({
|
|
targets: this.scene.currentBattle.trainer,
|
|
x: "+=16",
|
|
y: "-=16",
|
|
alpha: 0,
|
|
ease: "Sine.easeInOut",
|
|
duration: 750
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Will handle (in order):
|
|
* - Any encounter reward logic that is set within MysteryEncounter doEncounterExp
|
|
* - Any encounter reward logic that is set within MysteryEncounter doEncounterRewards
|
|
* - Otherwise, can add a no-reward-item shop with only Potions, etc. if addHealPhase is true
|
|
* - Queuing of the PostMysteryEncounterPhase
|
|
*/
|
|
export class MysteryEncounterRewardsPhase extends Phase {
|
|
addHealPhase: boolean;
|
|
|
|
constructor(scene: BattleScene, addHealPhase: boolean = false) {
|
|
super(scene);
|
|
this.addHealPhase = addHealPhase;
|
|
}
|
|
|
|
start() {
|
|
super.start();
|
|
|
|
this.scene.executeWithSeedOffset(() => {
|
|
if (this.scene.currentBattle.mysteryEncounter.doEncounterExp) {
|
|
this.scene.currentBattle.mysteryEncounter.doEncounterExp(this.scene);
|
|
}
|
|
|
|
if (this.scene.currentBattle.mysteryEncounter.doEncounterRewards) {
|
|
this.scene.currentBattle.mysteryEncounter.doEncounterRewards(this.scene);
|
|
} else if (this.addHealPhase) {
|
|
this.scene.tryRemovePhase(p => p instanceof SelectModifierPhase);
|
|
this.scene.unshiftPhase(new SelectModifierPhase(this.scene, 0, null, { fillRemaining: false, rerollMultiplier: 0 }));
|
|
}
|
|
// Do not use ME's seedOffset for rewards, these should always be consistent with waveIndex (once per wave)
|
|
}, this.scene.currentBattle.waveIndex * 1000);
|
|
|
|
this.scene.pushPhase(new PostMysteryEncounterPhase(this.scene));
|
|
this.end();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Will handle (in order):
|
|
* - 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 {
|
|
onPostOptionSelect: OptionPhaseCallback;
|
|
|
|
constructor(scene: BattleScene) {
|
|
super(scene);
|
|
this.onPostOptionSelect = this.scene.currentBattle.mysteryEncounter.selectedOption.onPostOptionPhase;
|
|
}
|
|
|
|
start() {
|
|
super.start();
|
|
|
|
if (this.onPostOptionSelect) {
|
|
this.scene.executeWithSeedOffset(async () => {
|
|
return await this.onPostOptionSelect(this.scene)
|
|
.then((result) => {
|
|
if (isNullOrUndefined(result) || result) {
|
|
this.continueEncounter();
|
|
}
|
|
});
|
|
}, this.scene.currentBattle.mysteryEncounter.getSeedOffset());
|
|
} else {
|
|
this.continueEncounter();
|
|
}
|
|
}
|
|
|
|
continueEncounter() {
|
|
const endPhase = () => {
|
|
this.scene.pushPhase(new NewBattlePhase(this.scene));
|
|
this.end();
|
|
};
|
|
|
|
const outroDialogue = this.scene.currentBattle?.mysteryEncounter?.dialogue?.outro;
|
|
if (outroDialogue?.length > 0) {
|
|
let i = 0;
|
|
const showNextDialogue = () => {
|
|
const nextAction = i === outroDialogue.length - 1 ? endPhase : showNextDialogue;
|
|
const dialogue = outroDialogue[i];
|
|
let title: string = null;
|
|
const text: string = getEncounterText(this.scene, dialogue.text);
|
|
if (dialogue.speaker) {
|
|
title = getEncounterText(this.scene, dialogue.speaker);
|
|
}
|
|
|
|
this.scene.ui.setMode(Mode.MESSAGE);
|
|
if (title) {
|
|
this.scene.ui.showDialogue(text, title, null, nextAction, 0, i === 0 ? 750 : 0);
|
|
} else {
|
|
this.scene.ui.showText(text, null, nextAction, i === 0 ? 750 : 0, true);
|
|
}
|
|
i++;
|
|
};
|
|
|
|
showNextDialogue();
|
|
} else {
|
|
endPhase();
|
|
}
|
|
}
|
|
}
|