From 69da96d543e7172050c33a8550787f06b37780f0 Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 3 Jun 2024 19:57:47 -0400 Subject: [PATCH] Settings Refactor (#1771) --- src/configs/inputs/cfg_keyboard_qwerty.ts | 2 +- src/configs/inputs/pad_dualshock.ts | 2 +- src/configs/inputs/pad_generic.ts | 2 +- src/configs/inputs/pad_procon.ts | 2 +- src/configs/inputs/pad_unlicensedSNES.ts | 2 +- src/configs/inputs/pad_xbox360.ts | 2 +- src/inputs-controller.ts | 4 +- src/phases.ts | 6 +- src/system/game-data.ts | 82 +-- src/system/settings.ts | 279 -------- src/system/{ => settings}/settings-gamepad.ts | 53 +- .../{ => settings}/settings-keyboard.ts | 76 +- src/system/settings/settings.ts | 453 ++++++++++++ src/test/helpers/inGameManip.ts | 2 +- src/test/helpers/menuManip.ts | 2 +- src/test/rebinding_setting.test.ts | 2 +- src/ui-inputs.ts | 22 +- .../abstract-control-settings-ui-handler.ts | 660 ++++++++++++++++++ .../settings/abstract-settings-ui-handler.ts | 608 +++++----------- src/ui/settings/navigationMenu.ts | 43 +- .../settings-accessiblity-ui-handler.ts | 20 + .../settings/settings-gamepad-ui-handler.ts | 70 +- .../settings/settings-keyboard-ui-handler.ts | 65 +- src/ui/settings/settings-ui-handler.ts | 350 +--------- src/ui/ui.ts | 4 + 25 files changed, 1487 insertions(+), 1326 deletions(-) delete mode 100644 src/system/settings.ts rename src/system/{ => settings}/settings-gamepad.ts (82%) rename src/system/{ => settings}/settings-keyboard.ts (84%) create mode 100644 src/system/settings/settings.ts create mode 100644 src/ui/settings/abstract-control-settings-ui-handler.ts create mode 100644 src/ui/settings/settings-accessiblity-ui-handler.ts diff --git a/src/configs/inputs/cfg_keyboard_qwerty.ts b/src/configs/inputs/cfg_keyboard_qwerty.ts index 869b763d6c1..83472529697 100644 --- a/src/configs/inputs/cfg_keyboard_qwerty.ts +++ b/src/configs/inputs/cfg_keyboard_qwerty.ts @@ -1,5 +1,5 @@ import {Button} from "#app/enums/buttons"; -import {SettingKeyboard} from "#app/system/settings-keyboard"; +import {SettingKeyboard} from "#app/system/settings/settings-keyboard"; const cfg_keyboard_qwerty = { padID: "default", diff --git a/src/configs/inputs/pad_dualshock.ts b/src/configs/inputs/pad_dualshock.ts index b0aaedf0ab8..d58a8b2879d 100644 --- a/src/configs/inputs/pad_dualshock.ts +++ b/src/configs/inputs/pad_dualshock.ts @@ -1,4 +1,4 @@ -import {SettingGamepad} from "../../system/settings-gamepad"; +import {SettingGamepad} from "../../system/settings/settings-gamepad"; import {Button} from "../../enums/buttons"; /** diff --git a/src/configs/inputs/pad_generic.ts b/src/configs/inputs/pad_generic.ts index f209f6858bd..916f8c82add 100644 --- a/src/configs/inputs/pad_generic.ts +++ b/src/configs/inputs/pad_generic.ts @@ -1,4 +1,4 @@ -import {SettingGamepad} from "../../system/settings-gamepad"; +import {SettingGamepad} from "../../system/settings/settings-gamepad"; import {Button} from "../../enums/buttons"; /** diff --git a/src/configs/inputs/pad_procon.ts b/src/configs/inputs/pad_procon.ts index ccaaf5fc635..4b7bd8457b0 100644 --- a/src/configs/inputs/pad_procon.ts +++ b/src/configs/inputs/pad_procon.ts @@ -1,4 +1,4 @@ -import {SettingGamepad} from "#app/system/settings-gamepad"; +import {SettingGamepad} from "#app/system/settings/settings-gamepad.js"; import {Button} from "#app/enums/buttons"; /** diff --git a/src/configs/inputs/pad_unlicensedSNES.ts b/src/configs/inputs/pad_unlicensedSNES.ts index 803d30442b5..106fbc9eb8f 100644 --- a/src/configs/inputs/pad_unlicensedSNES.ts +++ b/src/configs/inputs/pad_unlicensedSNES.ts @@ -1,4 +1,4 @@ -import {SettingGamepad} from "../../system/settings-gamepad"; +import {SettingGamepad} from "../../system/settings/settings-gamepad"; import {Button} from "../../enums/buttons"; /** diff --git a/src/configs/inputs/pad_xbox360.ts b/src/configs/inputs/pad_xbox360.ts index 213ed7f89fb..645829d5be2 100644 --- a/src/configs/inputs/pad_xbox360.ts +++ b/src/configs/inputs/pad_xbox360.ts @@ -1,4 +1,4 @@ -import {SettingGamepad} from "../../system/settings-gamepad"; +import {SettingGamepad} from "../../system/settings/settings-gamepad"; import {Button} from "#app/enums/buttons"; /** diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 2791f3b5b85..aa0f781cfef 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -19,8 +19,8 @@ import { getIconForLatestInput, swap, } from "#app/configs/inputs/configHandler"; import BattleScene from "./battle-scene"; -import {SettingGamepad} from "#app/system/settings-gamepad"; -import {SettingKeyboard} from "#app/system/settings-keyboard"; +import {SettingGamepad} from "#app/system/settings/settings-gamepad.js"; +import {SettingKeyboard} from "#app/system/settings/settings-keyboard"; export interface DeviceMapping { [key: string]: number; diff --git a/src/phases.ts b/src/phases.ts index 67f33b5e031..110c4155849 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -48,7 +48,7 @@ import { addPokeballCaptureStars, addPokeballOpenParticles } from "./field/anims import { SpeciesFormChangeActiveTrigger, SpeciesFormChangeManualTrigger, SpeciesFormChangeMoveLearnedTrigger, SpeciesFormChangePostMoveTrigger, SpeciesFormChangePreMoveTrigger } from "./data/pokemon-forms"; import { battleSpecDialogue, getCharVariantFromDialogue, miscDialogue } from "./data/dialogue"; import ModifierSelectUiHandler, { SHOP_OPTIONS_ROW_LIMIT } from "./ui/modifier-select-ui-handler"; -import { Setting } from "./system/settings"; +import { SettingKeys } from "./system/settings/settings"; import { Tutorial, handleTutorial } from "./tutorial"; import { TerrainType } from "./data/terrain"; import { OptionSelectConfig, OptionSelectItem } from "./ui/abstact-option-select-ui-handler"; @@ -477,7 +477,7 @@ export class SelectGenderPhase extends Phase { label: i18next.t("menu:boy"), handler: () => { this.scene.gameData.gender = PlayerGender.MALE; - this.scene.gameData.saveSetting(Setting.Player_Gender, 0); + this.scene.gameData.saveSetting(SettingKeys.Player_Gender, 0); this.scene.gameData.saveSystem().then(() => this.end()); return true; } @@ -486,7 +486,7 @@ export class SelectGenderPhase extends Phase { label: i18next.t("menu:girl"), handler: () => { this.scene.gameData.gender = PlayerGender.FEMALE; - this.scene.gameData.saveSetting(Setting.Player_Gender, 1); + this.scene.gameData.saveSetting(SettingKeys.Player_Gender, 1); this.scene.gameData.saveSystem().then(() => this.end()); return true; } diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 433ab13fd70..ec4a814b643 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -13,7 +13,7 @@ import { GameModes, gameModes } from "../game-mode"; import { BattleType } from "../battle"; import TrainerData from "./trainer-data"; import { trainerConfigs } from "../data/trainer-config"; -import { Setting, setSetting, settingDefaults } from "./settings"; +import { SettingKeys, resetSettings, setSetting } from "./settings/settings"; import { achvs } from "./achv"; import EggData from "./egg-data"; import { Egg } from "../data/egg"; @@ -30,9 +30,10 @@ import { allMoves } from "../data/move"; import { TrainerVariant } from "../field/trainer"; import { OutdatedPhase, ReloadSessionPhase } from "#app/phases"; import { Variant, variantData } from "#app/data/variant"; -import {setSettingGamepad, SettingGamepad, settingGamepadDefaults} from "./settings-gamepad"; -import {setSettingKeyboard, SettingKeyboard, settingKeyboardDefaults} from "#app/system/settings-keyboard"; +import {setSettingGamepad, SettingGamepad, settingGamepadDefaults} from "./settings/settings-gamepad"; +import {setSettingKeyboard, SettingKeyboard} from "#app/system/settings/settings-keyboard"; import { TerrainChangedEvent, WeatherChangedEvent } from "#app/field/arena-events.js"; +import { Device } from "#app/enums/devices.js"; const saveKey = "x0i2O7WRiANTqPmZ"; // Temporary; secure encryption is not yet necessary @@ -402,7 +403,7 @@ export class GameData { this.gender = systemData.gender; - this.saveSetting(Setting.Player_Gender, systemData.gender === PlayerGender.FEMALE ? 1 : 0); + this.saveSetting(SettingKeys.Player_Gender, systemData.gender === PlayerGender.FEMALE ? 1 : 0); const initStarterData = !systemData.starterData; @@ -568,19 +569,21 @@ export class GameData { } } - public saveSetting(setting: Setting, valueIndex: integer): boolean { + /** + * Saves a setting to localStorage + * @param setting string ideally of SettingKeys + * @param valueIndex index of the setting's option + * @returns true + */ + public saveSetting(setting: string, valueIndex: integer): boolean { let settings: object = {}; if (localStorage.hasOwnProperty("settings")) { settings = JSON.parse(localStorage.getItem("settings")); } - setSetting(this.scene, setting as Setting, valueIndex); + setSetting(this.scene, setting, valueIndex); - Object.keys(settingDefaults).forEach(s => { - if (s === setting) { - settings[s] = valueIndex; - } - }); + settings[setting] = valueIndex; localStorage.setItem("settings", JSON.stringify(settings)); @@ -653,61 +656,36 @@ export class GameData { * to update the specified setting with the new value. Finally, it saves the updated settings back * to localStorage and returns `true` to indicate success. */ - public saveGamepadSetting(setting: SettingGamepad, valueIndex: integer): boolean { - let settingsGamepad: object = {}; // Initialize an empty object to hold the gamepad settings + public saveControlSetting(device: Device, localStoragePropertyName: string, setting: SettingGamepad|SettingKeyboard, settingDefaults, valueIndex: integer): boolean { + let settingsControls: object = {}; // Initialize an empty object to hold the gamepad settings - if (localStorage.hasOwnProperty("settingsGamepad")) { // Check if 'settingsGamepad' exists in localStorage - settingsGamepad = JSON.parse(localStorage.getItem("settingsGamepad")); // Parse the existing 'settingsGamepad' from localStorage + if (localStorage.hasOwnProperty(localStoragePropertyName)) { // Check if 'settingsControls' exists in localStorage + settingsControls = JSON.parse(localStorage.getItem(localStoragePropertyName)); // Parse the existing 'settingsControls' from localStorage } - setSettingGamepad(this.scene, setting as SettingGamepad, valueIndex); // Set the gamepad setting in the current scene + if (device === Device.GAMEPAD) { + setSettingGamepad(this.scene, setting as SettingGamepad, valueIndex); // Set the gamepad setting in the current scene + } else if (device === Device.KEYBOARD) { + setSettingKeyboard(this.scene, setting as SettingKeyboard, valueIndex); // Set the keyboard setting in the current scene + } - Object.keys(settingGamepadDefaults).forEach(s => { // Iterate over the default gamepad settings + Object.keys(settingDefaults).forEach(s => { // Iterate over the default gamepad settings if (s === setting) {// If the current setting matches, update its value - settingsGamepad[s] = valueIndex; + settingsControls[s] = valueIndex; } }); - localStorage.setItem("settingsGamepad", JSON.stringify(settingsGamepad)); // Save the updated gamepad settings back to localStorage + localStorage.setItem(localStoragePropertyName, JSON.stringify(settingsControls)); // Save the updated gamepad settings back to localStorage return true; // Return true to indicate the operation was successful } /** - * Saves a keyboard setting to localStorage. - * - * @param setting - The keyboard setting to save. - * @param valueIndex - The index of the value to set for the keyboard setting. - * @returns `true` if the setting is successfully saved. - * - * @remarks - * This method initializes an empty object for keyboard settings if none exist in localStorage. - * It then updates the setting in the current scene and iterates over the default keyboard settings - * to update the specified setting with the new value. Finally, it saves the updated settings back - * to localStorage and returns `true` to indicate success. + * Loads Settings from local storage if available + * @returns true if succesful, false if not */ - public saveKeyboardSetting(setting: SettingKeyboard, valueIndex: integer): boolean { - let settingsKeyboard: object = {}; // Initialize an empty object to hold the keyboard settings - - if (localStorage.hasOwnProperty("settingsKeyboard")) { // Check if 'settingsKeyboard' exists in localStorage - settingsKeyboard = JSON.parse(localStorage.getItem("settingsKeyboard")); // Parse the existing 'settingsKeyboard' from localStorage - } - - setSettingKeyboard(this.scene, setting as SettingKeyboard, valueIndex); // Set the keyboard setting in the current scene - - Object.keys(settingKeyboardDefaults).forEach(s => { // Iterate over the default keyboard settings - if (s === setting) {// If the current setting matches, update its value - settingsKeyboard[s] = valueIndex; - } - }); - - localStorage.setItem("settingsKeyboard", JSON.stringify(settingsKeyboard)); // Save the updated keyboard settings back to localStorage - - return true; // Return true to indicate the operation was successful - } - private loadSettings(): boolean { - Object.values(Setting).map(setting => setting as Setting).forEach(setting => setSetting(this.scene, setting, settingDefaults[setting])); + resetSettings(this.scene); if (!localStorage.hasOwnProperty("settings")) { return false; @@ -716,7 +694,7 @@ export class GameData { const settings = JSON.parse(localStorage.getItem("settings")); for (const setting of Object.keys(settings)) { - setSetting(this.scene, setting as Setting, settings[setting]); + setSetting(this.scene, setting, settings[setting]); } } diff --git a/src/system/settings.ts b/src/system/settings.ts deleted file mode 100644 index 5f526376998..00000000000 --- a/src/system/settings.ts +++ /dev/null @@ -1,279 +0,0 @@ -import { Mode } from "#app/ui/ui"; -import i18next from "i18next"; -import BattleScene from "../battle-scene"; -import { hasTouchscreen } from "../touch-controls"; -import { updateWindowType } from "../ui/ui-theme"; -import { PlayerGender } from "./game-data"; -import { CandyUpgradeNotificationChangedEvent } from "#app/battle-scene-events.js"; -import { MoneyFormat } from "../enums/money-format"; -import SettingsUiHandler from "#app/ui/settings/settings-ui-handler"; - -export enum Setting { - Game_Speed = "GAME_SPEED", - Master_Volume = "MASTER_VOLUME", - BGM_Volume = "BGM_VOLUME", - SE_Volume = "SE_VOLUME", - Language = "LANGUAGE", - Damage_Numbers = "DAMAGE_NUMBERS", - UI_Theme = "UI_THEME", - Window_Type = "WINDOW_TYPE", - Tutorials = "TUTORIALS", - Enable_Retries = "ENABLE_RETRIES", - Skip_Seen_Dialogues = "SKIP_SEEN_DIALOGUES", - Candy_Upgrade_Notification = "CANDY_UPGRADE_NOTIFICATION", - Candy_Upgrade_Display = "CANDY_UPGRADE_DISPLAY", - Money_Format = "MONEY_FORMAT", - Sprite_Set = "SPRITE_SET", - Move_Animations = "MOVE_ANIMATIONS", - Show_Moveset_Flyout = "SHOW_MOVESET_FLYOUT", - Show_Stats_on_Level_Up = "SHOW_LEVEL_UP_STATS", - EXP_Gains_Speed = "EXP_GAINS_SPEED", - EXP_Party_Display = "EXP_PARTY_DISPLAY", - HP_Bar_Speed = "HP_BAR_SPEED", - Fusion_Palette_Swaps = "FUSION_PALETTE_SWAPS", - Player_Gender = "PLAYER_GENDER", - Touch_Controls = "TOUCH_CONTROLS", - Vibration = "VIBRATION" -} - -export interface SettingOptions { - [key: string]: string[] -} - -export interface SettingDefaults { - [key: string]: integer -} - -export const settingOptions: SettingOptions = { - [Setting.Game_Speed]: ["1x", "1.25x", "1.5x", "2x", "2.5x", "3x", "4x", "5x"], - [Setting.Master_Volume]: new Array(11).fill(null).map((_, i) => i ? (i * 10).toString() : "Mute"), - [Setting.BGM_Volume]: new Array(11).fill(null).map((_, i) => i ? (i * 10).toString() : "Mute"), - [Setting.SE_Volume]: new Array(11).fill(null).map((_, i) => i ? (i * 10).toString() : "Mute"), - [Setting.Language]: ["English", "Change"], - [Setting.Damage_Numbers]: ["Off", "Simple", "Fancy"], - [Setting.UI_Theme]: ["Default", "Legacy"], - [Setting.Window_Type]: new Array(5).fill(null).map((_, i) => (i + 1).toString()), - [Setting.Tutorials]: ["Off", "On"], - [Setting.Enable_Retries]: ["Off", "On"], - [Setting.Skip_Seen_Dialogues]: ["Off", "On"], - [Setting.Candy_Upgrade_Notification]: ["Off", "Passives Only", "On"], - [Setting.Candy_Upgrade_Display]: ["Icon", "Animation"], - [Setting.Money_Format]: ["Normal", "Abbreviated"], - [Setting.Sprite_Set]: ["Consistent", "Mixed Animated"], - [Setting.Move_Animations]: ["Off", "On"], - [Setting.Show_Moveset_Flyout]: ["Off", "On"], - [Setting.Show_Stats_on_Level_Up]: ["Off", "On"], - [Setting.EXP_Gains_Speed]: ["Normal", "Fast", "Faster", "Skip"], - [Setting.EXP_Party_Display]: ["Normal", "Level Up Notification", "Skip"], - [Setting.HP_Bar_Speed]: ["Normal", "Fast", "Faster", "Instant"], - [Setting.Fusion_Palette_Swaps]: ["Off", "On"], - [Setting.Player_Gender]: ["Boy", "Girl"], - [Setting.Touch_Controls]: ["Auto", "Disabled"], - [Setting.Vibration]: ["Auto", "Disabled"] -}; - -export const settingDefaults: SettingDefaults = { - [Setting.Game_Speed]: 3, - [Setting.Master_Volume]: 5, - [Setting.BGM_Volume]: 10, - [Setting.SE_Volume]: 10, - [Setting.Language]: 0, - [Setting.Damage_Numbers]: 0, - [Setting.UI_Theme]: 0, - [Setting.Window_Type]: 0, - [Setting.Tutorials]: 1, - [Setting.Enable_Retries]: 0, - [Setting.Skip_Seen_Dialogues]: 0, - [Setting.Candy_Upgrade_Notification]: 0, - [Setting.Candy_Upgrade_Display]: 0, - [Setting.Money_Format]: 0, - [Setting.Sprite_Set]: 0, - [Setting.Move_Animations]: 1, - [Setting.Show_Moveset_Flyout]: 1, - [Setting.Show_Stats_on_Level_Up]: 1, - [Setting.EXP_Gains_Speed]: 0, - [Setting.EXP_Party_Display]: 0, - [Setting.HP_Bar_Speed]: 0, - [Setting.Fusion_Palette_Swaps]: 1, - [Setting.Player_Gender]: 0, - [Setting.Touch_Controls]: 0, - [Setting.Vibration]: 0 -}; - -export const reloadSettings: Setting[] = [Setting.UI_Theme, Setting.Language, Setting.Sprite_Set, Setting.Candy_Upgrade_Display]; - -export function setSetting(scene: BattleScene, setting: Setting, value: integer): boolean { - switch (setting) { - case Setting.Game_Speed: - scene.gameSpeed = parseFloat(settingOptions[setting][value].replace("x", "")); - break; - case Setting.Master_Volume: - scene.masterVolume = value ? parseInt(settingOptions[setting][value]) * 0.01 : 0; - scene.updateSoundVolume(); - break; - case Setting.BGM_Volume: - scene.bgmVolume = value ? parseInt(settingOptions[setting][value]) * 0.01 : 0; - scene.updateSoundVolume(); - break; - case Setting.SE_Volume: - scene.seVolume = value ? parseInt(settingOptions[setting][value]) * 0.01 : 0; - scene.updateSoundVolume(); - break; - case Setting.Damage_Numbers: - scene.damageNumbersMode = value; - break; - case Setting.UI_Theme: - scene.uiTheme = value; - break; - case Setting.Window_Type: - updateWindowType(scene, parseInt(settingOptions[setting][value])); - break; - case Setting.Tutorials: - scene.enableTutorials = settingOptions[setting][value] === "On"; - break; - case Setting.Enable_Retries: - scene.enableRetries = settingOptions[setting][value] === "On"; - break; - case Setting.Candy_Upgrade_Notification: - if (scene.candyUpgradeNotification === value) { - break; - } - - scene.candyUpgradeNotification = value; - scene.eventTarget.dispatchEvent(new CandyUpgradeNotificationChangedEvent(value)); - break; - case Setting.Candy_Upgrade_Display: - scene.candyUpgradeDisplay = value; - case Setting.Money_Format: - switch (settingOptions[setting][value]) { - case "Normal": - scene.moneyFormat = MoneyFormat.NORMAL; - break; - case "Abbreviated": - scene.moneyFormat = MoneyFormat.ABBREVIATED; - break; - } - scene.updateMoneyText(false); - break; - case Setting.Sprite_Set: - scene.experimentalSprites = !!value; - if (value) { - scene.initExpSprites(); - } - break; - case Setting.Move_Animations: - scene.moveAnimations = settingOptions[setting][value] === "On"; - break; - case Setting.Show_Moveset_Flyout: - scene.showMovesetFlyout = settingOptions[setting][value] === "On"; - break; - case Setting.Show_Stats_on_Level_Up: - scene.showLevelUpStats = settingOptions[setting][value] === "On"; - break; - case Setting.EXP_Gains_Speed: - scene.expGainsSpeed = value; - break; - case Setting.EXP_Party_Display: - scene.expParty = value; - break; - case Setting.HP_Bar_Speed: - scene.hpBarSpeed = value; - break; - case Setting.Fusion_Palette_Swaps: - scene.fusionPaletteSwaps = !!value; - break; - case Setting.Player_Gender: - if (scene.gameData) { - const female = settingOptions[setting][value] === "Girl"; - scene.gameData.gender = female ? PlayerGender.FEMALE : PlayerGender.MALE; - scene.trainer.setTexture(scene.trainer.texture.key.replace(female ? "m" : "f", female ? "f" : "m")); - } else { - return false; - } - break; - case Setting.Touch_Controls: - scene.enableTouchControls = settingOptions[setting][value] !== "Disabled" && hasTouchscreen(); - const touchControls = document.getElementById("touchControls"); - if (touchControls) { - touchControls.classList.toggle("visible", scene.enableTouchControls); - } - break; - case Setting.Vibration: - scene.enableVibration = settingOptions[setting][value] !== "Disabled" && hasTouchscreen(); - break; - case Setting.Skip_Seen_Dialogues: - scene.skipSeenDialogues = settingOptions[setting][value] === "On"; - break; - case Setting.Language: - if (value) { - if (scene.ui) { - const cancelHandler = () => { - scene.ui.revertMode(); - (scene.ui.getHandler() as SettingsUiHandler).setOptionCursor(Object.values(Setting).indexOf(Setting.Language), 0, true); - }; - const changeLocaleHandler = (locale: string): boolean => { - try { - i18next.changeLanguage(locale); - localStorage.setItem("prLang", locale); - cancelHandler(); - // Reload the whole game to apply the new locale since also some constants are translated - window.location.reload(); - return true; - } catch (error) { - console.error("Error changing locale:", error); - return false; - } - }; - scene.ui.setOverlayMode(Mode.OPTION_SELECT, { - options: [ - { - label: "English", - handler: () => changeLocaleHandler("en") - }, - { - label: "Español", - handler: () => changeLocaleHandler("es") - }, - { - label: "Italiano", - handler: () => changeLocaleHandler("it") - }, - { - label: "Français", - handler: () => changeLocaleHandler("fr") - }, - { - label: "Deutsch", - handler: () => changeLocaleHandler("de") - }, - { - label: "Português (BR)", - handler: () => changeLocaleHandler("pt_BR") - }, - { - label: "简体中文", - handler: () => changeLocaleHandler("zh_CN") - }, - { - label: "繁體中文", - handler: () => changeLocaleHandler("zh_TW") - }, - { - label: "한국어", - handler: () => changeLocaleHandler("ko") - }, - { - label: "Cancel", - handler: () => cancelHandler() - } - ], - maxOptions: 7 - }); - return false; - } - } - break; - } - - return true; -} diff --git a/src/system/settings-gamepad.ts b/src/system/settings/settings-gamepad.ts similarity index 82% rename from src/system/settings-gamepad.ts rename to src/system/settings/settings-gamepad.ts index 22cc07efce1..909b78ffe6f 100644 --- a/src/system/settings-gamepad.ts +++ b/src/system/settings/settings-gamepad.ts @@ -1,10 +1,9 @@ -import BattleScene from "../battle-scene"; -import {SettingDefaults, SettingOptions} from "./settings"; -import SettingsGamepadUiHandler from "../ui/settings/settings-gamepad-ui-handler"; -import {Mode} from "../ui/ui"; -import {truncateString} from "../utils"; -import {Button} from "../enums/buttons"; -import {SettingKeyboard} from "#app/system/settings-keyboard"; +import BattleScene from "../../battle-scene"; +import SettingsGamepadUiHandler from "../../ui/settings/settings-gamepad-ui-handler"; +import {Mode} from "../../ui/ui"; +import {truncateString} from "../../utils"; +import {Button} from "../../enums/buttons"; +import {SettingKeyboard} from "#app/system/settings/settings-keyboard"; export enum SettingGamepad { Controller = "CONTROLLER", @@ -28,29 +27,31 @@ export enum SettingGamepad { Button_Submit = "BUTTON_SUBMIT", } -export const settingGamepadOptions: SettingOptions = { +const pressAction = "Press action to assign"; + +export const settingGamepadOptions = { [SettingGamepad.Controller]: ["Default", "Change"], [SettingGamepad.Gamepad_Support]: ["Auto", "Disabled"], - [SettingGamepad.Button_Up]: [`KEY ${Button.UP.toString()}`, "Press action to assign"], - [SettingGamepad.Button_Down]: [`KEY ${Button.DOWN.toString()}`, "Press action to assign"], - [SettingGamepad.Button_Left]: [`KEY ${Button.LEFT.toString()}`, "Press action to assign"], - [SettingGamepad.Button_Right]: [`KEY ${Button.RIGHT.toString()}`, "Press action to assign"], - [SettingGamepad.Button_Action]: [`KEY ${Button.ACTION.toString()}`, "Press action to assign"], - [SettingGamepad.Button_Cancel]: [`KEY ${Button.CANCEL.toString()}`, "Press action to assign"], - [SettingGamepad.Button_Menu]: [`KEY ${Button.MENU.toString()}`, "Press action to assign"], - [SettingGamepad.Button_Stats]: [`KEY ${Button.STATS.toString()}`, "Press action to assign"], - [SettingGamepad.Button_Cycle_Form]: [`KEY ${Button.CYCLE_FORM.toString()}`, "Press action to assign"], - [SettingGamepad.Button_Cycle_Shiny]: [`KEY ${Button.CYCLE_SHINY.toString()}`, "Press action to assign"], - [SettingGamepad.Button_Cycle_Gender]: [`KEY ${Button.CYCLE_GENDER.toString()}`, "Press action to assign"], - [SettingGamepad.Button_Cycle_Ability]: [`KEY ${Button.CYCLE_ABILITY.toString()}`, "Press action to assign"], - [SettingGamepad.Button_Cycle_Nature]: [`KEY ${Button.CYCLE_NATURE.toString()}`, "Press action to assign"], - [SettingGamepad.Button_Cycle_Variant]: [`KEY ${Button.V.toString()}`, "Press action to assign"], - [SettingGamepad.Button_Speed_Up]: [`KEY ${Button.SPEED_UP.toString()}`, "Press action to assign"], - [SettingGamepad.Button_Slow_Down]: [`KEY ${Button.SLOW_DOWN.toString()}`, "Press action to assign"], - [SettingGamepad.Button_Submit]: [`KEY ${Button.SUBMIT.toString()}`, "Press action to assign"], + [SettingGamepad.Button_Up]: [`KEY ${Button.UP.toString()}`, pressAction], + [SettingGamepad.Button_Down]: [`KEY ${Button.DOWN.toString()}`, pressAction], + [SettingGamepad.Button_Left]: [`KEY ${Button.LEFT.toString()}`, pressAction], + [SettingGamepad.Button_Right]: [`KEY ${Button.RIGHT.toString()}`, pressAction], + [SettingGamepad.Button_Action]: [`KEY ${Button.ACTION.toString()}`, pressAction], + [SettingGamepad.Button_Cancel]: [`KEY ${Button.CANCEL.toString()}`, pressAction], + [SettingGamepad.Button_Menu]: [`KEY ${Button.MENU.toString()}`, pressAction], + [SettingGamepad.Button_Stats]: [`KEY ${Button.STATS.toString()}`, pressAction], + [SettingGamepad.Button_Cycle_Form]: [`KEY ${Button.CYCLE_FORM.toString()}`, pressAction], + [SettingGamepad.Button_Cycle_Shiny]: [`KEY ${Button.CYCLE_SHINY.toString()}`, pressAction], + [SettingGamepad.Button_Cycle_Gender]: [`KEY ${Button.CYCLE_GENDER.toString()}`, pressAction], + [SettingGamepad.Button_Cycle_Ability]: [`KEY ${Button.CYCLE_ABILITY.toString()}`, pressAction], + [SettingGamepad.Button_Cycle_Nature]: [`KEY ${Button.CYCLE_NATURE.toString()}`, pressAction], + [SettingGamepad.Button_Cycle_Variant]: [`KEY ${Button.V.toString()}`, pressAction], + [SettingGamepad.Button_Speed_Up]: [`KEY ${Button.SPEED_UP.toString()}`, pressAction], + [SettingGamepad.Button_Slow_Down]: [`KEY ${Button.SLOW_DOWN.toString()}`, pressAction], + [SettingGamepad.Button_Submit]: [`KEY ${Button.SUBMIT.toString()}`, pressAction], }; -export const settingGamepadDefaults: SettingDefaults = { +export const settingGamepadDefaults = { [SettingGamepad.Controller]: 0, [SettingGamepad.Gamepad_Support]: 0, [SettingGamepad.Button_Up]: 0, diff --git a/src/system/settings-keyboard.ts b/src/system/settings/settings-keyboard.ts similarity index 84% rename from src/system/settings-keyboard.ts rename to src/system/settings/settings-keyboard.ts index 4ffe6ad3e70..c394b2ef8f0 100644 --- a/src/system/settings-keyboard.ts +++ b/src/system/settings/settings-keyboard.ts @@ -1,4 +1,3 @@ -import {SettingDefaults, SettingOptions} from "#app/system/settings"; import {Button} from "#app/enums/buttons"; import BattleScene from "#app/battle-scene"; import {Mode} from "#app/ui/ui"; @@ -42,46 +41,47 @@ export enum SettingKeyboard { Alt_Button_Submit = "ALT_BUTTON_SUBMIT", } -export const settingKeyboardOptions: SettingOptions = { - // [SettingKeyboard.Default_Layout]: ['Default'], - [SettingKeyboard.Button_Up]: [`KEY ${Button.UP.toString()}`, "Press action to assign"], - [SettingKeyboard.Button_Down]: [`KEY ${Button.DOWN.toString()}`, "Press action to assign"], - [SettingKeyboard.Alt_Button_Up]: [`KEY ${Button.UP.toString()}`, "Press action to assign"], - [SettingKeyboard.Button_Left]: [`KEY ${Button.LEFT.toString()}`, "Press action to assign"], - [SettingKeyboard.Button_Right]: [`KEY ${Button.RIGHT.toString()}`, "Press action to assign"], - [SettingKeyboard.Button_Action]: [`KEY ${Button.ACTION.toString()}`, "Press action to assign"], - [SettingKeyboard.Button_Menu]: [`KEY ${Button.MENU.toString()}`, "Press action to assign"], - [SettingKeyboard.Button_Submit]: [`KEY ${Button.SUBMIT.toString()}`, "Press action to assign"], +const pressAction = "Press action to assign"; - [SettingKeyboard.Alt_Button_Down]: [`KEY ${Button.DOWN.toString()}`, "Press action to assign"], - [SettingKeyboard.Alt_Button_Left]: [`KEY ${Button.LEFT.toString()}`, "Press action to assign"], - [SettingKeyboard.Alt_Button_Right]: [`KEY ${Button.RIGHT.toString()}`, "Press action to assign"], - [SettingKeyboard.Alt_Button_Action]: [`KEY ${Button.ACTION.toString()}`, "Press action to assign"], - [SettingKeyboard.Button_Cancel]: [`KEY ${Button.CANCEL.toString()}`, "Press action to assign"], - [SettingKeyboard.Alt_Button_Cancel]: [`KEY ${Button.CANCEL.toString()}`, "Press action to assign"], - [SettingKeyboard.Alt_Button_Menu]: [`KEY ${Button.MENU.toString()}`, "Press action to assign"], - [SettingKeyboard.Button_Stats]: [`KEY ${Button.STATS.toString()}`, "Press action to assign"], - [SettingKeyboard.Alt_Button_Stats]: [`KEY ${Button.STATS.toString()}`, "Press action to assign"], - [SettingKeyboard.Button_Cycle_Form]: [`KEY ${Button.CYCLE_FORM.toString()}`, "Press action to assign"], - [SettingKeyboard.Alt_Button_Cycle_Form]: [`KEY ${Button.CYCLE_FORM.toString()}`, "Press action to assign"], - [SettingKeyboard.Button_Cycle_Shiny]: [`KEY ${Button.CYCLE_SHINY.toString()}`, "Press action to assign"], - [SettingKeyboard.Alt_Button_Cycle_Shiny]: [`KEY ${Button.CYCLE_SHINY.toString()}`, "Press action to assign"], - [SettingKeyboard.Button_Cycle_Gender]: [`KEY ${Button.CYCLE_GENDER.toString()}`, "Press action to assign"], - [SettingKeyboard.Alt_Button_Cycle_Gender]: [`KEY ${Button.CYCLE_GENDER.toString()}`, "Press action to assign"], - [SettingKeyboard.Button_Cycle_Ability]: [`KEY ${Button.CYCLE_ABILITY.toString()}`, "Press action to assign"], - [SettingKeyboard.Alt_Button_Cycle_Ability]: [`KEY ${Button.CYCLE_ABILITY.toString()}`, "Press action to assign"], - [SettingKeyboard.Button_Cycle_Nature]: [`KEY ${Button.CYCLE_NATURE.toString()}`, "Press action to assign"], - [SettingKeyboard.Alt_Button_Cycle_Nature]: [`KEY ${Button.CYCLE_NATURE.toString()}`, "Press action to assign"], - [SettingKeyboard.Button_Cycle_Variant]: [`KEY ${Button.V.toString()}`, "Press action to assign"], - [SettingKeyboard.Alt_Button_Cycle_Variant]: [`KEY ${Button.V.toString()}`, "Press action to assign"], - [SettingKeyboard.Button_Speed_Up]: [`KEY ${Button.SPEED_UP.toString()}`, "Press action to assign"], - [SettingKeyboard.Alt_Button_Speed_Up]: [`KEY ${Button.SPEED_UP.toString()}`, "Press action to assign"], - [SettingKeyboard.Button_Slow_Down]: [`KEY ${Button.SLOW_DOWN.toString()}`, "Press action to assign"], - [SettingKeyboard.Alt_Button_Slow_Down]: [`KEY ${Button.SLOW_DOWN.toString()}`, "Press action to assign"], - [SettingKeyboard.Alt_Button_Submit]: [`KEY ${Button.SUBMIT.toString()}`, "Press action to assign"], +export const settingKeyboardOptions = { + // [SettingKeyboard.Default_Layout]: ['Default'], + [SettingKeyboard.Button_Up]: [`KEY ${Button.UP.toString()}`, pressAction], + [SettingKeyboard.Button_Down]: [`KEY ${Button.DOWN.toString()}`, pressAction], + [SettingKeyboard.Alt_Button_Up]: [`KEY ${Button.UP.toString()}`, pressAction], + [SettingKeyboard.Button_Left]: [`KEY ${Button.LEFT.toString()}`, pressAction], + [SettingKeyboard.Button_Right]: [`KEY ${Button.RIGHT.toString()}`, pressAction], + [SettingKeyboard.Button_Action]: [`KEY ${Button.ACTION.toString()}`, pressAction], + [SettingKeyboard.Button_Menu]: [`KEY ${Button.MENU.toString()}`, pressAction], + [SettingKeyboard.Button_Submit]: [`KEY ${Button.SUBMIT.toString()}`, pressAction], + [SettingKeyboard.Alt_Button_Down]: [`KEY ${Button.DOWN.toString()}`, pressAction], + [SettingKeyboard.Alt_Button_Left]: [`KEY ${Button.LEFT.toString()}`, pressAction], + [SettingKeyboard.Alt_Button_Right]: [`KEY ${Button.RIGHT.toString()}`, pressAction], + [SettingKeyboard.Alt_Button_Action]: [`KEY ${Button.ACTION.toString()}`, pressAction], + [SettingKeyboard.Button_Cancel]: [`KEY ${Button.CANCEL.toString()}`, pressAction], + [SettingKeyboard.Alt_Button_Cancel]: [`KEY ${Button.CANCEL.toString()}`, pressAction], + [SettingKeyboard.Alt_Button_Menu]: [`KEY ${Button.MENU.toString()}`, pressAction], + [SettingKeyboard.Button_Stats]: [`KEY ${Button.STATS.toString()}`, pressAction], + [SettingKeyboard.Alt_Button_Stats]: [`KEY ${Button.STATS.toString()}`, pressAction], + [SettingKeyboard.Button_Cycle_Form]: [`KEY ${Button.CYCLE_FORM.toString()}`, pressAction], + [SettingKeyboard.Alt_Button_Cycle_Form]: [`KEY ${Button.CYCLE_FORM.toString()}`, pressAction], + [SettingKeyboard.Button_Cycle_Shiny]: [`KEY ${Button.CYCLE_SHINY.toString()}`, pressAction], + [SettingKeyboard.Alt_Button_Cycle_Shiny]: [`KEY ${Button.CYCLE_SHINY.toString()}`, pressAction], + [SettingKeyboard.Button_Cycle_Gender]: [`KEY ${Button.CYCLE_GENDER.toString()}`, pressAction], + [SettingKeyboard.Alt_Button_Cycle_Gender]: [`KEY ${Button.CYCLE_GENDER.toString()}`, pressAction], + [SettingKeyboard.Button_Cycle_Ability]: [`KEY ${Button.CYCLE_ABILITY.toString()}`, pressAction], + [SettingKeyboard.Alt_Button_Cycle_Ability]: [`KEY ${Button.CYCLE_ABILITY.toString()}`, pressAction], + [SettingKeyboard.Button_Cycle_Nature]: [`KEY ${Button.CYCLE_NATURE.toString()}`, pressAction], + [SettingKeyboard.Alt_Button_Cycle_Nature]: [`KEY ${Button.CYCLE_NATURE.toString()}`, pressAction], + [SettingKeyboard.Button_Cycle_Variant]: [`KEY ${Button.V.toString()}`, pressAction], + [SettingKeyboard.Alt_Button_Cycle_Variant]: [`KEY ${Button.V.toString()}`, pressAction], + [SettingKeyboard.Button_Speed_Up]: [`KEY ${Button.SPEED_UP.toString()}`, pressAction], + [SettingKeyboard.Alt_Button_Speed_Up]: [`KEY ${Button.SPEED_UP.toString()}`, pressAction], + [SettingKeyboard.Button_Slow_Down]: [`KEY ${Button.SLOW_DOWN.toString()}`, pressAction], + [SettingKeyboard.Alt_Button_Slow_Down]: [`KEY ${Button.SLOW_DOWN.toString()}`, pressAction], + [SettingKeyboard.Alt_Button_Submit]: [`KEY ${Button.SUBMIT.toString()}`, pressAction], }; -export const settingKeyboardDefaults: SettingDefaults = { +export const settingKeyboardDefaults = { // [SettingKeyboard.Default_Layout]: 0, [SettingKeyboard.Button_Up]: 0, [SettingKeyboard.Button_Down]: 0, diff --git a/src/system/settings/settings.ts b/src/system/settings/settings.ts new file mode 100644 index 00000000000..e68e5ea8704 --- /dev/null +++ b/src/system/settings/settings.ts @@ -0,0 +1,453 @@ +import { Mode } from "#app/ui/ui"; +import i18next from "i18next"; +import BattleScene from "../../battle-scene"; +import { hasTouchscreen } from "../../touch-controls"; +import { updateWindowType } from "../../ui/ui-theme"; +import { PlayerGender } from "../game-data"; +import { CandyUpgradeNotificationChangedEvent } from "#app/battle-scene-events.js"; +import { MoneyFormat } from "../../enums/money-format"; +import SettingsUiHandler from "#app/ui/settings/settings-ui-handler"; + +const MUTE = "Mute"; +const VOLUME_OPTIONS = new Array(11).fill(null).map((_, i) => i ? (i * 10).toString() : MUTE); +const OFF_ON = ["Off", "On"]; +const AUTO_DISABLED = ["Auto", "Disabled"]; + +/** + * Types for helping separate settings to different menus + */ +export enum SettingType { + GENERAL, + ACCESSIBILITY +} + +export interface Setting { + key: string + label: string + options: Array + default: number + type: SettingType + requireReload?: boolean +} + +/** + * Setting Keys for existing settings + * to be used when trying to find or update Settings + */ +export const SettingKeys = { + Game_Speed: "GAME_SPEED", + Master_Volume: "MASTER_VOLUME", + BGM_Volume: "BGM_VOLUME", + SE_Volume: "SE_VOLUME", + Language: "LANGUAGE", + Damage_Numbers: "DAMAGE_NUMBERS", + UI_Theme: "UI_THEME", + Window_Type: "WINDOW_TYPE", + Tutorials: "TUTORIALS", + Enable_Retries: "ENABLE_RETRIES", + Skip_Seen_Dialogues: "SKIP_SEEN_DIALOGUES", + Candy_Upgrade_Notification: "CANDY_UPGRADE_NOTIFICATION", + Candy_Upgrade_Display: "CANDY_UPGRADE_DISPLAY", + Money_Format: "MONEY_FORMAT", + Sprite_Set: "SPRITE_SET", + Move_Animations: "MOVE_ANIMATIONS", + Show_Moveset_Flyout: "SHOW_MOVESET_FLYOUT", + Show_Stats_on_Level_Up: "SHOW_LEVEL_UP_STATS", + EXP_Gains_Speed: "EXP_GAINS_SPEED", + EXP_Party_Display: "EXP_PARTY_DISPLAY", + HP_Bar_Speed: "HP_BAR_SPEED", + Fusion_Palette_Swaps: "FUSION_PALETTE_SWAPS", + Player_Gender: "PLAYER_GENDER", + Touch_Controls: "TOUCH_CONTROLS", + Vibration: "VIBRATION" +}; + +/** + * All Settings not related to controls + */ +export const Setting: Array = [ + { + key: SettingKeys.Game_Speed, + label: "Game Speed", + options: ["1x", "1.25x", "1.5x", "2x", "2.5x", "3x", "4x", "5x"], + default: 3, + type: SettingType.GENERAL + }, + { + key: SettingKeys.Master_Volume, + label: "Master Volume", + options: VOLUME_OPTIONS, + default: 5, + type: SettingType.GENERAL + }, + { + key: SettingKeys.BGM_Volume, + label: "BGM Volume", + options: VOLUME_OPTIONS, + default: 10, + type: SettingType.GENERAL + }, + { + key: SettingKeys.SE_Volume, + label: "SE Volume", + options: VOLUME_OPTIONS, + default: 10, + type: SettingType.GENERAL + }, + { + key: SettingKeys.Language, + label: "Language", + options: ["English", "Change"], + default: 0, + type: SettingType.GENERAL, + requireReload: true + }, + { + key: SettingKeys.Damage_Numbers, + label: "Damage Numbers", + options: ["Off", "Simple", "Fancy"], + default: 0, + type: SettingType.GENERAL + }, + { + key: SettingKeys.UI_Theme, + label: "UI Theme", + options: ["Default", "Legacy"], + default: 0, + type: SettingType.GENERAL, + requireReload: true + }, + { + key: SettingKeys.Window_Type, + label: "Window Type", + options: new Array(5).fill(null).map((_, i) => (i + 1).toString()), + default: 0, + type: SettingType.GENERAL + }, + { + key: SettingKeys.Tutorials, + label: "Tutorials", + options: OFF_ON, + default: 1, + type: SettingType.GENERAL + }, + { + key: SettingKeys.Enable_Retries, + label: "Enable Retries", + options: OFF_ON, + default: 0, + type: SettingType.ACCESSIBILITY + }, + { + key: SettingKeys.Skip_Seen_Dialogues, + label: "Skip Seen Dialogues", + options: OFF_ON, + default: 0, + type: SettingType.GENERAL + }, + { + key: SettingKeys.Candy_Upgrade_Notification, + label: "Candy Upgrade Notification", + options: ["Off", "Passives Only", "On"], + default: 0, + type: SettingType.ACCESSIBILITY + }, + { + key: SettingKeys.Candy_Upgrade_Display, + label: "Candy Upgrade Display", + options: ["Icon", "Animation"], + default: 0, + type: SettingType.ACCESSIBILITY, + requireReload: true + }, + { + key: SettingKeys.Money_Format, + label: "Money Format", + options: ["Normal", "Abbreviated"], + default: 0, + type: SettingType.ACCESSIBILITY + }, + { + key: SettingKeys.Sprite_Set, + label: "Sprite Set", + options: ["Consistent", "Mixed Animated"], + default: 0, + type: SettingType.GENERAL, + requireReload: true + }, + { + key: SettingKeys.Move_Animations, + label: "Move Animations", + options: OFF_ON, + default: 1, + type: SettingType.GENERAL + }, + { + key: SettingKeys.Show_Moveset_Flyout, + label: "Show Moveset Flyout", + options: OFF_ON, + default: 1, + type: SettingType.ACCESSIBILITY + }, + { + key: SettingKeys.Show_Stats_on_Level_Up, + label: "Show Stats on Level Up", + options: OFF_ON, + default: 1, + type: SettingType.GENERAL + }, + { + key: SettingKeys.EXP_Gains_Speed, + label: "EXP Gains Speed", + options: ["Normal", "Fast", "Faster", "Skip"], + default: 0, + type: SettingType.GENERAL + }, + { + key: SettingKeys.EXP_Party_Display, + label: "EXP Party Display", + options: ["Normal", "Level Up Notification", "Skip"], + default: 0, + type: SettingType.GENERAL + }, + { + key: SettingKeys.HP_Bar_Speed, + label: "HP Bar Speed", + options: ["Normal", "Fast", "Faster", "Skip"], + default: 0, + type: SettingType.GENERAL + }, + { + key: SettingKeys.Fusion_Palette_Swaps, + label: "Fusion Palette Swaps", + options: OFF_ON, + default: 1, + type: SettingType.GENERAL + }, + { + key: SettingKeys.Player_Gender, + label: "Player Gender", + options: ["Boy", "Girl"], + default: 0, + type: SettingType.GENERAL + }, + { + key: SettingKeys.Touch_Controls, + label: "Touch Controls", + options: AUTO_DISABLED, + default: 0, + type: SettingType.GENERAL + }, + { + key: SettingKeys.Vibration, + label: "Vibration", + options: AUTO_DISABLED, + default: 0, + type: SettingType.GENERAL + } +]; + +/** + * Return the index of a Setting + * @param key SettingKey + * @returns index or -1 if doesn't exist + */ +export function settingIndex(key: string) { + return Setting.findIndex(s => s.key === key); +} + +/** + * Resets all settings to their defaults + * @param scene current BattleScene + */ +export function resetSettings(scene: BattleScene) { + Setting.forEach(s => setSetting(scene, s.key, s.default)); +} + +/** + * Updates a setting for current BattleScene + * @param scene current BattleScene + * @param setting string ideally from SettingKeys + * @param value value to update setting with + * @returns true if successful, false if not + */ +export function setSetting(scene: BattleScene, setting: string, value: integer): boolean { + const index: number = settingIndex(setting); + if ( index === -1) { + return false; + } + switch (Setting[index].key) { + case SettingKeys.Game_Speed: + scene.gameSpeed = parseFloat(Setting[index].options[value].replace("x", "")); + break; + case SettingKeys.Master_Volume: + scene.masterVolume = value ? parseInt(Setting[index].options[value]) * 0.01 : 0; + scene.updateSoundVolume(); + break; + case SettingKeys.BGM_Volume: + scene.bgmVolume = value ? parseInt(Setting[index].options[value]) * 0.01 : 0; + scene.updateSoundVolume(); + break; + case SettingKeys.SE_Volume: + scene.seVolume = value ? parseInt(Setting[index].options[value]) * 0.01 : 0; + scene.updateSoundVolume(); + break; + case SettingKeys.Damage_Numbers: + scene.damageNumbersMode = value; + break; + case SettingKeys.UI_Theme: + scene.uiTheme = value; + break; + case SettingKeys.Window_Type: + updateWindowType(scene, parseInt(Setting[index].options[value])); + break; + case SettingKeys.Tutorials: + scene.enableTutorials = Setting[index].options[value] === "On"; + break; + case SettingKeys.Enable_Retries: + scene.enableRetries = Setting[index].options[value] === "On"; + break; + case SettingKeys.Skip_Seen_Dialogues: + scene.skipSeenDialogues = Setting[index].options[value] === "On"; + break; + case SettingKeys.Candy_Upgrade_Notification: + if (scene.candyUpgradeNotification === value) { + break; + } + + scene.candyUpgradeNotification = value; + scene.eventTarget.dispatchEvent(new CandyUpgradeNotificationChangedEvent(value)); + break; + case SettingKeys.Candy_Upgrade_Display: + scene.candyUpgradeDisplay = value; + case SettingKeys.Money_Format: + switch (Setting[index].options[value]) { + case "Normal": + scene.moneyFormat = MoneyFormat.NORMAL; + break; + case "Abbreviated": + scene.moneyFormat = MoneyFormat.ABBREVIATED; + break; + } + scene.updateMoneyText(false); + break; + case SettingKeys.Sprite_Set: + scene.experimentalSprites = !!value; + if (value) { + scene.initExpSprites(); + } + break; + case SettingKeys.Move_Animations: + scene.moveAnimations = Setting[index].options[value] === "On"; + break; + case SettingKeys.Show_Moveset_Flyout: + scene.showMovesetFlyout = Setting[index].options[value] === "On"; + break; + case SettingKeys.Show_Stats_on_Level_Up: + scene.showLevelUpStats = Setting[index].options[value] === "On"; + break; + case SettingKeys.EXP_Gains_Speed: + scene.expGainsSpeed = value; + break; + case SettingKeys.EXP_Party_Display: + scene.expParty = value; + break; + case SettingKeys.HP_Bar_Speed: + scene.hpBarSpeed = value; + break; + case SettingKeys.Fusion_Palette_Swaps: + scene.fusionPaletteSwaps = !!value; + break; + case SettingKeys.Player_Gender: + if (scene.gameData) { + const female = Setting[index].options[value] === "Girl"; + scene.gameData.gender = female ? PlayerGender.FEMALE : PlayerGender.MALE; + scene.trainer.setTexture(scene.trainer.texture.key.replace(female ? "m" : "f", female ? "f" : "m")); + } else { + return false; + } + break; + case SettingKeys.Touch_Controls: + scene.enableTouchControls = Setting[index].options[value] !== "Disabled" && hasTouchscreen(); + const touchControls = document.getElementById("touchControls"); + if (touchControls) { + touchControls.classList.toggle("visible", scene.enableTouchControls); + } + break; + case SettingKeys.Vibration: + scene.enableVibration = Setting[index].options[value] !== "Disabled" && hasTouchscreen(); + break; + case SettingKeys.Language: + if (value) { + if (scene.ui) { + const cancelHandler = () => { + scene.ui.revertMode(); + const languageSetting = Setting.find(setting => setting.key === SettingKeys.Language); + (scene.ui.getHandler() as SettingsUiHandler).setOptionCursor(Setting.indexOf(languageSetting), 0, true); + }; + const changeLocaleHandler = (locale: string): boolean => { + try { + i18next.changeLanguage(locale); + localStorage.setItem("prLang", locale); + cancelHandler(); + // Reload the whole game to apply the new locale since also some constants are translated + window.location.reload(); + return true; + } catch (error) { + console.error("Error changing locale:", error); + return false; + } + }; + scene.ui.setOverlayMode(Mode.OPTION_SELECT, { + options: [ + { + label: "English", + handler: () => changeLocaleHandler("en") + }, + { + label: "Español", + handler: () => changeLocaleHandler("es") + }, + { + label: "Italiano", + handler: () => changeLocaleHandler("it") + }, + { + label: "Français", + handler: () => changeLocaleHandler("fr") + }, + { + label: "Deutsch", + handler: () => changeLocaleHandler("de") + }, + { + label: "Português (BR)", + handler: () => changeLocaleHandler("pt_BR") + }, + { + label: "简体中文", + handler: () => changeLocaleHandler("zh_CN") + }, + { + label: "繁體中文", + handler: () => changeLocaleHandler("zh_TW") + }, + { + label: "한국어", + handler: () => changeLocaleHandler("ko") + }, + { + label: "Cancel", + handler: () => cancelHandler() + } + ], + maxOptions: 7 + }); + return false; + } + } + break; + } + + return true; +} diff --git a/src/test/helpers/inGameManip.ts b/src/test/helpers/inGameManip.ts index c81602dff6d..1edab75fd27 100644 --- a/src/test/helpers/inGameManip.ts +++ b/src/test/helpers/inGameManip.ts @@ -3,7 +3,7 @@ import { getSettingNameWithKeycode } from "#app/configs/inputs/configHandler"; import {expect} from "vitest"; -import {SettingKeyboard} from "#app/system/settings-keyboard"; +import {SettingKeyboard} from "#app/system/settings/settings-keyboard"; export class InGameManip { private config; diff --git a/src/test/helpers/menuManip.ts b/src/test/helpers/menuManip.ts index 2377ab70c64..7b4080c3d75 100644 --- a/src/test/helpers/menuManip.ts +++ b/src/test/helpers/menuManip.ts @@ -8,7 +8,7 @@ import { assign, getSettingNameWithKeycode, canIAssignThisKey, canIDeleteThisKey, canIOverrideThisSetting } from "#app/configs/inputs/configHandler"; -import {SettingKeyboard} from "#app/system/settings-keyboard"; +import {SettingKeyboard} from "#app/system/settings/settings-keyboard"; export class MenuManip { private config; diff --git a/src/test/rebinding_setting.test.ts b/src/test/rebinding_setting.test.ts index 92376006263..03e8cbb51c4 100644 --- a/src/test/rebinding_setting.test.ts +++ b/src/test/rebinding_setting.test.ts @@ -10,7 +10,7 @@ import {InGameManip} from "#app/test/helpers/inGameManip"; import {Device} from "#app/enums/devices"; import {InterfaceConfig} from "#app/inputs-controller"; import cfg_keyboard_qwerty from "#app/configs/inputs/cfg_keyboard_qwerty"; -import {SettingKeyboard} from "#app/system/settings-keyboard"; +import {SettingKeyboard} from "#app/system/settings/settings-keyboard"; describe("Test Rebinding", () => { diff --git a/src/ui-inputs.ts b/src/ui-inputs.ts index a5fb631644a..c9bdc5feaf5 100644 --- a/src/ui-inputs.ts +++ b/src/ui-inputs.ts @@ -3,12 +3,13 @@ import {Mode} from "./ui/ui"; import {InputsController} from "./inputs-controller"; import MessageUiHandler from "./ui/message-ui-handler"; import StarterSelectUiHandler from "./ui/starter-select-ui-handler"; -import {Setting, settingOptions} from "./system/settings"; +import {Setting, SettingKeys, settingIndex} from "./system/settings/settings"; import SettingsUiHandler from "./ui/settings/settings-ui-handler"; import {Button} from "./enums/buttons"; import SettingsGamepadUiHandler from "./ui/settings/settings-gamepad-ui-handler"; import SettingsKeyboardUiHandler from "#app/ui/settings/settings-keyboard-ui-handler"; import BattleScene from "./battle-scene"; +import SettingsAccessibilityUiHandler from "./ui/settings/settings-accessiblity-ui-handler"; type ActionKeys = Record void>; @@ -161,7 +162,7 @@ export class UiInputs { } buttonCycleOption(button: Button): void { - const whitelist = [StarterSelectUiHandler, SettingsUiHandler, SettingsGamepadUiHandler, SettingsKeyboardUiHandler]; + const whitelist = [StarterSelectUiHandler, SettingsUiHandler, SettingsAccessibilityUiHandler, SettingsGamepadUiHandler, SettingsKeyboardUiHandler]; const uiHandler = this.scene.ui?.getHandler(); if (whitelist.some(handler => uiHandler instanceof handler)) { this.scene.ui.processInput(button); @@ -171,17 +172,14 @@ export class UiInputs { } buttonSpeedChange(up = true): void { - if (up) { - if (this.scene.gameSpeed < 5) { - this.scene.gameData.saveSetting(Setting.Game_Speed, settingOptions[Setting.Game_Speed].indexOf(`${this.scene.gameSpeed}x`) + 1); - if (this.scene.ui?.getMode() === Mode.SETTINGS) { - (this.scene.ui.getHandler() as SettingsUiHandler).show([]); - } + const settingGameSpeed = settingIndex(SettingKeys.Game_Speed); + if (up && this.scene.gameSpeed < 5) { + this.scene.gameData.saveSetting(SettingKeys.Game_Speed, Setting[settingGameSpeed].options.indexOf(`${this.scene.gameSpeed}x`) + 1); + if (this.scene.ui?.getMode() === Mode.SETTINGS) { + (this.scene.ui.getHandler() as SettingsUiHandler).show([]); } - return; - } - if (this.scene.gameSpeed > 1) { - this.scene.gameData.saveSetting(Setting.Game_Speed, Math.max(settingOptions[Setting.Game_Speed].indexOf(`${this.scene.gameSpeed}x`) - 1, 0)); + } else if (!up && this.scene.gameSpeed > 1) { + this.scene.gameData.saveSetting(SettingKeys.Game_Speed, Math.max(Setting[settingGameSpeed].options.indexOf(`${this.scene.gameSpeed}x`) - 1, 0)); if (this.scene.ui?.getMode() === Mode.SETTINGS) { (this.scene.ui.getHandler() as SettingsUiHandler).show([]); } diff --git a/src/ui/settings/abstract-control-settings-ui-handler.ts b/src/ui/settings/abstract-control-settings-ui-handler.ts new file mode 100644 index 00000000000..b8165d41980 --- /dev/null +++ b/src/ui/settings/abstract-control-settings-ui-handler.ts @@ -0,0 +1,660 @@ +import UiHandler from "../ui-handler"; +import BattleScene from "../../battle-scene"; +import {Mode} from "../ui"; +import {InterfaceConfig} from "../../inputs-controller"; +import {addWindow} from "../ui-theme"; +import {addTextObject, TextStyle} from "../text"; +import {Button} from "../../enums/buttons"; +import {getIconWithSettingName} from "#app/configs/inputs/configHandler"; +import NavigationMenu, {NavigationManager} from "#app/ui/settings/navigationMenu"; +import { Device } from "#app/enums/devices.js"; + +export interface InputsIcons { + [key: string]: Phaser.GameObjects.Sprite; +} + +export interface LayoutConfig { + optionsContainer: Phaser.GameObjects.Container; + inputsIcons: InputsIcons; + settingLabels: Phaser.GameObjects.Text[]; + optionValueLabels: Phaser.GameObjects.Text[][]; + optionCursors: integer[]; + keys: string[]; + bindingSettings: Array; +} +/** + * Abstract class for handling UI elements related to control settings. + */ +export default abstract class AbstractControlSettingsUiHandler extends UiHandler { + protected settingsContainer: Phaser.GameObjects.Container; + protected optionsContainer: Phaser.GameObjects.Container; + protected navigationContainer: NavigationMenu; + + protected scrollCursor: integer; + protected optionCursors: integer[]; + protected cursorObj: Phaser.GameObjects.NineSlice; + + protected optionsBg: Phaser.GameObjects.NineSlice; + protected actionsBg: Phaser.GameObjects.NineSlice; + + protected settingLabels: Phaser.GameObjects.Text[]; + protected optionValueLabels: Phaser.GameObjects.Text[][]; + + // layout will contain the 3 Gamepad tab for each config - dualshock, xbox, snes + protected layout: Map = new Map(); + // Will contain the input icons from the selected layout + protected inputsIcons: InputsIcons; + protected navigationIcons: InputsIcons; + // list all the setting keys used in the selected layout (because dualshock has more buttons than xbox) + protected keys: Array; + + // Store the specific settings related to key bindings for the current gamepad configuration. + protected bindingSettings: Array; + + protected setting; + protected settingBlacklisted; + protected settingDeviceDefaults; + protected settingDeviceOptions; + protected configs; + protected commonSettingsCount; + protected textureOverride; + protected titleSelected; + protected localStoragePropertyName; + protected rowsToDisplay: number; + protected device: Device; + + abstract saveSettingToLocalStorage(setting, cursor): void; + abstract setSetting(scene: BattleScene, setting, value: integer): boolean; + + /** + * Constructor for the AbstractSettingsUiHandler. + * + * @param scene - The BattleScene instance. + * @param mode - The UI mode. + */ + constructor(scene: BattleScene, mode?: Mode) { + super(scene, mode); + this.rowsToDisplay = 8; + } + + getLocalStorageSetting(): object { + // Retrieve the settings from local storage or use an empty object if none exist. + const settings: object = localStorage.hasOwnProperty(this.localStoragePropertyName) ? JSON.parse(localStorage.getItem(this.localStoragePropertyName)) : {}; + return settings; + } + + /** + * Setup UI elements. + */ + setup() { + const ui = this.getUi(); + this.navigationIcons = {}; + + this.settingsContainer = this.scene.add.container(1, -(this.scene.game.canvas.height / 6) + 1); + + this.settingsContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, this.scene.game.canvas.width / 6, this.scene.game.canvas.height / 6), Phaser.Geom.Rectangle.Contains); + + this.navigationContainer = new NavigationMenu(this.scene, 0, 0); + + this.optionsBg = addWindow(this.scene, 0, this.navigationContainer.height, (this.scene.game.canvas.width / 6) - 2, (this.scene.game.canvas.height / 6) - 16 - this.navigationContainer.height - 2); + this.optionsBg.setOrigin(0, 0); + + + this.actionsBg = addWindow(this.scene, 0, (this.scene.game.canvas.height / 6) - this.navigationContainer.height, (this.scene.game.canvas.width / 6) - 2, 22); + this.actionsBg.setOrigin(0, 0); + + const iconAction = this.scene.add.sprite(0, 0, "keyboard"); + iconAction.setOrigin(0, -0.1); + iconAction.setPositionRelative(this.actionsBg, this.navigationContainer.width - 32, 4); + this.navigationIcons["BUTTON_ACTION"] = iconAction; + + const actionText = addTextObject(this.scene, 0, 0, "Action", TextStyle.SETTINGS_LABEL); + actionText.setOrigin(0, 0.15); + actionText.setPositionRelative(iconAction, -actionText.width/6-2, 0); + + const iconCancel = this.scene.add.sprite(0, 0, "keyboard"); + iconCancel.setOrigin(0, -0.1); + iconCancel.setPositionRelative(this.actionsBg, this.navigationContainer.width - 100, 4); + this.navigationIcons["BUTTON_CANCEL"] = iconCancel; + + const cancelText = addTextObject(this.scene, 0, 0, "Cancel", TextStyle.SETTINGS_LABEL); + cancelText.setOrigin(0, 0.15); + cancelText.setPositionRelative(iconCancel, -cancelText.width/6-2, 0); + + const iconReset = this.scene.add.sprite(0, 0, "keyboard"); + iconReset.setOrigin(0, -0.1); + iconReset.setPositionRelative(this.actionsBg, this.navigationContainer.width - 180, 4); + this.navigationIcons["BUTTON_HOME"] = iconReset; + + const resetText = addTextObject(this.scene, 0, 0, "Reset all", TextStyle.SETTINGS_LABEL); + resetText.setOrigin(0, 0.15); + resetText.setPositionRelative(iconReset, -resetText.width/6-2, 0); + + this.settingsContainer.add(this.optionsBg); + this.settingsContainer.add(this.actionsBg); + this.settingsContainer.add(this.navigationContainer); + this.settingsContainer.add(iconAction); + this.settingsContainer.add(iconCancel); + this.settingsContainer.add(iconReset); + this.settingsContainer.add(actionText); + this.settingsContainer.add(cancelText); + this.settingsContainer.add(resetText); + + /// Initialize a new configuration "screen" for each type of gamepad. + for (const config of this.configs) { + // Create a map to store layout settings based on the pad type. + this.layout[config.padType] = new Map(); + // Create a container for gamepad options in the scene, initially hidden. + + const optionsContainer = this.scene.add.container(0, 0); + optionsContainer.setVisible(false); + + // Gather all binding settings from the configuration. + const bindingSettings = Object.keys(config.settings); + + // Array to hold labels for different settings such as 'Controller', 'Gamepad Support', etc. + const settingLabels: Phaser.GameObjects.Text[] = []; + + // Array to hold options for each setting, e.g., 'Auto', 'Disabled'. + const optionValueLabels: Phaser.GameObjects.GameObject[][] = []; + + // Object to store sprites for each button configuration. + const inputsIcons: InputsIcons = {}; + + // Fetch common setting keys such as 'Controller' and 'Gamepad Support' from gamepad settings. + const commonSettingKeys = Object.keys(this.setting).slice(0, this.commonSettingsCount).map(key => this.setting[key]); + // Combine common and specific bindings into a single array. + const specificBindingKeys = [...commonSettingKeys, ...Object.keys(config.settings)]; + // Fetch default values for these settings and prepare to highlight selected options. + const optionCursors = Object.values(Object.keys(this.settingDeviceDefaults).filter(s => specificBindingKeys.includes(s)).map(k => this.settingDeviceDefaults[k])); + // Filter out settings that are not relevant to the current gamepad configuration. + const settingFiltered = Object.keys(this.setting).filter(_key => specificBindingKeys.includes(this.setting[_key])); + // Loop through the filtered settings to manage display and options. + + settingFiltered.forEach((setting, s) => { + // Convert the setting key from format 'Key_Name' to 'Key name' for display. + const settingName = setting.replace(/\_/g, " "); + + // Create and add a text object for the setting name to the scene. + const isLock = this.settingBlacklisted.includes(this.setting[setting]); + const labelStyle = isLock ? TextStyle.SETTINGS_LOCKED : TextStyle.SETTINGS_LABEL; + settingLabels[s] = addTextObject(this.scene, 8, 28 + s * 16, settingName, labelStyle); + settingLabels[s].setOrigin(0, 0); + optionsContainer.add(settingLabels[s]); + + // Initialize an array to store the option labels for this setting. + const valueLabels: Phaser.GameObjects.GameObject[] = []; + + // Process each option for the current setting. + for (const [o, option] of this.settingDeviceOptions[this.setting[setting]].entries()) { + // Check if the current setting is for binding keys. + if (bindingSettings.includes(this.setting[setting])) { + // Create a label for non-null options, typically indicating actionable options like 'change'. + if (o) { + const valueLabel = addTextObject(this.scene, 0, 0, isLock ? "" : option, TextStyle.WINDOW); + valueLabel.setOrigin(0, 0); + optionsContainer.add(valueLabel); + valueLabels.push(valueLabel); + continue; + } + // For null options, add an icon for the key. + const icon = this.scene.add.sprite(0, 0, this.textureOverride ? this.textureOverride : config.padType); + icon.setOrigin(0, -0.15); + inputsIcons[this.setting[setting]] = icon; + optionsContainer.add(icon); + valueLabels.push(icon); + continue; + } + // For regular settings like 'Gamepad support', create a label and determine if it is selected. + const valueLabel = addTextObject(this.scene, 0, 0, option, this.settingDeviceDefaults[this.setting[setting]] === o ? TextStyle.SETTINGS_SELECTED : TextStyle.WINDOW); + valueLabel.setOrigin(0, 0); + + optionsContainer.add(valueLabel); + + //if a setting has 2 options, valueLabels will be an array of 2 elements + valueLabels.push(valueLabel); + } + // Collect all option labels for this setting into the main array. + optionValueLabels.push(valueLabels); + + // Calculate the total width of all option labels within a specific setting + // This is achieved by summing the width of each option label + const totalWidth = optionValueLabels[s].map((o) => (o as Phaser.GameObjects.Text).width).reduce((total, width) => total += width, 0); + + // Define the minimum width for a label, ensuring it's at least 78 pixels wide or the width of the setting label plus some padding + const labelWidth = Math.max(130, settingLabels[s].displayWidth + 8); + + // Calculate the total available space for placing option labels next to their setting label + // We reserve space for the setting label and then distribute the remaining space evenly + const totalSpace = (300 - labelWidth) - totalWidth / 6; + // Calculate the spacing between options based on the available space divided by the number of gaps between labels + const optionSpacing = Math.floor(totalSpace / (optionValueLabels[s].length - 1)); + + // Initialize xOffset to zero, which will be used to position each option label horizontally + let xOffset = 0; + + // Start positioning each option label one by one + for (const value of optionValueLabels[s]) { + // Set the option label's position right next to the setting label, adjusted by xOffset + (value as Phaser.GameObjects.Text).setPositionRelative(settingLabels[s], labelWidth + xOffset, 0); + // Move the xOffset to the right for the next label, ensuring each label is spaced evenly + xOffset += (value as Phaser.GameObjects.Text).width / 6 + optionSpacing; + } + }); + + // Assigning the newly created components to the layout map under the specific gamepad type. + this.layout[config.padType].optionsContainer = optionsContainer; // Container for this pad's options. + this.layout[config.padType].inputsIcons = inputsIcons; // Icons for each input specific to this pad. + this.layout[config.padType].settingLabels = settingLabels; // Text labels for each setting available on this pad. + this.layout[config.padType].optionValueLabels = optionValueLabels; // Labels for values corresponding to each setting. + this.layout[config.padType].optionCursors = optionCursors; // Cursors to navigate through the options. + this.layout[config.padType].keys = specificBindingKeys; // Keys that identify each setting specifically bound to this pad. + this.layout[config.padType].bindingSettings = bindingSettings; // Settings that define how the keys are bound. + + // Add the options container to the overall settings container to be displayed in the UI. + this.settingsContainer.add(optionsContainer); + } + // Add the settings container to the UI. + ui.add(this.settingsContainer); + + // Initially hide the settings container until needed (e.g., when a gamepad is connected or a button is pressed). + this.settingsContainer.setVisible(false); + } + + /** + * Get the active configuration. + * + * @returns The active configuration for current device + */ + getActiveConfig(): InterfaceConfig { + return this.scene.inputController.getActiveConfig(this.device); + } + + /** + * Update the bindings for the current active device configuration. + */ + updateBindings(): void { + // Hide the options container for all layouts to reset the UI visibility. + Object.keys(this.layout).forEach((key) => this.layout[key].optionsContainer.setVisible(false)); + // Fetch the active gamepad configuration from the input controller. + const activeConfig = this.getActiveConfig(); + + // Set the UI layout for the active configuration. If unsuccessful, exit the function early. + if (!this.setLayout(activeConfig)) { + return; + } + + // Retrieve the gamepad settings from local storage or use an empty object if none exist. + const settings: object = this.getLocalStorageSetting(); + + // Update the cursor for each key based on the stored settings or default cursors. + this.keys.forEach((key, index) => { + this.setOptionCursor(index, settings.hasOwnProperty(key as string) ? settings[key as string] : this.optionCursors[index]); + }); + + // If the active configuration has no custom bindings set, exit the function early. + // by default, if custom does not exists, a default is assigned to it + // it only means the gamepad is not yet initalized + if (!activeConfig.custom) { + return; + } + + // For each element in the binding settings, update the icon according to the current assignment. + for (const elm of this.bindingSettings) { + const icon = getIconWithSettingName(activeConfig, elm); + if (icon) { + this.inputsIcons[elm as string].setFrame(icon); + this.inputsIcons[elm as string].alpha = 1; + } else { + this.inputsIcons[elm as string].alpha = 0; + } + } + + // Set the cursor and scroll cursor to their initial positions. + this.setCursor(this.cursor); + this.setScrollCursor(this.scrollCursor); + } + + updateNavigationDisplay() { + const specialIcons = { + "BUTTON_HOME": "HOME.png", + "BUTTON_DELETE": "DEL.png", + }; + for (const settingName of Object.keys(this.navigationIcons)) { + if (Object.keys(specialIcons).includes(settingName)) { + this.navigationIcons[settingName].setTexture("keyboard"); + this.navigationIcons[settingName].setFrame(specialIcons[settingName]); + this.navigationIcons[settingName].alpha = 1; + continue; + } + const icon = this.scene.inputController?.getIconForLatestInputRecorded(settingName); + if (icon) { + const type = this.scene.inputController?.getLastSourceType(); + this.navigationIcons[settingName].setTexture(type); + this.navigationIcons[settingName].setFrame(icon); + this.navigationIcons[settingName].alpha = 1; + } else { + this.navigationIcons[settingName].alpha = 0; + } + } + } + + /** + * Show the UI with the provided arguments. + * + * @param args - Arguments to be passed to the show method. + * @returns `true` if successful. + */ + show(args: any[]): boolean { + super.show(args); + + this.updateNavigationDisplay(); + NavigationManager.getInstance().updateIcons(); + // Update the bindings for the current active gamepad configuration. + this.updateBindings(); + + // Make the settings container visible to the user. + this.settingsContainer.setVisible(true); + // Reset the scroll cursor to the top of the settings container. + this.resetScroll(); + + // Move the settings container to the end of the UI stack to ensure it is displayed on top. + this.getUi().moveTo(this.settingsContainer, this.getUi().length - 1); + + // Hide any tooltips that might be visible before showing the settings container. + this.getUi().hideTooltip(); + + // Return true to indicate the UI was successfully shown. + return true; + } + + /** + * Set the UI layout for the active device configuration. + * + * @param activeConfig - The active device configuration. + * @returns `true` if the layout was successfully applied, otherwise `false`. + */ + setLayout(activeConfig: InterfaceConfig): boolean { + // Check if there is no active configuration (e.g., no gamepad connected). + if (!activeConfig) { + // Retrieve the layout for when no gamepads are connected. + const layout = this.layout["noGamepads"]; + // Make the options container visible to show message. + layout.optionsContainer.setVisible(true); + // Return false indicating the layout application was not successful due to lack of gamepad. + return false; + } + // Extract the type of the gamepad from the active configuration. + const configType = activeConfig.padType; + + // Retrieve the layout settings based on the type of the gamepad. + const layout = this.layout[configType]; + // Update the main controller with configuration details from the selected layout. + this.keys = layout.keys; + this.optionsContainer = layout.optionsContainer; + this.optionsContainer.setVisible(true); + this.settingLabels = layout.settingLabels; + this.optionValueLabels = layout.optionValueLabels; + this.optionCursors = layout.optionCursors; + this.inputsIcons = layout.inputsIcons; + this.bindingSettings = layout.bindingSettings; + + // Return true indicating the layout was successfully applied. + return true; + } + + /** + * Process the input for the given button. + * + * @param button - The button to process. + * @returns `true` if the input was processed successfully. + */ + processInput(button: Button): boolean { + const ui = this.getUi(); + // Defines the maximum number of rows that can be displayed on the screen. + let success = false; + this.updateNavigationDisplay(); + + // Handle the input based on the button pressed. + if (button === Button.CANCEL) { + // Handle cancel button press, reverting UI mode to previous state. + success = true; + NavigationManager.getInstance().reset(); + this.scene.ui.revertMode(); + } else { + const cursor = this.cursor + this.scrollCursor; // Calculate the absolute cursor position. + const setting = this.setting[Object.keys(this.setting)[cursor]]; + switch (button) { + case Button.ACTION: + if (!this.optionCursors || !this.optionValueLabels) { + return; + } + if (this.settingBlacklisted.includes(setting) || !setting.includes("BUTTON_")) { + success = false; + } else { + success = this.setSetting(this.scene, setting, 1); + } + break; + case Button.UP: // Move up in the menu. + if (!this.optionValueLabels) { + return false; + } + if (cursor) { // If not at the top, move the cursor up. + if (this.cursor) { + success = this.setCursor(this.cursor - 1); + } else {// If at the top of the visible items, scroll up. + success = this.setScrollCursor(this.scrollCursor - 1); + } + } else { + // When at the top of the menu and pressing UP, move to the bottommost item. + // First, set the cursor to the last visible element, preparing for the scroll to the end. + const successA = this.setCursor(this.rowsToDisplay - 1); + // Then, adjust the scroll to display the bottommost elements of the menu. + const successB = this.setScrollCursor(this.optionValueLabels.length - this.rowsToDisplay); + success = successA && successB; // success is just there to play the little validation sound effect + } + break; + case Button.DOWN: // Move down in the menu. + if (!this.optionValueLabels) { + return false; + } + if (cursor < this.optionValueLabels.length - 1) { + if (this.cursor < this.rowsToDisplay - 1) { + success = this.setCursor(this.cursor + 1); + } else if (this.scrollCursor < this.optionValueLabels.length - this.rowsToDisplay) { + success = this.setScrollCursor(this.scrollCursor + 1); + } + } else { + // When at the bottom of the menu and pressing DOWN, move to the topmost item. + // First, set the cursor to the first visible element, resetting the scroll to the top. + const successA = this.setCursor(0); + // Then, reset the scroll to start from the first element of the menu. + const successB = this.setScrollCursor(0); + success = successA && successB; // Indicates a successful cursor and scroll adjustment. + } + break; + case Button.LEFT: // Move selection left within the current option set. + if (!this.optionCursors || !this.optionValueLabels) { + return; + } + if (this.settingBlacklisted.includes(setting) || setting.includes("BUTTON_")) { + success = false; + } else if (this.optionCursors[cursor]) { + success = this.setOptionCursor(cursor, this.optionCursors[cursor] - 1, true); + } + break; + case Button.RIGHT: // Move selection right within the current option set. + if (!this.optionCursors || !this.optionValueLabels) { + return; + } + if (this.settingBlacklisted.includes(setting) || setting.includes("BUTTON_")) { + success = false; + } else if (this.optionCursors[cursor] < this.optionValueLabels[cursor].length - 1) { + success = this.setOptionCursor(cursor, this.optionCursors[cursor] + 1, true); + } + break; + case Button.CYCLE_FORM: + case Button.CYCLE_SHINY: + success = this.navigationContainer.navigate(button); + break; + } + } + + // If a change occurred, play the selection sound. + if (success) { + ui.playSelect(); + } + + return success; // Return whether the input resulted in a successful action. + } + + resetScroll() { + this.cursorObj?.destroy(); + this.cursorObj = null; + this.cursor = null; + this.setCursor(0); + this.setScrollCursor(0); + this.updateSettingsScroll(); + } + + /** + * Set the cursor to the specified position. + * + * @param cursor - The cursor position to set. + * @returns `true` if the cursor was set successfully. + */ + setCursor(cursor: integer): boolean { + const ret = super.setCursor(cursor); + // If the optionsContainer is not initialized, return the result from the parent class directly. + if (!this.optionsContainer) { + return ret; + } + + // Check if the cursor object exists, if not, create it. + if (!this.cursorObj) { + this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", null, (this.scene.game.canvas.width / 6) - 10, 16, 1, 1, 1, 1); + this.cursorObj.setOrigin(0, 0); // Set the origin to the top-left corner. + this.optionsContainer.add(this.cursorObj); // Add the cursor to the options container. + } + + // Update the position of the cursor object relative to the options background based on the current cursor and scroll positions. + this.cursorObj.setPositionRelative(this.optionsBg, 4, 4 + (this.cursor + this.scrollCursor) * 16); + + return ret; // Return the result from the parent class's setCursor method. + } + + /** + * Set the scroll cursor to the specified position. + * + * @param scrollCursor - The scroll cursor position to set. + * @returns `true` if the scroll cursor was set successfully. + */ + setScrollCursor(scrollCursor: integer): boolean { + // Check if the new scroll position is the same as the current one; if so, do not update. + if (scrollCursor === this.scrollCursor) { + return false; + } + + // Update the internal scroll cursor state + this.scrollCursor = scrollCursor; + + // Apply the new scroll position to the settings UI. + this.updateSettingsScroll(); + + // Reset the cursor to its current position to adjust its visibility after scrolling. + this.setCursor(this.cursor); + + return true; // Return true to indicate the scroll cursor was successfully updated. + } + + /** + * Set the option cursor to the specified position. + * + * @param settingIndex - The index of the setting. + * @param cursor - The cursor position to set. + * @param save - Whether to save the setting to local storage. + * @returns `true` if the option cursor was set successfully. + */ + setOptionCursor(settingIndex: integer, cursor: integer, save?: boolean): boolean { + // Retrieve the specific setting using the settingIndex from the settingDevice enumeration. + const setting = this.setting[Object.keys(this.setting)[settingIndex]]; + + // Get the current cursor position for this setting. + const lastCursor = this.optionCursors[settingIndex]; + + // Check if the setting is not part of the bindings (i.e., it's a regular setting). + if (!this.bindingSettings.includes(setting) && !setting.includes("BUTTON_")) { + // Get the label of the last selected option and revert its color to the default. + const lastValueLabel = this.optionValueLabels[settingIndex][lastCursor]; + lastValueLabel.setColor(this.getTextColor(TextStyle.WINDOW)); + lastValueLabel.setShadowColor(this.getTextColor(TextStyle.WINDOW, true)); + + // Update the cursor for the setting to the new position. + this.optionCursors[settingIndex] = cursor; + + // Change the color of the new selected option to indicate it's selected. + const newValueLabel = this.optionValueLabels[settingIndex][cursor]; + newValueLabel.setColor(this.getTextColor(TextStyle.SETTINGS_SELECTED)); + newValueLabel.setShadowColor(this.getTextColor(TextStyle.SETTINGS_SELECTED, true)); + } + + // If the save flag is set, save the setting to local storage + if (save) { + this.saveSettingToLocalStorage(setting, cursor); + } + + return true; // Return true to indicate the cursor was successfully updated. + } + + /** + * Update the scroll position of the settings UI. + */ + updateSettingsScroll(): void { + // Return immediately if the options container is not initialized. + if (!this.optionsContainer) { + return; + } + + // Set the vertical position of the options container based on the current scroll cursor, multiplying by the item height. + this.optionsContainer.setY(-16 * this.scrollCursor); + + // Iterate over all setting labels to update their visibility. + for (let s = 0; s < this.settingLabels.length; s++) { + // Determine if the current setting should be visible based on the scroll position. + const visible = s >= this.scrollCursor && s < this.scrollCursor + this.rowsToDisplay; + + // Set the visibility of the setting label and its corresponding options. + this.settingLabels[s].setVisible(visible); + for (const option of this.optionValueLabels[s]) { + option.setVisible(visible); + } + } + } + + /** + * Clear the UI elements and state. + */ + clear(): void { + super.clear(); + + // Hide the settings container to remove it from the view. + this.settingsContainer.setVisible(false); + + // Remove the cursor from the UI. + this.eraseCursor(); + } + + /** + * Erase the cursor from the UI. + */ + eraseCursor(): void { + // Check if a cursor object exists. + if (this.cursorObj) { + this.cursorObj.destroy(); + } // Destroy the cursor object to clean up resources. + + // Set the cursor object reference to null to fully dereference it. + this.cursorObj = null; + } + +} diff --git a/src/ui/settings/abstract-settings-ui-handler.ts b/src/ui/settings/abstract-settings-ui-handler.ts index f37ce05b69f..87d6a611662 100644 --- a/src/ui/settings/abstract-settings-ui-handler.ts +++ b/src/ui/settings/abstract-settings-ui-handler.ts @@ -1,107 +1,74 @@ -import UiHandler from "../ui-handler"; import BattleScene from "../../battle-scene"; -import {Mode} from "../ui"; -import {InterfaceConfig} from "../../inputs-controller"; -import {addWindow} from "../ui-theme"; -import {addTextObject, TextStyle} from "../text"; +import { hasTouchscreen, isMobile } from "../../touch-controls"; +import { TextStyle, addTextObject } from "../text"; +import { Mode } from "../ui"; +import UiHandler from "../ui-handler"; +import { addWindow } from "../ui-theme"; import {Button} from "../../enums/buttons"; -import {getIconWithSettingName} from "#app/configs/inputs/configHandler"; +import {InputsIcons} from "#app/ui/settings/abstract-control-settings-ui-handler.js"; import NavigationMenu, {NavigationManager} from "#app/ui/settings/navigationMenu"; +import { Setting, SettingKeys } from "#app/system/settings/settings"; -export interface InputsIcons { - [key: string]: Phaser.GameObjects.Sprite; -} -export interface LayoutConfig { - optionsContainer: Phaser.GameObjects.Container; - inputsIcons: InputsIcons; - settingLabels: Phaser.GameObjects.Text[]; - optionValueLabels: Phaser.GameObjects.Text[][]; - optionCursors: integer[]; - keys: string[]; - bindingSettings: Array; -} /** * Abstract class for handling UI elements related to settings. */ -export default abstract class AbstractSettingsUiUiHandler extends UiHandler { - protected settingsContainer: Phaser.GameObjects.Container; - protected optionsContainer: Phaser.GameObjects.Container; - protected navigationContainer: NavigationMenu; +export default class AbstractSettingsUiHandler extends UiHandler { + private settingsContainer: Phaser.GameObjects.Container; + private optionsContainer: Phaser.GameObjects.Container; + private navigationContainer: NavigationMenu; - protected scrollCursor: integer; - protected optionCursors: integer[]; - protected cursorObj: Phaser.GameObjects.NineSlice; + private scrollCursor: integer; - protected optionsBg: Phaser.GameObjects.NineSlice; - protected actionsBg: Phaser.GameObjects.NineSlice; + private optionsBg: Phaser.GameObjects.NineSlice; - protected settingLabels: Phaser.GameObjects.Text[]; - protected optionValueLabels: Phaser.GameObjects.Text[][]; + private optionCursors: integer[]; + + private settingLabels: Phaser.GameObjects.Text[]; + private optionValueLabels: Phaser.GameObjects.Text[][]; - // layout will contain the 3 Gamepad tab for each config - dualshock, xbox, snes - protected layout: Map = new Map(); - // Will contain the input icons from the selected layout - protected inputsIcons: InputsIcons; protected navigationIcons: InputsIcons; - // list all the setting keys used in the selected layout (because dualshock has more buttons than xbox) - protected keys: Array; - // Store the specific settings related to key bindings for the current gamepad configuration. - protected bindingSettings: Array; + private cursorObj: Phaser.GameObjects.NineSlice; - protected settingDevice; - protected settingBlacklisted; - protected settingDeviceDefaults; - protected settingDeviceOptions; - protected configs; - protected commonSettingsCount; - protected textureOverride; - protected titleSelected; - protected localStoragePropertyName; - protected rowsToDisplay: number; + private reloadSettings: Array; + private reloadRequired: boolean; + private rowsToDisplay: number; - abstract getLocalStorageSetting(): object; - abstract navigateMenuLeft(): boolean; - abstract navigateMenuRight(): boolean; - abstract saveSettingToLocalStorage(setting, cursor): void; - abstract getActiveConfig(): InterfaceConfig; - abstract setSetting(scene: BattleScene, setting, value: integer): boolean; + protected title: string; + protected settings: Array; + protected localStorageKey: string; - /** - * Constructor for the AbstractSettingsUiUiHandler. - * - * @param scene - The BattleScene instance. - * @param mode - The UI mode. - */ constructor(scene: BattleScene, mode?: Mode) { super(scene, mode); + + this.reloadRequired = false; this.rowsToDisplay = 8; } /** - * Setup UI elements. - */ + * Setup UI elements + */ setup() { const ui = this.getUi(); - this.navigationIcons = {}; this.settingsContainer = this.scene.add.container(1, -(this.scene.game.canvas.height / 6) + 1); - this.settingsContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, this.scene.game.canvas.width / 6, this.scene.game.canvas.height / 6), Phaser.Geom.Rectangle.Contains); + this.settingsContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, this.scene.game.canvas.width / 6, this.scene.game.canvas.height / 6 - 20), Phaser.Geom.Rectangle.Contains); + + this.navigationIcons = {}; this.navigationContainer = new NavigationMenu(this.scene, 0, 0); this.optionsBg = addWindow(this.scene, 0, this.navigationContainer.height, (this.scene.game.canvas.width / 6) - 2, (this.scene.game.canvas.height / 6) - 16 - this.navigationContainer.height - 2); this.optionsBg.setOrigin(0, 0); - - this.actionsBg = addWindow(this.scene, 0, (this.scene.game.canvas.height / 6) - this.navigationContainer.height, (this.scene.game.canvas.width / 6) - 2, 22); - this.actionsBg.setOrigin(0, 0); + const actionsBg = addWindow(this.scene, 0, (this.scene.game.canvas.height / 6) - this.navigationContainer.height, (this.scene.game.canvas.width / 6) - 2, 22); + actionsBg.setOrigin(0, 0); const iconAction = this.scene.add.sprite(0, 0, "keyboard"); iconAction.setOrigin(0, -0.1); - iconAction.setPositionRelative(this.actionsBg, this.navigationContainer.width - 32, 4); + iconAction.setPositionRelative(actionsBg, this.navigationContainer.width - 32, 4); this.navigationIcons["BUTTON_ACTION"] = iconAction; const actionText = addTextObject(this.scene, 0, 0, "Action", TextStyle.SETTINGS_LABEL); @@ -110,207 +77,81 @@ export default abstract class AbstractSettingsUiUiHandler extends UiHandler { const iconCancel = this.scene.add.sprite(0, 0, "keyboard"); iconCancel.setOrigin(0, -0.1); - iconCancel.setPositionRelative(this.actionsBg, this.navigationContainer.width - 100, 4); + iconCancel.setPositionRelative(actionsBg, this.navigationContainer.width - 100, 4); this.navigationIcons["BUTTON_CANCEL"] = iconCancel; const cancelText = addTextObject(this.scene, 0, 0, "Cancel", TextStyle.SETTINGS_LABEL); cancelText.setOrigin(0, 0.15); cancelText.setPositionRelative(iconCancel, -cancelText.width/6-2, 0); - const iconReset = this.scene.add.sprite(0, 0, "keyboard"); - iconReset.setOrigin(0, -0.1); - iconReset.setPositionRelative(this.actionsBg, this.navigationContainer.width - 180, 4); - this.navigationIcons["BUTTON_HOME"] = iconReset; + this.optionsContainer = this.scene.add.container(0, 0); - const resetText = addTextObject(this.scene, 0, 0, "Reset all", TextStyle.SETTINGS_LABEL); - resetText.setOrigin(0, 0.15); - resetText.setPositionRelative(iconReset, -resetText.width/6-2, 0); + this.settingLabels = []; + this.optionValueLabels = []; - this.settingsContainer.add(this.optionsBg); - this.settingsContainer.add(this.actionsBg); - this.settingsContainer.add(this.navigationContainer); - this.settingsContainer.add(iconAction); - this.settingsContainer.add(iconCancel); - this.settingsContainer.add(iconReset); - this.settingsContainer.add(actionText); - this.settingsContainer.add(cancelText); - this.settingsContainer.add(resetText); + this.reloadSettings = this.settings.filter(s => s?.requireReload); - /// Initialize a new configuration "screen" for each type of gamepad. - for (const config of this.configs) { - // Create a map to store layout settings based on the pad type. - this.layout[config.padType] = new Map(); - // Create a container for gamepad options in the scene, initially hidden. + this.settings + .forEach((setting, s) => { + let settingName = setting.label; + if (setting?.requireReload) { + settingName += " (Requires Reload)"; + } - const optionsContainer = this.scene.add.container(0, 0); - optionsContainer.setVisible(false); + this.settingLabels[s] = addTextObject(this.scene, 8, 28 + s * 16, settingName, TextStyle.SETTINGS_LABEL); + this.settingLabels[s].setOrigin(0, 0); - // Gather all binding settings from the configuration. - const bindingSettings = Object.keys(config.settings); - - // Array to hold labels for different settings such as 'Controller', 'Gamepad Support', etc. - const settingLabels: Phaser.GameObjects.Text[] = []; - - // Array to hold options for each setting, e.g., 'Auto', 'Disabled'. - const optionValueLabels: Phaser.GameObjects.GameObject[][] = []; - - // Object to store sprites for each button configuration. - const inputsIcons: InputsIcons = {}; - - // Fetch common setting keys such as 'Controller' and 'Gamepad Support' from gamepad settings. - const commonSettingKeys = Object.keys(this.settingDevice).slice(0, this.commonSettingsCount).map(key => this.settingDevice[key]); - // Combine common and specific bindings into a single array. - const specificBindingKeys = [...commonSettingKeys, ...Object.keys(config.settings)]; - // Fetch default values for these settings and prepare to highlight selected options. - const optionCursors = Object.values(Object.keys(this.settingDeviceDefaults).filter(s => specificBindingKeys.includes(s)).map(k => this.settingDeviceDefaults[k])); - // Filter out settings that are not relevant to the current gamepad configuration. - const settingFiltered = Object.keys(this.settingDevice).filter(_key => specificBindingKeys.includes(this.settingDevice[_key])); - // Loop through the filtered settings to manage display and options. - - settingFiltered.forEach((setting, s) => { - // Convert the setting key from format 'Key_Name' to 'Key name' for display. - const settingName = setting.replace(/\_/g, " "); - - // Create and add a text object for the setting name to the scene. - const isLock = this.settingBlacklisted.includes(this.settingDevice[setting]); - const labelStyle = isLock ? TextStyle.SETTINGS_LOCKED : TextStyle.SETTINGS_LABEL; - settingLabels[s] = addTextObject(this.scene, 8, 28 + s * 16, settingName, labelStyle); - settingLabels[s].setOrigin(0, 0); - optionsContainer.add(settingLabels[s]); - - // Initialize an array to store the option labels for this setting. - const valueLabels: Phaser.GameObjects.GameObject[] = []; - - // Process each option for the current setting. - for (const [o, option] of this.settingDeviceOptions[this.settingDevice[setting]].entries()) { - // Check if the current setting is for binding keys. - if (bindingSettings.includes(this.settingDevice[setting])) { - // Create a label for non-null options, typically indicating actionable options like 'change'. - if (o) { - const valueLabel = addTextObject(this.scene, 0, 0, isLock ? "" : option, TextStyle.WINDOW); - valueLabel.setOrigin(0, 0); - optionsContainer.add(valueLabel); - valueLabels.push(valueLabel); - continue; - } - // For null options, add an icon for the key. - const icon = this.scene.add.sprite(0, 0, this.textureOverride ? this.textureOverride : config.padType); - icon.setOrigin(0, -0.15); - inputsIcons[this.settingDevice[setting]] = icon; - optionsContainer.add(icon); - valueLabels.push(icon); - continue; - } - // For regular settings like 'Gamepad support', create a label and determine if it is selected. - const valueLabel = addTextObject(this.scene, 0, 0, option, this.settingDeviceDefaults[this.settingDevice[setting]] === o ? TextStyle.SETTINGS_SELECTED : TextStyle.WINDOW); + this.optionsContainer.add(this.settingLabels[s]); + this.optionValueLabels.push(setting.options.map((option, o) => { + const valueLabel = addTextObject(this.scene, 0, 0, option, setting.default === o ? TextStyle.SETTINGS_SELECTED : TextStyle.WINDOW); valueLabel.setOrigin(0, 0); - optionsContainer.add(valueLabel); + this.optionsContainer.add(valueLabel); - //if a setting has 2 options, valueLabels will be an array of 2 elements - valueLabels.push(valueLabel); - } - // Collect all option labels for this setting into the main array. - optionValueLabels.push(valueLabels); + return valueLabel; + })); - // Calculate the total width of all option labels within a specific setting - // This is achieved by summing the width of each option label - const totalWidth = optionValueLabels[s].map((o) => (o as Phaser.GameObjects.Text).width).reduce((total, width) => total += width, 0); + const totalWidth = this.optionValueLabels[s].map(o => o.width).reduce((total, width) => total += width, 0); - // Define the minimum width for a label, ensuring it's at least 78 pixels wide or the width of the setting label plus some padding - const labelWidth = Math.max(130, settingLabels[s].displayWidth + 8); + const labelWidth = Math.max(78, this.settingLabels[s].displayWidth + 8); - // Calculate the total available space for placing option labels next to their setting label - // We reserve space for the setting label and then distribute the remaining space evenly const totalSpace = (300 - labelWidth) - totalWidth / 6; - // Calculate the spacing between options based on the available space divided by the number of gaps between labels - const optionSpacing = Math.floor(totalSpace / (optionValueLabels[s].length - 1)); + const optionSpacing = Math.floor(totalSpace / (this.optionValueLabels[s].length - 1)); - // Initialize xOffset to zero, which will be used to position each option label horizontally let xOffset = 0; - // Start positioning each option label one by one - for (const value of optionValueLabels[s]) { - // Set the option label's position right next to the setting label, adjusted by xOffset - (value as Phaser.GameObjects.Text).setPositionRelative(settingLabels[s], labelWidth + xOffset, 0); - // Move the xOffset to the right for the next label, ensuring each label is spaced evenly - xOffset += (value as Phaser.GameObjects.Text).width / 6 + optionSpacing; + for (const value of this.optionValueLabels[s]) { + value.setPositionRelative(this.settingLabels[s], labelWidth + xOffset, 0); + xOffset += value.width / 6 + optionSpacing; } }); - // Assigning the newly created components to the layout map under the specific gamepad type. - this.layout[config.padType].optionsContainer = optionsContainer; // Container for this pad's options. - this.layout[config.padType].inputsIcons = inputsIcons; // Icons for each input specific to this pad. - this.layout[config.padType].settingLabels = settingLabels; // Text labels for each setting available on this pad. - this.layout[config.padType].optionValueLabels = optionValueLabels; // Labels for values corresponding to each setting. - this.layout[config.padType].optionCursors = optionCursors; // Cursors to navigate through the options. - this.layout[config.padType].keys = specificBindingKeys; // Keys that identify each setting specifically bound to this pad. - this.layout[config.padType].bindingSettings = bindingSettings; // Settings that define how the keys are bound. + this.optionCursors = this.settings.map(setting => setting.default); + + this.settingsContainer.add(this.optionsBg); + this.settingsContainer.add(this.navigationContainer); + this.settingsContainer.add(actionsBg); + this.settingsContainer.add(this.optionsContainer); + this.settingsContainer.add(iconAction); + this.settingsContainer.add(iconCancel); + this.settingsContainer.add(actionText); + this.settingsContainer.add(cancelText); - // Add the options container to the overall settings container to be displayed in the UI. - this.settingsContainer.add(optionsContainer); - } - // Add the settings container to the UI. ui.add(this.settingsContainer); - // Initially hide the settings container until needed (e.g., when a gamepad is connected or a button is pressed). + this.setCursor(0); + this.setScrollCursor(0); + this.settingsContainer.setVisible(false); } - /** - * Update the bindings for the current active device configuration. - */ + * Update the bindings for the current active device configuration. + */ updateBindings(): void { - // Hide the options container for all layouts to reset the UI visibility. - Object.keys(this.layout).forEach((key) => this.layout[key].optionsContainer.setVisible(false)); - // Fetch the active gamepad configuration from the input controller. - const activeConfig = this.getActiveConfig(); - - // Set the UI layout for the active configuration. If unsuccessful, exit the function early. - if (!this.setLayout(activeConfig)) { - return; - } - - // Retrieve the gamepad settings from local storage or use an empty object if none exist. - const settings: object = this.getLocalStorageSetting(); - - // Update the cursor for each key based on the stored settings or default cursors. - this.keys.forEach((key, index) => { - this.setOptionCursor(index, settings.hasOwnProperty(key as string) ? settings[key as string] : this.optionCursors[index]); - }); - - // If the active configuration has no custom bindings set, exit the function early. - // by default, if custom does not exists, a default is assigned to it - // it only means the gamepad is not yet initalized - if (!activeConfig.custom) { - return; - } - - // For each element in the binding settings, update the icon according to the current assignment. - for (const elm of this.bindingSettings) { - const icon = getIconWithSettingName(activeConfig, elm); - if (icon) { - this.inputsIcons[elm as string].setFrame(icon); - this.inputsIcons[elm as string].alpha = 1; - } else { - this.inputsIcons[elm as string].alpha = 0; - } - } - - // Set the cursor and scroll cursor to their initial positions. - this.setCursor(this.cursor); - this.setScrollCursor(this.scrollCursor); - } - - updateNavigationDisplay() { - const specialIcons = { - "BUTTON_HOME": "HOME.png", - "BUTTON_DELETE": "DEL.png", - }; for (const settingName of Object.keys(this.navigationIcons)) { - if (Object.keys(specialIcons).includes(settingName)) { + if (settingName === "BUTTON_HOME") { this.navigationIcons[settingName].setTexture("keyboard"); - this.navigationIcons[settingName].setFrame(specialIcons[settingName]); + this.navigationIcons[settingName].setFrame("HOME.png"); this.navigationIcons[settingName].alpha = 1; continue; } @@ -324,112 +165,61 @@ export default abstract class AbstractSettingsUiUiHandler extends UiHandler { this.navigationIcons[settingName].alpha = 0; } } + NavigationManager.getInstance().updateIcons(); } /** - * Show the UI with the provided arguments. - * - * @param args - Arguments to be passed to the show method. - * @returns `true` if successful. + * Show the UI with the provided arguments. + * + * @param args - Arguments to be passed to the show method. + * @returns `true` if successful. */ show(args: any[]): boolean { super.show(args); - - this.updateNavigationDisplay(); - NavigationManager.getInstance().updateIcons(); - // Update the bindings for the current active gamepad configuration. this.updateBindings(); - // Make the settings container visible to the user. - this.settingsContainer.setVisible(true); - // Reset the scroll cursor to the top of the settings container. - this.resetScroll(); + const settings: object = localStorage.hasOwnProperty(this.localStorageKey) ? JSON.parse(localStorage.getItem(this.localStorageKey)) : {}; + + this.settings.forEach((setting, s) => this.setOptionCursor(s, settings.hasOwnProperty(setting.key) ? settings[setting.key] : this.settings[s].default)); + + this.settingsContainer.setVisible(true); + this.setCursor(0); - // Move the settings container to the end of the UI stack to ensure it is displayed on top. this.getUi().moveTo(this.settingsContainer, this.getUi().length - 1); - // Hide any tooltips that might be visible before showing the settings container. this.getUi().hideTooltip(); - // Return true to indicate the UI was successfully shown. return true; } /** - * Set the UI layout for the active device configuration. - * - * @param activeConfig - The active device configuration. - * @returns `true` if the layout was successfully applied, otherwise `false`. - */ - setLayout(activeConfig: InterfaceConfig): boolean { - // Check if there is no active configuration (e.g., no gamepad connected). - if (!activeConfig) { - // Retrieve the layout for when no gamepads are connected. - const layout = this.layout["noGamepads"]; - // Make the options container visible to show message. - layout.optionsContainer.setVisible(true); - // Return false indicating the layout application was not successful due to lack of gamepad. - return false; - } - // Extract the type of the gamepad from the active configuration. - const configType = activeConfig.padType; - - // Retrieve the layout settings based on the type of the gamepad. - const layout = this.layout[configType]; - // Update the main controller with configuration details from the selected layout. - this.keys = layout.keys; - this.optionsContainer = layout.optionsContainer; - this.optionsContainer.setVisible(true); - this.settingLabels = layout.settingLabels; - this.optionValueLabels = layout.optionValueLabels; - this.optionCursors = layout.optionCursors; - this.inputsIcons = layout.inputsIcons; - this.bindingSettings = layout.bindingSettings; - - // Return true indicating the layout was successfully applied. - return true; - } - - /** - * Process the input for the given button. - * - * @param button - The button to process. - * @returns `true` if the input was processed successfully. - */ + * Processes input from a specified button. + * This method handles navigation through a UI menu, including movement through menu items + * and handling special actions like cancellation. Each button press may adjust the cursor + * position or the menu scroll, and plays a sound effect if the action was successful. + * + * @param button - The button pressed by the user. + * @returns `true` if the action associated with the button was successfully processed, `false` otherwise. + */ processInput(button: Button): boolean { const ui = this.getUi(); // Defines the maximum number of rows that can be displayed on the screen. - let success = false; - this.updateNavigationDisplay(); - // Handle the input based on the button pressed. + let success = false; + if (button === Button.CANCEL) { - // Handle cancel button press, reverting UI mode to previous state. success = true; NavigationManager.getInstance().reset(); + // Reverts UI to its previous state on cancel. this.scene.ui.revertMode(); } else { - const cursor = this.cursor + this.scrollCursor; // Calculate the absolute cursor position. - const setting = this.settingDevice[Object.keys(this.settingDevice)[cursor]]; + const cursor = this.cursor + this.scrollCursor; switch (button) { - case Button.ACTION: - if (!this.optionCursors || !this.optionValueLabels) { - return; - } - if (this.settingBlacklisted.includes(setting) || !setting.includes("BUTTON_")) { - success = false; - } else { - success = this.setSetting(this.scene, setting, 1); - } - break; - case Button.UP: // Move up in the menu. - if (!this.optionValueLabels) { - return false; - } - if (cursor) { // If not at the top, move the cursor up. + case Button.UP: + if (cursor) { if (this.cursor) { success = this.setCursor(this.cursor - 1); - } else {// If at the top of the visible items, scroll up. + } else { success = this.setScrollCursor(this.scrollCursor - 1); } } else { @@ -441,12 +231,9 @@ export default abstract class AbstractSettingsUiUiHandler extends UiHandler { success = successA && successB; // success is just there to play the little validation sound effect } break; - case Button.DOWN: // Move down in the menu. - if (!this.optionValueLabels) { - return false; - } + case Button.DOWN: if (cursor < this.optionValueLabels.length - 1) { - if (this.cursor < this.rowsToDisplay - 1) { + if (this.cursor < this.rowsToDisplay - 1) {// if the visual cursor is in the frame of 0 to 8 success = this.setCursor(this.cursor + 1); } else if (this.scrollCursor < this.optionValueLabels.length - this.rowsToDisplay) { success = this.setScrollCursor(this.scrollCursor + 1); @@ -460,23 +247,14 @@ export default abstract class AbstractSettingsUiUiHandler extends UiHandler { success = successA && successB; // Indicates a successful cursor and scroll adjustment. } break; - case Button.LEFT: // Move selection left within the current option set. - if (!this.optionCursors || !this.optionValueLabels) { - return; - } - if (this.settingBlacklisted.includes(setting) || setting.includes("BUTTON_")) { - success = false; - } else if (this.optionCursors[cursor]) { + case Button.LEFT: + if (this.optionCursors[cursor]) {// Moves the option cursor left, if possible. success = this.setOptionCursor(cursor, this.optionCursors[cursor] - 1, true); } break; - case Button.RIGHT: // Move selection right within the current option set. - if (!this.optionCursors || !this.optionValueLabels) { - return; - } - if (this.settingBlacklisted.includes(setting) || setting.includes("BUTTON_")) { - success = false; - } else if (this.optionCursors[cursor] < this.optionValueLabels[cursor].length - 1) { + case Button.RIGHT: + // Moves the option cursor right, if possible. + if (this.optionCursors[cursor] < this.optionValueLabels[cursor].length - 1) { success = this.setOptionCursor(cursor, this.optionCursors[cursor] + 1, true); } break; @@ -487,130 +265,100 @@ export default abstract class AbstractSettingsUiUiHandler extends UiHandler { } } - // If a change occurred, play the selection sound. + // Plays a select sound effect if an action was successfully processed. if (success) { ui.playSelect(); } - return success; // Return whether the input resulted in a successful action. - } - - resetScroll() { - this.cursorObj?.destroy(); - this.cursorObj = null; - this.cursor = null; - this.setCursor(0); - this.setScrollCursor(0); - this.updateSettingsScroll(); + return success; } /** - * Set the cursor to the specified position. - * - * @param cursor - The cursor position to set. - * @returns `true` if the cursor was set successfully. - */ + * Set the cursor to the specified position. + * + * @param cursor - The cursor position to set. + * @returns `true` if the cursor was set successfully. + */ setCursor(cursor: integer): boolean { const ret = super.setCursor(cursor); - // If the optionsContainer is not initialized, return the result from the parent class directly. - if (!this.optionsContainer) { - return ret; - } - // Check if the cursor object exists, if not, create it. if (!this.cursorObj) { this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", null, (this.scene.game.canvas.width / 6) - 10, 16, 1, 1, 1, 1); - this.cursorObj.setOrigin(0, 0); // Set the origin to the top-left corner. - this.optionsContainer.add(this.cursorObj); // Add the cursor to the options container. + this.cursorObj.setOrigin(0, 0); + this.optionsContainer.add(this.cursorObj); } - // Update the position of the cursor object relative to the options background based on the current cursor and scroll positions. this.cursorObj.setPositionRelative(this.optionsBg, 4, 4 + (this.cursor + this.scrollCursor) * 16); - return ret; // Return the result from the parent class's setCursor method. + return ret; } /** - * Set the scroll cursor to the specified position. - * - * @param scrollCursor - The scroll cursor position to set. - * @returns `true` if the scroll cursor was set successfully. - */ + * Set the option cursor to the specified position. + * + * @param settingIndex - The index of the setting. + * @param cursor - The cursor position to set. + * @param save - Whether to save the setting to local storage. + * @returns `true` if the option cursor was set successfully. + */ + setOptionCursor(settingIndex: integer, cursor: integer, save?: boolean): boolean { + const setting = this.settings[settingIndex]; + + if (setting.key === SettingKeys.Touch_Controls && cursor && hasTouchscreen() && isMobile()) { + this.getUi().playError(); + return false; + } + + const lastCursor = this.optionCursors[settingIndex]; + + const lastValueLabel = this.optionValueLabels[settingIndex][lastCursor]; + lastValueLabel.setColor(this.getTextColor(TextStyle.WINDOW)); + lastValueLabel.setShadowColor(this.getTextColor(TextStyle.WINDOW, true)); + + this.optionCursors[settingIndex] = cursor; + + const newValueLabel = this.optionValueLabels[settingIndex][cursor]; + newValueLabel.setColor(this.getTextColor(TextStyle.SETTINGS_SELECTED)); + newValueLabel.setShadowColor(this.getTextColor(TextStyle.SETTINGS_SELECTED, true)); + + if (save) { + this.scene.gameData.saveSetting(setting.key, cursor); + if (this.reloadSettings.includes(setting)) { + this.reloadRequired = true; + } + } + + return true; + } + + /** + * Set the scroll cursor to the specified position. + * + * @param scrollCursor - The scroll cursor position to set. + * @returns `true` if the scroll cursor was set successfully. + */ setScrollCursor(scrollCursor: integer): boolean { - // Check if the new scroll position is the same as the current one; if so, do not update. if (scrollCursor === this.scrollCursor) { return false; } - // Update the internal scroll cursor state this.scrollCursor = scrollCursor; - // Apply the new scroll position to the settings UI. this.updateSettingsScroll(); - // Reset the cursor to its current position to adjust its visibility after scrolling. this.setCursor(this.cursor); - return true; // Return true to indicate the scroll cursor was successfully updated. + return true; } /** - * Set the option cursor to the specified position. - * - * @param settingIndex - The index of the setting. - * @param cursor - The cursor position to set. - * @param save - Whether to save the setting to local storage. - * @returns `true` if the option cursor was set successfully. - */ - setOptionCursor(settingIndex: integer, cursor: integer, save?: boolean): boolean { - // Retrieve the specific setting using the settingIndex from the settingDevice enumeration. - const setting = this.settingDevice[Object.keys(this.settingDevice)[settingIndex]]; - - // Get the current cursor position for this setting. - const lastCursor = this.optionCursors[settingIndex]; - - // Check if the setting is not part of the bindings (i.e., it's a regular setting). - if (!this.bindingSettings.includes(setting) && !setting.includes("BUTTON_")) { - // Get the label of the last selected option and revert its color to the default. - const lastValueLabel = this.optionValueLabels[settingIndex][lastCursor]; - lastValueLabel.setColor(this.getTextColor(TextStyle.WINDOW)); - lastValueLabel.setShadowColor(this.getTextColor(TextStyle.WINDOW, true)); - - // Update the cursor for the setting to the new position. - this.optionCursors[settingIndex] = cursor; - - // Change the color of the new selected option to indicate it's selected. - const newValueLabel = this.optionValueLabels[settingIndex][cursor]; - newValueLabel.setColor(this.getTextColor(TextStyle.SETTINGS_SELECTED)); - newValueLabel.setShadowColor(this.getTextColor(TextStyle.SETTINGS_SELECTED, true)); - } - - // If the save flag is set, save the setting to local storage - if (save) { - this.saveSettingToLocalStorage(setting, cursor); - } - - return true; // Return true to indicate the cursor was successfully updated. - } - - /** - * Update the scroll position of the settings UI. - */ + * Update the scroll position of the settings UI. + */ updateSettingsScroll(): void { - // Return immediately if the options container is not initialized. - if (!this.optionsContainer) { - return; - } - - // Set the vertical position of the options container based on the current scroll cursor, multiplying by the item height. this.optionsContainer.setY(-16 * this.scrollCursor); - // Iterate over all setting labels to update their visibility. for (let s = 0; s < this.settingLabels.length; s++) { - // Determine if the current setting should be visible based on the scroll position. const visible = s >= this.scrollCursor && s < this.scrollCursor + this.rowsToDisplay; - - // Set the visibility of the setting label and its corresponding options. this.settingLabels[s].setVisible(visible); for (const option of this.optionValueLabels[s]) { option.setVisible(visible); @@ -619,29 +367,25 @@ export default abstract class AbstractSettingsUiUiHandler extends UiHandler { } /** - * Clear the UI elements and state. - */ - clear(): void { + * Clear the UI elements and state. + */ + clear() { super.clear(); - - // Hide the settings container to remove it from the view. this.settingsContainer.setVisible(false); - - // Remove the cursor from the UI. this.eraseCursor(); + if (this.reloadRequired) { + this.reloadRequired = false; + this.scene.reset(true, false, true); + } } /** - * Erase the cursor from the UI. - */ - eraseCursor(): void { - // Check if a cursor object exists. + * Erase the cursor from the UI. + */ + eraseCursor() { if (this.cursorObj) { this.cursorObj.destroy(); - } // Destroy the cursor object to clean up resources. - - // Set the cursor object reference to null to fully dereference it. + } this.cursorObj = null; } - } diff --git a/src/ui/settings/navigationMenu.ts b/src/ui/settings/navigationMenu.ts index 843e9fd1f86..d9664276872 100644 --- a/src/ui/settings/navigationMenu.ts +++ b/src/ui/settings/navigationMenu.ts @@ -1,10 +1,13 @@ import BattleScene from "#app/battle-scene"; import {Mode} from "#app/ui/ui"; -import {InputsIcons} from "#app/ui/settings/abstract-settings-ui-handler"; +import {InputsIcons} from "#app/ui/settings/abstract-control-settings-ui-handler.js"; import {addTextObject, setTextStyle, TextStyle} from "#app/ui/text"; import {addWindow} from "#app/ui/ui-theme"; import {Button} from "#app/enums/buttons"; +const LEFT = "LEFT"; +const RIGHT = "RIGHT"; + /** * Manages navigation and menus tabs within the setting menu. */ @@ -24,14 +27,16 @@ export class NavigationManager { constructor() { this.modes = [ Mode.SETTINGS, + Mode.SETTINGS_ACCESSIBILITY, Mode.SETTINGS_GAMEPAD, Mode.SETTINGS_KEYBOARD, ]; - this.labels = ["General", "Gamepad", "Keyboard"]; + this.labels = ["General", "Accessibility", "Gamepad", "Keyboard"]; } public reset() { this.selectedMode = Mode.SETTINGS; + this.updateNavigationMenus(); } /** @@ -46,32 +51,20 @@ export class NavigationManager { } /** - * Navigates to the previous mode in the modes array. - * @param scene The current BattleScene instance. + * Navigates modes based on given direction + * @param scene The current BattleScene instance + * @param direction LEFT or RIGHT */ - public navigateLeft(scene) { + public navigate(scene, direction) { const pos = this.modes.indexOf(this.selectedMode); const maxPos = this.modes.length - 1; - if (pos === 0) { + const increment = direction === LEFT ? -1 : 1; + if (pos === 0 && direction === LEFT) { this.selectedMode = this.modes[maxPos]; - } else { - this.selectedMode = this.modes[pos - 1]; - } - scene.ui.setMode(this.selectedMode); - this.updateNavigationMenus(); - } - - /** - * Navigates to the next mode in the modes array. - * @param scene The current BattleScene instance. - */ - public navigateRight(scene) { - const pos = this.modes.indexOf(this.selectedMode); - const maxPos = this.modes.length - 1; - if (pos === maxPos) { + } else if (pos === maxPos && direction === RIGHT) { this.selectedMode = this.modes[0]; } else { - this.selectedMode = this.modes[pos + 1]; + this.selectedMode = this.modes[pos + increment]; } scene.ui.setMode(this.selectedMode); this.updateNavigationMenus(); @@ -204,13 +197,11 @@ export default class NavigationMenu extends Phaser.GameObjects.Container { const navigationManager = NavigationManager.getInstance(); switch (button) { case Button.CYCLE_FORM: - navigationManager.navigateLeft(this.scene); + navigationManager.navigate(this.scene, LEFT); return true; - break; case Button.CYCLE_SHINY: - navigationManager.navigateRight(this.scene); + navigationManager.navigate(this.scene, RIGHT); return true; - break; } return false; } diff --git a/src/ui/settings/settings-accessiblity-ui-handler.ts b/src/ui/settings/settings-accessiblity-ui-handler.ts new file mode 100644 index 00000000000..ef700a7a9ab --- /dev/null +++ b/src/ui/settings/settings-accessiblity-ui-handler.ts @@ -0,0 +1,20 @@ +import BattleScene from "../../battle-scene"; +import { Mode } from "../ui"; +"#app/inputs-controller.js"; +import AbstractSettingsUiHandler from "./abstract-settings-ui-handler"; +import { Setting, SettingType } from "#app/system/settings/settings"; + +export default class SettingsAccessibilityUiHandler extends AbstractSettingsUiHandler { + /** + * Creates an instance of SettingsGamepadUiHandler. + * + * @param scene - The BattleScene instance. + * @param mode - The UI mode, optional. + */ + constructor(scene: BattleScene, mode?: Mode) { + super(scene, mode); + this.title = "Accessibility"; + this.settings = Setting.filter(s => s.type === SettingType.ACCESSIBILITY); + this.localStorageKey = "settings"; + } +} diff --git a/src/ui/settings/settings-gamepad-ui-handler.ts b/src/ui/settings/settings-gamepad-ui-handler.ts index 5389d4a1940..20a713012ae 100644 --- a/src/ui/settings/settings-gamepad-ui-handler.ts +++ b/src/ui/settings/settings-gamepad-ui-handler.ts @@ -7,22 +7,22 @@ import { settingGamepadBlackList, settingGamepadDefaults, settingGamepadOptions -} from "../../system/settings-gamepad"; +} from "../../system/settings/settings-gamepad"; import pad_xbox360 from "#app/configs/inputs/pad_xbox360"; import pad_dualshock from "#app/configs/inputs/pad_dualshock"; import pad_unlicensedSNES from "#app/configs/inputs/pad_unlicensedSNES"; import {InterfaceConfig} from "#app/inputs-controller"; -import AbstractSettingsUiUiHandler from "#app/ui/settings/abstract-settings-ui-handler"; +import AbstractControlSettingsUiHandler from "#app/ui/settings/abstract-control-settings-ui-handler.js"; import {Device} from "#app/enums/devices"; import {truncateString} from "#app/utils"; /** * Class representing the settings UI handler for gamepads. * - * @extends AbstractSettingsUiUiHandler + * @extends AbstractControlSettingsUiHandler */ -export default class SettingsGamepadUiHandler extends AbstractSettingsUiUiHandler { +export default class SettingsGamepadUiHandler extends AbstractControlSettingsUiHandler { /** * Creates an instance of SettingsGamepadUiHandler. @@ -33,18 +33,17 @@ export default class SettingsGamepadUiHandler extends AbstractSettingsUiUiHandle constructor(scene: BattleScene, mode?: Mode) { super(scene, mode); this.titleSelected = "Gamepad"; - this.settingDevice = SettingGamepad; + this.setting = SettingGamepad; this.settingDeviceDefaults = settingGamepadDefaults; this.settingDeviceOptions = settingGamepadOptions; this.configs = [pad_xbox360, pad_dualshock, pad_unlicensedSNES]; this.commonSettingsCount = 2; this.localStoragePropertyName = "settingsGamepad"; this.settingBlacklisted = settingGamepadBlackList; + this.device = Device.GAMEPAD; } - setSetting(scene: BattleScene, setting, value: integer): boolean { - return setSettingGamepad(scene, setting, value); - } + setSetting = setSettingGamepad; /** * Setup UI elements. @@ -65,26 +64,6 @@ export default class SettingsGamepadUiHandler extends AbstractSettingsUiUiHandle this.layout["noGamepads"].label = label; } - /** - * Get the active configuration. - * - * @returns The active gamepad configuration. - */ - getActiveConfig(): InterfaceConfig { - return this.scene.inputController.getActiveConfig(Device.GAMEPAD); - } - - /** - * Get the gamepad settings from local storage. - * - * @returns The gamepad settings from local storage. - */ - getLocalStorageSetting(): object { - // Retrieve the gamepad settings from local storage or use an empty object if none exist. - const settings: object = localStorage.hasOwnProperty("settingsGamepad") ? JSON.parse(localStorage.getItem("settingsGamepad")) : {}; - return settings; - } - /** * Set the layout for the active configuration. * @@ -105,27 +84,6 @@ export default class SettingsGamepadUiHandler extends AbstractSettingsUiUiHandle return super.setLayout(activeConfig); } - - /** - * Navigate to the left menu tab. - * - * @returns `true` indicating the navigation was successful. - */ - navigateMenuLeft(): boolean { - this.scene.ui.setMode(Mode.SETTINGS); - return true; - } - - /** - * Navigate to the right menu tab. - * - * @returns `true` indicating the navigation was successful. - */ - navigateMenuRight(): boolean { - this.scene.ui.setMode(Mode.SETTINGS_KEYBOARD); - return true; - } - /** * Update the display of the chosen gamepad. */ @@ -135,11 +93,11 @@ export default class SettingsGamepadUiHandler extends AbstractSettingsUiUiHandle this.resetScroll(); // Iterate over the keys in the settingDevice enumeration. - for (const [index, key] of Object.keys(this.settingDevice).entries()) { - const setting = this.settingDevice[key]; // Get the actual setting value using the key. + for (const [index, key] of Object.keys(this.setting).entries()) { + const setting = this.setting[key]; // Get the actual setting value using the key. // Check if the current setting corresponds to the controller setting. - if (setting === this.settingDevice.Controller) { + if (setting === this.setting.Controller) { // Iterate over all layouts excluding the 'noGamepads' special case. for (const _key of Object.keys(this.layout)) { if (_key === "noGamepads") { @@ -157,12 +115,12 @@ export default class SettingsGamepadUiHandler extends AbstractSettingsUiUiHandle /** * Save the setting to local storage. * - * @param setting - The setting to save. + * @param settingName - The setting to save. * @param cursor - The cursor position to save. */ - saveSettingToLocalStorage(setting, cursor): void { - if (this.settingDevice[setting] !== this.settingDevice.Controller) { - this.scene.gameData.saveGamepadSetting(setting, cursor); + saveSettingToLocalStorage(settingName, cursor): void { + if (this.setting[settingName] !== this.setting.Controller) { + this.scene.gameData.saveControlSetting(this.device, this.localStoragePropertyName, settingName, this.settingDeviceDefaults, cursor); } } } diff --git a/src/ui/settings/settings-keyboard-ui-handler.ts b/src/ui/settings/settings-keyboard-ui-handler.ts index 3a7751c4522..59b409fe990 100644 --- a/src/ui/settings/settings-keyboard-ui-handler.ts +++ b/src/ui/settings/settings-keyboard-ui-handler.ts @@ -7,9 +7,9 @@ import { settingKeyboardBlackList, settingKeyboardDefaults, settingKeyboardOptions -} from "#app/system/settings-keyboard"; +} from "#app/system/settings/settings-keyboard"; import {reverseValueToKeySetting, truncateString} from "#app/utils"; -import AbstractSettingsUiUiHandler from "#app/ui/settings/abstract-settings-ui-handler"; +import AbstractControlSettingsUiHandler from "#app/ui/settings/abstract-control-settings-ui-handler.js"; import {InterfaceConfig} from "#app/inputs-controller"; import {addTextObject, TextStyle} from "#app/ui/text"; import {deleteBind} from "#app/configs/inputs/configHandler"; @@ -19,9 +19,9 @@ import {NavigationManager} from "#app/ui/settings/navigationMenu"; /** * Class representing the settings UI handler for keyboards. * - * @extends AbstractSettingsUiUiHandler + * @extends AbstractControlSettingsUiHandler */ -export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandler { +export default class SettingsKeyboardUiHandler extends AbstractControlSettingsUiHandler { /** * Creates an instance of SettingsKeyboardUiHandler. * @@ -31,7 +31,7 @@ export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandl constructor(scene: BattleScene, mode?: Mode) { super(scene, mode); this.titleSelected = "Keyboard"; - this.settingDevice = SettingKeyboard; + this.setting = SettingKeyboard; this.settingDeviceDefaults = settingKeyboardDefaults; this.settingDeviceOptions = settingKeyboardOptions; this.configs = [cfg_keyboard_qwerty]; @@ -39,6 +39,7 @@ export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandl this.textureOverride = "keyboard"; this.localStoragePropertyName = "settingsKeyboard"; this.settingBlacklisted = settingKeyboardBlackList; + this.device = Device.KEYBOARD; const deleteEvent = scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.DELETE); const restoreDefaultEvent = scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.HOME); @@ -46,9 +47,7 @@ export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandl restoreDefaultEvent.on("up", this.onHomeDown, this); } - setSetting(scene: BattleScene, setting, value: integer): boolean { - return setSettingKeyboard(scene, setting, value); - } + setSetting = setSettingKeyboard; /** * Setup UI elements. @@ -114,26 +113,6 @@ export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandl } } - /** - * Get the active configuration. - * - * @returns The active keyboard configuration. - */ - getActiveConfig(): InterfaceConfig { - return this.scene.inputController.getActiveConfig(Device.KEYBOARD); - } - - /** - * Get the keyboard settings from local storage. - * - * @returns The keyboard settings from local storage. - */ - getLocalStorageSetting(): object { - // Retrieve the gamepad settings from local storage or use an empty object if none exist. - const settings: object = localStorage.hasOwnProperty("settingsKeyboard") ? JSON.parse(localStorage.getItem("settingsKeyboard")) : {}; - return settings; - } - /** * Set the layout for the active configuration. * @@ -154,26 +133,6 @@ export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandl return super.setLayout(activeConfig); } - /** - * Navigate to the left menu tab. - * - * @returns `true` indicating the navigation was successful. - */ - navigateMenuLeft(): boolean { - this.scene.ui.setMode(Mode.SETTINGS_GAMEPAD); - return true; - } - - /** - * Navigate to the right menu tab. - * - * @returns `true` indicating the navigation was successful. - */ - navigateMenuRight(): boolean { - this.scene.ui.setMode(Mode.SETTINGS); - return true; - } - /** * Update the display of the chosen keyboard layout. */ @@ -182,11 +141,11 @@ export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandl this.updateBindings(); // Iterate over the keys in the settingDevice enumeration. - for (const [index, key] of Object.keys(this.settingDevice).entries()) { - const setting = this.settingDevice[key]; // Get the actual setting value using the key. + for (const [index, key] of Object.keys(this.setting).entries()) { + const setting = this.setting[key]; // Get the actual setting value using the key. // Check if the current setting corresponds to the layout setting. - if (setting === this.settingDevice.Default_Layout) { + if (setting === this.setting.Default_Layout) { // Iterate over all layouts excluding the 'noGamepads' special case. for (const _key of Object.keys(this.layout)) { if (_key === "noKeyboard") { @@ -217,8 +176,8 @@ export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandl * @param cursor - The cursor position to save. */ saveSettingToLocalStorage(settingName, cursor): void { - if (this.settingDevice[settingName] !== this.settingDevice.Default_Layout) { - this.scene.gameData.saveKeyboardSetting(settingName, cursor); + if (this.setting[settingName] !== this.setting.Default_Layout) { + this.scene.gameData.saveControlSetting(this.device, this.localStoragePropertyName, settingName, this.settingDeviceDefaults, cursor); } } } diff --git a/src/ui/settings/settings-ui-handler.ts b/src/ui/settings/settings-ui-handler.ts index 44dc90dff88..a6332be1343 100644 --- a/src/ui/settings/settings-ui-handler.ts +++ b/src/ui/settings/settings-ui-handler.ts @@ -1,345 +1,19 @@ import BattleScene from "../../battle-scene"; -import {Setting, reloadSettings, settingDefaults, settingOptions} from "../../system/settings"; -import { hasTouchscreen, isMobile } from "../../touch-controls"; -import { TextStyle, addTextObject } from "../text"; +import {Setting, SettingType} from "../../system/settings/settings"; import { Mode } from "../ui"; -import UiHandler from "../ui-handler"; -import { addWindow } from "../ui-theme"; -import {Button} from "../../enums/buttons"; -import {InputsIcons} from "#app/ui/settings/abstract-settings-ui-handler"; -import NavigationMenu, {NavigationManager} from "#app/ui/settings/navigationMenu"; - -export default class SettingsUiHandler extends UiHandler { - private settingsContainer: Phaser.GameObjects.Container; - private optionsContainer: Phaser.GameObjects.Container; - private navigationContainer: NavigationMenu; - - private scrollCursor: integer; - - private optionsBg: Phaser.GameObjects.NineSlice; - - private optionCursors: integer[]; - - private settingLabels: Phaser.GameObjects.Text[]; - private optionValueLabels: Phaser.GameObjects.Text[][]; - - protected navigationIcons: InputsIcons; - - private cursorObj: Phaser.GameObjects.NineSlice; - - private reloadRequired: boolean; - private reloadI18n: boolean; - private rowsToDisplay: number; +import AbstractSettingsUiHandler from "./abstract-settings-ui-handler"; +export default class SettingsUiHandler extends AbstractSettingsUiHandler { + /** + * Creates an instance of SettingsGamepadUiHandler. + * + * @param scene - The BattleScene instance. + * @param mode - The UI mode, optional. + */ constructor(scene: BattleScene, mode?: Mode) { super(scene, mode); - - this.reloadRequired = false; - this.reloadI18n = false; - this.rowsToDisplay = 8; - } - - setup() { - const ui = this.getUi(); - - this.settingsContainer = this.scene.add.container(1, -(this.scene.game.canvas.height / 6) + 1); - - this.settingsContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, this.scene.game.canvas.width / 6, this.scene.game.canvas.height / 6 - 20), Phaser.Geom.Rectangle.Contains); - - this.navigationIcons = {}; - - this.navigationContainer = new NavigationMenu(this.scene, 0, 0); - - this.optionsBg = addWindow(this.scene, 0, this.navigationContainer.height, (this.scene.game.canvas.width / 6) - 2, (this.scene.game.canvas.height / 6) - 16 - this.navigationContainer.height - 2); - this.optionsBg.setOrigin(0, 0); - - const actionsBg = addWindow(this.scene, 0, (this.scene.game.canvas.height / 6) - this.navigationContainer.height, (this.scene.game.canvas.width / 6) - 2, 22); - actionsBg.setOrigin(0, 0); - - const iconAction = this.scene.add.sprite(0, 0, "keyboard"); - iconAction.setOrigin(0, -0.1); - iconAction.setPositionRelative(actionsBg, this.navigationContainer.width - 32, 4); - this.navigationIcons["BUTTON_ACTION"] = iconAction; - - const actionText = addTextObject(this.scene, 0, 0, "Action", TextStyle.SETTINGS_LABEL); - actionText.setOrigin(0, 0.15); - actionText.setPositionRelative(iconAction, -actionText.width/6-2, 0); - - const iconCancel = this.scene.add.sprite(0, 0, "keyboard"); - iconCancel.setOrigin(0, -0.1); - iconCancel.setPositionRelative(actionsBg, this.navigationContainer.width - 100, 4); - this.navigationIcons["BUTTON_CANCEL"] = iconCancel; - - const cancelText = addTextObject(this.scene, 0, 0, "Cancel", TextStyle.SETTINGS_LABEL); - cancelText.setOrigin(0, 0.15); - cancelText.setPositionRelative(iconCancel, -cancelText.width/6-2, 0); - - this.optionsContainer = this.scene.add.container(0, 0); - - this.settingLabels = []; - this.optionValueLabels = []; - - Object.keys(Setting).forEach((setting, s) => { - let settingName = setting.replace(/\_/g, " "); - if (reloadSettings.includes(Setting[setting])) { - settingName += " (Requires Reload)"; - } - - this.settingLabels[s] = addTextObject(this.scene, 8, 28 + s * 16, settingName, TextStyle.SETTINGS_LABEL); - this.settingLabels[s].setOrigin(0, 0); - - this.optionsContainer.add(this.settingLabels[s]); - - this.optionValueLabels.push(settingOptions[Setting[setting]].map((option, o) => { - const valueLabel = addTextObject(this.scene, 0, 0, option, settingDefaults[Setting[setting]] === o ? TextStyle.SETTINGS_SELECTED : TextStyle.WINDOW); - valueLabel.setOrigin(0, 0); - - this.optionsContainer.add(valueLabel); - - return valueLabel; - })); - - const totalWidth = this.optionValueLabels[s].map(o => o.width).reduce((total, width) => total += width, 0); - - const labelWidth = Math.max(78, this.settingLabels[s].displayWidth + 8); - - const totalSpace = (300 - labelWidth) - totalWidth / 6; - const optionSpacing = Math.floor(totalSpace / (this.optionValueLabels[s].length - 1)); - - let xOffset = 0; - - for (const value of this.optionValueLabels[s]) { - value.setPositionRelative(this.settingLabels[s], labelWidth + xOffset, 0); - xOffset += value.width / 6 + optionSpacing; - } - }); - - this.optionCursors = Object.values(settingDefaults); - - this.settingsContainer.add(this.optionsBg); - this.settingsContainer.add(this.navigationContainer); - this.settingsContainer.add(actionsBg); - this.settingsContainer.add(this.optionsContainer); - this.settingsContainer.add(iconAction); - this.settingsContainer.add(iconCancel); - this.settingsContainer.add(actionText); - this.settingsContainer.add(cancelText); - - ui.add(this.settingsContainer); - - this.setCursor(0); - this.setScrollCursor(0); - - this.settingsContainer.setVisible(false); - } - - updateBindings(): void { - for (const settingName of Object.keys(this.navigationIcons)) { - if (settingName === "BUTTON_HOME") { - this.navigationIcons[settingName].setTexture("keyboard"); - this.navigationIcons[settingName].setFrame("HOME.png"); - this.navigationIcons[settingName].alpha = 1; - continue; - } - const icon = this.scene.inputController?.getIconForLatestInputRecorded(settingName); - if (icon) { - const type = this.scene.inputController?.getLastSourceType(); - this.navigationIcons[settingName].setTexture(type); - this.navigationIcons[settingName].setFrame(icon); - this.navigationIcons[settingName].alpha = 1; - } else { - this.navigationIcons[settingName].alpha = 0; - } - } - NavigationManager.getInstance().updateIcons(); - } - - show(args: any[]): boolean { - super.show(args); - this.updateBindings(); - - const settings: object = localStorage.hasOwnProperty("settings") ? JSON.parse(localStorage.getItem("settings")) : {}; - - Object.keys(settingDefaults).forEach((setting, s) => this.setOptionCursor(s, settings.hasOwnProperty(setting) ? settings[setting] : settingDefaults[setting])); - - this.settingsContainer.setVisible(true); - this.setCursor(0); - - this.getUi().moveTo(this.settingsContainer, this.getUi().length - 1); - - this.getUi().hideTooltip(); - - return true; - } - - /** - * Processes input from a specified button. - * This method handles navigation through a UI menu, including movement through menu items - * and handling special actions like cancellation. Each button press may adjust the cursor - * position or the menu scroll, and plays a sound effect if the action was successful. - * - * @param button - The button pressed by the user. - * @returns `true` if the action associated with the button was successfully processed, `false` otherwise. - */ - processInput(button: Button): boolean { - const ui = this.getUi(); - // Defines the maximum number of rows that can be displayed on the screen. - - let success = false; - - if (button === Button.CANCEL) { - success = true; - NavigationManager.getInstance().reset(); - // Reverts UI to its previous state on cancel. - this.scene.ui.revertMode(); - } else { - const cursor = this.cursor + this.scrollCursor; - switch (button) { - case Button.UP: - if (cursor) { - if (this.cursor) { - success = this.setCursor(this.cursor - 1); - } else { - success = this.setScrollCursor(this.scrollCursor - 1); - } - } else { - // When at the top of the menu and pressing UP, move to the bottommost item. - // First, set the cursor to the last visible element, preparing for the scroll to the end. - const successA = this.setCursor(this.rowsToDisplay - 1); - // Then, adjust the scroll to display the bottommost elements of the menu. - const successB = this.setScrollCursor(this.optionValueLabels.length - this.rowsToDisplay); - success = successA && successB; // success is just there to play the little validation sound effect - } - break; - case Button.DOWN: - if (cursor < this.optionValueLabels.length - 1) { - if (this.cursor < this.rowsToDisplay - 1) {// if the visual cursor is in the frame of 0 to 8 - success = this.setCursor(this.cursor + 1); - } else if (this.scrollCursor < this.optionValueLabels.length - this.rowsToDisplay) { - success = this.setScrollCursor(this.scrollCursor + 1); - } - } else { - // When at the bottom of the menu and pressing DOWN, move to the topmost item. - // First, set the cursor to the first visible element, resetting the scroll to the top. - const successA = this.setCursor(0); - // Then, reset the scroll to start from the first element of the menu. - const successB = this.setScrollCursor(0); - success = successA && successB; // Indicates a successful cursor and scroll adjustment. - } - break; - case Button.LEFT: - if (this.optionCursors[cursor]) {// Moves the option cursor left, if possible. - success = this.setOptionCursor(cursor, this.optionCursors[cursor] - 1, true); - } - break; - case Button.RIGHT: - // Moves the option cursor right, if possible. - if (this.optionCursors[cursor] < this.optionValueLabels[cursor].length - 1) { - success = this.setOptionCursor(cursor, this.optionCursors[cursor] + 1, true); - } - break; - case Button.CYCLE_FORM: - case Button.CYCLE_SHINY: - success = this.navigationContainer.navigate(button); - break; - } - } - - // Plays a select sound effect if an action was successfully processed. - if (success) { - ui.playSelect(); - } - - return success; - } - - setCursor(cursor: integer): boolean { - const ret = super.setCursor(cursor); - - if (!this.cursorObj) { - this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", null, (this.scene.game.canvas.width / 6) - 10, 16, 1, 1, 1, 1); - this.cursorObj.setOrigin(0, 0); - this.optionsContainer.add(this.cursorObj); - } - - this.cursorObj.setPositionRelative(this.optionsBg, 4, 4 + (this.cursor + this.scrollCursor) * 16); - - return ret; - } - - setOptionCursor(settingIndex: integer, cursor: integer, save?: boolean): boolean { - const setting = Setting[Object.keys(Setting)[settingIndex]]; - - if (setting === Setting.Touch_Controls && cursor && hasTouchscreen() && isMobile()) { - this.getUi().playError(); - return false; - } - - const lastCursor = this.optionCursors[settingIndex]; - - const lastValueLabel = this.optionValueLabels[settingIndex][lastCursor]; - lastValueLabel.setColor(this.getTextColor(TextStyle.WINDOW)); - lastValueLabel.setShadowColor(this.getTextColor(TextStyle.WINDOW, true)); - - this.optionCursors[settingIndex] = cursor; - - const newValueLabel = this.optionValueLabels[settingIndex][cursor]; - newValueLabel.setColor(this.getTextColor(TextStyle.SETTINGS_SELECTED)); - newValueLabel.setShadowColor(this.getTextColor(TextStyle.SETTINGS_SELECTED, true)); - - if (save) { - this.scene.gameData.saveSetting(setting, cursor); - if (reloadSettings.includes(setting)) { - this.reloadRequired = true; - if (setting === Setting.Language) { - this.reloadI18n = true; - } - } - } - - return true; - } - - setScrollCursor(scrollCursor: integer): boolean { - if (scrollCursor === this.scrollCursor) { - return false; - } - - this.scrollCursor = scrollCursor; - - this.updateSettingsScroll(); - - this.setCursor(this.cursor); - - return true; - } - - updateSettingsScroll(): void { - this.optionsContainer.setY(-16 * this.scrollCursor); - - for (let s = 0; s < this.settingLabels.length; s++) { - const visible = s >= this.scrollCursor && s < this.scrollCursor + this.rowsToDisplay; - this.settingLabels[s].setVisible(visible); - for (const option of this.optionValueLabels[s]) { - option.setVisible(visible); - } - } - } - - clear() { - super.clear(); - this.settingsContainer.setVisible(false); - this.eraseCursor(); - if (this.reloadRequired) { - this.reloadRequired = false; - this.scene.reset(true, false, true); - } - } - - eraseCursor() { - if (this.cursorObj) { - this.cursorObj.destroy(); - } - this.cursorObj = null; + this.title = "General"; + this.settings = Setting.filter(s => s.type === SettingType.GENERAL); + this.localStorageKey = "settings"; } } diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 90cba657fbf..b2df4d22259 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -42,6 +42,7 @@ import {PlayerGender} from "#app/system/game-data"; import GamepadBindingUiHandler from "./settings/gamepad-binding-ui-handler"; import SettingsKeyboardUiHandler from "#app/ui/settings/settings-keyboard-ui-handler"; import KeyboardBindingUiHandler from "#app/ui/settings/keyboard-binding-ui-handler"; +import SettingsAccessibilityUiHandler from "./settings/settings-accessiblity-ui-handler"; export enum Mode { MESSAGE, @@ -62,6 +63,7 @@ export enum Mode { MENU, MENU_OPTION_SELECT, SETTINGS, + SETTINGS_ACCESSIBILITY, SETTINGS_GAMEPAD, GAMEPAD_BINDING, SETTINGS_KEYBOARD, @@ -99,6 +101,7 @@ const noTransitionModes = [ Mode.GAMEPAD_BINDING, Mode.KEYBOARD_BINDING, Mode.SETTINGS, + Mode.SETTINGS_ACCESSIBILITY, Mode.SETTINGS_GAMEPAD, Mode.SETTINGS_KEYBOARD, Mode.ACHIEVEMENTS, @@ -151,6 +154,7 @@ export default class UI extends Phaser.GameObjects.Container { new MenuUiHandler(scene), new OptionSelectUiHandler(scene, Mode.MENU_OPTION_SELECT), new SettingsUiHandler(scene), + new SettingsAccessibilityUiHandler(scene), new SettingsGamepadUiHandler(scene), new GamepadBindingUiHandler(scene), new SettingsKeyboardUiHandler(scene),