diff --git a/src/configs/keyboard.ts b/src/configs/cfg_keyboard_azerty.ts similarity index 60% rename from src/configs/keyboard.ts rename to src/configs/cfg_keyboard_azerty.ts index d78064d93..77dc8b02a 100644 --- a/src/configs/keyboard.ts +++ b/src/configs/cfg_keyboard_azerty.ts @@ -1,12 +1,9 @@ -import {SettingGamepad} from "../system/settings-gamepad"; -import {Button} from "../enums/buttons"; +import {Button} from "#app/enums/buttons"; +import {SettingKeyboard} from "#app/system/settings-keyboard"; -/** - * 081f-e401 - UnlicensedSNES - */ -const pad_unlicensedSNES = { +const cfg_keyboard_azerty = { padID: 'keyboard', - padType: 'keyboard', + padType: 'azerty', gamepadMapping: { KEY_A: 0, KEY_B: 0, @@ -80,7 +77,8 @@ const pad_unlicensedSNES = { KEY_LEFT_BRACKET: 0, KEY_RIGHT_BRACKET: 0, KEY_SEMICOLON: 0, - KEY_ASTERISK: 0 + KEY_ASTERISK: 0, + T_Backspace_Alt_Key_Dark: 0 }, icons: { KEY_A: "T_A_Key_Dark.png", @@ -138,23 +136,23 @@ const pad_unlicensedSNES = { KEY_PAGE_DOWN: "T_PageDown_Key_Dark.png", KEY_PAGE_UP: "T_PageUp_Key_Dark.png", - KEY_CTRL: "T_CTRL_Key_Dark.png", - KEY_DEL: "T_DEL_Key_Dark.png", - KEY_END: "T_END_Key_Dark.png", - KEY_ENTER: "T_ENTER_Key_Dark.png", - KEY_ESC: "T_ESC_Key_Dark.png", - KEY_HOME: "T_HOME_Key_Dark.png", - KEY_INSERT: "T_INSERT_Key_Dark.png", + KEY_CTRL: "T_Crtl_Key_Dark.png", + KEY_DEL: "T_Del_Key_Dark.png", + KEY_END: "T_End_Key_Dark.png", + KEY_ENTER: "T_Enter_Alt_Key_Dark.png", + KEY_ESC: "T_Esc_Key_Dark.png", + KEY_HOME: "T_Home_Key_Dark.png", + KEY_INSERT: "T_Insert_Key_Dark.png", - KEY_PLUS: "T_PLUS_Key_Dark.png", - KEY_MINUS: "T_MINUS_Key_Dark.png", - KEY_QUOTATION: "T_QUOTATION_Key_Dark.png", - KEY_SHIFT: "T_SHIFT_Key_Dark.png", + KEY_PLUS: "T_Plus_Tall_Key_Dark.png", + KEY_MINUS: "T_Minus_Key_Dark.png", + KEY_QUOTATION: "T_Quotation_Key_Dark.png", + KEY_SHIFT: "T_Shift_Key_Dark.png", KEY_LEFT_SHIFT: "T_Shift_Super_Wide_Key_Dark.png", - KEY_SPACE: "T_SPACE_Key_Dark.png", - KEY_TAB: "T_TAB_Key_Dark.png", - KEY_TILDE: "T_TILDE_Key_Dark.png", + KEY_SPACE: "T_Space_Key_Dark.png", + KEY_TAB: "T_Tab_Key_Dark.png", + KEY_TILDE: "T_Tilde_Key_Dark.png", KEY_ARROW_UP: "T_Up_Key_Dark.png", KEY_ARROW_DOWN: "T_Down_Key_Dark.png", @@ -165,12 +163,48 @@ const pad_unlicensedSNES = { KEY_RIGHT_BRACKET: "T_Brackets_R_Key_Dark.png", KEY_SEMICOLON: "T_Semicolon_Key_Dark.png", - KEY_ASTERISK: "T_Asterisk_Key_Dark.png" + KEY_ASTERISK: "T_Asterisk_Key_Dark.png", + + KEY_BACKSPACE: "T_Backspace_Alt_Key_Dark.png" }, setting: { + KEY_ARROW_UP: SettingKeyboard.Button_Up, + KEY_ARROW_DOWN: SettingKeyboard.Button_Down, + KEY_ARROW_LEFT: SettingKeyboard.Button_Left, + KEY_ARROW_RIGHT: SettingKeyboard.Button_Right, + KEY_ENTER: SettingKeyboard.Button_Submit, + KEY_SPACE: SettingKeyboard.Button_Action, + KEY_BACKSPACE: SettingKeyboard.Button_Cancel, + KEY_ESC: SettingKeyboard.Button_Menu, + KEY_C: SettingKeyboard.Button_Stats, + KEY_R: SettingKeyboard.Button_Cycle_Shiny, + KEY_F: SettingKeyboard.Button_Cycle_Form, + KEY_G: SettingKeyboard.Button_Cycle_Gender, + KEY_E: SettingKeyboard.Button_Cycle_Ability, + KEY_N: SettingKeyboard.Button_Cycle_Nature, + KEY_V: SettingKeyboard.Button_Cycle_Variant, + KEY_PLUS: SettingKeyboard.Button_Speed_Up, + KEY_MINUS: SettingKeyboard.Button_Slow_Down, }, default: { + KEY_ARROW_UP: Button.UP, + KEY_ARROW_DOWN: Button.DOWN, + KEY_ARROW_LEFT: Button.LEFT, + KEY_ARROW_RIGHT: Button.RIGHT, + KEY_ENTER: Button.SUBMIT, + KEY_SPACE: Button.ACTION, + KEY_BACKSPACE: Button.CANCEL, + KEY_ESC: Button.MENU, + KEY_C: Button.STATS, + KEY_R: Button.CYCLE_SHINY, + KEY_F: Button.CYCLE_FORM, + KEY_G: Button.CYCLE_GENDER, + KEY_E: Button.CYCLE_ABILITY, + KEY_N: Button.CYCLE_NATURE, + KEY_V: Button.CYCLE_VARIANT, + KEY_PLUS: Button.SPEED_UP, + KEY_MINUS: Button.SLOW_DOWN, } }; -export default pad_unlicensedSNES; +export default cfg_keyboard_azerty; diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 0ea1ee5f3..3aa61443d 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -12,6 +12,8 @@ import { getCurrenlyAssignedIconFromInputIndex, getCurrentlyAssignedIconToSettingName, getKeyFromInputIndex, getCurrentlyAssignedToSettingName } from "./configs/gamepad-utils"; +import SettingsKeyboardUiHandler from "#app/ui/settings/settings-keyboard-ui-handler"; +import cfg_keyboard_azerty from "#app/configs/cfg_keyboard_azerty"; export interface GamepadMapping { [key: string]: number; @@ -74,10 +76,12 @@ export class InputsController { private interactions: Map> = new Map(); private time: Phaser.Time.Clock; private configs: Map = new Map(); + private keyboardConfigs: Map = new Map(); private gamepadSupport: boolean = true; public chosenGamepad: String; + public chosenKeyboard: string = "azerty"; private disconnectedGamepads: Array = new Array(); private pauseUpdate: boolean = false; @@ -154,8 +158,9 @@ export class InputsController { this.scene.input.gamepad.on('down', this.gamepadButtonDown, this); this.scene.input.gamepad.on('up', this.gamepadButtonUp, this); + this.scene.input.keyboard.on('keydown', this.keyboardKeyDown, this); + this.scene.input.keyboard.on('keyup', this.keyboardKeyUp, this); } - // Keyboard this.setupKeyboardControls(); } @@ -198,6 +203,10 @@ export class InputsController { this.deactivatePressedKey(); this.initChosenGamepad(gamepad) } + setChosenKeyboardLayout(layoutKeyboard: String): void { + this.deactivatePressedKey(); + this.initChosenLayoutKeyboard(layoutKeyboard) + } /** * Updates the interaction handling by processing input states. @@ -261,6 +270,17 @@ export class InputsController { handler && handler.updateChosenGamepadDisplay() } + initChosenLayoutKeyboard(layoutKeyboard?: String): void { + let name = layoutKeyboard; + if (layoutKeyboard) + this.chosenKeyboard = layoutKeyboard; + else + name = this.chosenKeyboard; + localStorage.setItem('chosenKeyboardLayout', name); + const handler = this.scene.ui?.handlers[Mode.SETTINGS_KEYBOARD] as SettingsKeyboardUiHandler; + handler && handler.updateChosenKeyboardDisplay() + } + /** * Handles the disconnection of a gamepad by adding its identifier to a list of disconnected gamepads. * This is necessary because Phaser retains memory of previously connected gamepads, and without tracking @@ -320,6 +340,16 @@ export class InputsController { if (this.chosenGamepad === thisGamepad.id) this.initChosenGamepad(this.chosenGamepad) } + setupKeyboard(): void { + for (const layout of ['azerty']) { + const config = this.getConfigKeyboard(layout); + config.custom = this.keyboardConfigs[layout]?.custom || {...config.default}; + this.keyboardConfigs[layout] = config; + this.scene.gameData?.saveCustomKeyboardMapping(this.chosenKeyboard, this.keyboardConfigs[layout]?.custom); + } + this.initChosenLayoutKeyboard(this.chosenKeyboard) + } + /** * Refreshes and re-indexes the list of connected gamepads. * @@ -339,6 +369,17 @@ export class InputsController { } } + keyboardKeyDown(event): void { + const keyDown = event.key; + const keyCode = event.keyCode + if (!this.keyboardConfigs[this.chosenKeyboard]?.padID) + this.setupKeyboard(); + } + + keyboardKeyUp(event): void { + + } + /** * Handles button press events on a gamepad. This method sets the gamepad as chosen on the first input if no gamepad is currently chosen. * It checks if gamepad support is enabled and if the event comes from the chosen gamepad. If so, it maps the button press to a specific @@ -504,6 +545,13 @@ export class InputsController { return pad_generic; } + getConfigKeyboard(id: string): GamepadConfig { + if (id === 'azerty') + return cfg_keyboard_azerty; + + return cfg_keyboard_azerty; + } + /** * repeatInputDurationJustPassed returns true if @param button has been held down long * enough to fire a repeated input. A button must claim the buttonLock before @@ -655,6 +703,17 @@ export class InputsController { return null; } + /** + * Retrieves the active configuration for the currently chosen gamepad. + * It checks if a specific gamepad ID is stored under the chosen gamepad's configurations and returns it. + * + * @returns GamepadConfig The configuration object for the active gamepad, or null if not set. + */ + getActiveKeyboardConfig(): GamepadConfig | null { + if (this.keyboardConfigs[this.chosenKeyboard]?.padID) return this.keyboardConfigs[this.chosenKeyboard] + return null; + } + /** * Determines icon for a button pressed on the currently chosen gamepad based on its configuration. * diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 524a750dd..9e1c97ae7 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -31,6 +31,7 @@ import { OutdatedPhase, ReloadSessionPhase } from "#app/phases"; import { Variant, variantData } from "#app/data/variant"; import {setSettingGamepad, SettingGamepad, settingGamepadDefaults} from "./settings-gamepad"; import {MappingLayout} from "#app/inputs-controller"; +import {setSettingKeyboard, SettingKeyboard, settingKeyboardDefaults} from "#app/system/settings-keyboard"; const saveKey = 'x0i2O7WRiANTqPmZ'; // Temporary; secure encryption is not yet necessary @@ -500,6 +501,15 @@ export class GameData { return true; } + public saveCustomKeyboardMapping(keyboardLayout: string, mapping: MappingLayout): boolean { + let customKeyboardMappings: object = {}; + if (localStorage.hasOwnProperty('customKeyboardMappings')) + customKeyboardMappings = JSON.parse(localStorage.getItem('customKeyboardMappings')); + customKeyboardMappings[keyboardLayout] = mapping; + localStorage.setItem('customKeyboardMappings', JSON.stringify(customKeyboardMappings)); + return true; + } + public loadCustomMapping(): boolean { console.log('loadCustomMapping'); if (!localStorage.hasOwnProperty('customMapping')) @@ -524,6 +534,20 @@ export class GameData { return true; } + public saveKeyboardSetting(setting: SettingKeyboard, valueIndex: integer): boolean { + let settingsKeyboard: object = {}; + if (localStorage.hasOwnProperty('settingsKeyboard')) + settingsKeyboard = JSON.parse(localStorage.getItem('settingsKeyboard')); + + setSettingKeyboard(this.scene, setting as SettingKeyboard, valueIndex); + Object.keys(settingKeyboardDefaults).forEach(s => { + if (s === setting) + settingsKeyboard[s] = valueIndex; + }); + localStorage.setItem('settingsKeyboard', JSON.stringify(settingsKeyboard)); + return true; + } + private loadSettings(): boolean { Object.values(Setting).map(setting => setting as Setting).forEach(setting => setSetting(this.scene, setting, settingDefaults[setting])); diff --git a/src/system/settings-keyboard.ts b/src/system/settings-keyboard.ts index de9406d01..167c170b8 100644 --- a/src/system/settings-keyboard.ts +++ b/src/system/settings-keyboard.ts @@ -1,10 +1,15 @@ import {SettingDefaults, SettingOptions} from "#app/system/settings"; import {Button} from "#app/enums/buttons"; import BattleScene from "#app/battle-scene"; -import {SettingGamepad} from "#app/system/settings-gamepad"; import {Mode} from "#app/ui/ui"; +import SettingsKeyboardUiHandler from "#app/ui/settings/settings-keyboard-ui-handler"; export enum SettingKeyboard { + Default_Layout = "DEFAULT_LAYOUT", + Button_Up = "BUTTON_UP", + Button_Down = "BUTTON_DOWN", + Button_Left = "BUTTON_LEFT", + Button_Right = "BUTTON_RIGHT", Button_Action = "BUTTON_ACTION", Button_Cancel = "BUTTON_CANCEL", Button_Menu = "BUTTON_MENU", @@ -21,6 +26,11 @@ export enum SettingKeyboard { } export const settingKeyboardOptions: SettingOptions = { + [SettingKeyboard.Default_Layout]: ['Default', 'Change'], + [SettingKeyboard.Button_Up]: [`KEY ${Button.UP.toString()}`, 'Change'], + [SettingKeyboard.Button_Down]: [`KEY ${Button.DOWN.toString()}`, 'Change'], + [SettingKeyboard.Button_Left]: [`KEY ${Button.LEFT.toString()}`, 'Change'], + [SettingKeyboard.Button_Right]: [`KEY ${Button.RIGHT.toString()}`, 'Change'], [SettingKeyboard.Button_Action]: [`KEY ${Button.ACTION.toString()}`, 'Change'], [SettingKeyboard.Button_Cancel]: [`KEY ${Button.CANCEL.toString()}`, 'Change'], [SettingKeyboard.Button_Menu]: [`KEY ${Button.MENU.toString()}`, 'Change'], @@ -37,6 +47,11 @@ export const settingKeyboardOptions: SettingOptions = { }; export const settingKeyboardDefaults: SettingDefaults = { + [SettingKeyboard.Default_Layout]: 0, + [SettingKeyboard.Button_Up]: 0, + [SettingKeyboard.Button_Down]: 0, + [SettingKeyboard.Button_Left]: 0, + [SettingKeyboard.Button_Right]: 0, [SettingKeyboard.Button_Action]: 0, [SettingKeyboard.Button_Cancel]: 0, [SettingKeyboard.Button_Menu]: 0, @@ -55,6 +70,10 @@ export const settingKeyboardDefaults: SettingDefaults = { export function setSettingKeyboard(scene: BattleScene, setting: SettingKeyboard, value: integer): boolean { switch (setting) { + case SettingKeyboard.Button_Up: + case SettingKeyboard.Button_Down: + case SettingKeyboard.Button_Left: + case SettingKeyboard.Button_Right: case SettingKeyboard.Button_Action: case SettingKeyboard.Button_Cancel: case SettingKeyboard.Button_Menu: @@ -81,6 +100,30 @@ export function setSettingKeyboard(scene: BattleScene, setting: SettingKeyboard, } } break; + case SettingKeyboard.Default_Layout: + if (value && scene.ui) { + const cancelHandler = () => { + scene.ui.revertMode(); + (scene.ui.getHandler() as SettingsKeyboardUiHandler).setOptionCursor(Object.values(SettingKeyboard).indexOf(SettingKeyboard.Default_Layout), 0, true); + (scene.ui.getHandler() as SettingsKeyboardUiHandler).updateBindings(); + return false; + }; + const changeKeyboardHandler = (keyboardLayout: string) => { + scene.inputController.setChosenKeyboardLayout(keyboardLayout); + cancelHandler(); + return true; + }; + scene.ui.setOverlayMode(Mode.OPTION_SELECT, { + options: [{ + label: 'azerty', + handler: changeKeyboardHandler, + }, { + label: 'qwerty', + handler: changeKeyboardHandler, + }] + }); + return false; + } } return true; diff --git a/src/ui/settings/settings-keyboard-ui-handler.ts b/src/ui/settings/settings-keyboard-ui-handler.ts index 88bff862c..eb6a0a374 100644 --- a/src/ui/settings/settings-keyboard-ui-handler.ts +++ b/src/ui/settings/settings-keyboard-ui-handler.ts @@ -2,26 +2,239 @@ import UiHandler from "../ui-handler"; import BattleScene from "../../battle-scene"; import {Mode} from "../ui"; import {Button} from "../../enums/buttons"; +import {addWindow} from "#app/ui/ui-theme"; +import {addTextObject, TextStyle} from "#app/ui/text"; +import {InputsIcons, LayoutConfig} from "#app/ui/settings/settings-gamepad-ui-handler"; +import cfg_keyboard_azerty from "#app/configs/cfg_keyboard_azerty"; +import {SettingKeyboard, settingKeyboardDefaults, settingKeyboardOptions} from "#app/system/settings-keyboard"; +import {getCurrentlyAssignedIconToSettingName, getKeyForSettingName} from "#app/configs/gamepad-utils"; +import {GamepadConfig} from "#app/inputs-controller"; +import {truncateString} from "#app/utils"; export default class SettingsKeyboardUiHandler extends UiHandler { + private settingsContainer: Phaser.GameObjects.Container; + private optionsContainer: Phaser.GameObjects.Container; + + private scrollCursor: integer; + private optionCursors: integer[]; + private cursorObj: Phaser.GameObjects.NineSlice; + + private optionsBg: Phaser.GameObjects.NineSlice; + + private settingLabels: Phaser.GameObjects.Text[]; + private optionValueLabels: Phaser.GameObjects.Text[][]; + + // layout will contain the 3 Gamepad tab for each config - dualshock, xbox, snes + private layout: Map = new Map(); + // Will contain the input icons from the selected layout + private inputsIcons: InputsIcons; + // list all the setting keys used in the selected layout (because dualshock has more buttons than xbox) + private keys: Array; + + // Store the specific settings related to key bindings for the current gamepad configuration. + private bindingSettings: Array; + constructor(scene: BattleScene, mode?: Mode) { super(scene, mode); } 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), Phaser.Geom.Rectangle.Contains); + + const headerBg = addWindow(this.scene, 0, 0, (this.scene.game.canvas.width / 6) - 2, 24); + headerBg.setOrigin(0, 0); + + const headerText = addTextObject(this.scene, 0, 0, 'General', TextStyle.SETTINGS_LABEL); + headerText.setOrigin(0, 0); + headerText.setPositionRelative(headerBg, 8, 4); + + const gamepadText = addTextObject(this.scene, 0, 0, 'Gamepad', TextStyle.SETTINGS_LABEL); + gamepadText.setOrigin(0, 0); + gamepadText.setPositionRelative(headerBg, 50, 4); + + const keyboardText = addTextObject(this.scene, 0, 0, 'Keyboard', TextStyle.SETTINGS_SELECTED); + keyboardText.setOrigin(0, 0); + keyboardText.setPositionRelative(headerBg, 97, 4); + + this.optionsBg = addWindow(this.scene, 0, headerBg.height, (this.scene.game.canvas.width / 6) - 2, (this.scene.game.canvas.height / 6) - headerBg.height - 2); + this.optionsBg.setOrigin(0, 0); + + this.settingsContainer.add(headerBg); + this.settingsContainer.add(headerText); + this.settingsContainer.add(gamepadText); + this.settingsContainer.add(keyboardText); + this.settingsContainer.add(this.optionsBg); + for (const config of [cfg_keyboard_azerty]) { + // 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 gamepad binding settings from the configuration. + const bindingSettings = Object.keys(config.setting).map(k => config.setting[k]); + + // Array to hold labels for different settings such as 'Default Controller', 'Gamepad Support', etc. + const settingLabels: Phaser.GameObjects.Text[] = []; + + // Array to hold options for each setting, e.g., 'Auto', 'Disabled'. + const optionValueLabels: Phaser.GameObjects.Text[][] = []; + + // Object to store sprites for each button configuration. + const inputsIcons: InputsIcons = {}; + + // Fetch common setting keys such as 'Default Controller' and 'Gamepad Support' from gamepad settings. + const commonSettingKeys = Object.keys(SettingKeyboard).slice(0, 1).map(key => SettingKeyboard[key]); + // Combine common and specific bindings into a single array. + const specificBindingKeys = [...commonSettingKeys, ...Object.keys(config.setting).map(k => config.setting[k])]; + // Fetch default values for these settings and prepare to highlight selected options. + const optionCursors = Object.values(Object.keys(settingKeyboardDefaults).filter(s => specificBindingKeys.includes(s)).map(k => settingKeyboardDefaults[k])); + // Filter out settings that are not relevant to the current gamepad configuration. + const SettingKeyboardFiltered = Object.keys(SettingKeyboard).filter(_key => specificBindingKeys.includes(SettingKeyboard[_key])); + // Loop through the filtered settings to manage display and options. + + SettingKeyboardFiltered.forEach((setting, s) => { + // Convert the setting key from format 'Key_Name' to 'Key name' for display. + let settingName = setting.replace(/\_/g, ' '); + + // Create and add a text object for the setting name to the scene. + settingLabels[s] = addTextObject(this.scene, 8, 28 + s * 16, settingName, TextStyle.SETTINGS_LABEL); + settingLabels[s].setOrigin(0, 0); + optionsContainer.add(settingLabels[s]); + + // Initialize an array to store the option labels for this setting. + const valueLabels: Phaser.GameObjects.Text[] = [] + + // Process each option for the current setting. + for (const [o, option] of settingKeyboardOptions[SettingKeyboard[setting]].entries()) { + // Check if the current setting is for binding keys. + if (bindingSettings.includes(SettingKeyboard[setting])) { + // Create a label for non-null options, typically indicating actionable options like 'change'. + if (o) { + const valueLabel = addTextObject(this.scene, 0, 0, option, TextStyle.WINDOW); + valueLabel.setOrigin(0, 0); + optionsContainer.add(valueLabel); + valueLabels.push(valueLabel); + continue; + } + // For null options, add an icon for the key. + const key = getKeyForSettingName(config as GamepadConfig, SettingKeyboard[setting]); + const icon = this.scene.add.sprite(0, 0, 'keyboard'); + icon.setScale(0.1); + icon.setOrigin(0, -0.1); + inputsIcons[key] = 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, settingKeyboardDefaults[SettingKeyboard[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.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(78, 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 (let value of optionValueLabels[s]) { + // Set the option label's position right next to the setting label, adjusted by xOffset + value.setPositionRelative(settingLabels[s], labelWidth + xOffset, 0); + // Move the xOffset to the right for the next label, ensuring each label is spaced evenly + 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. + + // 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); + } 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.scene.inputController.getActiveKeyboardConfig(); + // 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 = localStorage.hasOwnProperty('settingsKeyboard') ? JSON.parse(localStorage.getItem('settingsKeyboard')) : {}; + + // 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) ? settings[key] : 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 key = getKeyForSettingName(activeConfig, elm); // Get the key for the setting name. + const icon = getCurrentlyAssignedIconToSettingName(activeConfig, elm); // Fetch the currently assigned icon for the setting. + this.inputsIcons[key].setFrame(icon); // Set the icon frame to the inputs icon object. + } + + // Set the cursor and scroll cursor to their initial positions. + this.setCursor(0); + this.setScrollCursor(0); } show(args: any[]): boolean { super.show(args); + 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.setScrollCursor(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); + this.getUi().moveTo(this.settingsContainer, this.getUi().length - 1); // Hide any tooltips that might be visible before showing the settings container. this.getUi().hideTooltip(); @@ -30,48 +243,217 @@ export default class SettingsKeyboardUiHandler extends UiHandler { return true; } + setLayout(activeConfig: GamepadConfig): boolean { + // Extract the type of the gamepad from the active configuration. + const configType = activeConfig.padType; + + // If a cursor object exists, destroy it to clean up previous UI states. + this.cursorObj?.destroy(); + // Reset the cursor object and scroll cursor to ensure they are re-initialized correctly. + this.cursorObj = null; + this.scrollCursor = null; + + // 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; + } + processInput(button: Button): boolean { const ui = this.getUi(); - return false; + // Defines the maximum number of rows that can be displayed on the screen. + const rowsToDisplay = 9; + + let success = false; + + // Handle the input based on the button pressed. + if (button === Button.CANCEL) { + // Handle cancel button press, reverting UI mode to previous state. + success = true; + this.scene.ui.revertMode(); + } else { + const cursor = this.cursor + this.scrollCursor; // Calculate the absolute cursor position. + switch (button) { + case Button.UP: // Move up in the menu. + 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(rowsToDisplay - 1); + // Then, adjust the scroll to display the bottommost elements of the menu. + const successB = this.setScrollCursor(this.optionValueLabels.length - 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 (cursor < this.optionValueLabels.length - 1) { + if (this.cursor < rowsToDisplay - 1) + success = this.setCursor(this.cursor + 1); + else if (this.scrollCursor < this.optionValueLabels.length - 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) return; + 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) return; + if (this.optionCursors[cursor] < this.optionValueLabels[cursor].length - 1) + success = this.setOptionCursor(cursor, this.optionCursors[cursor] + 1, true); + break; + case Button.CYCLE_FORM: // Change the UI mode to SETTINGS mode. + this.scene.ui.setMode(Mode.SETTINGS_GAMEPAD) + success = true; + break; + case Button.CYCLE_SHINY: + this.scene.ui.setMode(Mode.SETTINGS) + success = true; + break; + } + } + + // If a change occurred, play the selection sound. + if (success) + ui.playSelect(); + + return success; // Return whether the input resulted in a successful action. } setCursor(cursor: integer): boolean { const ret = super.setCursor(cursor); - return ret; + // 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. + } + + updateChosenKeyboardDisplay(): void { + // Update any bindings that might have changed since the last update. + this.updateBindings(); + + // Iterate over the keys in the SettingKeyboard enumeration. + for (const [index, key] of Object.keys(SettingKeyboard).entries()) { + const setting = SettingKeyboard[key] // Get the actual setting value using the key. + + // Check if the current setting corresponds to the default controller setting. + if (setting === SettingKeyboard.Default_Layout) { + // Iterate over all layouts excluding the 'noGamepads' special case. + for (const _key of Object.keys(this.layout)) { + // Update the text of the first option label under the current setting to the name of the chosen gamepad, + // truncating the name to 30 characters if necessary. + this.layout[_key].optionValueLabels[index][0].setText(truncateString(this.scene.inputController.chosenKeyboard, 30)); + } + } + } + } setOptionCursor(settingIndex: integer, cursor: integer, save?: boolean): boolean { - return true; + // Retrieve the specific setting using the settingIndex from the SettingKeyboard enumeration. + const setting = SettingKeyboard[Object.keys(SettingKeyboard)[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 and the setting is not the default controller setting, save the setting to local storage + if (save) { + if (SettingKeyboard[setting] !== SettingKeyboard.Default_Layout) + this.scene.gameData.saveKeyboardSetting(setting, cursor) + } + + return true; // Return true to indicate the cursor was successfully updated. } setScrollCursor(scrollCursor: integer): boolean { - return true; + // 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. } - // 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 + 9; - // - // // Set the visibility of the setting label and its corresponding options. - // this.settingLabels[s].setVisible(visible); - // for (let option of this.optionValueLabels[s]) - // option.setVisible(visible); - // } - // } + 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 + 9; + + // Set the visibility of the setting label and its corresponding options. + this.settingLabels[s].setVisible(visible); + for (let option of this.optionValueLabels[s]) + option.setVisible(visible); + } + } clear(): void { super.clear(); // Hide the settings container to remove it from the view. - // this.settingsContainer.setVisible(false); + this.settingsContainer.setVisible(false); // Remove the cursor from the UI. this.eraseCursor(); @@ -79,11 +461,11 @@ export default class SettingsKeyboardUiHandler extends UiHandler { 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; + 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; } } \ No newline at end of file