diff --git a/src/ui/filter-text.ts b/src/ui/filter-text.ts index 973d06e7542..7d9f3ec05b5 100644 --- a/src/ui/filter-text.ts +++ b/src/ui/filter-text.ts @@ -19,6 +19,8 @@ export enum FilterTextRow{ export class FilterText extends Phaser.GameObjects.Container { private window: Phaser.GameObjects.NineSlice; private labels: Phaser.GameObjects.Text[] = []; + private selections: Phaser.GameObjects.Text[] = []; + private selectionStrings: string[] = []; // private dropDowns: DropDown[] = []; private rows: FilterTextRow[] = []; public cursorObj: Phaser.GameObjects.Image; @@ -33,9 +35,15 @@ export class FilterText extends Phaser.GameObjects.Container { private readonly textPadding = 8; private readonly defaultWordWrapWidth = 1224; - constructor(scene: BattleScene, x: number, y: number, width: number, height: number) { + private onChange: () => void; + + public defaultText: string = "---"; + + constructor(scene: BattleScene, x: number, y: number, width: number, height: number, onChange: () => void,) { super(scene, x, y); + this.onChange = onChange; + this.width = width; this.height = height; @@ -84,6 +92,7 @@ export class FilterText extends Phaser.GameObjects.Container { const paddingX = 6; const cursorOffset = 8; + const extraSpaceX = 50; if (this.rows.includes(row)) { return false; @@ -95,6 +104,12 @@ export class FilterText extends Phaser.GameObjects.Container { this.labels.push(filterTypesLabel); this.add(filterTypesLabel); + const filterTypesSelection = addTextObject(this.scene, paddingX + cursorOffset + extraSpaceX, 3, this.defaultText, TextStyle.TOOLTIP_CONTENT); + this.selections.push(filterTypesSelection); + this.add(filterTypesSelection); + + this.selectionStrings.push(""); + this.calcFilterPositions(); this.numFilters++; @@ -110,6 +125,11 @@ export class FilterText extends Phaser.GameObjects.Container { return this.dropDowns[this.rows.indexOf(row)]; } + resetSelection(index: number): void { + this.selections[index].setText(this.defaultText); + this.selectionStrings[index] = ""; + this.onChange(); + } startSearch(index: number, ui: UI): void { @@ -121,36 +141,27 @@ export class FilterText extends Phaser.GameObjects.Container { // ui.revertMode(); ui.playSelect(); const dialogueTestName = sanitizedName; + console.log("1", dialogueTestName); + //TODO: Is it really necessary to encode and decode? const dialogueName = decodeURIComponent(escape(atob(dialogueTestName))); + console.log("2", dialogueName); const handler = ui.getHandler() as AwaitableUiHandler; handler.tutorialActive = true; - const interpolatorOptions: any = {}; - const splitArr = dialogueName.split(" "); // this splits our inputted text into words to cycle through later - const translatedString = splitArr[0]; // this is our outputted i18 string - const regex = RegExp("\\{\\{(\\w*)\\}\\}", "g"); // this is a regex expression to find all the text between {{ }} in the i18 output - const matches = i18next.t(translatedString).match(regex) ?? []; - if (matches.length > 0) { - for (let match = 0; match < matches.length; match++) { - // we add 1 here because splitArr[0] is our first value for the translatedString, and after that is where the variables are - // the regex here in the replace (/\W/g) is to remove the {{ and }} and just give us all alphanumeric characters - if (typeof splitArr[match + 1] !== "undefined") { - interpolatorOptions[matches[match].replace(/\W/g, "")] = i18next.t(splitArr[match + 1]); - } - } - } // Switch to the dialog test window - this.setDialogTestMode(true); - ui.showText(String(i18next.t(translatedString, interpolatorOptions)), null, () => this.scene.ui.showText("", 0, () => { - handler.tutorialActive = false; - // Go back to the default message window - this.setDialogTestMode(false); - }), null, true); + console.log("6", "switch"); + this.selections[index].setText(String(i18next.t(dialogueName))); + console.log("6.5", "revert"); + ui.revertMode(); + this.onChange(); }, () => { + console.log("7", "revert"); ui.revertMode(); + this.onChange; } ]; - ui.setMode(Mode.TEST_DIALOGUE, buttonAction, prefilledText); + console.log("8", "setmode"); + ui.setOverlayMode(Mode.POKEDEX_SCAN, buttonAction, prefilledText, index); } @@ -206,9 +217,11 @@ export class FilterText extends Phaser.GameObjects.Container { for (let i = 0; i < this.labels.length; i++) { if (i === 0) { this.labels[i].y = paddingY; + this.selections[i].y = paddingY; } else { const lastBottom = this.labels[i - 1].y + this.labels[i - 1].displayHeight; this.labels[i].y = lastBottom + spacing; + this.selections[i].y = lastBottom + spacing; } // Uncomment and adjust if necessary to position dropdowns vertically // this.dropDowns[i].y = this.labels[i].y + this.labels[i].displayHeight + paddingY; @@ -264,8 +277,9 @@ export class FilterText extends Phaser.GameObjects.Container { this.dropDowns[this.lastCursor].toggleOptionState(); } - getVals(row: FilterTextRow): any[] { - return this.getFilter(row).getVals(); + getValue(row: number): string { + console.log("Getting value", this.selections[row].getWrappedText()[0]); + return this.selections[row].getWrappedText()[0]; } setValsToDefault(): void { diff --git a/src/ui/pokedex-scan-ui-handler.ts b/src/ui/pokedex-scan-ui-handler.ts new file mode 100644 index 00000000000..0f6627c335d --- /dev/null +++ b/src/ui/pokedex-scan-ui-handler.ts @@ -0,0 +1,211 @@ +import { FormModalUiHandler, InputFieldConfig } from "./form-modal-ui-handler"; +import { ModalConfig } from "./modal-ui-handler"; +import i18next from "i18next"; +import { PlayerPokemon } from "#app/field/pokemon"; +import { OptionSelectItem } from "./abstact-option-select-ui-handler"; +import { isNullOrUndefined } from "#app/utils"; +import { Mode } from "./ui"; +import { FilterTextRow } from "./filter-text"; + +export default class PokedexScanUiHandler extends FormModalUiHandler { + + keys: string[]; + reducedKeys: string[]; + parallelKeys: string[]; + + constructor(scene, mode) { + super(scene, mode); + } + + setup() { + super.setup(); + + const flattenKeys = (object?: any, topKey?: string, midleKey?: string[]): Array => { + return Object.keys(object ?? {}).map((t, i) => { + const value = Object.values(object)[i]; + + if (typeof value === "object" && !isNullOrUndefined(value)) { // we check for not null or undefined here because if the language json file has a null key, the typeof will still be an object, but that object will be null, causing issues + // If the value is an object, execute the same process + // si el valor es un objeto ejecuta el mismo proceso + + return flattenKeys(value, topKey ?? t, topKey ? midleKey ? [ ...midleKey, t ] : [ t ] : undefined).filter((t) => t.length > 0); + } else if (typeof value === "string" || isNullOrUndefined(value)) { // we check for null or undefined here as per above - the typeof is still an object but the value is null so we need to exit out of this and pass the null key + + // Return in the format expected by i18next + return midleKey ? `${topKey}:${midleKey.map((m) => m).join(".")}.${t}` : `${topKey}:${t}`; + } + }).filter((t) => t); + }; + + const keysInArrays = flattenKeys(i18next.getDataByLanguage(String(i18next.resolvedLanguage))).filter((t) => t.length > 0); // Array of arrays + const keys = keysInArrays.flat(Infinity).map(String); // One array of string + this.keys = keys; + } + + getModalTitle(config?: ModalConfig): string { + return "Choose option"; + } + + getWidth(config?: ModalConfig): number { + return 300; + } + + getMargin(config?: ModalConfig): [number, number, number, number] { + return [ 0, 0, 48, 0 ]; + } + + getButtonLabels(config?: ModalConfig): string[] { + return [ "Select", "Cancel" ]; + } + + getReadableErrorMessage(error: string): string { + const colonIndex = error?.indexOf(":"); + if (colonIndex > 0) { + error = error.slice(0, colonIndex); + } + + return super.getReadableErrorMessage(error); + } + + override getInputFieldConfigs(): InputFieldConfig[] { + return [{ label: "Dialogue" }]; + } + + reduceKeys(row: FilterTextRow): void { + console.log("Function was called!"); + console.log(this.keys); + switch (row) { + case FilterTextRow.NAME: { + const startString = "pokemon:"; + const endString = ""; + this.reducedKeys = this.keys.filter(str => str.startsWith(startString) && str.endsWith(endString)); + break; + } + case FilterTextRow.MOVE_1: + case FilterTextRow.MOVE_2: { + const startString = "move:"; + const endString = ".name"; + this.reducedKeys = this.keys.filter(str => str.startsWith(startString) && str.endsWith(endString)); + break; + } + case FilterTextRow.ABILITY_1: + case FilterTextRow.ABILITY_2: { + const startString = "ability:"; + const endString = ".name"; + this.reducedKeys = this.keys.filter(str => str.startsWith(startString) && str.endsWith(endString)); + break; + } + default: { + this.reducedKeys = this.keys; + } + } + console.log(this.reducedKeys); + + // this.parallelKeys = this.reducedKeys.map(key => this.translateKey(key)); + this.parallelKeys = this.reducedKeys.map(key => String(i18next.t(key))); + + console.log(this.parallelKeys); + } + + translateKey(key: string): string { + const interpolatorOptions: any = {}; + const splitArr = key.split(" "); // this splits our inputted text into words to cycle through later + const translatedString = splitArr[0]; // this is our outputted i18 string + const regex = RegExp("\\{\\{(\\w*)\\}\\}", "g"); // this is a regex expression to find all the text between {{ }} in the i18 output + const matches = i18next.t(translatedString).match(regex) ?? []; + if (matches.length > 0) { + for (let match = 0; match < matches.length; match++) { + // we add 1 here because splitArr[0] is our first value for the translatedString, and after that is where the variables are + // the regex here in the replace (/\W/g) is to remove the {{ and }} and just give us all alphanumeric characters + if (typeof splitArr[match + 1] !== "undefined") { + interpolatorOptions[matches[match].replace(/\W/g, "")] = i18next.t(splitArr[match + 1]); + } + } + } + + return String(i18next.t(translatedString, interpolatorOptions)); + } + + // args[2] is an index of FilterTextRow + show(args: any[]): boolean { + const ui = this.getUi(); + const hasTitle = !!this.getModalTitle(); + this.updateFields(this.getInputFieldConfigs(), hasTitle); + this.updateContainer(args[0] as ModalConfig); + const input = this.inputs[0]; + input.setMaxLength(255); + + console.log(args[2]); + this.reduceKeys(args[2]); + + input.on("keydown", (inputObject, evt: KeyboardEvent) => { + if ([ "escape", "space" ].some((v) => v === evt.key.toLowerCase() || v === evt.code.toLowerCase()) && ui.getMode() === Mode.AUTO_COMPLETE) { + // Delete autocomplete list and recovery focus. + inputObject.on("blur", () => inputObject.node.focus(), { once: true }); + ui.revertMode(); + } + }); + + input.on("textchange", (inputObject, evt: InputEvent) => { + // Delete autocomplete. + if (ui.getMode() === Mode.AUTO_COMPLETE) { + ui.revertMode(); + } + + let options: OptionSelectItem[] = []; + const splitArr = inputObject.text.split(" "); + const filteredKeys = this.parallelKeys.filter((command) => command.toLowerCase().includes(splitArr[splitArr.length - 1].toLowerCase())); + if (inputObject.text !== "" && filteredKeys.length > 0) { + // if performance is required, you could reduce the number of total results by changing the slice below to not have all ~8000 inputs going + options = filteredKeys.slice(0).map((value) => { + return { + label: value, + handler: () => { + // this is here to make sure that if you try to backspace then enter, the last known evt.data (backspace) is picked up + // this is because evt.data is null for backspace, so without this, the autocomplete windows just closes + if (!isNullOrUndefined(evt.data) || evt.inputType?.toLowerCase() === "deletecontentbackward") { + const separatedArray = inputObject.text.split(" "); + separatedArray[separatedArray.length - 1] = value; + inputObject.setText(separatedArray.join(" ")); + } + ui.revertMode(); + return true; + } + }; + }); + } + + if (options.length > 0) { + const modalOpts = { + options: options, + maxOptions: 5, + modalContainer: this.modalContainer + }; + ui.setOverlayMode(Mode.AUTO_COMPLETE, modalOpts); + } + + }); + + if (super.show(args)) { + const config = args[0] as ModalConfig; + this.inputs[0].resize(1150, 116); + this.inputContainers[0].list[0].width = 200; + if (args[1] && typeof (args[1] as PlayerPokemon).getNameToRender === "function") { + this.inputs[0].text = (args[1] as PlayerPokemon).getNameToRender(); + } else { + this.inputs[0].text = args[1]; + } + this.submitAction = (_) => { + if (ui.getMode() === Mode.POKEDEX_SCAN) { + this.sanitizeInputs(); + const sanitizedName = btoa(unescape(encodeURIComponent(this.inputs[0].text))); + config.buttonActions[0](sanitizedName); + return true; + } + return false; + }; + return true; + } + return false; + } +} diff --git a/src/ui/pokedex-ui-handler.ts b/src/ui/pokedex-ui-handler.ts index fe73ab0864c..ab65b887611 100644 --- a/src/ui/pokedex-ui-handler.ts +++ b/src/ui/pokedex-ui-handler.ts @@ -194,7 +194,7 @@ export default class PokedexUiHandler extends MessageUiHandler { // Create and initialise filter text fields this.filterTextContainer = this.scene.add.container(0, 0); - this.filterText = new FilterText(this.scene, 1, filterBarHeight + 2, 110, 100); + this.filterText = new FilterText(this.scene, 1, filterBarHeight + 2, 140, 100, this.updateStarters); // const nameTextField: TextField = new TextField(this.scene, 0, 0, genOptions, this.updateStarters, DropDownType.HYBRID); this.filterText.addFilter(FilterTextRow.NAME, i18next.t("filterText:nameField")); @@ -291,15 +291,15 @@ export default class PokedexUiHandler extends MessageUiHandler { // gen filter const genOptions: DropDownOption[] = [ - new DropDownOption(this.scene, 1, new DropDownLabel(i18next.t("PokedexUiHandler:gen1"))), - new DropDownOption(this.scene, 2, new DropDownLabel(i18next.t("PokedexUiHandler:gen2"))), - new DropDownOption(this.scene, 3, new DropDownLabel(i18next.t("PokedexUiHandler:gen3"))), - new DropDownOption(this.scene, 4, new DropDownLabel(i18next.t("PokedexUiHandler:gen4"))), - new DropDownOption(this.scene, 5, new DropDownLabel(i18next.t("PokedexUiHandler:gen5"))), - new DropDownOption(this.scene, 6, new DropDownLabel(i18next.t("PokedexUiHandler:gen6"))), - new DropDownOption(this.scene, 7, new DropDownLabel(i18next.t("PokedexUiHandler:gen7"))), - new DropDownOption(this.scene, 8, new DropDownLabel(i18next.t("PokedexUiHandler:gen8"))), - new DropDownOption(this.scene, 9, new DropDownLabel(i18next.t("PokedexUiHandler:gen9"))), + new DropDownOption(this.scene, 1, new DropDownLabel(i18next.t("pokedex-ui-handler:gen1"))), + new DropDownOption(this.scene, 2, new DropDownLabel(i18next.t("pokedex-ui-handler:gen2"))), + new DropDownOption(this.scene, 3, new DropDownLabel(i18next.t("pokedex-ui-handler:gen3"))), + new DropDownOption(this.scene, 4, new DropDownLabel(i18next.t("pokedex-ui-handler:gen4"))), + new DropDownOption(this.scene, 5, new DropDownLabel(i18next.t("pokedex-ui-handler:gen5"))), + new DropDownOption(this.scene, 6, new DropDownLabel(i18next.t("pokedex-ui-handler:gen6"))), + new DropDownOption(this.scene, 7, new DropDownLabel(i18next.t("pokedex-ui-handler:gen7"))), + new DropDownOption(this.scene, 8, new DropDownLabel(i18next.t("pokedex-ui-handler:gen8"))), + new DropDownOption(this.scene, 9, new DropDownLabel(i18next.t("pokedex-ui-handler:gen9"))), ]; const genDropDown: DropDown = new DropDown(this.scene, 0, 0, genOptions, this.updateStarters, DropDownType.HYBRID); this.filterBar.addFilter(DropDownColumn.GEN, i18next.t("filterBar:genFilter"), genDropDown); @@ -582,7 +582,7 @@ export default class PokedexUiHandler extends MessageUiHandler { console.log(this.filterTextOptions); - this.optionSelectText = addTextObject(this.scene, 0, 0, this.filterTextOptions.map(o => `${i18next.t(`pokedexUiHandler:${FilterTextOptions[o]}`)}`).join("\n"), TextStyle.WINDOW, { maxLines: this.filterTextOptions.length }); + this.optionSelectText = addTextObject(this.scene, 0, 0, this.filterTextOptions.map(o => `${i18next.t(`pokedex-ui-handler:${FilterTextOptions[o]}`)}`).join("\n"), TextStyle.WINDOW, { maxLines: this.filterTextOptions.length }); this.optionSelectText.setLineSpacing(12); // Positioning the menu @@ -951,12 +951,7 @@ export default class PokedexUiHandler extends MessageUiHandler { success = true; } else if (this.filterTextMode) { - if (numberOfStarters > 0) { - this.setFilterTextMode(false); - this.scrollCursor = 0; - this.updateScroll(); - this.setCursor(0); - } + this.filterText.resetSelection(this.filterTextCursor); success = true; } else if (this.statsMode) { this.toggleStatsMode(false); @@ -1116,7 +1111,7 @@ export default class PokedexUiHandler extends MessageUiHandler { }; if (!pokemonPrevolutions.hasOwnProperty(this.lastSpecies.speciesId)) { options.push({ - label: i18next.t("PokedexUiHandler:useCandies"), + label: i18next.t("pokedex-ui-handler:useCandies"), handler: () => { ui.setMode(Mode.POKEDEX, "refresh").then(() => showUseCandies()); return true; @@ -1357,10 +1352,15 @@ export default class PokedexUiHandler extends MessageUiHandler { container.cost = this.scene.gameData.getSpeciesStarterValue(container.species.speciesId); // First, ensure you have the caught attributes for the species else default to bigint 0 + // TODO: This might be removed depending on how accessible we want the pokedex function to be const caughtAttr = this.scene.gameData.dexData[container.species.speciesId]?.caughtAttr || BigInt(0); const starterData = this.scene.gameData.starterData[container.species.speciesId]; const isStarterProgressable = speciesEggMoves.hasOwnProperty(container.species.speciesId); + // Name filter + console.log(container.species.name); + const fitsName = [ container.species.name, this.filterText.defaultText ].includes(this.filterText.getValue(FilterTextRow.NAME)); + // Gen filter const fitsGen = this.filterBar.getVals(DropDownColumn.GEN).includes(container.species.generation); @@ -1482,7 +1482,7 @@ export default class PokedexUiHandler extends MessageUiHandler { } }); - if (fitsGen && fitsType && fitsCaught && fitsPassive && fitsCostReduction && fitsFavorite && fitsWin && fitsHA && fitsEgg && fitsPokerus) { + if (fitsName && fitsGen && fitsType && fitsCaught && fitsPassive && fitsCostReduction && fitsFavorite && fitsWin && fitsHA && fitsEgg && fitsPokerus) { this.filteredStarterContainers.push(container); } }); @@ -1615,6 +1615,12 @@ export default class PokedexUiHandler extends MessageUiHandler { const pos = calcStarterPosition(cursor, this.scrollCursor); this.cursorObj.setPosition(pos.x - 1, pos.y + 1); + + const species = this.filteredStarterContainers[cursor]?.species; + + if (species) { + this.setSpecies(species); + } } return changed; @@ -2064,7 +2070,7 @@ export default class PokedexUiHandler extends MessageUiHandler { this.clearText(); this.blockInput = false; }; - ui.showText(i18next.t("PokedexUiHandler:confirmExit"), null, () => { + ui.showText(i18next.t("pokedex-ui-handler:confirmExit"), null, () => { ui.setModeWithoutClear(Mode.CONFIRM, () => { ui.setMode(Mode.POKEDEX, "refresh"); console.log("Press exit", ui.getModeChain(), ui.getMode()); diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 847788a3f7c..79113e13258 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -54,6 +54,7 @@ import TestDialogueUiHandler from "#app/ui/test-dialogue-ui-handler"; import AutoCompleteUiHandler from "./autocomplete-ui-handler"; import { Device } from "#enums/devices"; import MysteryEncounterUiHandler from "./mystery-encounter-ui-handler"; +import PokedexScanUiHandler from "./pokedex-scan-ui-handler"; export enum Mode { MESSAGE, @@ -86,6 +87,7 @@ export enum Mode { EGG_LIST, EGG_GACHA, POKEDEX, + POKEDEX_SCAN, LOGIN_FORM, REGISTRATION_FORM, LOADING, @@ -130,6 +132,7 @@ const noTransitionModes = [ Mode.SETTINGS_KEYBOARD, Mode.ACHIEVEMENTS, Mode.GAME_STATS, + Mode.POKEDEX_SCAN, Mode.LOGIN_FORM, Mode.REGISTRATION_FORM, Mode.LOADING, @@ -196,6 +199,7 @@ export default class UI extends Phaser.GameObjects.Container { new EggListUiHandler(scene), new EggGachaUiHandler(scene), new PokedexUiHandler(scene), + new PokedexScanUiHandler(scene, Mode.TEST_DIALOGUE), new LoginFormUiHandler(scene), new RegistrationFormUiHandler(scene), new LoadingModalUiHandler(scene),