pokerogue/src/ui/mystery-encounter-ui-handler.ts
ImperialSympathizer acb2b66be4
[Feature] Add Mystery Encounters to the game (#3938)
* add .github/workflows/mystery-event.yml

* update mystery-event.yml

* mystery encounters: resolve review comments:

Lost at Sea:
-fix typo in handlePokemonGuidingYouPhase function

Mysterious Chest:
- remove obsolete commented code

mystery-encounter.ts
- remove unused `onDone` field from MysteryEncounterBuilder

* fix typo in CanLearnMoveRequirementOptions

* remove redundance from Pokemon.isAllowedInBattle()

* chore: jsdoc formatting

* fix lost-at-sea tests

* add fallback for biomeMysteryEncounters if empty

* lost-at-sea-encounter: fix and extend tests

* move "battle:fainted" into `koPlayerPokemon`

* add retries to quick-draw tests

* fix lost-at-sea-encounter tests

* clean up battle animation logic

* Update and rename mystery-event.yml to mystery-events.yml

* Update mystery-events.yml

* Fix typo

* Update mystery-events.yml

Fix debug runs

* clean up unit tests and utils

* attach github issues to all encounter jsdocs

* start dialogue refactor

* update sleeping snorlax encounter

* migrate encounters dialogue to new format

* cleanup and add jsdocs

* finish fiery fallout encounter

* fix unit test breaks

* add skeleton tests to fiery fallout

* commit latest test changes

* finish unit tests for fiery fallout

* bug fix for empty modifier shop

* stash working changes

* stash changes

* Update src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts

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

* Update src/test/utils/overridesHelper.ts

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

* Update src/test/utils/overridesHelper.ts

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

* Update src/test/utils/overridesHelper.ts

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

* Update src/test/utils/overridesHelper.ts

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

* Update src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts

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

* Update src/data/battle-anims.ts

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

* nit updates and cleanup

* Update src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts

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

* Apply suggestions from code review

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

* add jsdocs and more cleanup

* add more jsdoc

* add the strong stuff encounter

* add the strong stuff encounter and more unit tests

* cleanup container length checks in ME ui

* add retries to tests

* add retries to tests

* fix trainer wave disable override

* add shuckle juice modifier

* add dialogue bug fixes

* add dialogue bug fixes

* add pokemon salesman encounter and affects pokedex UI display

* add unit tests for pokemon salesman

* temp stash

* add offer you can't refuse

* add unit tests for offer you can't refuse encounter

* remove unnecessary prompt handlers

* add tests for disabled encounter options

* add delibird-y encounter

* add delibird-y encounter

* add absolute avarice encounter

* finish absolute avarice encounter

* add unit tests and enhancements for item overrides in tests

* fix unit test

* cleanup absolute avarice PR

* small bug fixes with latest sync from main

* update visuals loading for safari and stat trainer visuals

* update visuals loading for safari and stat trainer visuals

* update a trainer's test encounter and add unit tests

* add Trash to Treasure encounter

* clean up trash to treasure encounter

* clean up trash to treasure encounter

* add berries abound encounter

* start clowning around encounter

* first implementation pass at clowning around

* add unit tests for clowning around

* add unit tests for clowning around

* clean up ME unit tests

* clean up unit tests

* update unit tests

* add part timer and dancing lessons encounters

* add unit tests for Dancing Lessons and Part-Timer

* reordered biome list and adjusted redirection for project and labels

* Add Weird Dream encounter and slight reworks to Berries Abound/Fight or Flight

* adjusting yml to match new labels

* fix yml whoopsie

* Expanded 'Weird Dream' banlist and fixed a bug with the BST bump range

* adds Winstrate Challenge mystery encounter

* small cleanup for winstrates

* add unit tests for Winstrate Challenge

* fix pokemon not returning after winstrate battle

* commit latest beta merge updates

* fix ME null checks and unit tests with beta update

* fix ME null checks and unit tests with beta update

* MEs to pokerogue beta branch

* test dialogue changes

* test patch fix

* test patch fix

* test patch fix

* adds teleporting hijinks encounter

* add unit tests for Teleporting Hijinks

* small change to teleporting hijinks dialogue

* migrate ME translations to json

* add retries to berries-abound.Option1: should reward the player with X berries based on wave

* add missing ME dialogue back in

* revert template changes

* add ME unique trainer dialogue to both dialogue jsons

* fix hanging comma in json

* fix broken imports

* resolve lint issues

* fix flaky test

* balance tweaks to a few MEs, updates to bug superfan

* add unit tests for Bug-Type Superfan and clean up dialogue

* Adds Fun and Games mystery encounter

* add unit tests for Fun and Games encounter

* update jsdoc

* small ME balance changes

* small ME balance changes

* Adds Uncommon Breed ME and misc. ME bug fixes

* Update getFinalSessionData() to collect Mystery Encounter data

* adds GTS encounter

* various ME bug fixes and balance changes

* latest ME bug fixes

* clean up GTS Encounter and add unit tests

* small cleanup to MEs branch

* add BGM music names for ME music

* bug fixes and balance changes for MEs

* ME data schema updates

* balance changes and bug fixes to MEs

* balance changes and bug fixes to MEs

* update tests for MEs

* add jsdoc to party exp function

* dialogue updates and test fixes for MEs

* dialogue updates and test fixes for MEs

* PR suggestions and fixees

* stash PR feedback and bugfixes

* fix all tests for MEs and cleanup

* PR feedback

* update flaky ME test

* update tests, bug fix MEs, and sprite assets

* remove unintentional console log

* re-enable stubbed function for Phaser text styling

* handle undefined introVisuals properly

* PR feedback from NightKev

* disable Uncommon Breed tests

* locales updates and bug fixes for safari zone

* more PR feedback and update field trip with Rarer Candy

* fix unit test

* Change how reroll button gets disabled in Modifier Shop Phase

* update continue button text logic

* Update src/ui/modifier-select-ui-handler.ts

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

* fix money formatting and some nits

* more nits

* more nits

* update ME tsdocs with links

* update ME tsdocs with links

---------

Co-authored-by: Felix Staud <felix.staud@headwire.com>
Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>
Co-authored-by: ImperialSympathizer <imperialsympathizer@gmail.com>
Co-authored-by: InnocentGameDev <asdargmng@gmail.com>
Co-authored-by: Mumble <171087428+frutescens@users.noreply.github.com>
2024-09-14 03:05:58 +01:00

624 lines
24 KiB
TypeScript

import BattleScene from "../battle-scene";
import { addBBCodeTextObject, getBBCodeFrag, TextStyle } from "./text";
import { Mode } from "./ui";
import UiHandler from "./ui-handler";
import { Button } from "#enums/buttons";
import { addWindow, WindowVariant } from "./ui-theme";
import { MysteryEncounterPhase } from "../phases/mystery-encounter-phases";
import { PartyUiMode } from "./party-ui-handler";
import MysteryEncounterOption from "../data/mystery-encounters/mystery-encounter-option";
import * as Utils from "../utils";
import { isNullOrUndefined } from "../utils";
import { getPokeballAtlasKey } from "../data/pokeball";
import { OptionSelectSettings } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import i18next from "i18next";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext";
export default class MysteryEncounterUiHandler extends UiHandler {
private cursorContainer: Phaser.GameObjects.Container;
private cursorObj?: Phaser.GameObjects.Image;
private optionsContainer: Phaser.GameObjects.Container;
// Length = max number of allowable options (4)
private optionScrollTweens: (Phaser.Tweens.Tween | null)[] = new Array(4).fill(null);
private tooltipWindow: Phaser.GameObjects.NineSlice;
private tooltipContainer: Phaser.GameObjects.Container;
private tooltipScrollTween?: Phaser.Tweens.Tween;
private descriptionWindow: Phaser.GameObjects.NineSlice;
private descriptionContainer: Phaser.GameObjects.Container;
private descriptionScrollTween?: Phaser.Tweens.Tween;
private rarityBall: Phaser.GameObjects.Sprite;
private dexProgressWindow: Phaser.GameObjects.NineSlice;
private dexProgressContainer: Phaser.GameObjects.Container;
private showDexProgress: boolean = false;
private overrideSettings?: OptionSelectSettings;
private encounterOptions: MysteryEncounterOption[] = [];
private optionsMeetsReqs: boolean[];
protected viewPartyIndex: integer = 0;
protected blockInput: boolean = true;
constructor(scene: BattleScene) {
super(scene, Mode.MYSTERY_ENCOUNTER);
}
override setup() {
const ui = this.getUi();
this.cursorContainer = this.scene.add.container(18, -38.7);
this.cursorContainer.setVisible(false);
ui.add(this.cursorContainer);
this.optionsContainer = this.scene.add.container(12, -38.7);
this.optionsContainer.setVisible(false);
ui.add(this.optionsContainer);
this.dexProgressContainer = this.scene.add.container(214, -43);
this.dexProgressContainer.setVisible(false);
ui.add(this.dexProgressContainer);
this.descriptionContainer = this.scene.add.container(0, -152);
this.descriptionContainer.setVisible(false);
ui.add(this.descriptionContainer);
this.tooltipContainer = this.scene.add.container(210, -48);
this.tooltipContainer.setVisible(false);
ui.add(this.tooltipContainer);
this.setCursor(this.getCursor());
this.descriptionWindow = addWindow(this.scene, 0, 0, 150, 105, false, false, 0, 0, WindowVariant.THIN);
this.descriptionContainer.add(this.descriptionWindow);
this.tooltipWindow = addWindow(this.scene, 0, 0, 110, 48, false, false, 0, 0, WindowVariant.THIN);
this.tooltipContainer.add(this.tooltipWindow);
this.dexProgressWindow = addWindow(this.scene, 0, 0, 24, 28, false, false, 0, 0, WindowVariant.THIN);
this.dexProgressContainer.add(this.dexProgressWindow);
this.rarityBall = this.scene.add.sprite(141, 9, "pb");
this.rarityBall.setScale(0.75);
this.descriptionContainer.add(this.rarityBall);
const dexProgressIndicator = this.scene.add.sprite(12, 10, "encounter_radar");
dexProgressIndicator.setScale(0.80);
this.dexProgressContainer.add(dexProgressIndicator);
this.dexProgressContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, 24, 28), Phaser.Geom.Rectangle.Contains);
}
override show(args: any[]): boolean {
super.show(args);
this.overrideSettings = args[0] as OptionSelectSettings ?? {};
const showDescriptionContainer = isNullOrUndefined(this.overrideSettings?.hideDescription) ? true : !this.overrideSettings?.hideDescription;
const slideInDescription = isNullOrUndefined(this.overrideSettings?.slideInDescription) ? true : this.overrideSettings?.slideInDescription;
const startingCursorIndex = this.overrideSettings?.startingCursorIndex ?? 0;
this.cursorContainer.setVisible(true);
this.descriptionContainer.setVisible(showDescriptionContainer);
this.optionsContainer.setVisible(true);
this.dexProgressContainer.setVisible(true);
this.displayEncounterOptions(slideInDescription);
const cursor = this.getCursor();
if (cursor === (this.optionsContainer?.length || 0) - 1) {
// Always resets cursor on view party button if it was last there
this.setCursor(cursor);
} else {
this.setCursor(startingCursorIndex);
}
if (this.blockInput) {
setTimeout(() => {
this.unblockInput();
}, 1000);
}
this.displayOptionTooltip();
return true;
}
override processInput(button: Button): boolean {
const ui = this.getUi();
let success = false;
const cursor = this.getCursor();
if (button === Button.CANCEL || button === Button.ACTION) {
if (button === Button.ACTION) {
const selected = this.encounterOptions[cursor];
if (cursor === this.viewPartyIndex) {
// Handle view party
success = true;
const overrideSettings: OptionSelectSettings = {
...this.overrideSettings,
slideInDescription: false
};
this.scene.ui.setMode(Mode.PARTY, PartyUiMode.CHECK, -1, () => {
this.scene.ui.setMode(Mode.MYSTERY_ENCOUNTER, overrideSettings);
setTimeout(() => {
this.setCursor(this.viewPartyIndex);
this.unblockInput();
}, 300);
});
} else if (this.blockInput || (!this.optionsMeetsReqs[cursor] && (selected.optionMode === MysteryEncounterOptionMode.DISABLED_OR_DEFAULT || selected.optionMode === MysteryEncounterOptionMode.DISABLED_OR_SPECIAL))) {
success = false;
} else {
if ((this.scene.getCurrentPhase() as MysteryEncounterPhase).handleOptionSelect(selected, cursor)) {
success = true;
} else {
ui.playError();
}
}
} else {
// TODO: If we need to handle cancel option? Maybe default logic to leave/run from encounter idk
}
} else {
switch (this.optionsContainer.getAll()?.length) {
default:
case 3:
success = this.handleTwoOptionMoveInput(button);
break;
case 4:
success = this.handleThreeOptionMoveInput(button);
break;
case 5:
success = this.handleFourOptionMoveInput(button);
break;
}
this.displayOptionTooltip();
}
if (success) {
ui.playSelect();
}
return success;
}
private handleTwoOptionMoveInput(button: Button): boolean {
let success = false;
const cursor = this.getCursor();
switch (button) {
case Button.UP:
if (cursor < this.viewPartyIndex) {
success = this.setCursor(this.viewPartyIndex);
}
break;
case Button.DOWN:
if (cursor === this.viewPartyIndex) {
success = this.setCursor(1);
}
break;
case Button.LEFT:
if (cursor > 0) {
success = this.setCursor(cursor - 1);
}
break;
case Button.RIGHT:
if (cursor < this.viewPartyIndex) {
success = this.setCursor(cursor + 1);
}
break;
}
return success;
}
private handleThreeOptionMoveInput(button: Button): boolean {
let success = false;
const cursor = this.getCursor();
switch (button) {
case Button.UP:
if (cursor === 2) {
success = this.setCursor(cursor - 2);
} else {
success = this.setCursor(this.viewPartyIndex);
}
break;
case Button.DOWN:
if (cursor === this.viewPartyIndex) {
success = this.setCursor(1);
} else {
success = this.setCursor(2);
}
break;
case Button.LEFT:
if (cursor === this.viewPartyIndex) {
success = this.setCursor(1);
} else if (cursor === 1) {
success = this.setCursor(cursor - 1);
}
break;
case Button.RIGHT:
if (cursor === 1) {
success = this.setCursor(this.viewPartyIndex);
} else if (cursor < 1) {
success = this.setCursor(cursor + 1);
}
break;
}
return success;
}
private handleFourOptionMoveInput(button: Button): boolean {
let success = false;
const cursor = this.getCursor();
switch (button) {
case Button.UP:
if (cursor >= 2 && cursor !== this.viewPartyIndex) {
success = this.setCursor(cursor - 2);
} else {
success = this.setCursor(this.viewPartyIndex);
}
break;
case Button.DOWN:
if (cursor <= 1) {
success = this.setCursor(cursor + 2);
} else if (cursor === this.viewPartyIndex) {
success = this.setCursor(1);
}
break;
case Button.LEFT:
if (cursor === this.viewPartyIndex) {
success = this.setCursor(1);
} else if (cursor % 2 === 1) {
success = this.setCursor(cursor - 1);
}
break;
case Button.RIGHT:
if (cursor === 1) {
success = this.setCursor(this.viewPartyIndex);
} else if (cursor % 2 === 0 && cursor !== this.viewPartyIndex) {
success = this.setCursor(cursor + 1);
}
break;
}
return success;
}
/**
* When ME UI first displays, the option buttons will be disabled temporarily to prevent player accidentally clicking through hastily
* This method is automatically called after a short delay but can also be called manually
*/
unblockInput() {
if (this.blockInput) {
this.blockInput = false;
for (let i = 0; i < this.optionsContainer.length - 1; i++) {
const optionMode = this.encounterOptions[i].optionMode;
if (!this.optionsMeetsReqs[i] && (optionMode === MysteryEncounterOptionMode.DISABLED_OR_DEFAULT || optionMode === MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)) {
continue;
}
(this.optionsContainer.getAt(i) as Phaser.GameObjects.Text).setAlpha(1);
}
}
}
override getCursor(): integer {
return this.cursor ? this.cursor : 0;
}
override setCursor(cursor: integer): boolean {
const prevCursor = this.getCursor();
const changed = prevCursor !== cursor;
if (changed) {
this.cursor = cursor;
}
this.viewPartyIndex = this.optionsContainer.getAll()?.length - 1;
if (!this.cursorObj) {
this.cursorObj = this.scene.add.image(0, 0, "cursor");
this.cursorContainer.add(this.cursorObj);
}
if (cursor === this.viewPartyIndex) {
this.cursorObj.setPosition(246, -17);
} else if (this.optionsContainer.getAll()?.length === 3) { // 2 Options
this.cursorObj.setPosition(-10.5 + (cursor % 2 === 1 ? 100 : 0), 15);
} else if (this.optionsContainer.getAll()?.length === 4) { // 3 Options
this.cursorObj.setPosition(-10.5 + (cursor % 2 === 1 ? 100 : 0), 7 + (cursor > 1 ? 16 : 0));
} else if (this.optionsContainer.getAll()?.length === 5) { // 4 Options
this.cursorObj.setPosition(-10.5 + (cursor % 2 === 1 ? 100 : 0), 7 + (cursor > 1 ? 16 : 0));
}
return changed;
}
displayEncounterOptions(slideInDescription: boolean = true): void {
this.getUi().clearText();
const mysteryEncounter = this.scene.currentBattle.mysteryEncounter!;
this.encounterOptions = this.overrideSettings?.overrideOptions ?? mysteryEncounter.options;
this.optionsMeetsReqs = [];
const titleText: string | null = getEncounterText(this.scene, mysteryEncounter.dialogue.encounterOptionsDialogue?.title, TextStyle.TOOLTIP_TITLE);
const descriptionText: string | null = getEncounterText(this.scene, mysteryEncounter.dialogue.encounterOptionsDialogue?.description, TextStyle.TOOLTIP_CONTENT);
const queryText: string | null = getEncounterText(this.scene, mysteryEncounter.dialogue.encounterOptionsDialogue?.query, TextStyle.TOOLTIP_CONTENT);
// Clear options container (except cursor)
this.optionsContainer.removeAll(true);
// Options Window
for (let i = 0; i < this.encounterOptions.length; i++) {
const option = this.encounterOptions[i];
let optionText: BBCodeText;
switch (this.encounterOptions.length) {
default:
case 2:
optionText = addBBCodeTextObject(this.scene, i % 2 === 0 ? 0 : 100, 8, "-", TextStyle.WINDOW, { fontSize: "80px", lineSpacing: -8 });
break;
case 3:
optionText = addBBCodeTextObject(this.scene, i % 2 === 0 ? 0 : 100, i < 2 ? 0 : 16, "-", TextStyle.WINDOW, { fontSize: "80px", lineSpacing: -8 });
break;
case 4:
optionText = addBBCodeTextObject(this.scene, i % 2 === 0 ? 0 : 100, i < 2 ? 0 : 16, "-", TextStyle.WINDOW, { fontSize: "80px", lineSpacing: -8 });
break;
}
this.optionsMeetsReqs.push(option.meetsRequirements(this.scene));
const optionDialogue = option.dialogue!;
const label = !this.optionsMeetsReqs[i] && optionDialogue.disabledButtonLabel ? optionDialogue.disabledButtonLabel : optionDialogue.buttonLabel;
let text: string | null;
if (option.hasRequirements() && this.optionsMeetsReqs[i] && (option.optionMode === MysteryEncounterOptionMode.DEFAULT_OR_SPECIAL || option.optionMode === MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)) {
// Options with special requirements that are met are automatically colored green
text = getEncounterText(this.scene, label, TextStyle.SUMMARY_GREEN);
} else {
text = getEncounterText(this.scene, label, optionDialogue.style ? optionDialogue.style : TextStyle.WINDOW);
}
if (text) {
optionText.setText(text);
}
if (!this.optionsMeetsReqs[i] && (option.optionMode === MysteryEncounterOptionMode.DISABLED_OR_DEFAULT || option.optionMode === MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)) {
optionText.setAlpha(0.5);
}
if (this.blockInput) {
optionText.setAlpha(0.5);
}
// Sets up the mask that hides the option text to give an illusion of scrolling
const nonScrollWidth = 90;
const optionTextMaskRect = this.scene.make.graphics({});
optionTextMaskRect.setScale(6);
optionTextMaskRect.fillStyle(0xFFFFFF);
optionTextMaskRect.beginPath();
optionTextMaskRect.fillRect(optionText.x + 11, optionText.y + 140, nonScrollWidth, 18);
const optionTextMask = optionTextMaskRect.createGeometryMask();
optionText.setMask(optionTextMask);
const optionTextWidth = optionText.displayWidth;
const tween = this.optionScrollTweens[i];
if (tween) {
tween.remove();
this.optionScrollTweens[i] = null;
}
// Animates the option text scrolling sideways
if (optionTextWidth > nonScrollWidth) {
this.optionScrollTweens[i] = this.scene.tweens.add({
targets: optionText,
delay: Utils.fixedInt(2000),
loop: -1,
hold: Utils.fixedInt(2000),
duration: Utils.fixedInt((optionTextWidth - nonScrollWidth) / 15 * 2000),
x: `-=${(optionTextWidth - nonScrollWidth)}`
});
}
this.optionsContainer.add(optionText);
}
// View Party Button
const viewPartyText = addBBCodeTextObject(this.scene, 256, -24, getBBCodeFrag(i18next.t("mysteryEncounterMessages:view_party_button"), TextStyle.PARTY), TextStyle.PARTY);
this.optionsContainer.add(viewPartyText);
// Description Window
const titleTextObject = addBBCodeTextObject(this.scene, 0, 0, titleText ?? "", TextStyle.TOOLTIP_TITLE, { wordWrap: { width: 750 }, align: "center", lineSpacing: -8 });
this.descriptionContainer.add(titleTextObject);
titleTextObject.setPosition(72 - titleTextObject.displayWidth / 2, 5.5);
// Rarity of encounter
const index = mysteryEncounter.encounterTier === MysteryEncounterTier.COMMON ? 0 :
mysteryEncounter.encounterTier === MysteryEncounterTier.GREAT ? 1 :
mysteryEncounter.encounterTier === MysteryEncounterTier.ULTRA ? 2 :
mysteryEncounter.encounterTier === MysteryEncounterTier.ROGUE ? 3 : 4;
const ballType = getPokeballAtlasKey(index);
this.rarityBall.setTexture("pb", ballType);
const descriptionTextObject = addBBCodeTextObject(this.scene, 6, 25, descriptionText ?? "", TextStyle.TOOLTIP_CONTENT, { wordWrap: { width: 830 } });
// Sets up the mask that hides the description text to give an illusion of scrolling
const descriptionTextMaskRect = this.scene.make.graphics({});
descriptionTextMaskRect.setScale(6);
descriptionTextMaskRect.fillStyle(0xFFFFFF);
descriptionTextMaskRect.beginPath();
descriptionTextMaskRect.fillRect(6, 53, 206, 57);
const abilityDescriptionTextMask = descriptionTextMaskRect.createGeometryMask();
descriptionTextObject.setMask(abilityDescriptionTextMask);
const descriptionLineCount = Math.floor(descriptionTextObject.displayHeight / 10);
if (this.descriptionScrollTween) {
this.descriptionScrollTween.remove();
this.descriptionScrollTween = undefined;
}
// Animates the description text moving upwards
if (descriptionLineCount > 6) {
this.descriptionScrollTween = this.scene.tweens.add({
targets: descriptionTextObject,
delay: Utils.fixedInt(2000),
loop: -1,
hold: Utils.fixedInt(2000),
duration: Utils.fixedInt((descriptionLineCount - 6) * 2000),
y: `-=${10 * (descriptionLineCount - 6)}`
});
}
this.descriptionContainer.add(descriptionTextObject);
const queryTextObject = addBBCodeTextObject(this.scene, 0, 0, queryText ?? "", TextStyle.TOOLTIP_CONTENT, { wordWrap: { width: 830 } });
this.descriptionContainer.add(queryTextObject);
queryTextObject.setPosition(75 - queryTextObject.displayWidth / 2, 90);
// Slide in description container
if (slideInDescription) {
this.descriptionContainer.x -= 150;
this.scene.tweens.add({
targets: this.descriptionContainer,
x: "+=150",
ease: "Sine.easeInOut",
duration: 1000
});
}
}
/**
* Updates and displays the tooltip for a given option
* The tooltip will auto wrap and scroll if it is too long
*/
private displayOptionTooltip() {
const cursor = this.getCursor();
// Clear tooltip box
if (this.tooltipContainer.length > 1) {
this.tooltipContainer.removeBetween(1, this.tooltipContainer.length, true);
}
this.tooltipContainer.setVisible(true);
if (isNullOrUndefined(cursor) || cursor > this.optionsContainer.length - 2) {
// Ignore hovers on view party button
// Hide dex progress if visible
this.showHideDexProgress(false);
return;
}
let text: string | null;
const cursorOption = this.encounterOptions[cursor];
const optionDialogue = cursorOption.dialogue!;
if (!this.optionsMeetsReqs[cursor] && (cursorOption.optionMode === MysteryEncounterOptionMode.DISABLED_OR_DEFAULT || cursorOption.optionMode === MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) && optionDialogue.disabledButtonTooltip) {
text = getEncounterText(this.scene, optionDialogue.disabledButtonTooltip, TextStyle.TOOLTIP_CONTENT);
} else {
text = getEncounterText(this.scene, optionDialogue.buttonTooltip, TextStyle.TOOLTIP_CONTENT);
}
// Auto-color options green/blue for good/bad by looking for (+)/(-)
if (text) {
const primaryStyleString = [...text.match(new RegExp(/\[color=[^\[]*\]\[shadow=[^\[]*\]/i))!][0];
text = text.replace(/(\(\+\)[^\(\[]*)/gi, substring => "[/color][/shadow]" + getBBCodeFrag(substring, TextStyle.SUMMARY_GREEN) + "[/color][/shadow]" + primaryStyleString);
text = text.replace(/(\(\-\)[^\(\[]*)/gi, substring => "[/color][/shadow]" + getBBCodeFrag(substring, TextStyle.SUMMARY_BLUE) + "[/color][/shadow]" + primaryStyleString);
}
if (text) {
const tooltipTextObject = addBBCodeTextObject(this.scene, 6, 7, text, TextStyle.TOOLTIP_CONTENT, { wordWrap: { width: 600 }, fontSize: "72px" });
this.tooltipContainer.add(tooltipTextObject);
// Sets up the mask that hides the description text to give an illusion of scrolling
const tooltipTextMaskRect = this.scene.make.graphics({});
tooltipTextMaskRect.setScale(6);
tooltipTextMaskRect.fillStyle(0xFFFFFF);
tooltipTextMaskRect.beginPath();
tooltipTextMaskRect.fillRect(this.tooltipContainer.x, this.tooltipContainer.y + 188.5, 150, 32);
const textMask = tooltipTextMaskRect.createGeometryMask();
tooltipTextObject.setMask(textMask);
const tooltipLineCount = Math.floor(tooltipTextObject.displayHeight / 11.2);
if (this.tooltipScrollTween) {
this.tooltipScrollTween.remove();
this.tooltipScrollTween = undefined;
}
// Animates the tooltip text moving upwards
if (tooltipLineCount > 3) {
this.tooltipScrollTween = this.scene.tweens.add({
targets: tooltipTextObject,
delay: Utils.fixedInt(1200),
loop: -1,
hold: Utils.fixedInt(1200),
duration: Utils.fixedInt((tooltipLineCount - 3) * 1200),
y: `-=${11.2 * (tooltipLineCount - 3)}`
});
}
}
// Dex progress indicator
if (cursorOption.hasDexProgress && !this.showDexProgress) {
this.showHideDexProgress(true);
} else if (!cursorOption.hasDexProgress) {
this.showHideDexProgress(false);
}
}
override clear(): void {
super.clear();
this.overrideSettings = undefined;
this.optionsContainer.setVisible(false);
this.optionsContainer.removeAll(true);
this.dexProgressContainer.setVisible(false);
this.descriptionContainer.setVisible(false);
this.tooltipContainer.setVisible(false);
// Keeps container background and pokeball
this.descriptionContainer.removeBetween(2, this.descriptionContainer.length, true);
this.getUi().getMessageHandler().clearText();
this.eraseCursor();
}
private eraseCursor(): void {
if (this.cursorObj) {
this.cursorObj.destroy();
}
this.cursorObj = undefined;
}
/**
* Will show or hide the Dex progress icon for an option that has dex progress
* @param show - if true does show, if false does hide
*/
private showHideDexProgress(show: boolean) {
if (show && !this.showDexProgress) {
this.showDexProgress = true;
this.scene.tweens.killTweensOf(this.dexProgressContainer);
this.scene.tweens.add({
targets: this.dexProgressContainer,
y: -63,
ease: "Sine.easeInOut",
duration: 750,
onComplete: () => {
this.dexProgressContainer.on("pointerover", () => {
(this.scene as BattleScene).ui.showTooltip("", i18next.t("mysteryEncounterMessages:affects_pokedex"), true);
});
this.dexProgressContainer.on("pointerout", () => {
(this.scene as BattleScene).ui.hideTooltip();
});
}
});
} else if (!show && this.showDexProgress) {
this.showDexProgress = false;
this.scene.tweens.killTweensOf(this.dexProgressContainer);
this.scene.tweens.add({
targets: this.dexProgressContainer,
y: -43,
ease: "Sine.easeInOut",
duration: 750,
onComplete: () => {
this.dexProgressContainer.off("pointerover");
this.dexProgressContainer.off("pointerout");
}
});
}
}
}