diff --git a/src/locales/de/menu.json b/src/locales/de/menu.json index e5c1c700425..fa915681826 100644 --- a/src/locales/de/menu.json +++ b/src/locales/de/menu.json @@ -50,6 +50,7 @@ "choosePokemon": "Wähle ein Pokémon.", "renamePokemon": "Pokémon umbennenen", "rename": "Umbenennen", + "renameHelpEmoji": "Use \"/\" to access emoji library and enter a number to choose an emoji\n(e.g. \"/1\" for the first emoji)", "nickname": "Spitzname", "errorServerDown": "Ups! Es gab einen Fehler beim Versuch\nden Server zu kontaktieren\nLasse dieses Fenster offen\nDu wirst automatisch neu verbunden." } \ No newline at end of file diff --git a/src/locales/en/menu.json b/src/locales/en/menu.json index 97cfc0b019d..45cec66fdd3 100644 --- a/src/locales/en/menu.json +++ b/src/locales/en/menu.json @@ -50,6 +50,7 @@ "choosePokemon": "Choose a Pokémon.", "renamePokemon": "Rename Pokémon", "rename": "Rename", + "renameHelpEmoji": "Use \"/\" to access emoji library and enter a number to choose an emoji\n(e.g. \"/1\" for the first emoji)", "nickname": "Nickname", "errorServerDown": "Oops! There was an issue contacting the server.\n\nYou may leave this window open,\nthe game will automatically reconnect." } \ No newline at end of file diff --git a/src/locales/es/menu.json b/src/locales/es/menu.json index bd2479a02df..0a0aed68ca4 100644 --- a/src/locales/es/menu.json +++ b/src/locales/es/menu.json @@ -50,6 +50,7 @@ "choosePokemon": "Elige un Pokémon.", "renamePokemon": "Renombrar Pokémon.", "rename": "Renombrar", + "renameHelpEmoji": "Use \"/\" to access emoji library and enter a number to choose an emoji\n(e.g. \"/1\" for the first emoji)", "nickname": "Apodo", "errorServerDown": "¡Ups! Ha habido un problema al contactar con el servidor.\n\nPuedes mantener esta ventana abierta, el juego se reconectará automáticamente." } diff --git a/src/locales/fr/menu.json b/src/locales/fr/menu.json index 83626a1f33f..720759ff24c 100644 --- a/src/locales/fr/menu.json +++ b/src/locales/fr/menu.json @@ -50,6 +50,7 @@ "choosePokemon": "Sélectionnez un Pokémon.", "renamePokemon": "Renommer le Pokémon", "rename": "Renommer", + "renameHelpEmoji": "Use \"/\" to access emoji library and enter a number to choose an emoji\n(e.g. \"/1\" for the first emoji)", "nickname": "Surnom", "errorServerDown": "Oupsi ! Un problème de connexion au serveur est survenu.\n\nVous pouvez garder cette fenêtre ouverte,\nle jeu se reconnectera automatiquement." } diff --git a/src/locales/it/menu.json b/src/locales/it/menu.json index 2d37f9db912..852fcccddcc 100644 --- a/src/locales/it/menu.json +++ b/src/locales/it/menu.json @@ -50,6 +50,7 @@ "choosePokemon": "Scegli un Pokémon.", "renamePokemon": "Rinomina un Pokémon", "rename": "Rinomina", + "renameHelpEmoji": "Use \"/\" to access emoji library and enter a number to choose an emoji\n(e.g. \"/1\" for the first emoji)", "nickname": "Nickname", "errorServerDown": "Poffarbacco! C'è stato un errore nella comunicazione col server.\n\nPuoi lasciare questa finestra aperta,\nil gioco si riconnetterà automaticamente." } \ No newline at end of file diff --git a/src/locales/ko/menu.json b/src/locales/ko/menu.json index 7976e0bedcf..b2c0e4fcde3 100644 --- a/src/locales/ko/menu.json +++ b/src/locales/ko/menu.json @@ -50,6 +50,7 @@ "choosePokemon": "포켓몬을 선택하세요.", "renamePokemon": "포켓몬의 닉네임은?", "rename": "닉네임 바꾸기", + "renameHelpEmoji": "Use \"/\" to access emoji library and enter a number to choose an emoji\n(e.g. \"/1\" for the first emoji)", "nickname": "닉네임", "errorServerDown": "서버 연결 중 문제가 발생했습니다.\n\n이 창을 종료하지 않고 두면,\n게임은 자동으로 재접속됩니다." } diff --git a/src/locales/pt_BR/menu.json b/src/locales/pt_BR/menu.json index 415796f91ed..c8b9940ca74 100644 --- a/src/locales/pt_BR/menu.json +++ b/src/locales/pt_BR/menu.json @@ -50,6 +50,7 @@ "choosePokemon": "Escolha um Pokémon.", "renamePokemon": "Renomear Pokémon", "rename": "Renomear", + "renameHelpEmoji": "Use \"/\" to access emoji library and enter a number to choose an emoji\n(e.g. \"/1\" for the first emoji)", "nickname": "Apelido", "errorServerDown": "Opa! Não foi possível conectar-se ao servidor.\n\nVocê pode deixar essa janela aberta,\npois o jogo irá se reconectar automaticamente." } \ No newline at end of file diff --git a/src/locales/zh_CN/menu.json b/src/locales/zh_CN/menu.json index 59146d30ee9..3d5ff1cb550 100644 --- a/src/locales/zh_CN/menu.json +++ b/src/locales/zh_CN/menu.json @@ -50,6 +50,7 @@ "choosePokemon": "选择一只宝可梦。", "renamePokemon": "给宝可梦起名", "rename": "起名", + "renameHelpEmoji": "Use \"/\" to access emoji library and enter a number to choose an emoji\n(e.g. \"/1\" for the first emoji)", "nickname": "昵称", "errorServerDown": "糟糕!访问服务器时发生了错误。\n\n你可以保持页面开启,\n游戏会自动重新连接。" } \ No newline at end of file diff --git a/src/ui/autocomplete-ui-handler.ts b/src/ui/autocomplete-ui-handler.ts new file mode 100644 index 00000000000..0da35ecba14 --- /dev/null +++ b/src/ui/autocomplete-ui-handler.ts @@ -0,0 +1,113 @@ +import { Button } from "#enums/buttons"; +import BattleScene from "../battle-scene"; +import AbstractOptionSelectUiHandler from "./abstact-option-select-ui-handler"; +import { Mode } from "./ui"; +import InputText from "phaser3-rex-plugins/plugins/inputtext"; +// import * as Utils from "#app/utils"; + +export default class AutoCompleteUiHandler extends AbstractOptionSelectUiHandler { + modalContainer: Phaser.GameObjects.Container; + inputContainer: Phaser.GameObjects.Container; + handlerKeyDown: (inputObject: InputText, evt: KeyboardEvent) => void; + + + constructor(scene: BattleScene, mode: Mode = Mode.OPTION_SELECT, ...args) { + super(scene, mode); + this.config = { + options: [] + }; + this.handlerKeyDown = (inputObject, evt) => { + // Don't move inputText cursor + if (["arrowup"].some((key) => key === (evt.code || evt.key).toLowerCase())) { + evt.preventDefault(); + this.processInput(Button.UP); + } else if (["arrowdown"].some((key) => key === (evt.code || evt.key).toLowerCase())) { + evt.preventDefault(); + this.processInput(Button.DOWN); + } + + // Revert Mode when not press... + if (!["enter", "arrowup", "arrowdown"].some((key) => (evt.code || evt.key).toLowerCase().includes(key))) { + this.scene.ui.revertMode(); + } + + // Recovery focus + if (["escape"].some((key) => key === (evt.code || evt.key).toLowerCase())) { + const recoveryFocus = () => (inputObject.setFocus(), inputObject.off("blur", recoveryFocus)); + inputObject.on("blur", recoveryFocus); + } + }; + } + + getWindowWidth(): integer { + return 0; + } + + show(args: any[]): boolean { + if (args[0].modalContainer && args[0].inputContainer && args[0].inputContainer.list.some((el) => el instanceof InputText)) { + const { modalContainer, inputContainer } = args[0]; + args[0].options?.forEach((opt)=>{ + const originalHandler = opt.handler; + opt.handler = () => { + if (originalHandler()) { + ui.revertMode(); + return true; + } + return false; + }; + }); + this.modalContainer = modalContainer; + this.inputContainer = inputContainer; + const input = args[0].inputContainer.list.find((el) => el instanceof InputText); + const ui = this.getUi(); + + const originalsEvents = input.listeners("keydown"); + for (let i = 0; i < originalsEvents?.length; i++) { + input.off("keydown", originalsEvents[i]); + } + + const handlerBlur = () => { + ui.revertMode(); + input.off("blur", handlerBlur); + }; + const handlerPointerUp = () => { + ui.revertMode(); + this.modalContainer.off("pointerup", handlerPointerUp); + }; + + input.on("blur", handlerBlur); + input.on("keydown", this.handlerKeyDown); + this.modalContainer.on("pointerup", handlerPointerUp); + + for (let i = 0; i < originalsEvents?.length; i++) { + input.on("keydown", originalsEvents[i]); + } + + const show = super.show(args); + this.setupOptions(); + + return show; + } + return false; + } + + protected setupOptions() { + super.setupOptions(); + if (this.modalContainer) { + this.optionSelectContainer.setPositionRelative(this.modalContainer, this.optionSelectBg.width + this.inputContainer.x, this.optionSelectBg.height + this.inputContainer.y + (this.inputContainer.list.find((el) => el instanceof Phaser.GameObjects.NineSlice)?.height ?? 0)); + } + } + + // processInput(button: Button): boolean { + // if (button !== Button.CANCEL) { + // return super.processInput(button); + // } + // return false; + // } + + clear(): void { + super.clear(); + const input = this.inputContainer.list.find((el) => el instanceof InputText); + input?.off("keydown", this.handlerKeyDown); + } +} diff --git a/src/ui/rename-form-ui-handler.ts b/src/ui/rename-form-ui-handler.ts index b087d32d7dc..8644a3247e9 100644 --- a/src/ui/rename-form-ui-handler.ts +++ b/src/ui/rename-form-ui-handler.ts @@ -4,10 +4,17 @@ import i18next from "i18next"; import { PlayerPokemon } from "#app/field/pokemon.js"; import { Mode } from "./ui"; import { OptionSelectConfig, OptionSelectItem } from "./abstact-option-select-ui-handler"; +import { addWindow } from "./ui-theme"; +import { addTextObject, getTextStyleOptions, TextStyle } from "./text"; +import InputText from "phaser3-rex-plugins/plugins/inputtext"; +import BattleScene from "#app/battle-scene.js"; +import AutoCompleteUiHandler from "./autocomplete-ui-handler"; -const emojiAvailable = ["♪", "★", "♥", "♣"]; +const emojiAvailable = ["♪", "★", "♥", "♣", "☻", "ª", "☼", "►", "♫", "←", "→"]; export default class RenameFormUiHandler extends FormModalUiHandler { + protected autocomplete: AutoCompleteUiHandler; + getModalTitle(config?: ModalConfig): string { return i18next.t("menu:renamePokemon"); } @@ -37,38 +44,31 @@ export default class RenameFormUiHandler extends FormModalUiHandler { return super.getReadableErrorMessage(error); } - show(args: any[]): boolean { - const ui = this.getUi(); - const input = this.inputs[0]; - input.node.addEventListener("keydown",(e:KeyboardEvent)=>{ - if (e.key === "/") { - const emojiOptions = emojiAvailable.map((emoji): OptionSelectItem => { - return { - label: emoji, - handler: ()=> { - ui.revertMode(); - return true; - }, - keepOpen: true - }; - }); - const modalOptions: OptionSelectConfig = { - xOffset: 98, - yOffset: 48 / 2, - options: emojiOptions - }; - this.scene.ui.setOverlayMode(Mode.MENU_OPTION_SELECT, modalOptions); - // input.setText(input.text + emojiAvailable[0]); - } else if (input.cursorPosition === (input.text.split("").findIndex((value) => value === "/"))) { - // input.setData("filter", ) - console.log(input.text[input.text.split("").findIndex((value) => value === "/")]); - } else if (e.code === "Escape" && ui.getMode() === Mode.MENU_OPTION_SELECT) { - e.preventDefault(); - ui.revertMode(); - console.log(e, input); - } + setup(): void { + super.setup(); + const helpEmojiListContainer = this.scene.add.container(0, this.getHeight()); + const helpEmojiListBg = addWindow(this.scene, 0, 0, this.getWidth(), 52); + helpEmojiListContainer.add(helpEmojiListBg); + const scale = getTextStyleOptions(TextStyle.WINDOW, (this.scene as BattleScene).uiTheme).scale ?? 0.1666666667; + + const helpEmojiListText = addTextObject(this.scene, 8, 8, i18next.t("menu:renameHelpEmoji"), TextStyle.TOOLTIP_CONTENT, { + fontSize: "80px", }); + helpEmojiListText.setWordWrapWidth(this.modalContainer.getBounds().width * 0.95); + const height = ((Math.min((helpEmojiListText.getWrappedText(helpEmojiListText.text) || []).length, 99)) * 96 * scale) + helpEmojiListText.y; + helpEmojiListBg.setSize(helpEmojiListBg.width, height); + + helpEmojiListContainer.add(helpEmojiListText); + + this.modalContainer.add(helpEmojiListContainer); + } + + show(args: any[]): boolean { if (super.show(args)) { + const ui = this.getUi(); + const input = this.inputs[0]; + + // rename config const config = args[0] as ModalConfig; if (args[1] && typeof (args[1] as PlayerPokemon).getNameToRender === "function") { this.inputs[0].text = (args[1] as PlayerPokemon).getNameToRender(); @@ -76,11 +76,103 @@ export default class RenameFormUiHandler extends FormModalUiHandler { this.inputs[0].text = args[1]; } this.submitAction = (_) => { - this.sanitizeInputs(); - const sanitizedName = btoa(unescape(encodeURIComponent(this.inputs[0].text))); - config.buttonActions[0](sanitizedName); - return true; + if (ui.getMode() === Mode.RENAME_POKEMON) { + this.sanitizeInputs(); + const sanitizedName = btoa(unescape(encodeURIComponent(this.inputs[0].text))); + config.buttonActions[0](sanitizedName); + return true; + } }; + const originalCancel = config.buttonActions[1]; + config.buttonActions[1] = ()=>{ + if (ui.getMode() === Mode.RENAME_POKEMON) { + originalCancel(); + } + }; + + const maxEmojis = 6; + + const emojiOptions = emojiAvailable.map((emoji, index): OptionSelectItem => { + return { + label: `${emoji} /${index + 1}`, + handler: ()=> { + const command = input.text.split("").filter((_, i) => i >= (input.text.split("").filter((_, i) => i < input.cursorPosition).lastIndexOf("/")) && i < input.cursorPosition).join(""); + + const texto = input.text; + const textBeforeCursor = texto.substring(0, input.cursorPosition); + const textAfterCursor = texto.substring(input.cursorPosition); + + const exactlyCommand = textBeforeCursor.lastIndexOf(command); + if (exactlyCommand !== -1) { + const textReplace = textBeforeCursor.substring(0, exactlyCommand) + emoji + textAfterCursor; + input.setText(textReplace); + input.setCursorPosition(exactlyCommand + emoji.length); + return true; + } + return false; + }, + }; + }); + + interface OptionSelectConfigAC extends OptionSelectConfig { + inputContainer: Phaser.GameObjects.Container; + modalContainer: Phaser.GameObjects.Container; + } + + const modalOptions = { + options: emojiOptions, + modalContainer: this.modalContainer, + inputContainer: this.inputContainers[0], + maxOptions: 5 + }; + + input.on("textchange", (inputObject:InputText, evt:InputEvent) => { + if (input.text.split("").filter((char) => emojiAvailable.some((em) => em === char)).length < maxEmojis && input.text.split("").some((char, i) => char === "/" && i + 1 === input.cursorPosition) && input.text.length < input.maxLength) { + ui.setOverlayMode(Mode.AUTO_COMPLETE, modalOptions); + } + + if (input.text.split("").filter((char) => emojiAvailable.some((em) => em === char)).length > maxEmojis) { + const command = input.text.split("").filter((_, i) => evt.data && i >= (input.text.split("").filter((_, i) => i < input.cursorPosition).lastIndexOf(evt.data)) && i < input.cursorPosition).join(""); + + const texto = input.text; + const textBeforeCursor = texto.substring(0, input.cursorPosition); + const textAfterCursor = texto.substring(input.cursorPosition); + + const exactlyCommand = textBeforeCursor.lastIndexOf(command); + if (exactlyCommand !== -1 && evt.data) { + const textReplace = textBeforeCursor.substring(0, exactlyCommand) + textAfterCursor; + if (textReplace !== input.text) { + input.setText(textReplace); + input.setCursorPosition(exactlyCommand); + } + } + } + if (evt.data && input.text.split("").filter((char) => emojiAvailable.some((em) => em === char)).length < maxEmojis) { + if (evt.data === "/") { + ui.setOverlayMode(Mode.AUTO_COMPLETE, modalOptions); + } + } + }); + + input.on("keydown", (inputObject:InputText, evt:KeyboardEvent)=>{ + const command = input.text.split("").filter((_, i) => i >= (input.text.split("").filter((_, i) => i < input.cursorPosition).lastIndexOf("/")) && i < input.cursorPosition).join(""); + if (!isNaN(parseInt(evt.key))) { + input.setData("filter", input.getData("filter") ? input.getData("filter").toString().concat(evt.key) : evt.key.toString()); + } else { + input.setData("filter"); + } + if (command.includes("/") && emojiOptions.some((_, i) => (i + 1).toString().includes(input.getData("filter")))) { + const filterOptions: OptionSelectConfigAC = { + options: emojiOptions.filter((_, i) => (i + 1).toString().includes(input.getData("filter"))), + modalContainer: this.modalContainer, + inputContainer: this.inputContainers[0], + maxOptions: 5 + }; + + this.scene.ui.setOverlayMode(Mode.AUTO_COMPLETE, filterOptions); + } + }); + return true; } return false; diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 250a21544dc..a398a15542d 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -48,6 +48,7 @@ import BgmBar from "#app/ui/bgm-bar"; import RenameFormUiHandler from "./rename-form-ui-handler"; import RunHistoryUiHandler from "./run-history-ui-handler"; import RunInfoUiHandler from "./run-info-ui-handler"; +import AutoCompleteUiHandler from "./autocomplete-ui-handler"; export enum Mode { MESSAGE, @@ -88,6 +89,7 @@ export enum Mode { RENAME_POKEMON, RUN_HISTORY, RUN_INFO, + AUTO_COMPLETE, } const transitionModes = [ @@ -124,7 +126,8 @@ const noTransitionModes = [ Mode.SESSION_RELOAD, Mode.UNAVAILABLE, Mode.OUTDATED, - Mode.RENAME_POKEMON + Mode.RENAME_POKEMON, + Mode.AUTO_COMPLETE ]; export default class UI extends Phaser.GameObjects.Container { @@ -188,6 +191,7 @@ export default class UI extends Phaser.GameObjects.Container { new RenameFormUiHandler(scene), new RunHistoryUiHandler(scene), new RunInfoUiHandler(scene), + new AutoCompleteUiHandler(scene), ]; }