mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-06-21 00:52:47 +02:00
* Working ui, missing logic, logs * Filtering starters by name is working * Filtering moves and abilities correctly * Opening starter page on button.action * Removed ugly leftover from title * Added container for text with different colors and titles * Showing all species in pokedex with no decorations and shinies * Filtering includes extra forms; moving cursor from filterText to starters does not reset scrollIndex; toggle button for decorations * Can access evolution page * Abilities are colored properly (still missing info overlay) * Biome filter; displays for baseStats, biomes and evolutions * Removed lockable select ui handler, replaced by changes to standard ui handler. * Evolutions are selectable from list and displayed properly * Keeps shiny variant, gender and form when switching to evolutions; show ability descriptions; properly displaying sprites for megas and other forms * Listing prevolutions and base forms * Fixed filtering of baby forms with no biome assigned; Caught filter is ALL by default * Highlighting text filters, resetting all filters when starting up * No error messag when cursor on uncaught species, showing sprite again after toggling stats * Simplified Pokemon Scan logic, accepts separate words as input * Dynamically resizing ability box, showing ability description on first hover. Removed debug logs. * Removed some more debug messages. * Filter bar can adjust cursorOffset and x padding * Fixed some type definitions * Fixed more warnings; added localization strings in the pokedex scan overlay. * Fixed fatal bug due to using Object.keys * Removed debug messages * Added try catch construct to prevent error that was breaking reloadHelper tests * Added filter for starters / evolutions * Biome filter option for uncatchable mons * C and V buttons snap cursor to filters * Changing background to make instructions visible * Can buy candy upgrades through pokedex * Displaying base stats as bars in an overlay * Including baby forms among uncatchable mons * Including evolutions when filtering by biome * Working logic for select ui handler with skips and scroll * -Pokedex page showing biomes from prevolutions; displaying correct biomes for forms of Rotom, Burmy and Lycanroc * Fixed bug in base stats overlay * Regional forms display name of region in evolutions and prevolutions * Better messages for evolution conditions * Showing proper descriptions for menu * Adding sound effects to menu, and pokemon cry when opening page * Changing menu colors to textstyle options supporting a legacy version. * Fix to getStarterSpeciesId to work with all-unlocks files * Passing a TextStyle to option select ui handler to allow for shadowed text * Fixed bug of overlapping labels in text filters * Fixed bug with supportHover and skipped indices in option select ui handler * Localization of pokemon number label * Fix to pokemon number localization * Fix to pokemon number localization * Adding some comments, removing useless elements * More cleanup * Removed candy upgrade instructions from evolved pokemon; attempting to buy candies from evolution now gives error sound instead of crashing the game * Attempting to exit from filter text is now allowed if current option is empty * UI changes to make dex pages work in legacy style * Pokemon name shown while in alt form is no more capitalized * Handling uncaught pokemon * Showing types on Pokémon page * Introducing globalScene everywhere * Showing evolution requirements in message box * Displaying form changing items; now using pokemonFormChanges to only show reachable forms * Playing correct cry * Pokemon cry in setSpeciesDetails * Left and right buttons to turn previous or next pokedex page * Cleaned up "last" from this.species; turning pages now preserves memories of unlocks * Pokerus cursor is now treated as decoration * Correctly displaying prevolutions for Pikachu and Gholdengo * Uncaught forms can be cycled through (with black sprite and no options available) * Filtering by moves now shows icons to distinguish egg and tm moves * Added icons for passive abilities * Added icons to legacy mode; fixed bug that caused game to hang when switching to or from legacy mode * Pokedex entries are accessible through party screen * Adding sort criteria for consistency with starter select screen * Added options to cost reduction filter for consistency with starter select screen * Updating optionSelectUiHandler to simplify logic and fix bug of autocomplete showing options incorrectly * Adding Pokedéx option in starter select screen * Prevolutions are shown properly again; battle forms are considered caught as long as the base form is caught * Small fixes to evolution and form change descriptions * Reworked evolutions menu to incorporate condition descriptions * Moving evolution condition description logic entirely to the SpeciesEvolution class * Removed extra Miraidon and Koraidon forms * Properly showing evolution text for Dunsparce and Maushold * Displaying uncaught forms for Dudunsparce and Maushold properly * Displaying correct forms for Urshifu and Toxicitry after evolution * Cleared up comments * Updating test for tandemaus evolution * Localized labels for egg moves and abilities * Added button to show back sprites * Back to showing only caught battleforms; added dexForDevs option * Merging shiny and variant buttons * Uncaught battle forms options are shown in dark text, like evolutions * Showing proper gender for mons that can only be (or have only caught in) one gender * Apply suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Removed unused options from base-stats-overlay * Fixed import of BaseStatsOverlay * Displaying form-specific TMs properly; adjusting for passives rework * Removed logging messages * resetting containers to prevent memory leaks * Updating integer to number in pokedex * Implemented suggestion * Removed some stray comments * Fixed logic for cursor coming down from filter bar * Transition from filters to dex box now works in a visually pleasing way --------- Co-authored-by: Lugiad <2070109+Adri1@users.noreply.github.com> Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: damocleas <damocleas25@gmail.com>
409 lines
14 KiB
TypeScript
409 lines
14 KiB
TypeScript
import { globalScene } from "#app/global-scene";
|
|
import { TextStyle, addBBCodeTextObject, getTextColor, getTextStyleOptions } from "./text";
|
|
import { Mode } from "./ui";
|
|
import UiHandler from "./ui-handler";
|
|
import { addWindow } from "./ui-theme";
|
|
import * as Utils from "../utils";
|
|
import { argbFromRgba } from "@material/material-color-utilities";
|
|
import { Button } from "#enums/buttons";
|
|
import BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext";
|
|
|
|
export interface OptionSelectConfig {
|
|
xOffset?: number;
|
|
yOffset?: number;
|
|
options: OptionSelectItem[];
|
|
maxOptions?: number;
|
|
delay?: number;
|
|
noCancel?: boolean;
|
|
supportHover?: boolean;
|
|
}
|
|
|
|
export interface OptionSelectItem {
|
|
label: string;
|
|
handler: () => boolean;
|
|
onHover?: () => void;
|
|
skip?: boolean;
|
|
keepOpen?: boolean;
|
|
overrideSound?: boolean;
|
|
style?: TextStyle;
|
|
item?: string;
|
|
itemArgs?: any[];
|
|
}
|
|
|
|
const scrollUpLabel = "↑";
|
|
const scrollDownLabel = "↓";
|
|
|
|
export default abstract class AbstractOptionSelectUiHandler extends UiHandler {
|
|
protected optionSelectContainer: Phaser.GameObjects.Container;
|
|
protected optionSelectBg: Phaser.GameObjects.NineSlice;
|
|
protected optionSelectText: BBCodeText;
|
|
protected optionSelectIcons: Phaser.GameObjects.Sprite[];
|
|
|
|
protected config: OptionSelectConfig | null;
|
|
|
|
protected blockInput: boolean;
|
|
|
|
protected scrollCursor: number = 0;
|
|
protected fullCursor: number = 0;
|
|
|
|
protected scale: number = 0.1666666667;
|
|
|
|
private cursorObj: Phaser.GameObjects.Image | null;
|
|
|
|
protected unskippedIndices: number[] = [];
|
|
|
|
protected defaultTextStyle: TextStyle = TextStyle.WINDOW;
|
|
|
|
|
|
constructor(mode: Mode | null) {
|
|
super(mode);
|
|
}
|
|
|
|
abstract getWindowWidth(): number;
|
|
|
|
getWindowHeight(): number {
|
|
return (Math.min((this.config?.options || []).length, this.config?.maxOptions || 99) + 1) * 96 * this.scale;
|
|
}
|
|
|
|
setup() {
|
|
const ui = this.getUi();
|
|
|
|
this.optionSelectContainer = globalScene.add.container((globalScene.game.canvas.width / 6) - 1, -48);
|
|
this.optionSelectContainer.setName(`option-select-${this.mode ? Mode[this.mode] : "UNKNOWN"}`);
|
|
this.optionSelectContainer.setVisible(false);
|
|
ui.add(this.optionSelectContainer);
|
|
|
|
this.optionSelectBg = addWindow(0, 0, this.getWindowWidth(), this.getWindowHeight());
|
|
this.optionSelectBg.setName("option-select-bg");
|
|
this.optionSelectBg.setOrigin(1, 1);
|
|
this.optionSelectContainer.add(this.optionSelectBg);
|
|
|
|
this.optionSelectIcons = [];
|
|
|
|
this.scale = getTextStyleOptions(TextStyle.WINDOW, globalScene.uiTheme).scale;
|
|
|
|
this.setCursor(0);
|
|
}
|
|
|
|
protected setupOptions() {
|
|
const configOptions = this.config?.options ?? [];
|
|
|
|
const options: OptionSelectItem[] = configOptions;
|
|
|
|
this.unskippedIndices = this.getUnskippedIndices(configOptions);
|
|
|
|
if (this.optionSelectText) {
|
|
if (this.optionSelectText instanceof BBCodeText) {
|
|
try {
|
|
this.optionSelectText.destroy();
|
|
} catch (error) {
|
|
console.error("Error while destroying optionSelectText:", error);
|
|
}
|
|
} else {
|
|
console.warn("optionSelectText is not an instance of BBCodeText.");
|
|
}
|
|
}
|
|
|
|
if (this.optionSelectIcons?.length) {
|
|
this.optionSelectIcons.map(i => i.destroy());
|
|
this.optionSelectIcons.splice(0, this.optionSelectIcons.length);
|
|
}
|
|
|
|
const optionsWithScroll = (this.config?.options && this.config?.options.length > (this.config?.maxOptions!)) ? this.getOptionsWithScroll() : options;
|
|
|
|
// Setting the initial text to establish the width of the select object. We consider all options, even ones that are not displayed,
|
|
// Except in the case of autocomplete, where we don't want to set up a text element with potentially hundreds of lines.
|
|
const optionsForWidth = globalScene.ui.getMode() === Mode.AUTO_COMPLETE ? optionsWithScroll : options;
|
|
this.optionSelectText = addBBCodeTextObject(
|
|
0, 0, optionsForWidth.map(o => o.item
|
|
? `[shadow=${getTextColor(o.style ?? this.defaultTextStyle, true, globalScene.uiTheme)}][color=${getTextColor(o.style ?? TextStyle.WINDOW, false, globalScene.uiTheme)}] ${o.label}[/color][/shadow]`
|
|
: `[shadow=${getTextColor(o.style ?? this.defaultTextStyle, true, globalScene.uiTheme)}][color=${getTextColor(o.style ?? TextStyle.WINDOW, false, globalScene.uiTheme)}]${o.label}[/color][/shadow]`
|
|
).join("\n"),
|
|
TextStyle.WINDOW, { maxLines: options.length, lineSpacing: 12 }
|
|
);
|
|
this.optionSelectText.setOrigin(0, 0);
|
|
this.optionSelectText.setName("text-option-select");
|
|
this.optionSelectContainer.add(this.optionSelectText);
|
|
this.optionSelectContainer.setPosition((globalScene.game.canvas.width / 6) - 1 - (this.config?.xOffset || 0), -48 + (this.config?.yOffset || 0));
|
|
this.optionSelectBg.width = Math.max(this.optionSelectText.displayWidth + 24, this.getWindowWidth());
|
|
this.optionSelectBg.height = this.getWindowHeight();
|
|
this.optionSelectText.setPosition(this.optionSelectBg.x - this.optionSelectBg.width + 12 + 24 * this.scale, this.optionSelectBg.y - this.optionSelectBg.height + 2 + 42 * this.scale);
|
|
|
|
// Now that the container and background widths are established, we can set up the proper text restricted to visible options
|
|
this.optionSelectText.setText(optionsWithScroll.map(o => o.item
|
|
? `[shadow=${getTextColor(o.style ?? this.defaultTextStyle, true, globalScene.uiTheme)}][color=${getTextColor(o.style ?? TextStyle.WINDOW, false, globalScene.uiTheme)}] ${o.label}[/color][/shadow]`
|
|
: `[shadow=${getTextColor(o.style ?? this.defaultTextStyle, true, globalScene.uiTheme)}][color=${getTextColor(o.style ?? TextStyle.WINDOW, false, globalScene.uiTheme)}]${o.label}[/color][/shadow]`
|
|
).join("\n")
|
|
|
|
);
|
|
|
|
options.forEach((option: OptionSelectItem, i: number) => {
|
|
if (option.item) {
|
|
const itemIcon = globalScene.add.sprite(0, 0, "items", option.item);
|
|
itemIcon.setScale(3 * this.scale);
|
|
this.optionSelectIcons.push(itemIcon);
|
|
|
|
this.optionSelectContainer.add(itemIcon);
|
|
|
|
itemIcon.setPositionRelative(this.optionSelectText, 36 * this.scale, 7 + i * (114 * this.scale - 3));
|
|
|
|
if (option.item === "candy") {
|
|
const itemOverlayIcon = globalScene.add.sprite(0, 0, "items", "candy_overlay");
|
|
itemOverlayIcon.setScale(3 * this.scale);
|
|
this.optionSelectIcons.push(itemOverlayIcon);
|
|
|
|
this.optionSelectContainer.add(itemOverlayIcon);
|
|
|
|
itemOverlayIcon.setPositionRelative(this.optionSelectText, 36 * this.scale, 7 + i * (114 * this.scale - 3));
|
|
|
|
if (option.itemArgs) {
|
|
itemIcon.setTint(argbFromRgba(Utils.rgbHexToRgba(option.itemArgs[0])));
|
|
itemOverlayIcon.setTint(argbFromRgba(Utils.rgbHexToRgba(option.itemArgs[1])));
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
show(args: any[]): boolean {
|
|
if (!args.length || !args[0].hasOwnProperty("options") || !args[0].options.length) {
|
|
return false;
|
|
}
|
|
|
|
super.show(args);
|
|
|
|
this.config = args[0] as OptionSelectConfig;
|
|
this.setupOptions();
|
|
|
|
globalScene.ui.bringToTop(this.optionSelectContainer);
|
|
|
|
this.optionSelectContainer.setVisible(true);
|
|
this.scrollCursor = 0;
|
|
this.fullCursor = 0;
|
|
this.setCursor(0);
|
|
|
|
if (this.config.delay) {
|
|
this.blockInput = true;
|
|
this.optionSelectText.setAlpha(0.5);
|
|
this.cursorObj?.setAlpha(0.8);
|
|
globalScene.time.delayedCall(Utils.fixedInt(this.config.delay), () => this.unblockInput());
|
|
}
|
|
|
|
if (this.config?.supportHover) {
|
|
// handle hover code if the element supports hover-handlers and the option has the optional hover-handler set.
|
|
this.config?.options[this.unskippedIndices[this.fullCursor]]?.onHover?.();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
processInput(button: Button): boolean {
|
|
const ui = this.getUi();
|
|
|
|
let success = false;
|
|
|
|
let playSound = true;
|
|
|
|
if (button === Button.ACTION || button === Button.CANCEL) {
|
|
if (this.blockInput) {
|
|
ui.playError();
|
|
return false;
|
|
}
|
|
|
|
success = true;
|
|
if (button === Button.CANCEL) {
|
|
if (this.config?.maxOptions && this.config.options.length > this.config.maxOptions) {
|
|
this.setCursor(this.unskippedIndices.length - 1);
|
|
} else if (!this.config?.noCancel) {
|
|
this.setCursor(this.unskippedIndices.length - 1);
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
const option = this.config?.options[this.unskippedIndices[this.fullCursor]];
|
|
if (option?.handler()) {
|
|
if (!option.keepOpen) {
|
|
this.clear();
|
|
}
|
|
playSound = !option.overrideSound;
|
|
} else {
|
|
ui.playError();
|
|
}
|
|
} else if (button === Button.SUBMIT && ui.getMode() === Mode.AUTO_COMPLETE) {
|
|
// this is here to differentiate between a Button.SUBMIT vs Button.ACTION within the autocomplete handler
|
|
// this is here because Button.ACTION is picked up as z on the keyboard, meaning if you're typing and hit z, it'll select the option you've chosen
|
|
success = true;
|
|
const option = this.config?.options[this.unskippedIndices[this.fullCursor]];
|
|
if (option?.handler()) {
|
|
if (!option.keepOpen) {
|
|
this.clear();
|
|
}
|
|
playSound = !option.overrideSound;
|
|
} else {
|
|
ui.playError();
|
|
}
|
|
} else {
|
|
switch (button) {
|
|
case Button.UP:
|
|
if (this.fullCursor === 0) {
|
|
success = this.setCursor(this.unskippedIndices.length - 1);
|
|
} else if (this.fullCursor) {
|
|
success = this.setCursor(this.fullCursor - 1);
|
|
}
|
|
break;
|
|
case Button.DOWN:
|
|
if (this.fullCursor < this.unskippedIndices.length - 1) {
|
|
success = this.setCursor(this.fullCursor + 1);
|
|
} else {
|
|
success = this.setCursor(0);
|
|
}
|
|
break;
|
|
}
|
|
if (this.config?.supportHover) {
|
|
// handle hover code if the element supports hover-handlers and the option has the optional hover-handler set.
|
|
this.config?.options[this.unskippedIndices[this.fullCursor]]?.onHover?.();
|
|
}
|
|
}
|
|
|
|
if (success && playSound) {
|
|
ui.playSelect();
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
unblockInput(): void {
|
|
if (!this.blockInput) {
|
|
return;
|
|
}
|
|
|
|
this.blockInput = false;
|
|
this.optionSelectText.setAlpha(1);
|
|
this.cursorObj?.setAlpha(1);
|
|
}
|
|
|
|
getOptionsWithScroll(): OptionSelectItem[] {
|
|
if (!this.config) {
|
|
return [];
|
|
}
|
|
|
|
const options = this.config.options.slice(0);
|
|
|
|
if (!this.config.maxOptions || this.config.options.length < this.config.maxOptions) {
|
|
return options;
|
|
}
|
|
|
|
const optionsScrollTotal = options.length;
|
|
const optionStartIndex = this.scrollCursor;
|
|
const optionEndIndex = Math.min(optionsScrollTotal, optionStartIndex +
|
|
(!optionStartIndex || this.scrollCursor + (this.config.maxOptions - 1) >= optionsScrollTotal ? this.config.maxOptions - 1 : this.config.maxOptions - 2)
|
|
);
|
|
|
|
if (this.config?.maxOptions && options.length > this.config.maxOptions) {
|
|
options.splice(optionEndIndex, optionsScrollTotal);
|
|
options.splice(0, optionStartIndex);
|
|
if (optionStartIndex) {
|
|
options.unshift({
|
|
label: scrollUpLabel,
|
|
handler: () => true,
|
|
style: this.defaultTextStyle
|
|
});
|
|
}
|
|
if (optionEndIndex < optionsScrollTotal) {
|
|
options.push({
|
|
label: scrollDownLabel,
|
|
handler: () => true,
|
|
style: this.defaultTextStyle
|
|
});
|
|
}
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
getUnskippedIndices(options: OptionSelectItem[]) {
|
|
const unskippedIndices = options
|
|
.map((option, index) => (option.skip ? null : index)) // Map to index or null if skipped
|
|
.filter(index => index !== null) as number[];
|
|
return unskippedIndices;
|
|
}
|
|
|
|
setCursor(fullCursor: number): boolean {
|
|
const changed = this.fullCursor !== fullCursor;
|
|
|
|
if (changed && this.config?.maxOptions && this.config.options.length > this.config.maxOptions) {
|
|
|
|
// If the fullCursor is the last possible value, we go to the bottom
|
|
if (fullCursor === this.unskippedIndices.length - 1) {
|
|
this.fullCursor = fullCursor;
|
|
this.cursor = this.config.maxOptions - (this.config.options.length - this.unskippedIndices[fullCursor]);
|
|
this.scrollCursor = this.config.options.length - this.config.maxOptions + 1;
|
|
// If the fullCursor is the first possible value, we go to the top
|
|
} else if (fullCursor === 0) {
|
|
this.fullCursor = fullCursor;
|
|
this.cursor = this.unskippedIndices[fullCursor];
|
|
this.scrollCursor = 0;
|
|
} else {
|
|
const isDown = fullCursor && fullCursor > this.fullCursor;
|
|
|
|
if (isDown) {
|
|
// If there are skipped options under the next selection, we show them
|
|
const jumpFromCurrent = this.unskippedIndices[fullCursor] - this.unskippedIndices[this.fullCursor];
|
|
const skipsFromNext = this.unskippedIndices[fullCursor + 1] - this.unskippedIndices[fullCursor] - 1;
|
|
|
|
if (this.cursor + jumpFromCurrent + skipsFromNext >= this.config.maxOptions - 1) {
|
|
this.fullCursor = fullCursor;
|
|
this.cursor = this.config.maxOptions - 2 - skipsFromNext;
|
|
this.scrollCursor = this.unskippedIndices[this.fullCursor] - this.cursor + 1;
|
|
} else {
|
|
this.fullCursor = fullCursor;
|
|
this.cursor = this.unskippedIndices[fullCursor] - this.scrollCursor + (this.scrollCursor ? 1 : 0);
|
|
}
|
|
} else {
|
|
const jumpFromPrevious = this.unskippedIndices[fullCursor] - this.unskippedIndices[fullCursor - 1];
|
|
|
|
if (this.cursor - jumpFromPrevious < 1) {
|
|
this.fullCursor = fullCursor;
|
|
this.cursor = 1;
|
|
this.scrollCursor = this.unskippedIndices[this.fullCursor] - this.cursor + 1;
|
|
} else {
|
|
this.fullCursor = fullCursor;
|
|
this.cursor = this.unskippedIndices[fullCursor] - this.scrollCursor + (this.scrollCursor ? 1 : 0);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
this.fullCursor = fullCursor;
|
|
this.cursor = this.unskippedIndices[fullCursor];
|
|
}
|
|
|
|
this.setupOptions();
|
|
|
|
if (!this.cursorObj) {
|
|
this.cursorObj = globalScene.add.image(0, 0, "cursor");
|
|
this.optionSelectContainer.add(this.cursorObj);
|
|
}
|
|
|
|
this.cursorObj.setScale(this.scale * 6);
|
|
this.cursorObj.setPositionRelative(this.optionSelectBg, 12, 102 * this.scale + this.cursor * (114 * this.scale - 3));
|
|
|
|
return changed;
|
|
}
|
|
|
|
clear() {
|
|
super.clear();
|
|
this.config = null;
|
|
this.optionSelectContainer.setVisible(false);
|
|
this.fullCursor = 0;
|
|
this.scrollCursor = 0;
|
|
this.eraseCursor();
|
|
}
|
|
|
|
eraseCursor() {
|
|
if (this.cursorObj) {
|
|
this.cursorObj.destroy();
|
|
}
|
|
this.cursorObj = null;
|
|
}
|
|
}
|