From 22c81f1a801900caaf68d3e761eaf4ef8f52657d Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Wed, 8 May 2024 16:04:33 +0200 Subject: [PATCH 01/81] renamed buttons to LB & RB + added whitelist for these buttons to specific handler --- src/enums/buttons.ts | 4 ++-- src/inputs-controller.ts | 8 ++++---- src/ui-inputs.ts | 8 +++++--- src/ui/settings-ui-handler.ts | 6 ++++++ src/ui/starter-select-ui-handler.ts | 4 ++-- 5 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/enums/buttons.ts b/src/enums/buttons.ts index 034c5a2af..f4e92b2ae 100644 --- a/src/enums/buttons.ts +++ b/src/enums/buttons.ts @@ -8,8 +8,8 @@ export enum Button { CANCEL, MENU, STATS, - CYCLE_SHINY, - CYCLE_FORM, + RB, // CYCLE_SHINY + LB, // CYCLE_FORM CYCLE_GENDER, CYCLE_ABILITY, CYCLE_NATURE, diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 5f5615358..b1656dbfa 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -130,8 +130,8 @@ export class InputsController { gamepadMapping[this.player.mapping.RC_E] = this.scene.abSwapped ? Button.ACTION : Button.CANCEL; gamepadMapping[this.player.mapping.SELECT] = Button.STATS; gamepadMapping[this.player.mapping.START] = Button.MENU; - gamepadMapping[this.player.mapping.RB] = Button.CYCLE_SHINY; - gamepadMapping[this.player.mapping.LB] = Button.CYCLE_FORM; + gamepadMapping[this.player.mapping.RB] = Button.RB; + gamepadMapping[this.player.mapping.LB] = Button.LB; gamepadMapping[this.player.mapping.LT] = Button.CYCLE_GENDER; gamepadMapping[this.player.mapping.RT] = Button.CYCLE_ABILITY; gamepadMapping[this.player.mapping.RC_W] = Button.CYCLE_NATURE; @@ -180,8 +180,8 @@ export class InputsController { [Button.CANCEL]: [keyCodes.BACKSPACE, keyCodes.X], [Button.MENU]: [keyCodes.ESC, keyCodes.M], [Button.STATS]: [keyCodes.SHIFT, keyCodes.C], - [Button.CYCLE_SHINY]: [keyCodes.R], - [Button.CYCLE_FORM]: [keyCodes.F], + [Button.RB]: [keyCodes.R], + [Button.LB]: [keyCodes.F], [Button.CYCLE_GENDER]: [keyCodes.G], [Button.CYCLE_ABILITY]: [keyCodes.E], [Button.CYCLE_NATURE]: [keyCodes.N], diff --git a/src/ui-inputs.ts b/src/ui-inputs.ts index 38d8e7830..b304880d0 100644 --- a/src/ui-inputs.ts +++ b/src/ui-inputs.ts @@ -57,8 +57,8 @@ export class UiInputs { actions[Button.CANCEL] = () => this.buttonAb(Button.CANCEL); actions[Button.MENU] = () => this.buttonMenu(); actions[Button.STATS] = () => this.buttonStats(true); - actions[Button.CYCLE_SHINY] = () => this.buttonCycleOption(Button.CYCLE_SHINY); - actions[Button.CYCLE_FORM] = () => this.buttonCycleOption(Button.CYCLE_FORM); + actions[Button.RB] = () => this.buttonCycleOption(Button.RB); + actions[Button.LB] = () => this.buttonCycleOption(Button.LB); actions[Button.CYCLE_GENDER] = () => this.buttonCycleOption(Button.CYCLE_GENDER); actions[Button.CYCLE_ABILITY] = () => this.buttonCycleOption(Button.CYCLE_ABILITY); actions[Button.CYCLE_NATURE] = () => this.buttonCycleOption(Button.CYCLE_NATURE); @@ -130,7 +130,9 @@ export class UiInputs { } buttonCycleOption(button: Button): void { - if (this.scene.ui?.getHandler() instanceof StarterSelectUiHandler) { + const whitelist = [StarterSelectUiHandler, SettingsUiHandler]; + const uiHandler = this.scene.ui?.getHandler(); + if (whitelist.some(handler => uiHandler instanceof handler)) { this.scene.ui.processInput(button); } } diff --git a/src/ui/settings-ui-handler.ts b/src/ui/settings-ui-handler.ts index 3ed83268e..c769068d8 100644 --- a/src/ui/settings-ui-handler.ts +++ b/src/ui/settings-ui-handler.ts @@ -155,6 +155,12 @@ export default class SettingsUiHandler extends UiHandler { if (this.optionCursors[cursor] < this.optionValueLabels[cursor].length - 1) success = this.setOptionCursor(cursor, this.optionCursors[cursor] + 1, true); break; + case Button.LB: + console.log('lb'); + break; + case Button.RB: + console.log('rb'); + break; } } diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index 0de2ba9ac..ca3815844 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -969,7 +969,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { const row = Math.floor(this.cursor / 9); const props = this.scene.gameData.getSpeciesDexAttrProps(this.lastSpecies, this.dexAttrCursor); switch (button) { - case Button.CYCLE_SHINY: + case Button.RB: if (this.canCycleShiny) { this.setSpeciesDetails(this.lastSpecies, !props.shiny, undefined, undefined, props.shiny ? 0 : undefined, undefined, undefined); if (this.dexAttrCursor & DexAttr.SHINY) @@ -978,7 +978,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { success = true; } break; - case Button.CYCLE_FORM: + case Button.LB: if (this.canCycleForm) { const formCount = this.lastSpecies.forms.length; let newFormIndex = props.formIndex; From 7a75fc925a0ad16139d1fc6ea3ac417b6849c7aa Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Wed, 8 May 2024 16:40:41 +0200 Subject: [PATCH 02/81] added gamepad section in Game Settings and related option into it --- src/system/game-data.ts | 18 ++ src/system/settings-gamepad.ts | 30 +++ src/system/settings.ts | 12 -- src/ui-inputs.ts | 3 +- src/ui/settings-gamepad-ui-handler.ts | 252 ++++++++++++++++++++++++++ src/ui/settings-ui-handler.ts | 13 +- src/ui/ui.ts | 3 + 7 files changed, 315 insertions(+), 16 deletions(-) create mode 100644 src/system/settings-gamepad.ts create mode 100644 src/ui/settings-gamepad-ui-handler.ts diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 60fe7ac8e..418965b06 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -29,6 +29,7 @@ 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 "./system/settings-gamepad"; const saveKey = 'x0i2O7WRiANTqPmZ'; // Temporary; secure encryption is not yet necessary @@ -464,31 +465,48 @@ export class GameData { public saveSetting(setting: Setting, valueIndex: integer): boolean { let settings: object = {}; + let settingsGamepad: object = {}; if (localStorage.hasOwnProperty('settings')) settings = JSON.parse(localStorage.getItem('settings')); + if (localStorage.hasOwnProperty('settingsGamepad')) + settingsGamepad = JSON.parse(localStorage.getItem('settingsGamepad')); setSetting(this.scene, setting as Setting, valueIndex); + setSettingGamepad(this.scene, settingsGamepad as SettingGamepad, valueIndex); Object.keys(settingDefaults).forEach(s => { if (s === setting) settings[s] = valueIndex; }); + Object.keys(settingGamepadDefaults).forEach(s => { + if (s === setting) + settingsGamepad[s] = valueIndex; + }); + localStorage.setItem('settings', JSON.stringify(settings)); + localStorage.setItem('settingsGamepad', JSON.stringify(settingsGamepad)); return true; } private loadSettings(): boolean { Object.values(Setting).map(setting => setting as Setting).forEach(setting => setSetting(this.scene, setting, settingDefaults[setting])); + Object.values(SettingGamepad).map(setting => setting as SettingGamepad).forEach(setting => setSettingGamepad(this.scene, setting, settingGamepadDefaults[setting])); if (!localStorage.hasOwnProperty('settings')) return false; + if (!localStorage.hasOwnProperty('settingsGamepad')) + return false; const settings = JSON.parse(localStorage.getItem('settings')); + const settingsGamepad = JSON.parse(localStorage.getItem('settingsGamepad')); for (let setting of Object.keys(settings)) setSetting(this.scene, setting as Setting, settings[setting]); + + for (let setting of Object.keys(settingsGamepad)) + setSettingGamepad(this.scene, setting as SettingGamepad, settingGamepadDefaults[setting]); } public saveTutorialFlag(tutorial: Tutorial, flag: boolean): boolean { diff --git a/src/system/settings-gamepad.ts b/src/system/settings-gamepad.ts new file mode 100644 index 000000000..605f02006 --- /dev/null +++ b/src/system/settings-gamepad.ts @@ -0,0 +1,30 @@ +import BattleScene from "../battle-scene"; +import {SettingDefaults, SettingOptions} from "#app/system/settings"; + +export enum SettingGamepad { + Gamepad_Support = "GAMEPAD_SUPPORT", + Swap_A_and_B = "SWAP_A_B", // Swaps which gamepad button handles ACTION and CANCEL +} + +export const settingGamepadOptions: SettingOptions = { + [SettingGamepad.Gamepad_Support]: [ 'Auto', 'Disabled' ], + [SettingGamepad.Swap_A_and_B]: [ 'Enabled', 'Disabled' ], +}; + +export const settingGamepadDefaults: SettingDefaults = { + [SettingGamepad.Gamepad_Support]: 0, + [SettingGamepad.Swap_A_and_B]: 1, // Set to 'Disabled' by default +}; + +export function setSettingGamepad(scene: BattleScene, setting: SettingGamepad, value: integer): boolean { + switch (setting) { + case SettingGamepad.Gamepad_Support: + scene.gamepadSupport = settingGamepadOptions[setting][value] !== 'Disabled'; + break; + case SettingGamepad.Swap_A_and_B: + scene.abSwapped = settingGamepadOptions[setting][value] !== 'Disabled'; + break; + } + + return true; +} diff --git a/src/system/settings.ts b/src/system/settings.ts index 3805a35bb..9196e5945 100644 --- a/src/system/settings.ts +++ b/src/system/settings.ts @@ -24,8 +24,6 @@ export enum Setting { HP_Bar_Speed = "HP_BAR_SPEED", Fusion_Palette_Swaps = "FUSION_PALETTE_SWAPS", Player_Gender = "PLAYER_GENDER", - Gamepad_Support = "GAMEPAD_SUPPORT", - Swap_A_and_B = "SWAP_A_B", // Swaps which gamepad button handles ACTION and CANCEL Touch_Controls = "TOUCH_CONTROLS", Vibration = "VIBRATION" } @@ -56,8 +54,6 @@ export const settingOptions: SettingOptions = { [Setting.HP_Bar_Speed]: [ 'Normal', 'Fast', 'Faster', 'Instant' ], [Setting.Fusion_Palette_Swaps]: [ 'Off', 'On' ], [Setting.Player_Gender]: [ 'Boy', 'Girl' ], - [Setting.Gamepad_Support]: [ 'Auto', 'Disabled' ], - [Setting.Swap_A_and_B]: [ 'Enabled', 'Disabled' ], [Setting.Touch_Controls]: [ 'Auto', 'Disabled' ], [Setting.Vibration]: [ 'Auto', 'Disabled' ] }; @@ -80,8 +76,6 @@ export const settingDefaults: SettingDefaults = { [Setting.HP_Bar_Speed]: 0, [Setting.Fusion_Palette_Swaps]: 1, [Setting.Player_Gender]: 0, - [Setting.Gamepad_Support]: 0, - [Setting.Swap_A_and_B]: 1, // Set to 'Disabled' by default [Setting.Touch_Controls]: 0, [Setting.Vibration]: 0 }; @@ -148,12 +142,6 @@ export function setSetting(scene: BattleScene, setting: Setting, value: integer) } else return false; break; - case Setting.Gamepad_Support: - scene.gamepadSupport = settingOptions[setting][value] !== 'Disabled'; - break; - case Setting.Swap_A_and_B: - scene.abSwapped = settingOptions[setting][value] !== 'Disabled'; - break; case Setting.Touch_Controls: scene.enableTouchControls = settingOptions[setting][value] !== 'Disabled' && hasTouchscreen(); const touchControls = document.getElementById('touchControls'); diff --git a/src/ui-inputs.ts b/src/ui-inputs.ts index b304880d0..9f484355e 100644 --- a/src/ui-inputs.ts +++ b/src/ui-inputs.ts @@ -6,6 +6,7 @@ import StarterSelectUiHandler from "./ui/starter-select-ui-handler"; import {Setting, settingOptions} from "./system/settings"; import SettingsUiHandler from "./ui/settings-ui-handler"; import {Button} from "./enums/buttons"; +import SettingsGamepadUiHandler from "#app/ui/settings-gamepad-ui-handler"; export interface ActionKeys { [key in Button]: () => void; @@ -130,7 +131,7 @@ export class UiInputs { } buttonCycleOption(button: Button): void { - const whitelist = [StarterSelectUiHandler, SettingsUiHandler]; + const whitelist = [StarterSelectUiHandler, SettingsUiHandler, SettingsGamepadUiHandler]; const uiHandler = this.scene.ui?.getHandler(); if (whitelist.some(handler => uiHandler instanceof handler)) { this.scene.ui.processInput(button); diff --git a/src/ui/settings-gamepad-ui-handler.ts b/src/ui/settings-gamepad-ui-handler.ts new file mode 100644 index 000000000..89b6f1c8c --- /dev/null +++ b/src/ui/settings-gamepad-ui-handler.ts @@ -0,0 +1,252 @@ +import BattleScene from "../battle-scene"; +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 {SettingGamepad, settingGamepadDefaults, settingGamepadOptions} from "../system/settings-gamepad"; + +export default class SettingsGamepadUiHandler extends UiHandler { + private settingsContainer: Phaser.GameObjects.Container; + private optionsContainer: Phaser.GameObjects.Container; + + private scrollCursor: integer; + + private optionsBg: Phaser.GameObjects.NineSlice; + + private optionCursors: integer[]; + + private settingLabels: Phaser.GameObjects.Text[]; + private optionValueLabels: Phaser.GameObjects.Text[][]; + + private cursorObj: Phaser.GameObjects.NineSlice; + + private reloadRequired: boolean; + private reloadI18n: boolean; + + constructor(scene: BattleScene, mode?: Mode) { + super(scene, mode); + + this.reloadRequired = false; + this.reloadI18n = false; + } + + 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_SELECTED); + gamepadText.setOrigin(0, 0); + gamepadText.setPositionRelative(headerBg, 50, 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.optionsContainer = this.scene.add.container(0, 0); + + this.settingLabels = []; + this.optionValueLabels = []; + + Object.keys(SettingGamepad).forEach((setting, s) => { + let settingName = setting.replace(/\_/g, ' '); + + 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(settingGamepadOptions[SettingGamepad[setting]].map((option, o) => { + const valueLabel = addTextObject(this.scene, 0, 0, option, settingGamepadDefaults[SettingGamepad[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 (let value of this.optionValueLabels[s]) { + value.setPositionRelative(this.settingLabels[s], labelWidth + xOffset, 0); + xOffset += value.width / 6 + optionSpacing; + } + }); + + this.optionCursors = Object.values(settingGamepadDefaults); + + this.settingsContainer.add(headerBg); + this.settingsContainer.add(headerText); + this.settingsContainer.add(gamepadText); + this.settingsContainer.add(this.optionsBg); + this.settingsContainer.add(this.optionsContainer); + + ui.add(this.settingsContainer); + + this.setCursor(0); + this.setScrollCursor(0); + + this.settingsContainer.setVisible(false); + } + + show(args: any[]): boolean { + super.show(args); + + const settings: object = localStorage.hasOwnProperty('settings') ? JSON.parse(localStorage.getItem('settings')) : {}; + + Object.keys(settingGamepadDefaults).forEach((setting, s) => this.setOptionCursor(s, settings.hasOwnProperty(setting) ? settings[setting] : settingGamepadDefaults[setting])); + + this.settingsContainer.setVisible(true); + this.setCursor(0); + + this.getUi().moveTo(this.settingsContainer, this.getUi().length - 1); + + this.getUi().hideTooltip(); + + return true; + } + + processInput(button: Button): boolean { + const ui = this.getUi(); + + let success = false; + + if (button === Button.CANCEL) { + success = true; + 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); + } + break; + case Button.DOWN: + if (cursor < this.optionValueLabels.length) { + if (this.cursor < 8) + success = this.setCursor(this.cursor + 1); + else if (this.scrollCursor < this.optionValueLabels.length - 9) + success = this.setScrollCursor(this.scrollCursor + 1); + } + break; + case Button.LEFT: + if (this.optionCursors[cursor]) + success = this.setOptionCursor(cursor, this.optionCursors[cursor] - 1, true); + break; + case Button.RIGHT: + if (this.optionCursors[cursor] < this.optionValueLabels[cursor].length - 1) + success = this.setOptionCursor(cursor, this.optionCursors[cursor] + 1, true); + break; + case Button.LB: + this.scene.ui.setMode(Mode.SETTINGS) + success = true; + case Button.RB: + this.scene.ui.setMode(Mode.SETTINGS) + success = true; + break; + } + } + + 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 = SettingGamepad[Object.keys(SettingGamepad)[settingIndex]]; + + 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) + } + + 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 + 9; + this.settingLabels[s].setVisible(visible); + for (let 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; + } +} \ No newline at end of file diff --git a/src/ui/settings-ui-handler.ts b/src/ui/settings-ui-handler.ts index c769068d8..87892bb6d 100644 --- a/src/ui/settings-ui-handler.ts +++ b/src/ui/settings-ui-handler.ts @@ -42,10 +42,14 @@ export default class SettingsUiHandler extends UiHandler { 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, 'Options', TextStyle.SETTINGS_LABEL); + const headerText = addTextObject(this.scene, 0, 0, 'General', TextStyle.SETTINGS_SELECTED); 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); + 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); @@ -92,6 +96,7 @@ export default class SettingsUiHandler extends UiHandler { this.settingsContainer.add(headerBg); this.settingsContainer.add(headerText); + this.settingsContainer.add(gamepadText); this.settingsContainer.add(this.optionsBg); this.settingsContainer.add(this.optionsContainer); @@ -156,10 +161,12 @@ export default class SettingsUiHandler extends UiHandler { success = this.setOptionCursor(cursor, this.optionCursors[cursor] + 1, true); break; case Button.LB: - console.log('lb'); + this.scene.ui.setMode(Mode.SETTINGS_GAMEPAD) + success = true; break; case Button.RB: - console.log('rb'); + this.scene.ui.setMode(Mode.SETTINGS_GAMEPAD) + success = true; break; } } diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 09deb2bdd..2b2c8f756 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -13,6 +13,7 @@ import StarterSelectUiHandler from './starter-select-ui-handler'; import EvolutionSceneHandler from './evolution-scene-handler'; import TargetSelectUiHandler from './target-select-ui-handler'; import SettingsUiHandler from './settings-ui-handler'; +import SettingsGamepadUiHandler from "./settings-gamepad-ui-handler"; import { TextStyle, addTextObject } from './text'; import AchvBar from './achv-bar'; import MenuUiHandler from './menu-ui-handler'; @@ -56,6 +57,7 @@ export enum Mode { MENU, MENU_OPTION_SELECT, SETTINGS, + SETTINGS_GAMEPAD, ACHIEVEMENTS, GAME_STATS, VOUCHERS, @@ -137,6 +139,7 @@ export default class UI extends Phaser.GameObjects.Container { new MenuUiHandler(scene), new OptionSelectUiHandler(scene, Mode.MENU_OPTION_SELECT), new SettingsUiHandler(scene), + new SettingsGamepadUiHandler(scene), new AchvsUiHandler(scene), new GameStatsUiHandler(scene), new VouchersUiHandler(scene), From c5decd5cc13e2d9461ee7cb67c6990ab63c9d62e Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Wed, 8 May 2024 16:41:02 +0200 Subject: [PATCH 03/81] fix import --- src/system/game-data.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 418965b06..991147579 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -29,7 +29,7 @@ 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 "./system/settings-gamepad"; +import {setSettingGamepad, SettingGamepad, settingGamepadDefaults} from "./settings-gamepad"; const saveKey = 'x0i2O7WRiANTqPmZ'; // Temporary; secure encryption is not yet necessary From 80e8d43c56975e92e6fc5a9cea956d108f7ba6b4 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Wed, 8 May 2024 17:16:20 +0200 Subject: [PATCH 04/81] added basic key settings --- src/inputs-controller.ts | 2 ++ src/system/settings-gamepad.ts | 51 +++++++++++++++++++++++++++ src/ui/settings-gamepad-ui-handler.ts | 8 ++--- 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index b1656dbfa..51383c9eb 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -36,6 +36,8 @@ export class InputsController { private time: Time; private player: Map = new Map(); + public customGamepadMapping = new Map(); + constructor(scene: Phaser.Scene) { this.scene = scene; this.time = this.scene.time; diff --git a/src/system/settings-gamepad.ts b/src/system/settings-gamepad.ts index 605f02006..a9118f269 100644 --- a/src/system/settings-gamepad.ts +++ b/src/system/settings-gamepad.ts @@ -1,19 +1,56 @@ import BattleScene from "../battle-scene"; import {SettingDefaults, SettingOptions} from "#app/system/settings"; +import {Button} from "../enums/buttons"; export enum SettingGamepad { Gamepad_Support = "GAMEPAD_SUPPORT", Swap_A_and_B = "SWAP_A_B", // Swaps which gamepad button handles ACTION and CANCEL + Button_Action = "BUTTON_ACTION", + Button_Cancel = "BUTTON_CANCEL", + Button_Menu = "BUTTON_MENU", + Button_Stats = "BUTTON_STATS", + Button_Cycle_Shiny = "BUTTON_CYCLE_SHINY", + Button_Cycle_Form = "BUTTON_CYCLE_FORM", + Button_Cycle_Gender = "BUTTON_CYCLE_GENDER", + Button_Cycle_Ability = "BUTTON_CYCLE_ABILITY", + Button_Cycle_Nature = "BUTTON_CYCLE_NATURE", + Button_Cycle_Variant = "BUTTON_CYCLE_VARIANT", + Button_Speed_Up = "BUTTON_SPEED_UP", + Button_Slow_Down = "BUTTON_SLOW_DOWN", } export const settingGamepadOptions: SettingOptions = { [SettingGamepad.Gamepad_Support]: [ 'Auto', 'Disabled' ], [SettingGamepad.Swap_A_and_B]: [ 'Enabled', 'Disabled' ], + [SettingGamepad.Button_Action]: [`KEY ${Button.ACTION.toString()}`, 'Change'], + [SettingGamepad.Button_Cancel]: [`KEY ${Button.CANCEL.toString()}`, 'Change'], + [SettingGamepad.Button_Menu]: [`KEY ${Button.MENU.toString()}`, 'Change'], + [SettingGamepad.Button_Stats]: [`KEY ${Button.STATS.toString()}`, 'Change'], + [SettingGamepad.Button_Cycle_Shiny]: [`KEY ${Button.RB.toString()}`, 'Change'], + [SettingGamepad.Button_Cycle_Form]: [`KEY ${Button.LB.toString()}`, 'Change'], + [SettingGamepad.Button_Cycle_Gender]: [`KEY ${Button.CYCLE_GENDER.toString()}`, 'Change'], + [SettingGamepad.Button_Cycle_Ability]: [`KEY ${Button.CYCLE_ABILITY.toString()}`, 'Change'], + [SettingGamepad.Button_Cycle_Nature]: [`KEY ${Button.CYCLE_NATURE.toString()}`, 'Change'], + [SettingGamepad.Button_Cycle_Variant]: [`KEY ${Button.CYCLE_VARIANT.toString()}`, 'Change'], + [SettingGamepad.Button_Speed_Up]: [`KEY ${Button.SPEED_UP.toString()}`, 'Change'], + [SettingGamepad.Button_Slow_Down]: [`KEY ${Button.SLOW_DOWN.toString()}`, 'Change'] }; export const settingGamepadDefaults: SettingDefaults = { [SettingGamepad.Gamepad_Support]: 0, [SettingGamepad.Swap_A_and_B]: 1, // Set to 'Disabled' by default + [SettingGamepad.Button_Action]: Button.ACTION, + [SettingGamepad.Button_Cancel]: Button.CANCEL, + [SettingGamepad.Button_Menu]: Button.MENU, + [SettingGamepad.Button_Stats]: Button.STATS, + [SettingGamepad.Button_Cycle_Shiny]: Button.RB, + [SettingGamepad.Button_Cycle_Form]: Button.LB, + [SettingGamepad.Button_Cycle_Gender]: Button.CYCLE_GENDER, + [SettingGamepad.Button_Cycle_Ability]: Button.CYCLE_ABILITY, + [SettingGamepad.Button_Cycle_Nature]: Button.CYCLE_NATURE, + [SettingGamepad.Button_Cycle_Variant]: Button.CYCLE_VARIANT, + [SettingGamepad.Button_Speed_Up]: Button.SPEED_UP, + [SettingGamepad.Button_Slow_Down]: Button.SLOW_DOWN, }; export function setSettingGamepad(scene: BattleScene, setting: SettingGamepad, value: integer): boolean { @@ -24,6 +61,20 @@ export function setSettingGamepad(scene: BattleScene, setting: SettingGamepad, v case SettingGamepad.Swap_A_and_B: scene.abSwapped = settingGamepadOptions[setting][value] !== 'Disabled'; break; + case SettingGamepad.Button_Action: + case SettingGamepad.Button_Cancel: + case SettingGamepad.Button_Menu: + case SettingGamepad.Button_Stats: + case SettingGamepad.Button_Cycle_Shiny: + case SettingGamepad.Button_Cycle_Form: + case SettingGamepad.Button_Cycle_Gender: + case SettingGamepad.Button_Cycle_Ability: + case SettingGamepad.Button_Cycle_Nature: + case SettingGamepad.Button_Cycle_Variant: + case SettingGamepad.Button_Speed_Up: + case SettingGamepad.Button_Slow_Down: + scene.inputController.customGamepadMapping[setting] = value; + break; } return true; diff --git a/src/ui/settings-gamepad-ui-handler.ts b/src/ui/settings-gamepad-ui-handler.ts index 89b6f1c8c..871b4bcda 100644 --- a/src/ui/settings-gamepad-ui-handler.ts +++ b/src/ui/settings-gamepad-ui-handler.ts @@ -194,14 +194,14 @@ export default class SettingsGamepadUiHandler extends UiHandler { const lastCursor = this.optionCursors[settingIndex]; const lastValueLabel = this.optionValueLabels[settingIndex][lastCursor]; - lastValueLabel.setColor(this.getTextColor(TextStyle.WINDOW)); - lastValueLabel.setShadowColor(this.getTextColor(TextStyle.WINDOW, true)); + 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)); + newValueLabel?.setColor(this.getTextColor(TextStyle.SETTINGS_SELECTED)); + newValueLabel?.setShadowColor(this.getTextColor(TextStyle.SETTINGS_SELECTED, true)); if (save) { this.scene.gameData.saveSetting(setting, cursor) From 45e40f66ce7bc822d416c95a204633b77f0c44e6 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Wed, 8 May 2024 23:07:17 +0200 Subject: [PATCH 05/81] fix "lost focus behaviour" when disabling gamepad and thus repeating RIGHT key --- src/battle-scene.ts | 1 - src/inputs-controller.ts | 47 ++++++++++++++++++++++++++++++---------- src/system/settings.ts | 2 +- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index f117615e5..14fddfd36 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -103,7 +103,6 @@ export default class BattleScene extends SceneBase { public expGainsSpeed: integer = 0; public hpBarSpeed: integer = 0; public fusionPaletteSwaps: boolean = true; - public gamepadSupport: boolean = true; public enableTouchControls: boolean = false; public enableVibration: boolean = false; public abSwapped: boolean = false; diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 5f5615358..6b3e6182a 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -36,6 +36,8 @@ export class InputsController { private time: Time; private player: Map = new Map(); + private gamepadSupport: boolean = true; + constructor(scene: Phaser.Scene) { this.scene = scene; this.time = this.scene.time; @@ -45,6 +47,7 @@ export class InputsController { this.interactions[b] = { pressTime: false, isPressed: false, + source: null, } } // We don't want the menu key to be repeated @@ -87,16 +90,32 @@ export class InputsController { this.deactivatePressedKey(); } + setGamepadSupport(value: boolean): void { + if (value) { + this.gamepadSupport = true; + } else { + this.gamepadSupport = false; + this.deactivatePressedKey(); + } + } + update(): void { // reversed to let the cancel button have a kinda priority on the action button for (const b of Utils.getEnumValues(Button).reverse()) { - if (!this.interactions.hasOwnProperty(b)) continue; - if (this.repeatInputDurationJustPassed(b) && this.interactions[b].isPressed) { + if ( + this.interactions.hasOwnProperty(b) && + this.repeatInputDurationJustPassed(b) && + this.interactions[b].isPressed + ) { + if (!this.gamepadSupport && this.interactions[b].source === 'gamepad') { + this.delLastProcessedMovementTime(b); + return; + } this.events.emit('input_down', { - controller_type: 'repeated', + controller_type: this.interactions[b].source, button: b, }); - this.setLastProcessedMovementTime(b); + this.setLastProcessedMovementTime(b, this.interactions[b].source); } } } @@ -143,7 +162,7 @@ export class InputsController { } gamepadButtonDown(pad: Phaser.Input.Gamepad.Gamepad, button: Phaser.Input.Gamepad.Button, value: number): void { - if (!this.scene.gamepadSupport) return; + if (!this.gamepadSupport) return; const actionMapping = this.getActionGamepadMapping(); const buttonDown = actionMapping.hasOwnProperty(button.index) && actionMapping[button.index]; if (buttonDown !== undefined) { @@ -151,12 +170,12 @@ export class InputsController { controller_type: 'gamepad', button: buttonDown, }); - this.setLastProcessedMovementTime(buttonDown); + this.setLastProcessedMovementTime(buttonDown, 'gamepad'); } } gamepadButtonUp(pad: Phaser.Input.Gamepad.Gamepad, button: Phaser.Input.Gamepad.Button, value: number): void { - if (!this.scene.gamepadSupport) return; + if (!this.gamepadSupport) return; const actionMapping = this.getActionGamepadMapping(); const buttonUp = actionMapping.hasOwnProperty(button.index) && actionMapping[button.index]; if (buttonUp !== undefined) { @@ -212,7 +231,7 @@ export class InputsController { controller_type: 'keyboard', button: index, }); - this.setLastProcessedMovementTime(index); + this.setLastProcessedMovementTime(index, 'keyboard'); }); key.on('up', () => { this.events.emit('input_up', { @@ -251,11 +270,12 @@ export class InputsController { } } - setLastProcessedMovementTime(button: Button): void { + setLastProcessedMovementTime(button: Button, source: String = 'keyboard'): void { if (!this.interactions.hasOwnProperty(button)) return; this.setButtonLock(button); this.interactions[button].pressTime = this.time.now; this.interactions[button].isPressed = true; + this.interactions[button].source = source; } delLastProcessedMovementTime(button: Button): void { @@ -263,15 +283,18 @@ export class InputsController { this.releaseButtonLock(button); this.interactions[button].pressTime = null; this.interactions[button].isPressed = false; + this.interactions[button].source = null; } deactivatePressedKey(): void { this.releaseButtonLock(this.buttonLock); this.releaseButtonLock(this.buttonLock2); for (const b of Utils.getEnumValues(Button)) { - if (!this.interactions.hasOwnProperty(b)) return; - this.interactions[b].pressTime = null; - this.interactions[b].isPressed = false; + if (this.interactions.hasOwnProperty(b)) { + this.interactions[b].pressTime = null; + this.interactions[b].isPressed = false; + this.interactions[b].source = null; + } } } diff --git a/src/system/settings.ts b/src/system/settings.ts index 054bdc9fc..b0c61fabf 100644 --- a/src/system/settings.ts +++ b/src/system/settings.ts @@ -149,7 +149,7 @@ export function setSetting(scene: BattleScene, setting: Setting, value: integer) return false; break; case Setting.Gamepad_Support: - scene.gamepadSupport = settingOptions[setting][value] !== 'Disabled'; + scene.inputController.setGamepadSupport(settingOptions[setting][value] !== 'Disabled'); break; case Setting.Swap_A_and_B: scene.abSwapped = settingOptions[setting][value] !== 'Disabled'; From b35af93ecba384ce9189452d0b1bad3f685d29ef Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Thu, 9 May 2024 03:33:25 +0200 Subject: [PATCH 06/81] added setting selector for controller like lang + populate the setting with detected devices --- src/inputs-controller.ts | 17 + src/system/game-data.ts | 43 ++- src/system/settings-gamepad.ts | 175 +++++++---- src/system/settings.ts | 6 - src/ui/settings-gamepad-ui-handler.ts | 432 +++++++++++++------------- 5 files changed, 374 insertions(+), 299 deletions(-) diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index bad263961..c25457931 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -39,6 +39,7 @@ export class InputsController { private gamepadSupport: boolean = true; public customGamepadMapping = new Map(); + public chosenGamepad; constructor(scene: Phaser.Scene) { this.scene = scene; @@ -60,6 +61,10 @@ export class InputsController { init(): void { this.events = new Phaser.Events.EventEmitter(); + + if (localStorage.hasOwnProperty('chosenGamepad')) { + this.chosenGamepad = localStorage.getItem('chosenGamepad'); + } // Handle the game losing focus this.scene.game.events.on(Phaser.Core.Events.BLUR, () => { this.loseFocus() @@ -69,6 +74,7 @@ export class InputsController { this.scene.input.gamepad.on('connected', function (thisGamepad) { this.refreshGamepads(); this.setupGamepad(thisGamepad); + this.populateSetting(); }, this); // Check to see if the gamepad has already been setup by the browser @@ -101,6 +107,10 @@ export class InputsController { } } + setChosenGamepad(gamepad: String): void { + this.chosenGamepad = gamepad; + } + update(): void { // reversed to let the cancel button have a kinda priority on the action button for (const b of Utils.getEnumValues(Button).reverse()) { @@ -122,6 +132,13 @@ export class InputsController { } } + populateSetting(): void { + const gamepadsName = this.gamepads.map(g => g.id); + localStorage.setItem('gamepadsConnected', JSON.stringify(gamepadsName)); + if (!this.chosenGamepad) this.chosenGamepad = gamepadsName[0]; + localStorage.setItem('chosenGamepad', this.chosenGamepad); + } + setupGamepad(thisGamepad: Phaser.Input.Gamepad.Gamepad): void { let gamepadID = thisGamepad.id.toLowerCase(); const mappedPad = this.mapGamepad(gamepadID); diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 3470db726..61139fe62 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -226,6 +226,7 @@ export class GameData { constructor(scene: BattleScene) { this.scene = scene; this.loadSettings(); + this.loadGamepadSettings(); this.trainerId = Utils.randInt(65536); this.secretId = Utils.randInt(65536); this.starterData = {}; @@ -465,48 +466,66 @@ export class GameData { public saveSetting(setting: Setting, valueIndex: integer): boolean { let settings: object = {}; - let settingsGamepad: object = {}; if (localStorage.hasOwnProperty('settings')) settings = JSON.parse(localStorage.getItem('settings')); - if (localStorage.hasOwnProperty('settingsGamepad')) - settingsGamepad = JSON.parse(localStorage.getItem('settingsGamepad')); setSetting(this.scene, setting as Setting, valueIndex); - setSettingGamepad(this.scene, settingsGamepad as SettingGamepad, valueIndex); Object.keys(settingDefaults).forEach(s => { if (s === setting) settings[s] = valueIndex; }); + + localStorage.setItem('settings', JSON.stringify(settings)); + + return true; + } + + public saveGamepadSetting(setting: SettingGamepad, valueIndex: integer): boolean { + let settingsGamepad: object = {}; + if (localStorage.hasOwnProperty('settingsGamepad')) + settingsGamepad = JSON.parse(localStorage.getItem('settingsGamepad')); + + let gamepadsConnected = null; + if (localStorage.hasOwnProperty('gamepadsConnected')) { + gamepadsConnected = JSON.parse(localStorage.getItem('gamepadsConnected')); + } + setSettingGamepad(this.scene, setting as SettingGamepad, valueIndex, gamepadsConnected); Object.keys(settingGamepadDefaults).forEach(s => { if (s === setting) settingsGamepad[s] = valueIndex; }); - - localStorage.setItem('settings', JSON.stringify(settings)); localStorage.setItem('settingsGamepad', JSON.stringify(settingsGamepad)); - return true; } private loadSettings(): boolean { Object.values(Setting).map(setting => setting as Setting).forEach(setting => setSetting(this.scene, setting, settingDefaults[setting])); - Object.values(SettingGamepad).map(setting => setting as SettingGamepad).forEach(setting => setSettingGamepad(this.scene, setting, settingGamepadDefaults[setting])); if (!localStorage.hasOwnProperty('settings')) return false; - if (!localStorage.hasOwnProperty('settingsGamepad')) - return false; const settings = JSON.parse(localStorage.getItem('settings')); - const settingsGamepad = JSON.parse(localStorage.getItem('settingsGamepad')); for (let setting of Object.keys(settings)) setSetting(this.scene, setting as Setting, settings[setting]); + } + + private loadGamepadSettings(): boolean { + Object.values(SettingGamepad).map(setting => setting as SettingGamepad).forEach(setting => setSettingGamepad(this.scene, setting, settingGamepadDefaults[setting])); + + if (!localStorage.hasOwnProperty('settingsGamepad')) + return false; + + let gamepadConnected = null; + if (localStorage.hasOwnProperty('gamepadConnected')) { + gamepadConnected = JSON.parse(localStorage.getItem('gamepadConnected')); + } + const settingsGamepad = JSON.parse(localStorage.getItem('settingsGamepad')); for (let setting of Object.keys(settingsGamepad)) - setSettingGamepad(this.scene, setting as SettingGamepad, settingGamepadDefaults[setting]); + setSettingGamepad(this.scene, setting as SettingGamepad, settingsGamepad[setting], gamepadConnected); } public saveTutorialFlag(tutorial: Tutorial, flag: boolean): boolean { diff --git a/src/system/settings-gamepad.ts b/src/system/settings-gamepad.ts index a9118f269..6e019442d 100644 --- a/src/system/settings-gamepad.ts +++ b/src/system/settings-gamepad.ts @@ -1,81 +1,120 @@ import BattleScene from "../battle-scene"; import {SettingDefaults, SettingOptions} from "#app/system/settings"; -import {Button} from "../enums/buttons"; +import SettingsGamepadUiHandler from "#app/ui/settings-gamepad-ui-handler"; +import {Mode} from "#app/ui/ui"; export enum SettingGamepad { - Gamepad_Support = "GAMEPAD_SUPPORT", - Swap_A_and_B = "SWAP_A_B", // Swaps which gamepad button handles ACTION and CANCEL - Button_Action = "BUTTON_ACTION", - Button_Cancel = "BUTTON_CANCEL", - Button_Menu = "BUTTON_MENU", - Button_Stats = "BUTTON_STATS", - Button_Cycle_Shiny = "BUTTON_CYCLE_SHINY", - Button_Cycle_Form = "BUTTON_CYCLE_FORM", - Button_Cycle_Gender = "BUTTON_CYCLE_GENDER", - Button_Cycle_Ability = "BUTTON_CYCLE_ABILITY", - Button_Cycle_Nature = "BUTTON_CYCLE_NATURE", - Button_Cycle_Variant = "BUTTON_CYCLE_VARIANT", - Button_Speed_Up = "BUTTON_SPEED_UP", - Button_Slow_Down = "BUTTON_SLOW_DOWN", + Default_Controller = "DEFAULT_CONTROLLER", + Gamepad_Support = "GAMEPAD_SUPPORT", + Swap_A_and_B = "SWAP_A_B", // Swaps which gamepad button handles ACTION and CANCEL + // Button_Action = "BUTTON_ACTION", + // Button_Cancel = "BUTTON_CANCEL", + // Button_Menu = "BUTTON_MENU", + // Button_Stats = "BUTTON_STATS", + // Button_Cycle_Shiny = "BUTTON_CYCLE_SHINY", + // Button_Cycle_Form = "BUTTON_CYCLE_FORM", + // Button_Cycle_Gender = "BUTTON_CYCLE_GENDER", + // Button_Cycle_Ability = "BUTTON_CYCLE_ABILITY", + // Button_Cycle_Nature = "BUTTON_CYCLE_NATURE", + // Button_Cycle_Variant = "BUTTON_CYCLE_VARIANT", + // Button_Speed_Up = "BUTTON_SPEED_UP", + // Button_Slow_Down = "BUTTON_SLOW_DOWN", } export const settingGamepadOptions: SettingOptions = { - [SettingGamepad.Gamepad_Support]: [ 'Auto', 'Disabled' ], - [SettingGamepad.Swap_A_and_B]: [ 'Enabled', 'Disabled' ], - [SettingGamepad.Button_Action]: [`KEY ${Button.ACTION.toString()}`, 'Change'], - [SettingGamepad.Button_Cancel]: [`KEY ${Button.CANCEL.toString()}`, 'Change'], - [SettingGamepad.Button_Menu]: [`KEY ${Button.MENU.toString()}`, 'Change'], - [SettingGamepad.Button_Stats]: [`KEY ${Button.STATS.toString()}`, 'Change'], - [SettingGamepad.Button_Cycle_Shiny]: [`KEY ${Button.RB.toString()}`, 'Change'], - [SettingGamepad.Button_Cycle_Form]: [`KEY ${Button.LB.toString()}`, 'Change'], - [SettingGamepad.Button_Cycle_Gender]: [`KEY ${Button.CYCLE_GENDER.toString()}`, 'Change'], - [SettingGamepad.Button_Cycle_Ability]: [`KEY ${Button.CYCLE_ABILITY.toString()}`, 'Change'], - [SettingGamepad.Button_Cycle_Nature]: [`KEY ${Button.CYCLE_NATURE.toString()}`, 'Change'], - [SettingGamepad.Button_Cycle_Variant]: [`KEY ${Button.CYCLE_VARIANT.toString()}`, 'Change'], - [SettingGamepad.Button_Speed_Up]: [`KEY ${Button.SPEED_UP.toString()}`, 'Change'], - [SettingGamepad.Button_Slow_Down]: [`KEY ${Button.SLOW_DOWN.toString()}`, 'Change'] + [SettingGamepad.Default_Controller]: [ 'Default', 'Change' ], + [SettingGamepad.Gamepad_Support]: [ 'Auto', 'Disabled' ], + [SettingGamepad.Swap_A_and_B]: [ 'Enabled', 'Disabled' ], + // [SettingGamepad.Button_Action]: [`KEY ${Button.ACTION.toString()}`, 'Change'], + // [SettingGamepad.Button_Cancel]: [`KEY ${Button.CANCEL.toString()}`, 'Change'], + // [SettingGamepad.Button_Menu]: [`KEY ${Button.MENU.toString()}`, 'Change'], + // [SettingGamepad.Button_Stats]: [`KEY ${Button.STATS.toString()}`, 'Change'], + // [SettingGamepad.Button_Cycle_Shiny]: [`KEY ${Button.RB.toString()}`, 'Change'], + // [SettingGamepad.Button_Cycle_Form]: [`KEY ${Button.LB.toString()}`, 'Change'], + // [SettingGamepad.Button_Cycle_Gender]: [`KEY ${Button.CYCLE_GENDER.toString()}`, 'Change'], + // [SettingGamepad.Button_Cycle_Ability]: [`KEY ${Button.CYCLE_ABILITY.toString()}`, 'Change'], + // [SettingGamepad.Button_Cycle_Nature]: [`KEY ${Button.CYCLE_NATURE.toString()}`, 'Change'], + // [SettingGamepad.Button_Cycle_Variant]: [`KEY ${Button.CYCLE_VARIANT.toString()}`, 'Change'], + // [SettingGamepad.Button_Speed_Up]: [`KEY ${Button.SPEED_UP.toString()}`, 'Change'], + // [SettingGamepad.Button_Slow_Down]: [`KEY ${Button.SLOW_DOWN.toString()}`, 'Change'] }; export const settingGamepadDefaults: SettingDefaults = { - [SettingGamepad.Gamepad_Support]: 0, - [SettingGamepad.Swap_A_and_B]: 1, // Set to 'Disabled' by default - [SettingGamepad.Button_Action]: Button.ACTION, - [SettingGamepad.Button_Cancel]: Button.CANCEL, - [SettingGamepad.Button_Menu]: Button.MENU, - [SettingGamepad.Button_Stats]: Button.STATS, - [SettingGamepad.Button_Cycle_Shiny]: Button.RB, - [SettingGamepad.Button_Cycle_Form]: Button.LB, - [SettingGamepad.Button_Cycle_Gender]: Button.CYCLE_GENDER, - [SettingGamepad.Button_Cycle_Ability]: Button.CYCLE_ABILITY, - [SettingGamepad.Button_Cycle_Nature]: Button.CYCLE_NATURE, - [SettingGamepad.Button_Cycle_Variant]: Button.CYCLE_VARIANT, - [SettingGamepad.Button_Speed_Up]: Button.SPEED_UP, - [SettingGamepad.Button_Slow_Down]: Button.SLOW_DOWN, + [SettingGamepad.Default_Controller]: 0, + [SettingGamepad.Gamepad_Support]: 0, + [SettingGamepad.Swap_A_and_B]: 1, // Set to 'Disabled' by default + // [SettingGamepad.Button_Action]: Button.ACTION, + // [SettingGamepad.Button_Cancel]: Button.CANCEL, + // [SettingGamepad.Button_Menu]: Button.MENU, + // [SettingGamepad.Button_Stats]: Button.STATS, + // [SettingGamepad.Button_Cycle_Shiny]: Button.RB, + // [SettingGamepad.Button_Cycle_Form]: Button.LB, + // [SettingGamepad.Button_Cycle_Gender]: Button.CYCLE_GENDER, + // [SettingGamepad.Button_Cycle_Ability]: Button.CYCLE_ABILITY, + // [SettingGamepad.Button_Cycle_Nature]: Button.CYCLE_NATURE, + // [SettingGamepad.Button_Cycle_Variant]: Button.CYCLE_VARIANT, + // [SettingGamepad.Button_Speed_Up]: Button.SPEED_UP, + // [SettingGamepad.Button_Slow_Down]: Button.SLOW_DOWN, }; -export function setSettingGamepad(scene: BattleScene, setting: SettingGamepad, value: integer): boolean { - switch (setting) { - case SettingGamepad.Gamepad_Support: - scene.gamepadSupport = settingGamepadOptions[setting][value] !== 'Disabled'; - break; - case SettingGamepad.Swap_A_and_B: - scene.abSwapped = settingGamepadOptions[setting][value] !== 'Disabled'; - break; - case SettingGamepad.Button_Action: - case SettingGamepad.Button_Cancel: - case SettingGamepad.Button_Menu: - case SettingGamepad.Button_Stats: - case SettingGamepad.Button_Cycle_Shiny: - case SettingGamepad.Button_Cycle_Form: - case SettingGamepad.Button_Cycle_Gender: - case SettingGamepad.Button_Cycle_Ability: - case SettingGamepad.Button_Cycle_Nature: - case SettingGamepad.Button_Cycle_Variant: - case SettingGamepad.Button_Speed_Up: - case SettingGamepad.Button_Slow_Down: - scene.inputController.customGamepadMapping[setting] = value; - break; - } - - return true; +function truncateString(str: String, maxLength: number = 10) { + if (str.length > maxLength) { + return str.slice(0, maxLength - 3) + "..."; // Subtract 3 to accommodate the ellipsis + } + return str; +} + +export function setSettingGamepad(scene: BattleScene, setting: SettingGamepad, value: integer, gamepads?: Array): boolean { + switch (setting) { + case SettingGamepad.Gamepad_Support: + console.log('setting:', setting, settingGamepadOptions[setting][value]); + scene.inputController.setGamepadSupport(settingGamepadOptions[setting][value] !== 'Disabled'); + break; + case SettingGamepad.Swap_A_and_B: + console.log('settingGamepadOptions[setting][value]:', settingGamepadOptions[setting][value]); + console.log('settingGamepadOptions[setting]:', settingGamepadOptions[setting]); + console.log('value:', value); + scene.abSwapped = settingGamepadOptions[setting][value] !== 'Disabled'; + break; + // case SettingGamepad.Button_Action: + // case SettingGamepad.Button_Cancel: + // case SettingGamepad.Button_Menu: + // case SettingGamepad.Button_Stats: + // case SettingGamepad.Button_Cycle_Shiny: + // case SettingGamepad.Button_Cycle_Form: + // case SettingGamepad.Button_Cycle_Gender: + // case SettingGamepad.Button_Cycle_Ability: + // case SettingGamepad.Button_Cycle_Nature: + // case SettingGamepad.Button_Cycle_Variant: + // case SettingGamepad.Button_Speed_Up: + // case SettingGamepad.Button_Slow_Down: + // scene.inputController.customGamepadMapping[setting] = value; + // break; + case SettingGamepad.Default_Controller: + if (value) { + if (scene.ui && gamepads) { + const cancelHandler = () => { + scene.ui.revertMode(); + (scene.ui.getHandler() as SettingsGamepadUiHandler).setOptionCursor(Object.values(SettingGamepad).indexOf(SettingGamepad.Default_Controller), 0, true); + return false; + }; + const changeGamepadHandler = (gamepad: string) => { + scene.inputController.setChosenGamepad(gamepad); + localStorage.setItem('chosenGamepad', gamepad); + cancelHandler(); + return true; + }; + scene.ui.setOverlayMode(Mode.OPTION_SELECT, { + options: [...gamepads.map((g) => ({label: truncateString(g, 30), handler: () => changeGamepadHandler(g)})), { + label: 'Cancel', + handler: cancelHandler, + }] + }); + return false; + } + } + break; + } + + return true; } diff --git a/src/system/settings.ts b/src/system/settings.ts index 8c4c7dfba..5f4ab2fce 100644 --- a/src/system/settings.ts +++ b/src/system/settings.ts @@ -142,12 +142,6 @@ export function setSetting(scene: BattleScene, setting: Setting, value: integer) } else return false; break; - case Setting.Gamepad_Support: - scene.inputController.setGamepadSupport(settingOptions[setting][value] !== 'Disabled'); - break; - case Setting.Swap_A_and_B: - scene.abSwapped = settingOptions[setting][value] !== 'Disabled'; - break; case Setting.Touch_Controls: scene.enableTouchControls = settingOptions[setting][value] !== 'Disabled' && hasTouchscreen(); const touchControls = document.getElementById('touchControls'); diff --git a/src/ui/settings-gamepad-ui-handler.ts b/src/ui/settings-gamepad-ui-handler.ts index 871b4bcda..8fccdae15 100644 --- a/src/ui/settings-gamepad-ui-handler.ts +++ b/src/ui/settings-gamepad-ui-handler.ts @@ -5,248 +5,254 @@ import { Mode } from "./ui"; import UiHandler from "./ui-handler"; import { addWindow } from "./ui-theme"; import {Button} from "../enums/buttons"; -import {SettingGamepad, settingGamepadDefaults, settingGamepadOptions} from "../system/settings-gamepad"; +import { + SettingGamepad, + settingGamepadDefaults, + settingGamepadOptions +} from "../system/settings-gamepad"; export default class SettingsGamepadUiHandler extends UiHandler { - private settingsContainer: Phaser.GameObjects.Container; - private optionsContainer: Phaser.GameObjects.Container; + private settingsContainer: Phaser.GameObjects.Container; + private optionsContainer: Phaser.GameObjects.Container; - private scrollCursor: integer; + private scrollCursor: integer; - private optionsBg: Phaser.GameObjects.NineSlice; + private optionsBg: Phaser.GameObjects.NineSlice; - private optionCursors: integer[]; + private optionCursors: integer[]; - private settingLabels: Phaser.GameObjects.Text[]; - private optionValueLabels: Phaser.GameObjects.Text[][]; + private settingLabels: Phaser.GameObjects.Text[]; + private optionValueLabels: Phaser.GameObjects.Text[][]; - private cursorObj: Phaser.GameObjects.NineSlice; + private cursorObj: Phaser.GameObjects.NineSlice; - private reloadRequired: boolean; - private reloadI18n: boolean; + private reloadRequired: boolean; + private reloadI18n: boolean; + private gamepads: Array; - constructor(scene: BattleScene, mode?: Mode) { - super(scene, mode); + constructor(scene: BattleScene, mode?: Mode) { + super(scene, mode); - this.reloadRequired = false; - this.reloadI18n = false; - } - - 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_SELECTED); - gamepadText.setOrigin(0, 0); - gamepadText.setPositionRelative(headerBg, 50, 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.optionsContainer = this.scene.add.container(0, 0); - - this.settingLabels = []; - this.optionValueLabels = []; - - Object.keys(SettingGamepad).forEach((setting, s) => { - let settingName = setting.replace(/\_/g, ' '); - - 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(settingGamepadOptions[SettingGamepad[setting]].map((option, o) => { - const valueLabel = addTextObject(this.scene, 0, 0, option, settingGamepadDefaults[SettingGamepad[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 (let value of this.optionValueLabels[s]) { - value.setPositionRelative(this.settingLabels[s], labelWidth + xOffset, 0); - xOffset += value.width / 6 + optionSpacing; - } - }); - - this.optionCursors = Object.values(settingGamepadDefaults); - - this.settingsContainer.add(headerBg); - this.settingsContainer.add(headerText); - this.settingsContainer.add(gamepadText); - this.settingsContainer.add(this.optionsBg); - this.settingsContainer.add(this.optionsContainer); - - ui.add(this.settingsContainer); - - this.setCursor(0); - this.setScrollCursor(0); - - this.settingsContainer.setVisible(false); - } - - show(args: any[]): boolean { - super.show(args); - - const settings: object = localStorage.hasOwnProperty('settings') ? JSON.parse(localStorage.getItem('settings')) : {}; - - Object.keys(settingGamepadDefaults).forEach((setting, s) => this.setOptionCursor(s, settings.hasOwnProperty(setting) ? settings[setting] : settingGamepadDefaults[setting])); - - this.settingsContainer.setVisible(true); - this.setCursor(0); - - this.getUi().moveTo(this.settingsContainer, this.getUi().length - 1); - - this.getUi().hideTooltip(); - - return true; - } - - processInput(button: Button): boolean { - const ui = this.getUi(); - - let success = false; - - if (button === Button.CANCEL) { - success = true; - 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); - } - break; - case Button.DOWN: - if (cursor < this.optionValueLabels.length) { - if (this.cursor < 8) - success = this.setCursor(this.cursor + 1); - else if (this.scrollCursor < this.optionValueLabels.length - 9) - success = this.setScrollCursor(this.scrollCursor + 1); - } - break; - case Button.LEFT: - if (this.optionCursors[cursor]) - success = this.setOptionCursor(cursor, this.optionCursors[cursor] - 1, true); - break; - case Button.RIGHT: - if (this.optionCursors[cursor] < this.optionValueLabels[cursor].length - 1) - success = this.setOptionCursor(cursor, this.optionCursors[cursor] + 1, true); - break; - case Button.LB: - this.scene.ui.setMode(Mode.SETTINGS) - success = true; - case Button.RB: - this.scene.ui.setMode(Mode.SETTINGS) - success = true; - break; - } + this.reloadRequired = false; + this.reloadI18n = false; + this.gamepads = null; } - if (success) - ui.playSelect(); + setup() { + const ui = this.getUi(); - return success; - } + this.settingsContainer = this.scene.add.container(1, -(this.scene.game.canvas.height / 6) + 1); - setCursor(cursor: integer): boolean { - const ret = super.setCursor(cursor); + 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); - 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); + 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_SELECTED); + gamepadText.setOrigin(0, 0); + gamepadText.setPositionRelative(headerBg, 50, 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.optionsContainer = this.scene.add.container(0, 0); + + this.settingLabels = []; + this.optionValueLabels = []; + + Object.keys(SettingGamepad).forEach((setting, s) => { + let settingName = setting.replace(/\_/g, ' '); + + 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(settingGamepadOptions[SettingGamepad[setting]].map((option, o) => { + const valueLabel = addTextObject(this.scene, 0, 0, option, settingGamepadDefaults[SettingGamepad[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 (let value of this.optionValueLabels[s]) { + value.setPositionRelative(this.settingLabels[s], labelWidth + xOffset, 0); + xOffset += value.width / 6 + optionSpacing; + } + }); + + this.optionCursors = Object.values(settingGamepadDefaults); + + this.settingsContainer.add(headerBg); + this.settingsContainer.add(headerText); + this.settingsContainer.add(gamepadText); + this.settingsContainer.add(this.optionsBg); + this.settingsContainer.add(this.optionsContainer); + + ui.add(this.settingsContainer); + + this.setCursor(0); + this.setScrollCursor(0); + + this.settingsContainer.setVisible(false); } - this.cursorObj.setPositionRelative(this.optionsBg, 4, 4 + (this.cursor + this.scrollCursor) * 16); + show(args: any[]): boolean { + super.show(args); - return ret; - } + const settings: object = localStorage.hasOwnProperty('settingsGamepad') ? JSON.parse(localStorage.getItem('settingsGamepad')) : {}; + console.log('from here'); + Object.keys(settingGamepadDefaults).forEach((setting, s) => this.setOptionCursor(s, settings.hasOwnProperty(setting) ? settings[setting] : settingGamepadDefaults[setting])); - setOptionCursor(settingIndex: integer, cursor: integer, save?: boolean): boolean { - const setting = SettingGamepad[Object.keys(SettingGamepad)[settingIndex]]; + this.settingsContainer.setVisible(true); + this.setCursor(0); - const lastCursor = this.optionCursors[settingIndex]; + this.getUi().moveTo(this.settingsContainer, this.getUi().length - 1); - const lastValueLabel = this.optionValueLabels[settingIndex][lastCursor]; - lastValueLabel?.setColor(this.getTextColor(TextStyle.WINDOW)); - lastValueLabel?.setShadowColor(this.getTextColor(TextStyle.WINDOW, true)); + this.getUi().hideTooltip(); - 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) + return true; } - return true; - } + processInput(button: Button): boolean { + const ui = this.getUi(); - setScrollCursor(scrollCursor: integer): boolean { - if (scrollCursor === this.scrollCursor) - return false; + let success = false; - this.scrollCursor = scrollCursor; + if (button === Button.CANCEL) { + success = true; + 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); + } + break; + case Button.DOWN: + if (cursor < this.optionValueLabels.length) { + if (this.cursor < 8) + success = this.setCursor(this.cursor + 1); + else if (this.scrollCursor < this.optionValueLabels.length - 9) + success = this.setScrollCursor(this.scrollCursor + 1); + } + break; + case Button.LEFT: + if (this.optionCursors[cursor]) + success = this.setOptionCursor(cursor, this.optionCursors[cursor] - 1, true); + break; + case Button.RIGHT: + if (this.optionCursors[cursor] < this.optionValueLabels[cursor].length - 1) + success = this.setOptionCursor(cursor, this.optionCursors[cursor] + 1, true); + break; + case Button.LB: + this.scene.ui.setMode(Mode.SETTINGS) + success = true; + case Button.RB: + this.scene.ui.setMode(Mode.SETTINGS) + success = true; + break; + } + } - this.updateSettingsScroll(); + if (success) + ui.playSelect(); - 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 + 9; - this.settingLabels[s].setVisible(visible); - for (let option of this.optionValueLabels[s]) - option.setVisible(visible); + return success; } - } - clear() { - super.clear(); - this.settingsContainer.setVisible(false); - this.eraseCursor(); - if (this.reloadRequired) { - this.reloadRequired = false; - this.scene.reset(true, false, true); - } - } + setCursor(cursor: integer): boolean { + const ret = super.setCursor(cursor); - eraseCursor() { - if (this.cursorObj) - this.cursorObj.destroy(); - this.cursorObj = null; - } + 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 = SettingGamepad[Object.keys(SettingGamepad)[settingIndex]]; + + 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) { + if (SettingGamepad[setting] !== SettingGamepad.Default_Controller) + this.scene.gameData.saveGamepadSetting(setting, cursor) + } + + 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 + 9; + this.settingLabels[s].setVisible(visible); + for (let 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; + } } \ No newline at end of file From e162edf6f553f70e11efe3d00128208a84b2b6c6 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Thu, 9 May 2024 03:37:08 +0200 Subject: [PATCH 07/81] restrict inputs to chosen controller --- src/inputs-controller.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index c25457931..94bba5dac 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -140,7 +140,7 @@ export class InputsController { } setupGamepad(thisGamepad: Phaser.Input.Gamepad.Gamepad): void { - let gamepadID = thisGamepad.id.toLowerCase(); + let gamepadID = this.chosenGamepad?.toLowerCase() || thisGamepad.id.toLowerCase(); const mappedPad = this.mapGamepad(gamepadID); this.player['mapping'] = mappedPad.gamepadMapping; } @@ -181,7 +181,7 @@ export class InputsController { } gamepadButtonDown(pad: Phaser.Input.Gamepad.Gamepad, button: Phaser.Input.Gamepad.Button, value: number): void { - if (!this.gamepadSupport) return; + if (!this.gamepadSupport || pad.id.toLowerCase() !== this.chosenGamepad.toLowerCase()) return; const actionMapping = this.getActionGamepadMapping(); const buttonDown = actionMapping.hasOwnProperty(button.index) && actionMapping[button.index]; if (buttonDown !== undefined) { @@ -194,7 +194,7 @@ export class InputsController { } gamepadButtonUp(pad: Phaser.Input.Gamepad.Gamepad, button: Phaser.Input.Gamepad.Button, value: number): void { - if (!this.gamepadSupport) return; + if (!this.gamepadSupport || pad.id.toLowerCase() !== this.chosenGamepad.toLowerCase()) return; const actionMapping = this.getActionGamepadMapping(); const buttonUp = actionMapping.hasOwnProperty(button.index) && actionMapping[button.index]; if (buttonUp !== undefined) { From 92c493fe83e9d1967994c8364bad8064277df685 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Thu, 9 May 2024 04:06:32 +0200 Subject: [PATCH 08/81] added some comment --- src/inputs-controller.ts | 16 +++++++++++++--- src/system/settings.ts | 2 ++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 6b3e6182a..552ebf222 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -91,10 +91,13 @@ export class InputsController { } setGamepadSupport(value: boolean): void { + // value is true if we activate gamepad + // value is false if we deactivate gamepad if (value) { this.gamepadSupport = true; } else { this.gamepadSupport = false; + // if we disable the gamepad, we want to release every key pressed this.deactivatePressedKey(); } } @@ -103,11 +106,17 @@ export class InputsController { // reversed to let the cancel button have a kinda priority on the action button for (const b of Utils.getEnumValues(Button).reverse()) { if ( - this.interactions.hasOwnProperty(b) && - this.repeatInputDurationJustPassed(b) && - this.interactions[b].isPressed + this.interactions.hasOwnProperty(b) && // if the button is in the interactions dict + this.repeatInputDurationJustPassed(b) && // if we hold enough the button + this.interactions[b].isPressed // if the button is pressed ) { + // if the gamepad support is disable and the source is a gamepad + // we don't want to repeat the button + // even if we have disabled the gamepad, at the exact moment of the change in the menu + // we can be here and have an infinite loop since the code can't know we + // have released the key since the gamepad is not there anymore if (!this.gamepadSupport && this.interactions[b].source === 'gamepad') { + // if we are here and the gamepad is disabled, we delete the latest interracted button this.delLastProcessedMovementTime(b); return; } @@ -270,6 +279,7 @@ export class InputsController { } } + // added source so when we repeat the key, we can also tell from which source the key is from setLastProcessedMovementTime(button: Button, source: String = 'keyboard'): void { if (!this.interactions.hasOwnProperty(button)) return; this.setButtonLock(button); diff --git a/src/system/settings.ts b/src/system/settings.ts index b0c61fabf..f25356dc7 100644 --- a/src/system/settings.ts +++ b/src/system/settings.ts @@ -149,6 +149,8 @@ export function setSetting(scene: BattleScene, setting: Setting, value: integer) return false; break; case Setting.Gamepad_Support: + // if we change the value of the gamepad support, we call a method in the inputController to + // activate or deactivate the controller listener scene.inputController.setGamepadSupport(settingOptions[setting][value] !== 'Disabled'); break; case Setting.Swap_A_and_B: From b58a444f805914b7a4fa1ed85aba210fad18c138 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Thu, 9 May 2024 12:55:00 +0200 Subject: [PATCH 09/81] remove localStorage for gamepads connected + fix lost focus effect on controller change --- src/inputs-controller.ts | 29 +++++++++++++++++---------- src/system/game-data.ts | 13 ++---------- src/system/settings-gamepad.ts | 11 ++++------ src/ui/settings-gamepad-ui-handler.ts | 1 - 4 files changed, 24 insertions(+), 30 deletions(-) diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 94bba5dac..9e2be0e2a 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -39,7 +39,7 @@ export class InputsController { private gamepadSupport: boolean = true; public customGamepadMapping = new Map(); - public chosenGamepad; + public chosenGamepad: String; constructor(scene: Phaser.Scene) { this.scene = scene; @@ -74,7 +74,6 @@ export class InputsController { this.scene.input.gamepad.on('connected', function (thisGamepad) { this.refreshGamepads(); this.setupGamepad(thisGamepad); - this.populateSetting(); }, this); // Check to see if the gamepad has already been setup by the browser @@ -108,6 +107,7 @@ export class InputsController { } setChosenGamepad(gamepad: String): void { + this.deactivatePressedKey(); this.chosenGamepad = gamepad; } @@ -119,7 +119,10 @@ export class InputsController { this.repeatInputDurationJustPassed(b) && this.interactions[b].isPressed ) { - if (!this.gamepadSupport && this.interactions[b].source === 'gamepad') { + if ( + (!this.gamepadSupport && this.interactions[b].source === 'gamepad') || + (this.interactions[b].sourceName !== null && this.interactions[b].sourceName !== this.chosenGamepad) + ) { this.delLastProcessedMovementTime(b); return; } @@ -127,22 +130,23 @@ export class InputsController { controller_type: this.interactions[b].source, button: b, }); - this.setLastProcessedMovementTime(b, this.interactions[b].source); + this.setLastProcessedMovementTime(b, this.interactions[b].source, this.interactions[b].sourceName); } } } - populateSetting(): void { - const gamepadsName = this.gamepads.map(g => g.id); - localStorage.setItem('gamepadsConnected', JSON.stringify(gamepadsName)); - if (!this.chosenGamepad) this.chosenGamepad = gamepadsName[0]; - localStorage.setItem('chosenGamepad', this.chosenGamepad); + getGamepadsName(): Array { + return this.gamepads.map(g => g.id); } setupGamepad(thisGamepad: Phaser.Input.Gamepad.Gamepad): void { let gamepadID = this.chosenGamepad?.toLowerCase() || thisGamepad.id.toLowerCase(); const mappedPad = this.mapGamepad(gamepadID); this.player['mapping'] = mappedPad.gamepadMapping; + if (!this.chosenGamepad) { + this.chosenGamepad = thisGamepad.id; + localStorage.setItem('chosenGamepad', this.chosenGamepad); + } } refreshGamepads(): void { @@ -189,7 +193,7 @@ export class InputsController { controller_type: 'gamepad', button: buttonDown, }); - this.setLastProcessedMovementTime(buttonDown, 'gamepad'); + this.setLastProcessedMovementTime(buttonDown, 'gamepad', pad.id); } } @@ -289,12 +293,13 @@ export class InputsController { } } - setLastProcessedMovementTime(button: Button, source: String = 'keyboard'): void { + setLastProcessedMovementTime(button: Button, source: String = 'keyboard', sourceName: String): void { if (!this.interactions.hasOwnProperty(button)) return; this.setButtonLock(button); this.interactions[button].pressTime = this.time.now; this.interactions[button].isPressed = true; this.interactions[button].source = source; + this.interactions[button].sourceName = sourceName; } delLastProcessedMovementTime(button: Button): void { @@ -303,6 +308,7 @@ export class InputsController { this.interactions[button].pressTime = null; this.interactions[button].isPressed = false; this.interactions[button].source = null; + this.interactions[button].sourceName = null; } deactivatePressedKey(): void { @@ -313,6 +319,7 @@ export class InputsController { this.interactions[b].pressTime = null; this.interactions[b].isPressed = false; this.interactions[b].source = null; + this.interactions[b].sourceName = null; } } } diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 61139fe62..125720475 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -487,11 +487,7 @@ export class GameData { if (localStorage.hasOwnProperty('settingsGamepad')) settingsGamepad = JSON.parse(localStorage.getItem('settingsGamepad')); - let gamepadsConnected = null; - if (localStorage.hasOwnProperty('gamepadsConnected')) { - gamepadsConnected = JSON.parse(localStorage.getItem('gamepadsConnected')); - } - setSettingGamepad(this.scene, setting as SettingGamepad, valueIndex, gamepadsConnected); + setSettingGamepad(this.scene, setting as SettingGamepad, valueIndex); Object.keys(settingGamepadDefaults).forEach(s => { if (s === setting) settingsGamepad[s] = valueIndex; @@ -517,15 +513,10 @@ export class GameData { if (!localStorage.hasOwnProperty('settingsGamepad')) return false; - - let gamepadConnected = null; - if (localStorage.hasOwnProperty('gamepadConnected')) { - gamepadConnected = JSON.parse(localStorage.getItem('gamepadConnected')); - } const settingsGamepad = JSON.parse(localStorage.getItem('settingsGamepad')); for (let setting of Object.keys(settingsGamepad)) - setSettingGamepad(this.scene, setting as SettingGamepad, settingsGamepad[setting], gamepadConnected); + setSettingGamepad(this.scene, setting as SettingGamepad, settingsGamepad[setting]); } public saveTutorialFlag(tutorial: Tutorial, flag: boolean): boolean { diff --git a/src/system/settings-gamepad.ts b/src/system/settings-gamepad.ts index 6e019442d..1b22b4dae 100644 --- a/src/system/settings-gamepad.ts +++ b/src/system/settings-gamepad.ts @@ -64,16 +64,12 @@ function truncateString(str: String, maxLength: number = 10) { return str; } -export function setSettingGamepad(scene: BattleScene, setting: SettingGamepad, value: integer, gamepads?: Array): boolean { +export function setSettingGamepad(scene: BattleScene, setting: SettingGamepad, value: integer): boolean { switch (setting) { case SettingGamepad.Gamepad_Support: - console.log('setting:', setting, settingGamepadOptions[setting][value]); scene.inputController.setGamepadSupport(settingGamepadOptions[setting][value] !== 'Disabled'); break; case SettingGamepad.Swap_A_and_B: - console.log('settingGamepadOptions[setting][value]:', settingGamepadOptions[setting][value]); - console.log('settingGamepadOptions[setting]:', settingGamepadOptions[setting]); - console.log('value:', value); scene.abSwapped = settingGamepadOptions[setting][value] !== 'Disabled'; break; // case SettingGamepad.Button_Action: @@ -92,7 +88,8 @@ export function setSettingGamepad(scene: BattleScene, setting: SettingGamepad, v // break; case SettingGamepad.Default_Controller: if (value) { - if (scene.ui && gamepads) { + const gp = scene.inputController.getGamepadsName(); + if (scene.ui && gp) { const cancelHandler = () => { scene.ui.revertMode(); (scene.ui.getHandler() as SettingsGamepadUiHandler).setOptionCursor(Object.values(SettingGamepad).indexOf(SettingGamepad.Default_Controller), 0, true); @@ -105,7 +102,7 @@ export function setSettingGamepad(scene: BattleScene, setting: SettingGamepad, v return true; }; scene.ui.setOverlayMode(Mode.OPTION_SELECT, { - options: [...gamepads.map((g) => ({label: truncateString(g, 30), handler: () => changeGamepadHandler(g)})), { + options: [...gp.map((g) => ({label: truncateString(g, 30), handler: () => changeGamepadHandler(g)})), { label: 'Cancel', handler: cancelHandler, }] diff --git a/src/ui/settings-gamepad-ui-handler.ts b/src/ui/settings-gamepad-ui-handler.ts index 8fccdae15..ca56a7d94 100644 --- a/src/ui/settings-gamepad-ui-handler.ts +++ b/src/ui/settings-gamepad-ui-handler.ts @@ -116,7 +116,6 @@ export default class SettingsGamepadUiHandler extends UiHandler { super.show(args); const settings: object = localStorage.hasOwnProperty('settingsGamepad') ? JSON.parse(localStorage.getItem('settingsGamepad')) : {}; - console.log('from here'); Object.keys(settingGamepadDefaults).forEach((setting, s) => this.setOptionCursor(s, settings.hasOwnProperty(setting) ? settings[setting] : settingGamepadDefaults[setting])); this.settingsContainer.setVisible(true); From d34d48f1b55d63282c4f17a0d8a14bcdb2b870f2 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Thu, 9 May 2024 17:02:07 +0200 Subject: [PATCH 10/81] remove localStorage for gamepads connected + fix lost focus effect on controller change + disconnect/reconnect handling --- src/inputs-controller.ts | 101 +++++++++++++++++++------- src/system/settings-gamepad.ts | 9 +-- src/ui/settings-gamepad-ui-handler.ts | 10 +++ src/utils.ts | 9 ++- 4 files changed, 92 insertions(+), 37 deletions(-) diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 9e2be0e2a..e887a7e6c 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -6,6 +6,8 @@ import pad_unlicensedSNES from "./configs/pad_unlicensedSNES"; import pad_xbox360 from "./configs/pad_xbox360"; import pad_dualshock from "./configs/pad_dualshock"; import {Button} from "./enums/buttons"; +import {Mode} from "./ui/ui"; +import SettingsGamepadUiHandler from "./ui/settings-gamepad-ui-handler"; export interface GamepadMapping { [key: string]: number; @@ -34,17 +36,19 @@ export class InputsController { private buttonLock2: Button; private interactions: Map> = new Map(); private time: Time; - private player: Map = new Map(); + private player; private gamepadSupport: boolean = true; public customGamepadMapping = new Map(); public chosenGamepad: String; + private disconnectedGamepads: Array = new Array(); constructor(scene: Phaser.Scene) { this.scene = scene; this.time = this.scene.time; this.buttonKeys = []; + this.player = {}; for (const b of Utils.getEnumValues(Button)) { this.interactions[b] = { @@ -74,6 +78,10 @@ export class InputsController { this.scene.input.gamepad.on('connected', function (thisGamepad) { this.refreshGamepads(); this.setupGamepad(thisGamepad); + this.onReconnect(thisGamepad); + }, this); + this.scene.input.gamepad.on('disconnected', function (thisGamepad) { + this.onDisconnect(thisGamepad); }, this); // Check to see if the gamepad has already been setup by the browser @@ -108,7 +116,7 @@ export class InputsController { setChosenGamepad(gamepad: String): void { this.deactivatePressedKey(); - this.chosenGamepad = gamepad; + this.initChosenGamepad(gamepad) } update(): void { @@ -136,17 +144,52 @@ export class InputsController { } getGamepadsName(): Array { - return this.gamepads.map(g => g.id); + return this.gamepads.filter(g => !this.disconnectedGamepads.includes(g.id)).map(g => g.id); + } + + initChosenGamepad(gamepadName?: String): void { + let name = gamepadName; + if (gamepadName) + this.chosenGamepad = gamepadName; + else + name = this.chosenGamepad; + localStorage.setItem('chosenGamepad', name); + const handler = this.scene.ui?.handlers[Mode.SETTINGS_GAMEPAD] as SettingsGamepadUiHandler; + handler && handler.updateChosenGamepadDisplay() + } + + clearChosenGamepad() { + this.chosenGamepad = null; + if (localStorage.hasOwnProperty('chosenGamepad')) + localStorage.removeItem('chosenGamepad'); + } + + onDisconnect(thisGamepad: Phaser.Input.Gamepad.Gamepad): void { + this.disconnectedGamepads.push(thisGamepad.id); + const gamepadsLeft = this.gamepads.filter(g => !this.disconnectedGamepads.includes(g.id)).map(g => g); + const chosenIsConnected = gamepadsLeft.some(g => g.id === this.chosenGamepad); + if (!chosenIsConnected && gamepadsLeft?.length) { + this.clearChosenGamepad(); + this.setChosenGamepad(gamepadsLeft[0].id); + return; + } + } + + onReconnect(thisGamepad: Phaser.Input.Gamepad.Gamepad): void { + if (this.disconnectedGamepads.some(g => g === thisGamepad.id)) { + this.disconnectedGamepads = this.disconnectedGamepads.filter(g => g !== thisGamepad.id); + } } setupGamepad(thisGamepad: Phaser.Input.Gamepad.Gamepad): void { - let gamepadID = this.chosenGamepad?.toLowerCase() || thisGamepad.id.toLowerCase(); - const mappedPad = this.mapGamepad(gamepadID); - this.player['mapping'] = mappedPad.gamepadMapping; - if (!this.chosenGamepad) { - this.chosenGamepad = thisGamepad.id; - localStorage.setItem('chosenGamepad', this.chosenGamepad); + const allGamepads = this.getGamepadsName(); + for (const gamepad of allGamepads) { + const gamepadID = gamepad.toLowerCase(); + const mappedPad = this.mapGamepad(gamepadID); + if (!this.player[gamepad]) this.player[gamepad] = {}; + this.player[gamepad]['mapping'] = mappedPad.gamepadMapping; } + if (this.chosenGamepad === thisGamepad.id) this.initChosenGamepad(this.chosenGamepad) } refreshGamepads(): void { @@ -162,29 +205,31 @@ export class InputsController { getActionGamepadMapping(): ActionGamepadMapping { const gamepadMapping = {}; - if (!this.player?.mapping) return gamepadMapping; - gamepadMapping[this.player.mapping.LC_N] = Button.UP; - gamepadMapping[this.player.mapping.LC_S] = Button.DOWN; - gamepadMapping[this.player.mapping.LC_W] = Button.LEFT; - gamepadMapping[this.player.mapping.LC_E] = Button.RIGHT; - gamepadMapping[this.player.mapping.TOUCH] = Button.SUBMIT; - gamepadMapping[this.player.mapping.RC_S] = this.scene.abSwapped ? Button.CANCEL : Button.ACTION; - gamepadMapping[this.player.mapping.RC_E] = this.scene.abSwapped ? Button.ACTION : Button.CANCEL; - gamepadMapping[this.player.mapping.SELECT] = Button.STATS; - gamepadMapping[this.player.mapping.START] = Button.MENU; - gamepadMapping[this.player.mapping.RB] = Button.RB; - gamepadMapping[this.player.mapping.LB] = Button.LB; - gamepadMapping[this.player.mapping.LT] = Button.CYCLE_GENDER; - gamepadMapping[this.player.mapping.RT] = Button.CYCLE_ABILITY; - gamepadMapping[this.player.mapping.RC_W] = Button.CYCLE_NATURE; - gamepadMapping[this.player.mapping.RC_N] = Button.CYCLE_VARIANT; - gamepadMapping[this.player.mapping.LS] = Button.SPEED_UP; - gamepadMapping[this.player.mapping.RS] = Button.SLOW_DOWN; + if (!this.player[this.chosenGamepad] || !this.player[this.chosenGamepad]?.mapping || !this.chosenGamepad) return gamepadMapping; + gamepadMapping[this.player[this.chosenGamepad].mapping.LC_N] = Button.UP; + gamepadMapping[this.player[this.chosenGamepad].mapping.LC_S] = Button.DOWN; + gamepadMapping[this.player[this.chosenGamepad].mapping.LC_W] = Button.LEFT; + gamepadMapping[this.player[this.chosenGamepad].mapping.LC_E] = Button.RIGHT; + gamepadMapping[this.player[this.chosenGamepad].mapping.TOUCH] = Button.SUBMIT; + gamepadMapping[this.player[this.chosenGamepad].mapping.RC_S] = this.scene.abSwapped ? Button.CANCEL : Button.ACTION; + gamepadMapping[this.player[this.chosenGamepad].mapping.RC_E] = this.scene.abSwapped ? Button.ACTION : Button.CANCEL; + gamepadMapping[this.player[this.chosenGamepad].mapping.SELECT] = Button.STATS; + gamepadMapping[this.player[this.chosenGamepad].mapping.START] = Button.MENU; + gamepadMapping[this.player[this.chosenGamepad].mapping.RB] = Button.RB; + gamepadMapping[this.player[this.chosenGamepad].mapping.LB] = Button.LB; + gamepadMapping[this.player[this.chosenGamepad].mapping.LT] = Button.CYCLE_GENDER; + gamepadMapping[this.player[this.chosenGamepad].mapping.RT] = Button.CYCLE_ABILITY; + gamepadMapping[this.player[this.chosenGamepad].mapping.RC_W] = Button.CYCLE_NATURE; + gamepadMapping[this.player[this.chosenGamepad].mapping.RC_N] = Button.CYCLE_VARIANT; + gamepadMapping[this.player[this.chosenGamepad].mapping.LS] = Button.SPEED_UP; + gamepadMapping[this.player[this.chosenGamepad].mapping.RS] = Button.SLOW_DOWN; return gamepadMapping; } gamepadButtonDown(pad: Phaser.Input.Gamepad.Gamepad, button: Phaser.Input.Gamepad.Button, value: number): void { + if (!this.chosenGamepad) + this.setChosenGamepad(pad.id); if (!this.gamepadSupport || pad.id.toLowerCase() !== this.chosenGamepad.toLowerCase()) return; const actionMapping = this.getActionGamepadMapping(); const buttonDown = actionMapping.hasOwnProperty(button.index) && actionMapping[button.index]; @@ -198,7 +243,7 @@ export class InputsController { } gamepadButtonUp(pad: Phaser.Input.Gamepad.Gamepad, button: Phaser.Input.Gamepad.Button, value: number): void { - if (!this.gamepadSupport || pad.id.toLowerCase() !== this.chosenGamepad.toLowerCase()) return; + if (!this.gamepadSupport || pad.id !== this.chosenGamepad) return; const actionMapping = this.getActionGamepadMapping(); const buttonUp = actionMapping.hasOwnProperty(button.index) && actionMapping[button.index]; if (buttonUp !== undefined) { diff --git a/src/system/settings-gamepad.ts b/src/system/settings-gamepad.ts index 1b22b4dae..e22cbd71e 100644 --- a/src/system/settings-gamepad.ts +++ b/src/system/settings-gamepad.ts @@ -2,6 +2,7 @@ import BattleScene from "../battle-scene"; import {SettingDefaults, SettingOptions} from "#app/system/settings"; import SettingsGamepadUiHandler from "#app/ui/settings-gamepad-ui-handler"; import {Mode} from "#app/ui/ui"; +import {truncateString} from "../utils"; export enum SettingGamepad { Default_Controller = "DEFAULT_CONTROLLER", @@ -57,13 +58,6 @@ export const settingGamepadDefaults: SettingDefaults = { // [SettingGamepad.Button_Slow_Down]: Button.SLOW_DOWN, }; -function truncateString(str: String, maxLength: number = 10) { - if (str.length > maxLength) { - return str.slice(0, maxLength - 3) + "..."; // Subtract 3 to accommodate the ellipsis - } - return str; -} - export function setSettingGamepad(scene: BattleScene, setting: SettingGamepad, value: integer): boolean { switch (setting) { case SettingGamepad.Gamepad_Support: @@ -97,7 +91,6 @@ export function setSettingGamepad(scene: BattleScene, setting: SettingGamepad, v }; const changeGamepadHandler = (gamepad: string) => { scene.inputController.setChosenGamepad(gamepad); - localStorage.setItem('chosenGamepad', gamepad); cancelHandler(); return true; }; diff --git a/src/ui/settings-gamepad-ui-handler.ts b/src/ui/settings-gamepad-ui-handler.ts index ca56a7d94..98a3ad991 100644 --- a/src/ui/settings-gamepad-ui-handler.ts +++ b/src/ui/settings-gamepad-ui-handler.ts @@ -10,6 +10,7 @@ import { settingGamepadDefaults, settingGamepadOptions } from "../system/settings-gamepad"; +import {truncateString} from "../utils"; export default class SettingsGamepadUiHandler extends UiHandler { private settingsContainer: Phaser.GameObjects.Container; @@ -193,6 +194,15 @@ export default class SettingsGamepadUiHandler extends UiHandler { return ret; } + updateChosenGamepadDisplay(): void { + for (const [index, key] of Object.keys(SettingGamepad).entries()) { + const setting = SettingGamepad[key] + if (setting === SettingGamepad.Default_Controller) { + this.optionValueLabels[index][0].setText(truncateString(this.scene.inputController.chosenGamepad, 30)); + } + } + } + setOptionCursor(settingIndex: integer, cursor: integer, save?: boolean): boolean { const setting = SettingGamepad[Object.keys(SettingGamepad)[settingIndex]]; diff --git a/src/utils.ts b/src/utils.ts index ef277630d..01656b8ed 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -356,4 +356,11 @@ export function rgbHexToRgba(hex: string) { export function rgbaToInt(rgba: integer[]): integer { return (rgba[0] << 24) + (rgba[1] << 16) + (rgba[2] << 8) + rgba[3]; -} \ No newline at end of file +} + +export function truncateString(str: String, maxLength: number = 10) { + if (str.length > maxLength) { + return str.slice(0, maxLength - 3) + "..."; // Subtract 3 to accommodate the ellipsis + } + return str; +} From ecbcb0bf7074674b39f92c44d18abe5e6e696d7e Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Thu, 9 May 2024 17:14:59 +0200 Subject: [PATCH 11/81] added some comment --- src/inputs-controller.ts | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index e887a7e6c..7562a2b49 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -66,8 +66,10 @@ export class InputsController { init(): void { this.events = new Phaser.Events.EventEmitter(); + // at the launch, we retrieved the previously chosen gamepad if (localStorage.hasOwnProperty('chosenGamepad')) { this.chosenGamepad = localStorage.getItem('chosenGamepad'); + this.initChosenGamepad(this.chosenGamepad, false) } // Handle the game losing focus this.scene.game.events.on(Phaser.Core.Events.BLUR, () => { @@ -80,8 +82,9 @@ export class InputsController { this.setupGamepad(thisGamepad); this.onReconnect(thisGamepad); }, this); + this.scene.input.gamepad.on('disconnected', function (thisGamepad) { - this.onDisconnect(thisGamepad); + this.onDisconnect(thisGamepad); // when a gamepad is disconnected }, this); // Check to see if the gamepad has already been setup by the browser @@ -147,13 +150,16 @@ export class InputsController { return this.gamepads.filter(g => !this.disconnectedGamepads.includes(g.id)).map(g => g.id); } - initChosenGamepad(gamepadName?: String): void { + initChosenGamepad(gamepadName?: String, save: boolean = true): void { + // if we have a gamepad name in parameter, we set the chosen gamepad with this value let name = gamepadName; if (gamepadName) this.chosenGamepad = gamepadName; else - name = this.chosenGamepad; - localStorage.setItem('chosenGamepad', name); + name = this.chosenGamepad; // otherwise we use the chosen gamepad's name + if (save) // we always set the session variable unless it's called from init() + localStorage.setItem('chosenGamepad', name); + // we update the ui with the chosen gamepad const handler = this.scene.ui?.handlers[Mode.SETTINGS_GAMEPAD] as SettingsGamepadUiHandler; handler && handler.updateChosenGamepadDisplay() } @@ -165,31 +171,43 @@ export class InputsController { } onDisconnect(thisGamepad: Phaser.Input.Gamepad.Gamepad): void { + // We need to add the disconnected gamepad into a local array + // Because Phaser keep in memory the previously connected gamepad + // If we don't do that, we have no way to determine if the gamepad is connected or not. + // We want to know that because we want to hide it in the selection menu of gamepad to use this.disconnectedGamepads.push(thisGamepad.id); + // we look for gamepads still connected by substracting the 2 arrays const gamepadsLeft = this.gamepads.filter(g => !this.disconnectedGamepads.includes(g.id)).map(g => g); + // we check if the chosen gamepad is still connected const chosenIsConnected = gamepadsLeft.some(g => g.id === this.chosenGamepad); + // if the chosen gamepad is disconnected, and we got others gamepad connected if (!chosenIsConnected && gamepadsLeft?.length) { + // We remove the previously chosen gamepad this.clearChosenGamepad(); + // and we set the first of the gamepad still connected as the chosen one. this.setChosenGamepad(gamepadsLeft[0].id); return; } } onReconnect(thisGamepad: Phaser.Input.Gamepad.Gamepad): void { - if (this.disconnectedGamepads.some(g => g === thisGamepad.id)) { - this.disconnectedGamepads = this.disconnectedGamepads.filter(g => g !== thisGamepad.id); - } + // We check if a gamepad reconnect by looking in the disconnectedGamepads array if is there + // If he is there, we remove it. + this.disconnectedGamepads = this.disconnectedGamepads.filter(g => g !== thisGamepad.id); + // if (this.disconnectedGamepads.some(g => g === thisGamepad.id)) { + // } } setupGamepad(thisGamepad: Phaser.Input.Gamepad.Gamepad): void { + // we fetch all the gamepads name const allGamepads = this.getGamepadsName(); for (const gamepad of allGamepads) { + // for each gamepad, we set its mapping in this.player const gamepadID = gamepad.toLowerCase(); const mappedPad = this.mapGamepad(gamepadID); if (!this.player[gamepad]) this.player[gamepad] = {}; this.player[gamepad]['mapping'] = mappedPad.gamepadMapping; } - if (this.chosenGamepad === thisGamepad.id) this.initChosenGamepad(this.chosenGamepad) } refreshGamepads(): void { @@ -228,7 +246,7 @@ export class InputsController { } gamepadButtonDown(pad: Phaser.Input.Gamepad.Gamepad, button: Phaser.Input.Gamepad.Button, value: number): void { - if (!this.chosenGamepad) + if (!this.chosenGamepad) // at the very first input, if we have not yet a chosen gamepad, we set it this.setChosenGamepad(pad.id); if (!this.gamepadSupport || pad.id.toLowerCase() !== this.chosenGamepad.toLowerCase()) return; const actionMapping = this.getActionGamepadMapping(); From 22e7845480afffd4ec7e64d156ccdcbfbe6880fd Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Thu, 9 May 2024 17:38:59 +0200 Subject: [PATCH 12/81] fix changes made while commenting --- src/inputs-controller.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 7562a2b49..b13126a1f 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -69,7 +69,6 @@ export class InputsController { // at the launch, we retrieved the previously chosen gamepad if (localStorage.hasOwnProperty('chosenGamepad')) { this.chosenGamepad = localStorage.getItem('chosenGamepad'); - this.initChosenGamepad(this.chosenGamepad, false) } // Handle the game losing focus this.scene.game.events.on(Phaser.Core.Events.BLUR, () => { @@ -150,15 +149,14 @@ export class InputsController { return this.gamepads.filter(g => !this.disconnectedGamepads.includes(g.id)).map(g => g.id); } - initChosenGamepad(gamepadName?: String, save: boolean = true): void { + initChosenGamepad(gamepadName?: String): void { // if we have a gamepad name in parameter, we set the chosen gamepad with this value let name = gamepadName; if (gamepadName) this.chosenGamepad = gamepadName; else name = this.chosenGamepad; // otherwise we use the chosen gamepad's name - if (save) // we always set the session variable unless it's called from init() - localStorage.setItem('chosenGamepad', name); + localStorage.setItem('chosenGamepad', name); // we update the ui with the chosen gamepad const handler = this.scene.ui?.handlers[Mode.SETTINGS_GAMEPAD] as SettingsGamepadUiHandler; handler && handler.updateChosenGamepadDisplay() @@ -194,8 +192,6 @@ export class InputsController { // We check if a gamepad reconnect by looking in the disconnectedGamepads array if is there // If he is there, we remove it. this.disconnectedGamepads = this.disconnectedGamepads.filter(g => g !== thisGamepad.id); - // if (this.disconnectedGamepads.some(g => g === thisGamepad.id)) { - // } } setupGamepad(thisGamepad: Phaser.Input.Gamepad.Gamepad): void { @@ -208,6 +204,7 @@ export class InputsController { if (!this.player[gamepad]) this.player[gamepad] = {}; this.player[gamepad]['mapping'] = mappedPad.gamepadMapping; } + if (this.chosenGamepad === thisGamepad.id) this.initChosenGamepad(this.chosenGamepad) } refreshGamepads(): void { From 3d668080162d9cbc0e926630e9225be5d7144913 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Thu, 9 May 2024 20:21:26 +0200 Subject: [PATCH 13/81] fix visual selection of default gamepad --- src/ui/settings-gamepad-ui-handler.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ui/settings-gamepad-ui-handler.ts b/src/ui/settings-gamepad-ui-handler.ts index 98a3ad991..77caa34f3 100644 --- a/src/ui/settings-gamepad-ui-handler.ts +++ b/src/ui/settings-gamepad-ui-handler.ts @@ -117,7 +117,9 @@ export default class SettingsGamepadUiHandler extends UiHandler { super.show(args); const settings: object = localStorage.hasOwnProperty('settingsGamepad') ? JSON.parse(localStorage.getItem('settingsGamepad')) : {}; - Object.keys(settingGamepadDefaults).forEach((setting, s) => this.setOptionCursor(s, settings.hasOwnProperty(setting) ? settings[setting] : settingGamepadDefaults[setting])); + // in the menu, for each line, we set the cursor position for each option, either on the previously selected, or the default value. + // if it's the default gamepad, we always want it by default to be on the very first option. never on "Change" + Object.keys(settingGamepadDefaults).forEach((setting, s) => this.setOptionCursor(s, settings.hasOwnProperty(setting) ? ( setting === SettingGamepad.Default_Controller ? 0 : settings[setting]) : settingGamepadDefaults[setting])); this.settingsContainer.setVisible(true); this.setCursor(0); From 59832c6befb76d41ad4a18433f28f55baf8a6bb5 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Thu, 9 May 2024 20:26:00 +0200 Subject: [PATCH 14/81] merge with branch fix lost focus on disabled gamepad --- src/system/settings-gamepad.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/system/settings-gamepad.ts b/src/system/settings-gamepad.ts index e22cbd71e..a9ccc6090 100644 --- a/src/system/settings-gamepad.ts +++ b/src/system/settings-gamepad.ts @@ -61,6 +61,8 @@ export const settingGamepadDefaults: SettingDefaults = { export function setSettingGamepad(scene: BattleScene, setting: SettingGamepad, value: integer): boolean { switch (setting) { case SettingGamepad.Gamepad_Support: + // if we change the value of the gamepad support, we call a method in the inputController to + // activate or deactivate the controller listener scene.inputController.setGamepadSupport(settingGamepadOptions[setting][value] !== 'Disabled'); break; case SettingGamepad.Swap_A_and_B: From 0d2eb1d9832dfd684b69060a3a3abcbf96d70ee0 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Thu, 9 May 2024 23:23:09 +0200 Subject: [PATCH 15/81] working button detection in menu --- src/inputs-controller.ts | 11 ++++ src/system/settings-gamepad.ts | 44 ++++++++++---- src/ui/gamepad-binding-ui-handler.ts | 87 +++++++++++++++++++++++++++ src/ui/settings-gamepad-ui-handler.ts | 17 +++--- src/ui/ui.ts | 5 ++ 5 files changed, 145 insertions(+), 19 deletions(-) create mode 100644 src/ui/gamepad-binding-ui-handler.ts diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 9c24c3cfd..940464975 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -8,6 +8,7 @@ import pad_dualshock from "./configs/pad_dualshock"; import {Button} from "./enums/buttons"; import {Mode} from "./ui/ui"; import SettingsGamepadUiHandler from "./ui/settings-gamepad-ui-handler"; +import {SettingGamepad} from "./system/settings-gamepad"; export interface GamepadMapping { [key: string]: number; @@ -252,6 +253,12 @@ export class InputsController { return gamepadMapping; } + getButtonLabel(button: Phaser.Input.Gamepad.Button) { + const mapping = this.player[this.chosenGamepad]['mapping']; + console.log('mapping', mapping); + return Object.keys(mapping).find(key => mapping[key] === button.index); + } + gamepadButtonDown(pad: Phaser.Input.Gamepad.Gamepad, button: Phaser.Input.Gamepad.Button, value: number): void { if (!this.chosenGamepad) // at the very first input, if we have not yet a chosen gamepad, we set it this.setChosenGamepad(pad.id); @@ -411,4 +418,8 @@ export class InputsController { if (this.buttonLock === button) this.buttonLock = null; else if (this.buttonLock2 === button) this.buttonLock2 = null; } + + setBind(setting: SettingGamepad, button: Button) { + console.log('button,', button); + } } \ No newline at end of file diff --git a/src/system/settings-gamepad.ts b/src/system/settings-gamepad.ts index a9ccc6090..ce4fa77ea 100644 --- a/src/system/settings-gamepad.ts +++ b/src/system/settings-gamepad.ts @@ -1,15 +1,16 @@ import BattleScene from "../battle-scene"; -import {SettingDefaults, SettingOptions} from "#app/system/settings"; -import SettingsGamepadUiHandler from "#app/ui/settings-gamepad-ui-handler"; +import {SettingDefaults, SettingOptions} from "./settings"; +import SettingsGamepadUiHandler from "../ui/settings-gamepad-ui-handler"; import {Mode} from "#app/ui/ui"; import {truncateString} from "../utils"; +import {Button} from "../enums/buttons"; export enum SettingGamepad { Default_Controller = "DEFAULT_CONTROLLER", Gamepad_Support = "GAMEPAD_SUPPORT", Swap_A_and_B = "SWAP_A_B", // Swaps which gamepad button handles ACTION and CANCEL - // Button_Action = "BUTTON_ACTION", - // Button_Cancel = "BUTTON_CANCEL", + Button_Action = "BUTTON_ACTION", + Button_Cancel = "BUTTON_CANCEL", // Button_Menu = "BUTTON_MENU", // Button_Stats = "BUTTON_STATS", // Button_Cycle_Shiny = "BUTTON_CYCLE_SHINY", @@ -26,8 +27,8 @@ export const settingGamepadOptions: SettingOptions = { [SettingGamepad.Default_Controller]: [ 'Default', 'Change' ], [SettingGamepad.Gamepad_Support]: [ 'Auto', 'Disabled' ], [SettingGamepad.Swap_A_and_B]: [ 'Enabled', 'Disabled' ], - // [SettingGamepad.Button_Action]: [`KEY ${Button.ACTION.toString()}`, 'Change'], - // [SettingGamepad.Button_Cancel]: [`KEY ${Button.CANCEL.toString()}`, 'Change'], + [SettingGamepad.Button_Action]: [`KEY ${Button.ACTION.toString()}`, 'Change'], + [SettingGamepad.Button_Cancel]: [`KEY ${Button.CANCEL.toString()}`, 'Change'], // [SettingGamepad.Button_Menu]: [`KEY ${Button.MENU.toString()}`, 'Change'], // [SettingGamepad.Button_Stats]: [`KEY ${Button.STATS.toString()}`, 'Change'], // [SettingGamepad.Button_Cycle_Shiny]: [`KEY ${Button.RB.toString()}`, 'Change'], @@ -44,8 +45,8 @@ export const settingGamepadDefaults: SettingDefaults = { [SettingGamepad.Default_Controller]: 0, [SettingGamepad.Gamepad_Support]: 0, [SettingGamepad.Swap_A_and_B]: 1, // Set to 'Disabled' by default - // [SettingGamepad.Button_Action]: Button.ACTION, - // [SettingGamepad.Button_Cancel]: Button.CANCEL, + [SettingGamepad.Button_Action]: 0, + [SettingGamepad.Button_Cancel]: 0, // [SettingGamepad.Button_Menu]: Button.MENU, // [SettingGamepad.Button_Stats]: Button.STATS, // [SettingGamepad.Button_Cycle_Shiny]: Button.RB, @@ -58,6 +59,11 @@ export const settingGamepadDefaults: SettingDefaults = { // [SettingGamepad.Button_Slow_Down]: Button.SLOW_DOWN, }; +export const noOptionsCursors : Array = [ + SettingGamepad.Button_Action, + SettingGamepad.Button_Cancel, +]; + export function setSettingGamepad(scene: BattleScene, setting: SettingGamepad, value: integer): boolean { switch (setting) { case SettingGamepad.Gamepad_Support: @@ -68,8 +74,8 @@ export function setSettingGamepad(scene: BattleScene, setting: SettingGamepad, v case SettingGamepad.Swap_A_and_B: scene.abSwapped = settingGamepadOptions[setting][value] !== 'Disabled'; break; - // case SettingGamepad.Button_Action: - // case SettingGamepad.Button_Cancel: + case SettingGamepad.Button_Action: + case SettingGamepad.Button_Cancel: // case SettingGamepad.Button_Menu: // case SettingGamepad.Button_Stats: // case SettingGamepad.Button_Cycle_Shiny: @@ -80,8 +86,22 @@ export function setSettingGamepad(scene: BattleScene, setting: SettingGamepad, v // case SettingGamepad.Button_Cycle_Variant: // case SettingGamepad.Button_Speed_Up: // case SettingGamepad.Button_Slow_Down: - // scene.inputController.customGamepadMapping[setting] = value; - // break; + if (value) { + if (scene.ui) { + const cancelHandler = () => { + scene.ui.revertMode(); + (scene.ui.getHandler() as SettingsGamepadUiHandler).setOptionCursor(Object.values(SettingGamepad).indexOf(setting), 0, true); + return false; + }; + scene.ui.setOverlayMode(Mode.GAMEPAD_BINDING, { + options: [{ + label: 'Press a button on your gamepad', + handler: cancelHandler, + }] + }); + } + } + break; case SettingGamepad.Default_Controller: if (value) { const gp = scene.inputController.getGamepadsName(); diff --git a/src/ui/gamepad-binding-ui-handler.ts b/src/ui/gamepad-binding-ui-handler.ts new file mode 100644 index 000000000..6f1422e67 --- /dev/null +++ b/src/ui/gamepad-binding-ui-handler.ts @@ -0,0 +1,87 @@ +import UiHandler from "./ui-handler"; +import BattleScene from "#app/battle-scene"; +import {Mode} from "./ui"; +import {Button} from "../enums/buttons"; +import {addWindow} from "./ui-theme"; +import {addTextObject, TextStyle} from "#app/ui/text"; +import Phaser from "phaser"; + + +export default class GamepadBindingUiHandler extends UiHandler { + protected optionSelectContainer: Phaser.GameObjects.Container; + protected optionSelectBg: Phaser.GameObjects.NineSlice; + private unlockText: Phaser.GameObjects.Text; + private keyPressed: Phaser.GameObjects.Text; + private listening: boolean = false; + private buttonPressed = ''; + + constructor(scene: BattleScene, mode: Mode = Mode.GAMEPAD_BINDING) { + super(scene, mode); + scene.input.gamepad.on('down', this.gamepadButtonDown, this); + } + + // const loadSessionBg = this.scene.add.rectangle(this.scene.game.canvas.width / 24, -this.scene.game.canvas.height / 24, this.scene.game.canvas.width / 12, -this.scene.game.canvas.height / 12, 0x006860); + // loadSessionBg.setOrigin(0, 0); + // this.optionSelectContainer.add(loadSessionBg); + setup() { + const ui = this.getUi(); + this.optionSelectContainer = this.scene.add.container(0, 0); + this.optionSelectContainer.setVisible(false); + ui.add(this.optionSelectContainer); + + // this.optionSelectBg = addWindow(this.scene, this.scene.game.canvas.width / 12, -this.scene.game.canvas.height / 12, this.getWindowWidth(), -this.getWindowHeight()); + this.optionSelectBg = addWindow(this.scene, (this.scene.game.canvas.width / 6) - this.getWindowWidth(), -(this.scene.game.canvas.height / 6) + this.getWindowHeight() + 28, this.getWindowWidth(), this.getWindowHeight()); + this.optionSelectBg.setOrigin(0.5); + this.optionSelectContainer.add(this.optionSelectBg); + + this.unlockText = addTextObject(this.scene, 0, 0, 'Press a button...', TextStyle.WINDOW); + this.unlockText.setOrigin(0, 0); + this.unlockText.setPositionRelative(this.optionSelectBg, 36, 4); + + this.keyPressed = addTextObject(this.scene, 0, 0, '', TextStyle.WINDOW); + this.keyPressed.setOrigin(0, 0); + this.keyPressed.setPositionRelative(this.unlockText, 0, 12); + this.keyPressed.setVisible(false); + + + this.optionSelectContainer.add(this.unlockText); + this.optionSelectContainer.add(this.keyPressed); + } + + gamepadButtonDown(pad: Phaser.Input.Gamepad.Gamepad, button: Phaser.Input.Gamepad.Button, value: number): void { + if (!this.listening) return; + this.buttonPressed = button.index; + const buttonLabel = this.scene.inputController.getButtonLabel(button); + this.keyPressed.setText(buttonLabel); + this.keyPressed.setVisible(true); + } + + show(args: any[]): boolean { + console.log('args', args); + super.show(args); + + this.getUi().bringToTop(this.optionSelectContainer); + + this.optionSelectContainer.setVisible(true); + setTimeout(() => this.listening = true, 150); + return true; + } + + getWindowWidth(): number { + return 160; + } + + getWindowHeight(): number { + return 64; + } + + processInput(button: Button): boolean { + const ui = this.getUi(); + return true; + } + + clear() { + super.clear(); + } + +} \ No newline at end of file diff --git a/src/ui/settings-gamepad-ui-handler.ts b/src/ui/settings-gamepad-ui-handler.ts index 77caa34f3..b044dd6f7 100644 --- a/src/ui/settings-gamepad-ui-handler.ts +++ b/src/ui/settings-gamepad-ui-handler.ts @@ -6,6 +6,7 @@ import UiHandler from "./ui-handler"; import { addWindow } from "./ui-theme"; import {Button} from "../enums/buttons"; import { + noOptionsCursors, SettingGamepad, settingGamepadDefaults, settingGamepadOptions @@ -210,14 +211,16 @@ export default class SettingsGamepadUiHandler extends UiHandler { const lastCursor = this.optionCursors[settingIndex]; - const lastValueLabel = this.optionValueLabels[settingIndex][lastCursor]; - lastValueLabel.setColor(this.getTextColor(TextStyle.WINDOW)); - lastValueLabel.setShadowColor(this.getTextColor(TextStyle.WINDOW, true)); + if (!noOptionsCursors.includes(setting)) { + 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)); + 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) { if (SettingGamepad[setting] !== SettingGamepad.Default_Controller) diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 2b2c8f756..fa2090f4a 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -37,6 +37,7 @@ import UnavailableModalUiHandler from './unavailable-modal-ui-handler'; import OutdatedModalUiHandler from './outdated-modal-ui-handler'; import SessionReloadModalUiHandler from './session-reload-modal-ui-handler'; import {Button} from "../enums/buttons"; +import GamepadBindingUiHandler from "./ui/gamepad-binding-ui-handler"; export enum Mode { MESSAGE, @@ -58,6 +59,7 @@ export enum Mode { MENU_OPTION_SELECT, SETTINGS, SETTINGS_GAMEPAD, + GAMEPAD_BINDING, ACHIEVEMENTS, GAME_STATS, VOUCHERS, @@ -88,7 +90,9 @@ const noTransitionModes = [ Mode.OPTION_SELECT, Mode.MENU, Mode.MENU_OPTION_SELECT, + Mode.GAMEPAD_BINDING, Mode.SETTINGS, + Mode.SETTINGS_GAMEPAD, Mode.ACHIEVEMENTS, Mode.GAME_STATS, Mode.VOUCHERS, @@ -140,6 +144,7 @@ export default class UI extends Phaser.GameObjects.Container { new OptionSelectUiHandler(scene, Mode.MENU_OPTION_SELECT), new SettingsUiHandler(scene), new SettingsGamepadUiHandler(scene), + new GamepadBindingUiHandler(scene), new AchvsUiHandler(scene), new GameStatsUiHandler(scene), new VouchersUiHandler(scene), From 421e28b3f858b3d1b7bfd47389ad566ce7f43cb9 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Thu, 9 May 2024 23:23:29 +0200 Subject: [PATCH 16/81] fix import --- src/ui/ui.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/ui.ts b/src/ui/ui.ts index fa2090f4a..3e6ba9795 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -37,7 +37,7 @@ import UnavailableModalUiHandler from './unavailable-modal-ui-handler'; import OutdatedModalUiHandler from './outdated-modal-ui-handler'; import SessionReloadModalUiHandler from './session-reload-modal-ui-handler'; import {Button} from "../enums/buttons"; -import GamepadBindingUiHandler from "./ui/gamepad-binding-ui-handler"; +import GamepadBindingUiHandler from "./gamepad-binding-ui-handler"; export enum Mode { MESSAGE, From 58845410e6493bd874d8cd86281891e3e72f5afd Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Fri, 10 May 2024 02:16:03 +0200 Subject: [PATCH 17/81] added full TSDoc, helped by chatGPT, refined every entry manually of course --- src/inputs-controller.ts | 280 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 260 insertions(+), 20 deletions(-) diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 552ebf222..7de096986 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -23,13 +23,33 @@ export interface ActionGamepadMapping { const repeatInputDelayMillis = 250; +/** + * Manages and handles all input controls for the game, including keyboard and gamepad interactions. + * + * @remarks + * This class is designed to centralize input management across the game. It facilitates the setup, + * configuration, and handling of all game inputs, making it easier to manage various input devices + * such as keyboards and gamepads. The class provides methods for setting up input devices, handling + * their events, and responding to changes in input state (e.g., button presses, releases). + * + * The `InputsController` class also includes mechanisms to handle game focus events to ensure input + * states are correctly reset and managed when the game loses or regains focus, maintaining robust + * and responsive control handling throughout the game's lifecycle. + * + * Key responsibilities include: + * - Initializing and configuring gamepad and keyboard controls. + * - Emitting events related to specific input actions. + * - Responding to external changes such as gamepad connection/disconnection. + * - Managing game state transitions in response to input events, particularly focus loss and recovery. + * + * Usage of this class is intended to simplify input management across various parts of the game, + * providing a unified interface for all input-related interactions. + */ export class InputsController { private buttonKeys: Phaser.Input.Keyboard.Key[][]; private gamepads: Array = new Array(); private scene: Phaser.Scene; - // buttonLock ensures only a single movement key is firing repeated inputs - // (i.e. by holding down a button) at a time private buttonLock: Button; private buttonLock2: Button; private interactions: Map> = new Map(); @@ -38,6 +58,17 @@ export class InputsController { private gamepadSupport: boolean = true; + /** + * Initializes a new instance of the game control system, setting up initial state and configurations. + * + * @param scene - The Phaser scene associated with this instance. + * + * @remarks + * This constructor initializes the game control system with necessary setups for handling inputs. + * It prepares an interactions array indexed by button identifiers and configures default states for each button. + * Specific buttons like MENU and STATS are set not to repeat their actions. + * It concludes by calling the `init` method to complete the setup. + */ constructor(scene: Phaser.Scene) { this.scene = scene; this.time = this.scene.time; @@ -56,12 +87,19 @@ export class InputsController { this.init(); } + /** + * Sets up event handlers and initializes gamepad and keyboard controls. + * + * @remarks + * This method configures event listeners for both gamepad and keyboard inputs. + * It handles gamepad connections/disconnections and button press events, and ensures keyboard controls are set up. + * Additionally, it manages the game's behavior when it loses focus to prevent unwanted game actions during this state. + */ init(): void { this.events = new Phaser.Events.EventEmitter(); - // Handle the game losing focus - this.scene.game.events.on(Phaser.Core.Events.BLUR, () => { - this.loseFocus() - }) + this.scene.game.events.on(Phaser.Core.Events.BLUR, () => { + this.loseFocus() + }) if (typeof this.scene.input.gamepad !== 'undefined') { this.scene.input.gamepad.on('connected', function (thisGamepad) { @@ -86,13 +124,25 @@ export class InputsController { this.setupKeyboardControls(); } + /** + * Handles actions to take when the game loses focus, such as deactivating pressed keys. + * + * @remarks + * This method is triggered when the game or the browser tab loses focus. It ensures that any keys pressed are deactivated to prevent stuck keys affecting gameplay when the game is not active. + */ loseFocus(): void { this.deactivatePressedKey(); } + /** + * Enables or disables support for gamepad input. + * + * @param value - A boolean indicating whether gamepad support should be enabled (true) or disabled (false). + * + * @remarks + * This method toggles gamepad support. If disabled, it also ensures that all currently pressed gamepad buttons are deactivated to avoid stuck inputs. + */ setGamepadSupport(value: boolean): void { - // value is true if we activate gamepad - // value is false if we deactivate gamepad if (value) { this.gamepadSupport = true; } else { @@ -102,24 +152,33 @@ export class InputsController { } } + /** + * Updates the interaction handling by processing input states. + * This method gives priority to certain buttons by reversing the order in which they are checked. + * + * @remarks + * The method iterates over all possible buttons, checking for specific conditions such as: + * - If the button is registered in the `interactions` dictionary. + * - If the button has been held down long enough. + * - If the button is currently pressed. + * + * Special handling is applied if gamepad support is disabled but a gamepad source is still triggering inputs, + * preventing potential infinite loops by removing the last processed movement time for the button. + */ update(): void { - // reversed to let the cancel button have a kinda priority on the action button for (const b of Utils.getEnumValues(Button).reverse()) { if ( - this.interactions.hasOwnProperty(b) && // if the button is in the interactions dict - this.repeatInputDurationJustPassed(b) && // if we hold enough the button - this.interactions[b].isPressed // if the button is pressed + this.interactions.hasOwnProperty(b) && + this.repeatInputDurationJustPassed(b) && + this.interactions[b].isPressed ) { - // if the gamepad support is disable and the source is a gamepad - // we don't want to repeat the button - // even if we have disabled the gamepad, at the exact moment of the change in the menu - // we can be here and have an infinite loop since the code can't know we - // have released the key since the gamepad is not there anymore + // Prevents repeating button interactions when gamepad support is disabled. if (!this.gamepadSupport && this.interactions[b].source === 'gamepad') { - // if we are here and the gamepad is disabled, we delete the latest interracted button + // Deletes the last interaction for a button if gamepad is disabled. this.delLastProcessedMovementTime(b); return; } + // Emits an event for the button press. this.events.emit('input_down', { controller_type: this.interactions[b].source, button: b, @@ -129,12 +188,30 @@ export class InputsController { } } + /** + * Configures a gamepad for use based on its device ID. + * + * @param thisGamepad - The gamepad to set up. + * + * @remarks + * This method initializes a gamepad by mapping its ID to a predefined configuration. + * It updates the player's gamepad mapping based on the identified configuration, ensuring + * that the gamepad controls are correctly mapped to in-game actions. + */ setupGamepad(thisGamepad: Phaser.Input.Gamepad.Gamepad): void { let gamepadID = thisGamepad.id.toLowerCase(); const mappedPad = this.mapGamepad(gamepadID); this.player['mapping'] = mappedPad.gamepadMapping; } + /** + * Refreshes and re-indexes the list of connected gamepads. + * + * @remarks + * This method updates the list of gamepads to exclude any that are undefined. + * It corrects the index of each gamepad to account for any previously undefined entries, + * ensuring that all gamepads are properly indexed and can be accurately referenced within the game. + */ refreshGamepads(): void { // Sometimes, gamepads are undefined. For some reason. this.gamepads = this.scene.input.gamepad.gamepads.filter(function (el) { @@ -146,6 +223,17 @@ export class InputsController { } } + /** + * Retrieves the current gamepad mapping for in-game actions. + * + * @returns An object mapping gamepad buttons to in-game actions based on the player's current gamepad configuration. + * + * @remarks + * This method constructs a mapping of gamepad buttons to in-game action buttons according to the player's + * current gamepad configuration. If no configuration is available, it returns an empty mapping. + * The mapping includes directional controls, action buttons, and system commands among others, + * adjusted for any custom settings such as swapped action buttons. + */ getActionGamepadMapping(): ActionGamepadMapping { const gamepadMapping = {}; if (!this.player?.mapping) return gamepadMapping; @@ -170,6 +258,19 @@ export class InputsController { return gamepadMapping; } + /** + * Handles the 'down' event for gamepad buttons, emitting appropriate events and updating the interaction state. + * + * @param pad - The gamepad on which the button press occurred. + * @param button - The button that was pressed. + * @param value - The value associated with the button press, typically indicating pressure or degree of activation. + * + * @remarks + * This method is triggered when a gamepad button is pressed. If gamepad support is enabled, it: + * - Retrieves the current gamepad action mapping. + * - Checks if the pressed button is mapped to a game action. + * - If mapped, emits an 'input_down' event with the controller type and button action, and updates the interaction of this button. + */ gamepadButtonDown(pad: Phaser.Input.Gamepad.Gamepad, button: Phaser.Input.Gamepad.Button, value: number): void { if (!this.gamepadSupport) return; const actionMapping = this.getActionGamepadMapping(); @@ -183,6 +284,19 @@ export class InputsController { } } + /** + * Handles the 'up' event for gamepad buttons, emitting appropriate events and clearing the interaction state. + * + * @param pad - The gamepad on which the button release occurred. + * @param button - The button that was released. + * @param value - The value associated with the button release, typically indicating pressure or degree of deactivation. + * + * @remarks + * This method is triggered when a gamepad button is released. If gamepad support is enabled, it: + * - Retrieves the current gamepad action mapping. + * - Checks if the released button is mapped to a game action. + * - If mapped, emits an 'input_up' event with the controller type and button action, and clears the interaction for this button. + */ gamepadButtonUp(pad: Phaser.Input.Gamepad.Gamepad, button: Phaser.Input.Gamepad.Button, value: number): void { if (!this.gamepadSupport) return; const actionMapping = this.getActionGamepadMapping(); @@ -196,6 +310,24 @@ export class InputsController { } } + /** + * Configures keyboard controls for the game, mapping physical keys to game actions. + * + * @remarks + * This method sets up keyboard bindings for game controls using Phaser's `KeyCodes`. Each game action, represented + * by a button in the `Button` enum, is associated with one or more physical keys. For example, movement actions + * (up, down, left, right) are mapped to both arrow keys and WASD keys. Actions such as submit, cancel, and other + * game-specific functions are mapped to appropriate keys like Enter, Space, etc. + * + * The method does the following: + * - Defines a `keyConfig` object that associates each `Button` enum value with an array of `KeyCodes`. + * - Iterates over all values of the `Button` enum to set up these key bindings within the Phaser game scene. + * - For each button, it adds the respective keys to the game's input system and stores them in `this.buttonKeys`. + * - Additional configurations for mobile or alternative input schemes are stored in `mobileKeyConfig`. + * + * Post-setup, it initializes touch controls (if applicable) and starts listening for keyboard inputs using + * `listenInputKeyboard`, ensuring that all configured keys are actively monitored for player interactions. + */ setupKeyboardControls(): void { const keyCodes = Phaser.Input.Keyboard.KeyCodes; const keyConfig = { @@ -232,6 +364,24 @@ export class InputsController { this.listenInputKeyboard(); } + /** + * Sets up event listeners for keyboard inputs on all registered keys. + * + * @remarks + * This method iterates over an array of keyboard button rows (`this.buttonKeys`), adding 'down' and 'up' + * event listeners for each key. These listeners handle key press and release actions respectively. + * + * - **Key Down Event**: When a key is pressed down, the method emits an 'input_down' event with the button + * and the source ('keyboard'). It also records the time and state of the key press by calling + * `setLastProcessedMovementTime`. + * + * - **Key Up Event**: When a key is released, the method emits an 'input_up' event similarly, specifying the button + * and source. It then clears the recorded press time and state by calling + * `delLastProcessedMovementTime`. + * + * This setup ensures that each key on the keyboard is monitored for press and release events, + * and that these events are properly communicated within the system. + */ listenInputKeyboard(): void { this.buttonKeys.forEach((row, index) => { for (const key of row) { @@ -253,6 +403,19 @@ export class InputsController { }); } + /** + * Maps a gamepad ID to a specific gamepad configuration based on the ID's characteristics. + * + * @param id - The gamepad ID string, typically representing a unique identifier for a gamepad model or make. + * @returns A `GamepadConfig` object corresponding to the identified gamepad model. + * + * @remarks + * This function analyzes the provided gamepad ID and matches it to a predefined configuration based on known identifiers: + * - If the ID includes both '081f' and 'e401', it is identified as an unlicensed SNES gamepad. + * - If the ID contains 'xbox' and '360', it is identified as an Xbox 360 gamepad. + * - If the ID contains '054c', it is identified as a DualShock gamepad. + * If no specific identifiers are recognized, a generic gamepad configuration is returned. + */ mapGamepad(id: string): GamepadConfig { id = id.toLowerCase(); @@ -279,7 +442,21 @@ export class InputsController { } } - // added source so when we repeat the key, we can also tell from which source the key is from + /** + * This method updates the interaction state to reflect that the button is pressed. + * + * @param button - The button for which to set the interaction. + * @param source - The source of the input (defaults to 'keyboard'). This helps identify the origin of the input, especially useful in environments with multiple input devices. + * + * @remarks + * This method is responsible for updating the interaction state of a button within the `interactions` dictionary. If the button is not already registered, this method returns immediately. + * When invoked, it performs the following updates: + * - `pressTime`: Sets this to the current time, representing when the button was initially pressed. + * - `isPressed`: Marks the button as currently being pressed. + * - `source`: Identifies the source device of the input, which can vary across different hardware (e.g., keyboard, gamepad). + * + * Additionally, this method locks the button (by calling `setButtonLock`) to prevent it from being re-processed until it is released, ensuring that each press is handled distinctly. + */ setLastProcessedMovementTime(button: Button, source: String = 'keyboard'): void { if (!this.interactions.hasOwnProperty(button)) return; this.setButtonLock(button); @@ -288,6 +465,20 @@ export class InputsController { this.interactions[button].source = source; } + /** + * Clears the last interaction for a specified button. + * + * @param button - The button for which to clear the interaction. + * + * @remarks + * This method resets the interaction details of the button, allowing it to be processed as a new input when pressed again. + * If the button is not registered in the `interactions` dictionary, this method returns immediately, otherwise: + * - `pressTime` is cleared. This was previously storing the timestamp of when the button was initially pressed. + * - `isPressed` is set to false, indicating that the button is no longer being pressed. + * - `source` is set to null, which had been indicating the device from which the button input was originating. + * + * It releases the button lock, which prevents the button from being processed repeatedly until it's explicitly released. + */ delLastProcessedMovementTime(button: Button): void { if (!this.interactions.hasOwnProperty(button)) return; this.releaseButtonLock(button); @@ -296,6 +487,24 @@ export class InputsController { this.interactions[button].source = null; } + /** + * Deactivates all currently pressed keys and resets their interaction states. + * + * @remarks + * This method is used to reset the state of all buttons within the `interactions` dictionary, + * effectively deactivating any currently pressed keys. It performs the following actions: + * + * - Releases button locks for predefined buttons (`buttonLock` and `buttonLock2`), allowing them + * to be pressed again or properly re-initialized in future interactions. + * - Iterates over all possible button values obtained via `Utils.getEnumValues(Button)`, and for + * each button: + * - Checks if the button is currently registered in the `interactions` dictionary. + * - Resets `pressTime` to null, indicating that there is no ongoing interaction. + * - Sets `isPressed` to false, marking the button as not currently active. + * - Clears the `source` field, removing the record of which device the button press came from. + * + * This method is typically called when needing to ensure that all inputs are neutralized. + */ deactivatePressedKey(): void { this.releaseButtonLock(this.buttonLock); this.releaseButtonLock(this.buttonLock2); @@ -308,18 +517,49 @@ export class InputsController { } } + /** + * Checks if a specific button is currently locked. + * + * @param button - The button to check for a lock status. + * @returns `true` if the button is either of the two potentially locked buttons (`buttonLock` or `buttonLock2`), otherwise `false`. + * + * @remarks + * This method is used to determine if a given button is currently prevented from being processed due to a lock. + * It checks against two separate lock variables, allowing for up to two buttons to be locked simultaneously. + */ isButtonLocked(button: Button): boolean { return (this.buttonLock === button || this.buttonLock2 === button); } + /** + * Sets a lock on a given button if it is not already locked. + * + * @param button - The button to lock. + * + * @remarks + * This method ensures that a button is not processed multiple times inadvertently. + * It checks if the button is already locked by either of the two lock variables (`buttonLock` or `buttonLock2`). + * If not, it locks the button using the first available lock variable. + * This mechanism allows for up to two buttons to be locked at the same time. + */ setButtonLock(button: Button): void { if (this.buttonLock === button || this.buttonLock2 === button) return; if (this.buttonLock === button) this.buttonLock2 = button; else if (this.buttonLock2 === button) this.buttonLock = button; - else if(!!this.buttonLock) this.buttonLock2 = button; + else if (!!this.buttonLock) this.buttonLock2 = button; else this.buttonLock = button; } + /** + * Releases a lock on a specific button, allowing it to be processed again. + * + * @param button - The button whose lock is to be released. + * + * @remarks + * This method checks both lock variables (`buttonLock` and `buttonLock2`). + * If either lock matches the specified button, that lock is cleared. + * This action frees the button to be processed again, ensuring it can respond to new inputs. + */ releaseButtonLock(button: Button): void { if (this.buttonLock === button) this.buttonLock = null; else if (this.buttonLock2 === button) this.buttonLock2 = null; From c72a50a38c2ca5cdad13e0bea482f3e069514db0 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Fri, 10 May 2024 15:02:00 +0200 Subject: [PATCH 18/81] added controller input sprite --- public/images/inputs/dualshock.json | 348 ++++++++++++++++++++++++++ public/images/inputs/dualshock.png | Bin 0 -> 82008 bytes public/images/inputs/nswitch.json | 356 +++++++++++++++++++++++++++ public/images/inputs/nswitch.png | Bin 0 -> 31039 bytes public/images/inputs/xbox.json | 348 ++++++++++++++++++++++++++ public/images/inputs/xbox.png | Bin 0 -> 67950 bytes src/configs/pad_dualshock.ts | 22 +- src/configs/pad_generic.ts | 21 +- src/configs/pad_unlicensedSNES.ts | 16 +- src/configs/pad_xbox360.ts | 19 ++ src/inputs-controller.ts | 15 +- src/loading-scene.ts | 6 + src/ui/gamepad-binding-ui-handler.ts | 58 +++-- 13 files changed, 1190 insertions(+), 19 deletions(-) create mode 100644 public/images/inputs/dualshock.json create mode 100644 public/images/inputs/dualshock.png create mode 100644 public/images/inputs/nswitch.json create mode 100644 public/images/inputs/nswitch.png create mode 100644 public/images/inputs/xbox.json create mode 100644 public/images/inputs/xbox.png diff --git a/public/images/inputs/dualshock.json b/public/images/inputs/dualshock.json new file mode 100644 index 000000000..484ead036 --- /dev/null +++ b/public/images/inputs/dualshock.json @@ -0,0 +1,348 @@ +{"frames": { + +"T_P4_Circle_Color_Default.png": +{ + "frame": {"x":0,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_Circle_Default.png": +{ + "frame": {"x":128,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_Cross_Color_Default.png": +{ + "frame": {"x":256,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_Cross_Default.png": +{ + "frame": {"x":384,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_Dpad_Default.png": +{ + "frame": {"x":512,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_Dpad_Down_Default.png": +{ + "frame": {"x":640,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_Dpad_Left_Default.png": +{ + "frame": {"x":768,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_Dpad_Right_Default.png": +{ + "frame": {"x":896,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_Dpad_UP_Default.png": +{ + "frame": {"x":1024,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_Dpad_X_Default.png": +{ + "frame": {"x":1152,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_Dpad_Y_Default.png": +{ + "frame": {"x":1280,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_L1_Default.png": +{ + "frame": {"x":1408,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_L2_Default.png": +{ + "frame": {"x":1536,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_L_2D_Default.png": +{ + "frame": {"x":1664,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_L_Default.png": +{ + "frame": {"x":0,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_L_Down_Default.png": +{ + "frame": {"x":128,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_L_Left_Default.png": +{ + "frame": {"x":256,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_L_Right_Default.png": +{ + "frame": {"x":384,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_L_UP_Default.png": +{ + "frame": {"x":512,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_L_X_Default.png": +{ + "frame": {"x":640,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_L_Y_Default.png": +{ + "frame": {"x":768,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_Left_Stick_Click_Alt_Default.png": +{ + "frame": {"x":896,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_Left_Stick_Click_Default-1.png": +{ + "frame": {"x":1024,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_Left_Stick_Click_Default.png": +{ + "frame": {"x":1152,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_Options_Default.png": +{ + "frame": {"x":1280,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_R1_Default.png": +{ + "frame": {"x":1408,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_R2_Default.png": +{ + "frame": {"x":1536,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_R_2D_Default.png": +{ + "frame": {"x":1664,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_R_Default.png": +{ + "frame": {"x":0,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_R_Down_Default.png": +{ + "frame": {"x":128,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_R_Left_Default.png": +{ + "frame": {"x":256,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_R_Right_Default.png": +{ + "frame": {"x":384,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_R_UP_Default.png": +{ + "frame": {"x":512,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_R_X_Default.png": +{ + "frame": {"x":640,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_R_Y_Default.png": +{ + "frame": {"x":768,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_Right_Stick_Click_Alt_Default.png": +{ + "frame": {"x":896,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_Share_Default.png": +{ + "frame": {"x":1024,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_Square_Color_Default.png": +{ + "frame": {"x":1152,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_Square_Default.png": +{ + "frame": {"x":1280,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_Touch_Pad_Default.png": +{ + "frame": {"x":1408,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_Triangle_Color_Default.png": +{ + "frame": {"x":1536,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_P4_Triangle_Default.png": +{ + "frame": {"x":1664,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}}, +"meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "dualshock.png", + "format": "RGBA8888", + "size": {"w":1792,"h":384}, + "scale": "1", + "smartupdate": "$TexturePacker:SmartUpdate:987743e379168b07fdf2bb169758063f:318efd1c2db07d7c85f4d230278c0da3:adc25708364be3d9e70074e95305c745$" +} +} diff --git a/public/images/inputs/dualshock.png b/public/images/inputs/dualshock.png new file mode 100644 index 0000000000000000000000000000000000000000..84bd1a022de66991fd3b0437102abd3ec0479ce9 GIT binary patch literal 82008 zcmZU51yq#L*7neXARtIeC@n255`xkxC?YA{-3mh~QX(SK(%sz*As{W?(hMQpFwFdC zy!ZY$zO{6X3xVN1XP>>FI)rMd$rBOK5I`UhB1MI#&mj;z@J|f?8@S-5z|-Rzyx==2 z=(<86H_6a{F(8S_R1gRwMDghpZLgU>&Gv3ulL=?n3lF_iG0O=D%nmVSin7wNDENC~ z;-1$#k2LYTCBRp-UbQTqdd#AACCmMx%Cz*VzFhGwZy`7Nusd&IC%HHSmkbtG)&#-5 z=5Q@e>@SLc7(1}PL_`uQvc?$qea?ystxhHW5-~VDye%dVzq@%^oE`g0_>nvuR_c2? zjcP6p4`93z_}_mr6c&^9Ftjo2i2P4_!{e~nDUm|7ID7=gn5`uKo*0DuU2>aBTe>UO zY!JSe%Q=NlNU;CskEXHHV4oQLB>Bi;uwx}Tq#m=BbTGG33|_cNu&uV&%lYU)sp0ju&!_h~c*kuY+u*(&jh)o@wEdABDPmI}TSE9hZ}NmB_(>=gbaaPc_qgw{ zGs`B;Z3=L|NCf6Wbxa~#i&j36jR<+2Uv@=CT2tX<|4|8 zI^86GLYV(|1=~M^=MQjt>$1Wov-m^t{2gz{o)iHLMKs-AJqsfWG3EaAg7Zx|mx}ub z34dz=Q@gKY&nxLv#4&2P2aN2Ei+&hzO$HN6|L4B6??3hQ)5d&7e(mUe41@frhEB1! zvO$C~j0G=!C|lW0?LL;Uf+f+sL|U`8&Sd!dVW$6Lew-psE^z%tQ|su)-A~nd`M%84 zsVb0nuKWq=-t<`7n2$RIQ|+FW&u#bo_d^ik!JBAW?IAC*9$TDqv|+b?fZT=b^OzQ; zmI#A4+1y7VnKzh)~7J-M6tH3O>P_lC~i2lzKvv_ycxFx^}FY zkvuWD(Ddfx?>q)6l^x|2bR}?O;W)(;F`|fatgNDBVg4O8oh$xMiTMGlS&g5QA^+T? zh|6E7_|-~=#jET~h~pl8RgFWhZNZS8LNo1W z$Fv52u_)p%yVwKES_GGuF&GNyQy4rHMzXlk%i-NXsX!y)qj;9U!OcxOJaSnyCX)4xWqN_( z0fuCr1_LZZg_aQ#>S2~6taW!5c!EDeh6gW&C^o<*4>+M|&OrT{LXh?rt7g(BCr{3>bAnr>-fP^pXpliOJ#fEw_DJ zd=DI>F%%BeNrHW@p<*N+SbwY!_VnrW7G3Ox{&>2IPGpQSjYpPUy3|@YK%d$1kOFQ}oFs-)K_Y@n24L zW4m#3g*@E2#rs5xgg`Z8P}4E~YwBiK{B737r-F2A?A3v0_Ob)>Oa15yQkV??R|WB% z=l<8*tX^&tju>TnNaHegN`Xxwe}_;`gn|mPv7KO~y2}~Am-qwwZe*fA*%-|ad)Wqx ztaf>hJcHkr1l9Zz;wfv82c)+aaJz~=t_D?Ovcsjt?w5M8Qt^@Emu*1B7~c0B^Z2KG zJBZ*UFFBCsVLQ#<*{Q!{yatIIV=$JvW-0}3UNgj&tKlr_EA;xLNJ;o+Z8=}9%V5!B z*~6|rFj!E5~?GoFwj3Bzx?#@Yt z#bF9c07fCk z+|9H{7Z3QqYoqe`jbo9BeUjD?h?bQ9q!n#V%f@f1=kzW56^U_+hFzKx5=5M)cjG^C z3}R4RLwTmTG7cE5MUt`N*Nq9|zw@*WQg(kZUY0d(LOr|*n>e2`8A)AE{m*%+@zHGE zh>32A6obO(2Ty4kQgX@6NddjXT>-|CouDN^7xP(zQ-=zb_mCuti?a- z=if6r`1Za7dBO!{$J$I-;*7j;ITyKp)P`DB#*oULW?Uk2rfE|JET_56B3!tA^3fKT z$2rF=9mcqE(IXpNdWh-JkZWU4^iX$oRD83&cl@!sJHC3$J2NQl2gpr&eeX#r% z7E)(rfd?-elf}#gY=6ycl)!qj;$FfBtyJ~JvG>l&4SWGT`5XxOV_5X0dE3Ib*g-j6 zYYvM3lptFr<#IPav2TN^AvmsZ<4uuXEl-W;vCKkVL6NA3gR}7K)7l}e(dU<}B(pmm zDG3w-w~|FerHug&Pz;tyPe6T;iNP1SRasAH;Y!W*?LA-8K-u3O>EBsFQnS`Kk}_`t zDHJ1)!YoSRm%HRtv8!dcc@(jNdbdwFh^M}iSuSus8yDv!>4j_L>rHCp>y8h-I?Pyf zMD1?bI*q?@v~*leXfpV^Jm(j4r;gEzbKV`Hg!tYao?LkL__0h~b1dEXXU0hmBipK&F(EP8s( z!eV+!$}pQkj-14)F?xrq_ljH}_0P0=dly{$F*KbCOTOE)f9G{+pUm)9s%=%dbD^c_ zBMhw2N-C5PLPh`1e%%x5c_LJ2y(FUP{8q@p&_&)w_Ok$|sE^04_^g`T{}%C6oBKzS zMcl|{6G4b)EHSsH{P-6?}MgWe*~|D21R9^{JNeH`V>fUtb| z!AMv|&EPvf1%`3koLJq}<#w6F<`**yM(TjDIf$3L;F;_5>uW6er6an)$wv9=Bmish z0dPCNc7#AcL6ptvWRyGmJ``wGlJkGpV0gDsIKfK<-gAOlY_8>hJQY=IcxG;XZr*`@ zKw@=R5crD)iT4D%?A^0-z8j>jm=s`!{T!hMX5YU7Z5H#FweY|F{nfBxT#g*u<-4op zg@W~Dolzd?`;>0hkWb<%FP&N$Dq0`sd0R z8|-<=O34z~p`b1sA;MB_J_n6OJIELp;D5HT+?HSJbyATW3wIfA7 zQQT5b;-=pcpxD#Poh1N#V>yYU!WbP#qtl4vA^+WLrio3b9$}}JL}j~+tF69puk8~_ zLfEL3Y42J!^zsiA;>wa?X~Af(Bl4zWrze9Cfu~>Y%Zz)Kd8AZDEp~~&=l*Z+%-9GW zMBNQwU(rf2HPB3gLM$HYPw#N9lv3?rGP|^A)skKIZx+FmiCr{@)fr6?^>`tgC(Fv` z7fC^h@lX>r1FCNI>u&I&)UvZ{(#=~$dGKTgYE+N<_Z-%^k|R_D{bJDPUsC@7b>C)p zpW50cvBscG0OGf@aWSdi;qiKV#(cr~l6apf*>yyUNc=Q%Og%&yw~g!sg?_}f)?0{W z%R(ooGe<#w)Q@+99=N=SHv)bKl`p{Kw=hT2oK$w?Fo~mfxHi=R$oT&|`pB0RzLtDs zLqGl1IoMUQ-8+6%S)Et%_BiWi7d2LOojr-UuZ{h5u(WY}AgO<$Y2kVCmO>5J{cyB*^U2xFdrXQN4#?dGkFS6 z%qAN4-}tU{h31hqE7<3LgB?uC>YlU%b04cjmX;lLx+%OK8!$V6o#K%u9dI+heOanB z=+Mwj?JCr7T*s^ezV%{7c;P+ zIsU_N@0tVom!JF{@iHrxht`&6F3HtTOrG$hzbzW>mu1q_8i3e;(#zbj{{RLD828V$ zo-Xr9w=_&qev|inAR^!2vhQW47VRkypL;1!$&=M67watNqi$Bh^O#iV|WGt9>PO&7c(M!`H(Ude$#VUPC1x}$}` z3Dm~HGjbG~Tm9?2M(4tv4SA}mC_i%jJUMNFfPRmS*>U4ND^YZG?kh9^Fwy;(m?&>t z`kLdrEyRCoBV*-?%9QN#3U|rFAAiQcwl60z9_#r{qlG^3*`4xbWUcGVn}zLz3KM02 zB6k*b-!xWmy^bFkF2vAwr3Zst+<%hFG*hnWQ{v9wk)6og4{P3wN?Tt{59C9cBsU)8 z*xYIfG+-NI7G~-4k@S3&(kU_(?;*GCi@$%m`y}={AaLtEsEuEHnjtt>>p_&zUZ{Tf za)#}KZf+=BBGmEXs;v4N#dG6`x9c4AWsx2C^X;Yi+bi@FqbJL{VgCigeokt6i+nL# z)-}@&oR|;>LxF_wJJXT6B{n7-n1hKNQX^v6A>2l26?GCv-}aL%Nxbm{C+L10__d9uU7>w@1|Q*&ln2cp>G!R&|WDyQ^d5 ze#mw4I#Hqb>v!%1F7OZ5`tG_d(>l~PvVWj{H$$z;&)7O%o^5<25h8OBFl8BbavA0A z%8Z2y`(*W?@-G>`U|@0BRzfdfF2;DbVA}DWez42K`?p&5Y2Tf!J$u}ANu7QfVZZHj zz%?ON|8e=hV7|5=Yxmaq8D_wnF|mnZ9xklqlih#?D0%a5hhZLmZCA;iu|9oSx5@mp zT_Zlx4`3B<^An|O+&~6QYHD3sA!Z99$CMTforO%{BY18vBzFHZ4V%T{n;cNAhP@?5U8X%B(`gD~zP%I}d=lkot|YfiSqYuC#} zRiNvl<{%ZZx+#M~Mpr>YV^FG*1J5-7j00>x@lEwM?9D6zOR3WXk3Y2e;LAPs=JdUSvN9TDZeVU(bJ&|#<0Ul zbB$*>A4!R1j4lxG_whhD2mjAjGU`Xwg`R-c;{M6~>v|>R#(SAOS3%~iFzjDNC-sWd zL!cwLffID!lMtpc?CMRz*E&_Z9RaG@m$O^+Rb$zRT zuiG>mEi_U4uc+n59{oUcbXp)gr>@#QX5ov5A2ETCR{v^o%&Jr$gH5rbOud@leo27H zM^yMQ(gK$KO1Yf{|B^E4f^wyJ@`_niIwpVhqb2IbfA0)CA(`3tdt^CfH*mWbH|Os8 z#EH8**~~8=aed&@P+HlUXY#XsaqnHJRk2i$0hE7bXl}128H@mDWS_4B6c!{qLTlu{GWF2wDG_oB)5zi1d9x zccqq&c&8}*#DD)BtS__nE#|JmgHeAM$$6}@V42V8`CTEEmvF$w`n=SI41hJI8rrf* z<*!YnH@*2G9n#`YV_>^&Z`@KJX&tvUo&}NGF{tGETH(NPv~h&^hbsTdb|o2;3)}Nd z?kkm;;OEWFRoyW;+Ka^!=i7%1kWOCPiCSb3RmzvT z`B{S<8?LK3t3wy`C?-_=KJRvelSTy-(-BBMsS+Jssut0&6+#uZEZ311Gt9Cy2 z$B%Wj!Hysw{V^%!9jtfjZW987INpAqDUTy}WG_g|i`<0Kv^4t&zQh32{FHsvr}^@A z7+|S4kh_2J`v_%xa$@^I6pEIOH)7?SEp>hE?!0VA-DaYg*YdYcokYULy8zpMSVwbM z+IrWpl4FvMz0JAk+T!1Ut)jJRyFO#R7DD0Khd_aN#dVVmQe1_9Vekge_JTR9MWZ&7KMNrx5{bWrlH@9aOt=1YBRhlt9u<7Xpb-Zip~VMNk?_ zyfr-0&X$sr{P?y6re>_-(lxiD2Dd$$+tp<9P#SJ9vR^iS*;pw8SZi(RQ`1o&Ah_}+ z^^G8D!_AjeS|`RH6EHqE8{31@wY10)X?)CbUQthQiDxS^2k4^y^Nb0f*0=fh@ah z2FA|^)nYL}=AqiI&*{Me+{gN?gEeJWIX+RuuYI+D6_o*cEZ~4yDy`R$-O;}K zf+S2q6Qu6Sf)Bt@#x7eG4?;LOQ;6VU1cZ+-d=bdE9&@hO?$im;e966geVB-bEeG%) z>gVp`^Z4R;u_i7yN?S8ux5c1#xXc?ydX;k36H>gk%X)ss3q z<}Htx+x%;0haUVpAATZkVt*jAM0R$+wjZFuAEg@ygkpQ`@e2WnlsWbQESgVVG~Pxx`c@OnYbvU(Vkv0*?j z4OVCx{-EuyBci&>?yLD_HqEN|U0dh-`>bFIUUc37?n0K!jZ$F~Nx0RPdH}NY?o`0R zkW@~i`|U2Ns~e?vRi^0b8@4-1!%+9B(org5m}OEFZ;WfdH- zy|1jm1c=y3qf(utK|Q67UvsDJ_F!Y=B;qLMvNUI%^hKl;$LrU^8AG+|+Lg-Gz@-SC z36dL=;y=kvZh(qFw{I2sY#Ehp;rc{9CDO*z<(+D>HJwxq?h4iq=U7u@E#k`|G9v9l;No=!A#r z_&`e2<9quvnc1dY60$uc-~`&d#o8+8ct4Kh+AqI1{P7hvA}R~6Z-3O?5V#R8lTHGV z;B7fp+tH48znrcRKhPbMp60w54~IT?J2NtyOeG>xb{s#(+*2hz;d z@YVZ(_Nfn!b@L9ggdF!9q1czawe#6|Ner_mlT&quqkCHiZh-zg8y@U+Fbb7ikUV%X z-})-_-f`3}l57JNp6VwBpNA5c`rr~5-`;w6ZcJ$?RwqljptE_@=-oqSbvd~ZYHHJQ z$wqm{98p^(8UBr8iNGk%ws6crOjY9&5kZf7P+Povo|0YuZu|AGP3Le{s28y*3(+yV z%q3Zt)RRlHt*z3^N+#fVP_)Z4CaxZPHT;^X;7~t+8Q73INtxzcDbR&MgMN@iq3Y+} z1=S3VaBq4Z0-X`qW`Zz}=pvv}VgSsYv{!~E}uRA_;t-LsraS-C>ZkK+XO7I-G zcRtO5*0uM#R5;z{;`B1zQ5G}7q_#-&86AZ&?7KKq1YPkKH|4eQkv9hh0;LzOkT?9c zq)MjXyz%0AFw}QCpbCw?uLB1sAlDEXo`DNTHEnwo>9O*Ouu++Y!|Uf8jLeyLH=3K@ z{u*1=7L504ZZxiZv9WwrFl_Da_Bf=AH6ULQqpb6_2SvhRh;Li;`3&F~s9;JDiMUbA zc|@`Juj9%EMWh-rYueWfkt6@}K8BT!QAhZ3eWBku@APFNK+ z1)cO(EB4oiD3h^?qUfmso%4gz!?f~8kF3V3=oGdG3#d&?hckit@EM7{sh(E~^JBa| znxiOM#(xUg_c=?;AI{$@kBNt7LK>s^g_X865VR{kTYV66TJ7i9L8gFqC|9})CiVTA zIwqu{owWSwUX)+1D~5$IXM(irnjH+zJ{pPQLn= z`hKj41p3jfWDAb#fK;)Rlf`!XHKiP+KA-0;4=Eq$gwQ!_F3$>DVnr*`fUgaP_c#29 z{QTCTj;GKqqullR2=<($U<6xBy(sc=FX=5QBICwEygd^(kR13&7QpIcxe8OMQ zVT{~Z{PgA(<3#K%6N@GlKL|a`6%Fc z2&zZ5mb}0wj6wJ5XI(wm1uq(YAeRFolrM?i>L1mR7hT{zt%ya-8X?MM)D}YUrbPanP`NN# z|K%Cz+XV*Ae)7HvL=u?u&3h=eTX;h&b$KP<$xCsE1`sHTNuv;mU3M~@=7p7{E_W{i~OtW){x3Cu%?#Cp0z?=-sRHL-L8(p@1bum@hROA z%IRPc2N9gh=R@6s(m`G$Y&A4m9kB~vFT;=76rh2}Y==d#F7jApvbs2imh1OV!WjGa zT92E?m~|hLmF28!9$!#^X&-6Anl`Qf0d%nZe%Z|YewbBpYHqa1XqY+#!Dz~|gGQ^t z56eqKzQ}ep8Ra!{1lIvr$9~s6FtMukNeFJ5*LOZ(3x)|V9k`|EoRSV>-v2!yHpzS^ zFJl%8rl(Gl=mHp@&#O@t{3uYmrjtFR8U) zu?Kpa>hoyC{MngjT_Zj+*cJlO+hRb8;W*mf* zz(mdY>2G#sUHzJoPW%j42a9BbH}{l*(STSz>|GAlz!+j1u_gt2!`|w<=fum(`x*AeZYCb zLP}YhwO#-jB|l&H;7J6qpMVwLS;)zZkNx3}o;SPZO_kbbU7owNv&4SSpkmdiW$%rv z?SuU-%aLwBdX-j{0hrFMKdnn~tCR(XCu6Y1Mq&aRj8oJGzOrT~RJl)XSWVRW@C~Mg z4U*%<;%oJTQs6&tvikm4T!JzV>7DiS7#<5@7(3@T6|7$RHbuY6)2mv3F1qd7Bh|}I zm37YBbPi9B`ms0o8c02Dbt+aDa_YEg;0I2juN})sE&3IXVO7rcBeXRs@U$=9y1an< zd4`20e}cqCmxbO33{>h-f%CVWd+9@`ZoAU9wtAYI%>21xVAx#z#>tDv07CC;CXpD< zvAd6hwJ2Aisj8j}pFU*Q<{*-QaOy;i`^$GLqJXmK&1SJvNm;h)&4%qUY6Rk3yduhT z=w$NES?1ZzXQp<)AJ^*XyqCxf;Qj^cMM8iDP$AXWaiTv{dxvy_x)Le{9|kKDW zJe(C7GsnlGIf~t1dy$)q_VYWN50m~CZehEQOoP%>ao406B_A=(_~u5G`g&JMXm`O< z3-ROI5wv07JAyyWWSRY8FE{?Y4C7l#C6u zCOhe3+3L}hL;cy>I}_o!Hap>)mkbxF90~KBdyNoPCEs6FR3-8=-wmT>P51 zC@GiZbvzWZzqDC`*TC|QbrLd0q;}t0pQm8I(y9sx zwR%hSC2Y;^G6<6o1xb7DpqF;eyLt{MTNXCz|6$USX*>^bbp-JnM3YA`Q2U0B!YO*r8^I?3Xf#2RzN3RkW z=x@JUthhQg8a@?voFv~5x?!|%&sjcQ{94Qs^BQ%z<444vmCK8fXd)FX;rUTnF z=MDraIf-PvSg4=}NR!Y6+4<_(f%*Y&e0**n*<=Op>Z zslXHAD1P$o79hQXI;ZQi48>MxB$h9H#OvCEo-g;^s*~Zb1GBrXe9@CKtE}sdn3fO zSbJbxvwBpDqyBIfIOaT)6ysX@Kw|*pI(+uypG#g4BI2DeGDUm)9UGU=JQakyg-hf^w(FZV?oQKvQtEo8jtk|SqE3nQ z>$${X(V`F$mI(UvX+X?2Uirc3r%&)-Q_S&3gY4!aQFe82%^hx@o#c;*B(=wPw? zWAE{&*B1ioU7sgHA%KfpSs@-0 zkqw-VA36#lk4s}PEZh;qAe8aU#xX0&%hTO%xE~SzZs<+jqneM8^qg}m=;_ttrZ$Jd zfK&%V<#}}mIUL6+q4K|7;2C#WDO+N8MKtQGN=u+S(0Cjrx&y7OJI*_24>enOJn4q^ z)qGuLPZzi=iwcx;~gxp`lZDxC>DseEs6oVAB3#Pw(TNIu+`$RKT5`zlBi$) zV09CqPRlmDNO3!qKU^}}@rd?nW}a0*Ckr{VVYe88c}SFwe!Y`vwZahnVpHGIUavrc znMxocc1(Udxw`weK+*sxKUrXS%jnerHa+s>pEEIDj39pyeW>9 ze}4wUVH`2o<`)zKX;hfc2NlPbild0*IHu#EtFN^Quas!Fo( zBWb*7mwD`#)#DxPb7B8#%U|?G%T+%wwrB;$AL<2M?m8?nK!Yx{p!*=Df??!gGZ+nvFa@-5qwzeoh#bdsA{DJqsq1G+=E@ zWf2IKe!OvO`y~pbB8o3D#?|l&D1k$Tw`|+njzfpq)DWl3GwhezSuql|>hrt>Me}ff zQDJ-yo{PLerLAmW(VrWsCb)F`1zx2e$qja71NqPBXdR z>$dpp9fl+q$sOER!ujcio$$QVk`23(w-1hZ$rGQVBfxl{?mI1;^}<8!;Fs5aJvc(T zT!wFEMd4z(8!vxARx{evAINfPVK>e{1!VI5pQ=Qq!_j~h4$t#qJ&(daul>6vFSOqc zxC`=k;9TZD{PB?Gh2Mg)lZs9uULs8jhXJZ9=cK}sJQVBJHC1c!O`UiU%}9+~DKN*} z8USsKc;T)5+$Mz^!t&B7?*o3G$dP*CdDJ>nts(V|_ky~eam?-txG|8C7|G{n-(Jl` zGjqZ!ea_E-(`ajp6j8&$WV+AF~Y2{@xTbPCc)xRVy;T=W5%zxu90JU{F< zD;TELRmG>4J^ z%f6z*+d4G3Uc6wWDd5FWWAp0meBv{#aY_8vuoduiEz%`nTV1fblCIL34Zidw|FUlLvpqULv$l-! z3;I)~@8Sn{c)9Uzauy5-B_IEgXDU4u6M7Fa#{VemGkp|s#V6-2nf6Z_w#PHJ;zbdJ ztq(e}NX4=qv`}w9J_eSJFdqP`3f?QRdS%)+FXSuQ93?kFtdK|8Uy|mnlw-Hf4E9*& z1!a8>eq&1U{5%s5HEKGo3O`T&NWJ46#(g5q0*is%=FE1hJ)E8NRH$sg1{k8HB~Hsm z0W5O;R;etyfM+f;4GAfek<*&T2Mkx=arx>X{^M{H%$#AV$ctRJ0!5*{STrxhkLW^4 zb4Yhiy4^13PSVh8lpA%CRRRJ|)|law+atK!)-5k@fh==YN#^noik#K3A1B}0&XVt+ zAYJH=2vm#TWknBu1<;OV!4YWQakjRBOALi)Y}F*KcEXnPlFS}tI&h!?7Wc&SEB%3x z-$nO#-IHC&V%swjmg{-YzfXPnG8#XVbxNJ-CvX=HI0?TCOO2U++1Rh?zB#MBV$+Z*CpfS)t+5ZOAA3?cpvkit;-thKKq~ zP{&tMN;^~iYt!un`)wazCfr+q5Qg9oZO?ToS4ce^cc|~M9NM-yGl~E?$3MPO_5Ag2 ze)WF@({d##cZ-r)8cu2MiH0mrr~M(W0@1s}oTBb=W`G&iYAllM6Ts&wd2WS0b{M$* zy#u)a)-LoeU=$`n`3J*2FdhmQGOkEbt|t2X01y)nEtwu1H1o9sf_v!EFTXWUM!sZs z467IPugTb~9{|X0gnxZ<6CDB)hklX1)I#*N5p=YrkOuKszumCxuM+(tms*Y0#CB*h z`0>E9A&UR>HNiFD;}%WZU+mE!{L49I4o3AIPnuspZ>4r?>P%|ziBB@yFc9aD9Jv@W zK!?ETcB_BGqzBS<99qwmYBDQLK_(!u)|2?1Z-~pxlN8$r8lfJx0|ui-+t=BDf{wL` zAA+W;W$M~qSiJ^acZqcE+JhP(zD3kG_itnNia@Bz1i3=BV|ASY|H~HaJ>7BwD+v;h zUyvnWY@tMre)G(sNR0G`t0!+1ehGTo=xeT&sWMck_`LXYpSf*W{5x{bfk%jr_NERv znbwQ-NPr}m0FrQ$WLFXBclmiybaoymTf&+~+7Elz4KviSe=l=yOe~AOze?|d4)|Mx z=KUI;>9B@yd+Ccp-CEfAqn*}vbeJ9MWf?<~%RFg?|8e;$#q!0&I+m9OHv(S_iL6h({B4GtGf<=Qa(Cqi zM$RcN30@`UyLI?cn?T}s_5QTCUhZ2y(1Q=Dr2SHm^I4tt`SIjChKN2Ei(lD=XOxcT z)NP1SM&$d3@4tr9xW5H81H(g*9j-Q*D+{hguq9w`;FZO@`tka2aR!-;lK7|VvqDYa z8M(0;0j*`C?g}jal>lm|79zX+K@NSW6p0&veifQe!q~$S2&;9Lj5OQ1x7`?CJ|G2I zQbHTo_`mP;YjN}brt^Pav7#OkteakkLB&rSyeU=vT|hHT#S@jC#6|#2cNhEjlUMSg zNM#!N`I#b5O(m|+dHv7(l>GN}Hjt%-DWJE;6w6e27=sfZBOjedsk6~=#FTv0O%)@I zh~jZo+U%4Z}aBfoz@1<#{lH1*~QO7HtNN-;VdKb`=Mac|o|WEjLDcOs!IKRdC% z3gq>{gAC|bP&Ic4zvo}H}-W%KlWBPn{~edwxx zN#u>YxH`6$9Q4SDIL;+#77Qmc<_4y5C9!(WLS_drONO3u-r;$7TF&G4Q;^Q;(Xj~< zqgBVmHMTr39GZ>((9F&?^@zdX@|- z7rEWs?7sCM!ndZrxHc=Fz)!QbF%!wp&7OHec;dy`HuoZoA>WnX_~#VZ$KziLLcF~b zKP2DDfAoln%0C~sH{XVKD>LTj6JORD40a{0d=_-OPx#Ax;s_*>u}@3z^Vp8mgU)LS zt_*#PArw*{*$inXD0Hj2&z4ek!WobtvNbP>P$n*Oea$WndKT^nP!2lsP^>OY(6ds9 zYBEI%)~eobz^XIqKyxZMx5Qh9K3IR`|NcX8K(^fFP_Sf$#umFp1(WdPq4w|J{e+oz zMAG?wj4!w~aJH|_or+nBGMndHT-MDiUnmSNb=yexCUAcJnVi#J$63ykMu59$uFLSP z7xLAXXFW(;oKefSR^{_K(uXP;J^&e$^=bQqXLR`J0O5Ii`jn&H^;`*$LwcoW9D6M3 zB5rktLLb4Y3i|Qyei))#YO=Z{K^2o7cBr>Uj8X()IW+N>hnmWO>BU(HdxFk*x?Uq+ zlHUh}Vv_ewBxb8eoB=DqPL!uK-!Ko;+-enDC(PTTSFcO^vX_OL!R z)@Q~WGPP#5m|MdLCKon6GL5KpBjlTg!ku^rMgzDN39R_MckhvWvxAOoF4Z|2!*E>_ z5rBm+%|p7Oa*;66W!SIJF}T%KR}kPohoj&tP~hM84!(M}5d?5sr(<&tLNnNT{fO>C z{5B=w_aNw^Rj*FTg2?Er0hXMgwzTu&XQ94xRb-1no#KN7binxP%CYQ za8#)77b})rpg<#$O895RJcVM(f>kEN20a5>Ar^%{*l`UhFx?XacfSsP$yhOIX&rq$ zFV@W7MmZ1abe~bSExvj}?T^+VmRntdJKC%$1TIP*<87o!^7_8YCF zt695aWj}59+0k;j3aseI!1bjULyPGzwgr$^Auo%&fg_WCQ-=|d;P7wXA`?))eRAO6 z%9Mlt%O)&`W_OF@5(Fp@tDAy(8Wa^s3UKwt7wnrGnFmPyv)$2x}V8 z%7XNDseXh;&^JJJfiDMcBY}FpstV)2B?c?4HnrM1gYb_K8CG4(Iyj(4DN zK@2N!fwx#$dAlq5QuUzvO+?CzrQ;BYhmjp+83C?wei1RS<&uv*<@g{dXTL*(8aPj{ z5X3UK>6Au-k=+K8)$48-JjgU2_GwYK9@jWw_t;Y3>lV~=Tz}pwy6}Qh){%Abh0YN7 z`MJ&){Q+14v{V3`j)T%hq-T8{>IK-fOb2cQ5aP-Gv>zVd%_o#*U& zCACWZ(dSJd`dO1acAYd*Eww>XVOb1I_uBaO%Wf1f9I5ZCmKYxQn5l{D$O<|0Na%mR ze=1KcPz;IJ+qqIp=X#*(+wQ zn&P>ZJ9JG>PW*B*eH(Wp?{pzvoqhT<7TXufSA%YiRV}z_2Xqxpcg${!+#EQ8-^WsD z&loo{)*6;N%|CEvn{e5dJ4 zYLKBKe(zcl6^Obx(;rk!gFP5AsUFE9o+@FP*WhC`U z4W_bfA0by{4*7c}MD%m7h#`%XX{~??y^~ zl*B3M8SiwpP`vwF(?qi?P`Oocv)7Z2 zZ00YUw9OJR1i1u$IG@k<)8IKDXDy!2t$=KKNee$)oT%V^bviu#}<~z0fZe18Q;)>)SF;@e{y0}K`~12NNL(A)EPyi2T9ZInL34)YdTEw|h+#&I^SEfsuvLAR`= z!kkD}B>!F(e*}7YX_yn9`r+GzrND24mmn24QO>+RTv?F*?U_NVTPmkgHo|^>Bic^w zHwYK0MTu{IphsIl`a>+&b45h0Kk6kCZkFEOzE&%P&+4o~`xuu|$e?3gT%;#0gSxMQwzS-ZjAR=*t zkZpbwo~L&c$S{Mm8RJcxz?@EO%G6{Osp&M)tc%ewwprRPT`(^)TemEJ?!Rt>?EKh# zw!bxoYxIYz^zK~r*`6qhww=bGm5hJ~xkgyz!jH1}9n}&yxEM~yMAo5kA<0^L!224b z2u8zL9)kLT=L~Ajbr56UOHf6+REghP4v$p7>7i3Liy$MizH=Ad>-h^z$@U8hOWPs=uH-s)f6@heja6v2 zzR!cLMB#K!8hS;X-Qu>=ud zM~sdmMY?HMZ*{*02FljpALiRDGA3uKG^P|vs05tpcqs5M!}=Zj<}uj+pp@p4kNo`i z6Trz&;|T3Nw6bA2J)E`q5Uihx>5z3>G~iZDsd5)jeAE#Id2L)*Ua%v_hv_K?O?cwSJ^QvDcR_jsH>VIMy#O9|tSDr$A`kG^0sL?Hr(77x&L0WEgHb)Z4$ zS59GyF=x~Q+KVeexrw*VRGc@}{Ddh^&?{|WAe)|C`rkuyL$2@JSN95VrZ)MPi%aW% zcJIR2zaj->A4$v#2%|uztE#4k!$n_A2oiIQuRDgLay}W@@>JbgS^{JZD2_s8V#K&{ zqxtx+TpMAzC1-5!6+>_Se9D$=cV?KQ@;BDBnFZtJ*m{= zQVitT#*3FUUpZ;!{@T&2tP#_86XV8N+3u#j7-`@$Y;EXCi7ihurO39RN3{+^eQ#nx z7A8b`*+HuJ9EUCFzRkB(TiJp&%fg5)uOf0@5KMH7E@N(jC$u-QCjN49v`TM&I||`}_XD8ku>{ zIs5Ebd#%mk=tFGQHue5K!R5Xlf8dBNaOL`k#)5$~1?WH`klWYeykzgn7Y~MGZ8V4G zRhg)Dz(qvcG(6=H@#RPx8@EQWq+sJwlqQNd(p69L9_m9~nxX+J0Ipqed)qxxq}ma2oLNRF6#ouax~aZ-f6G5+L`@ix*0K_bX;D zkh&TJ;f&1Ja#vAFJlWv%)YlZL0N^y2Pa&XXfqv-dMG$CvMmf3tpqi|_Qjo0xL#mM+ z)f;y(K#thB5CG;TPGC`|Vvh2yn+tnY3@~EwRI1kCK zL`oieqZlgMd_3U>@9nYP6J2=HrifReo>w<_WM-5FFgGKF`zKA>lIK}9fQrxmG(**S zOS6Qq^aWsVRW9&KjELXZ4{QkWg7o0~NOZdO6)pzL*>( z^TwVX*eC41n~+&7y1%CcoL{O@(+>`wj12@4#vWD`ZUb7E8EuQK?u zvt5N3MZAD+>rLj>6#aCw1I&3i(@xBRdfxId)kpxdYL^bug4?Pj?h>p#QBc)e0D5`W%A9h z7%bQCWVSf-yG)k!7EJO!AoP)1pJaK8d}y9?5*}$>T#G!a1l$XLVE2yNpT{keuiO5v zKImfG@}VllK-TDgR$88D171Ik+UC>^>QxNfBNHFDpf2UKaNii+(&t0uauDeoor4Nd z9o>iP-IMSirVa;kdV_V#SfR_rWrV4i^wq_$LWYgfG;y2a`Y&g+e=y&P$ z5=Ph<87tno1Fg_BwQ}PJrE^rr2xtsx9}}>#+W~Qwc-nnvn4~lNtDbSgw--;B_89MB zDn%uZU%o*2^*RSQd92|95|D31gWI(S+0p5e6`Xw=(~nu2%_2eoZ|dAEItfmNYNA;A zvW-X0%MQr5e!ofuQg`xYK0U9;g!tNh8?d@%_f&kNW4B6((~gY2g$`%s!)JDKo=60r zyS+Ivj6pbgAFEpNnGTyl{CKRYg4F_H|6H`qn9{WkgS>rKCheZ*<~Vkho`Rg_Hx;qp zfC#3LObC%XXt;KB;N8CBCK}fZba@}XqjF7`&Z@VPDP8byF9$XBhGWXC$8NUoPrZB1 z^>OYApvKI!6M2%i0E@?weL^5@cqxC~u_8H2F+HMVNv(0o0Q7n~9_d;Bc8)w$n^ws0 zm3G82#-3tc(+ZM*3i#Gidm7$zq$5eiG;u)pJb0^p@+x zmHD5#UG<0XU+aJ}Y2Wkf)O;-b&l=FNS8mWRyQ zfUllrZhr3JrpTA~9Iw0Zy5v+EfCqo`2E8TX`XXW)uCx-nA&xS=tZjdRboZZI+w9C* z?3Obf7&`==!Wuy6Wyz(nEHkcB#avItGaxyvKYQssXL88`YHp0~K735~#Y^b9^{^XK zO?wgcQ3vE_E9P%-%o#CK#p*1^-Z=nB;vOytTQ1rc)k{7e>$F)@Kx}eA_tkG`2&Yi1 zMRKZhl-xaxbtRRDdprVIVu1zf2#QUX`xu^lyBSA@{Igw^L-|v>1K-n6g+4=Ih7?zx zn3Pe_VXnGe$knI5YEyVKI$j*l|2D{&8E9{`Hiv=MIduTcUV5{xcJ%=s(ZZ(zcwt(<@^6G>uhnW93kOzsowA*V=)QbbL6<}Dh<>DI#8$6v3;jn&A zj~Vekn=FTAw8?h6K1-?3bRc-~a`Fvm7+UtY#3Dwk3GzepK4szwB>aj@{~GRYi|v-m zugt93HO*5DWePXMDa0ao{y2%zr_PIW+q3Ox-8>*(y@^C4OV%WzbGtuzWd^*_yBM>k zq}lANtC&=dJ2F=>FB0G%96I{uehu9n0-K|AvB$c%GyITFOASM%WWn;9C0F7s%JA0g zlcGR^o=WZ8s3`C(Gc&N9kQBGY33fn5K!k5@heJ=qrRm}uuCznh)aQ~ zT|nZOKA|hAX;gfh>+;s|XN_fJthPfdaq)Vf&EmUId?4Z2->anpc}?Ju#o#s2Q`8YP zJaC;^a53y^E~(M!mw1n7cjS9;&n8m{RFHwRWo)$t0Kx}Hg`Qmd&O84Up9B=rS?~AM zK3#iVsOP!nGy|o?wFZ*du@iKFCF>8MAROU&`Qc-_lE%n+5Ufl?fQS}&6mJfU0_U}r zTR_-&wwZb^h40LGLzkkIpP&Hiq>2#Uxi8aw+(*WsRc zHaZi4+2j5?#2z}lkz7Kpa$zcRt!sseWzZYWHa(=?)K)(>HoUeJX2@MXIAs%jpXmFT zPfEnmXcHR+xElYAWV4Y*$;{d%d@O|k_I7y4JPqOHRM0&=KBxc$Vyvh@raje6xxsqN z(7zP0w`mc z>9oX8A3sMtFI<=@fk&wwkhhC5yanowP7&3Gd^w4nrw3|zr@e35tm>Xp7*|yTE!aFx zAm)Nxxyb#rT$WLp`pbpVfeklQ;@U$KsdL6*H6+LK9Ve7Fwx{>+Xgw;_rCGbj{My4$ z4&9dLTihTz-EnJmmff)QcpvOR&@uNn{Xz)RmAjYxF$&PV+{(b)2x*Fd~@f^Q1 z0!~W^4rEc6f^s8fIzGZRhG?A5(m1_%572M+8dFaer>g``Q74;NRy=kBk#mXc>&?>T zm=^3m?9`jKRll?NCU7bCjr+D@0vybVpv*+$ElQ2*BC=u^g(e9^08Jhgv;Zobx7_6i z^+_MtcmaX1t|88#S`fuRGKQ^75=oOoN2uM|7ag8XTm&r<>T}nXRgEHI36C&f8qYka z&egYE&Ojyf#vEt4^TNW08hnh`H^A;JQDjWf`lQC%3~wGYwkExy4KK@%iH^JIcsIa- z44*1^eYxzH!nqb9GWYU-4ccHAspQ_)9sEV-uE~o;#8U-aBjuv)clWVFg{~G9QWN_< z0XWm`-+BYb6t()G|4+B?);;D#hv`36^BZHlG=@Jfmhc5&gb|Gw7HT7Mx7*iG8Np;8 zncT-;%xt>FI`{Mt14c&(pGsMmLTvQy3F6V(BA2psz!-t-0dFVtK4NcvZ3;<{E#e{9!(T+t*7MiCi(t#7a}?LmA%;-z1)U zKX@9p0ITCEX{*m{W^qx3#wW_tE$9J!IgNid+`rC=4P6V|pjNh3_6cqzdLLoSt}|?i<)%M5$dB z#m*Ot=jQ$a5RCXwRe;-V$w;S@*7G`;trWBVXQqZHvSyfHxmT+o+J1tT!&5=!(0 zfJh$-NQ~dONu!ns^G$=VOJVY9dqu8aS?9~VnH=4b0hp(G2T?gFf?Xt|cwrXjqJeoz zUS{J@MFgUM(co7HBc?%pXjp(VIldXZ?cidJqQc9*C<)m?1DhyG&I{3B6%MY)l0-SerW4;Z_voKuS()YFExaMFj4w5 z3aTBNZi@Q@=pb}Nhf9((2g;#w&os5xl;6L96cnHICFO-tMc-Uoxgg*jm%~i}MHV$2QTrNCUyEgUO8|?{ayX8NCRnGuI`%X0! zw3q>pRVf`eq8ms-h#34RoiHs2{h_ z_p?Arj8AnrP@&aHs@>&saT$K#^7gZfUxb|##*7l&(; zpVX5&;@0Sspjv>-1fxc-!=2(Yj=;FX5X}DfHbE~AJOsd0z;K44SH@3((A$|==5CTNz1cO9%D_(2&j4wjOrYBvvLX7s2Xn1;;}^a1)Zb+wPF*x* z&!C?o8`d__vQu7SDV^EIW2{eky$>*6L9Pv zK_7tuG#Tt1pm!?;kasZ4peVQsC?7O58`TSP_Gl{wjWu%hWuZ~8kNCdC&p@1HFj$d; z3I-{P?L0eu8brNXUSC#&V6=+D711jfhAXDTFLtQYWM z9W~f3dAQVX(;)l3;(8tG7qu+dfX27+_Kjt6G|{T<_ZBZqZ^^C@iD9d^W8TAMlVE|R zUl&0|x6uT^!BGW1slae%6Z8RrVuVV=(lX;m783SD?;QL&(rpZ)_Jq`WqiUz7o*;5s zxX^W;{|GTugHt|Uaf9;yX9*{B{O{Qe)e!`n@7Xg8z3a4!jj z%GM658-D7xhw6GI3{jX{aPQrja_HH)@T`9&mVRCoHg(Q*(Gpwt?10bd7-7ADL04ZA z6Lg^At2=WRK8EGb+t{EUpy%5d=cdU?Sn+02yR_G$=r)0x-R0S@I|XPu zKK%<(ccHyr0f!6YJu7D#X*pu9Hbyf1MIJG(0MkT>>&JVP!a4`NWd5f2`1wEZtfyYH zh56sZ8C}~3_de?ttgfS>kk(p0r+)M#gnI&RHP>OYK3l?i z^C%N_WnC=`#_W4gU&g$MH|3M~xPDv!W50Qu9jDFZwEXf+|I;579!jwqr%oj2bcVyH?K5o(KGbI z^GS9*4@s2utMy~~&d7H>yx=Z7xQU)C1S;dN-6yN>?fdnvkXK6X4)Vky2VZI2rdVY=!!L6~d-6J6~<$x+*dkw2;Gc|gil4UES-o6*Da9`>72`=Kbi(c1gq;&I7 z-YIY~cy$dH-rY|M&k1Hw;QAffHD-67A~!-bMl2vxn_BS7u<@?hs2J1n&_nz3J7rbw z@**L|VkZIyYC_2v-KUez$k=j;7f(*eCEZKYHQ_W*QtD^ngg1NZ%0_2Jk)Iw-^EE%P zG9j`oD@~Qk#2~gO!rI&jCiF2d*JT->D)@ctb55CaaC%aN%)S)h7nL9i7GB>x5{gHt z9j($UnqL*}wB?s+X{qJ4%aUAGpe2N?KnrUrd+INvD%HM`1PSzhl%06LDNp=*(wg7t z6y^VezIq~>>rwHVVn`C)@CD&ysEGp_`pzCQ!+4n;&eKe`D%Dg!bCO=>gd>5msotek zvmC9b$COftR(<%H_BmMO3NrpADMKpCY71}oGASMDwY~Q=r+EadZdH+GVWUnv(r@)y|iFkPBuN5=z&ZYteCtNDDP;tLU$YP~n)m94aZZ<^|upOY?1(C#K9< zm+;=e_btoZQ37%L%dK#3;sZok?v>p$_@rM!b)D8O(#CIWu=5>8@tWflC&sE;1S1zTY27Z({Jeqg2iEgv+rcjr~ywMt0%lq`Aix!3+bzg!%Ph{Wlyw7z#~o87AG!}dnMva$5I&DdG~Ss%LORfd&+a} zfVhX^$X*|gpqFEo14bdwN2 z)^p(?WGqcA=^QzFUgsNHz0m#OdCOfnw)80Xs+R9m_r?jZR7?159=w8?}tt z1f9olM)NY}2SsBMvc4$OV(q1=O{jb&ml_5f3f_tPy8Z>qxqgraN$Y_A$?k99S{HPz zcFytdDqp+d;x|xAl5GHyE-5i&e)mq}1Dj{9<4g8OjpDmkUBvh#)kPFhYwjY3 z?M{`~^Is~C7ZjA=2%641Tair-`oT>u6yn+KByJ^W{l@$gP3VoB2{3z=qGkpaAUz}f zYC*bt<9Tl zwal0nUDxZrc*As}Atf_iw-}d24HMXKkJBlAEy&%P8Z$P}!h!FrM)=wlRbN$ZfgCcJ zE+UC20BJh;e6pI;oDFY{$=VJEILT{67r2nM^9hHSZ!snCfA3Y=@F?02!m81k%DF&@ z2MIH!xY^4@6?j!TH3;jX`6on5SE$o*RZYvEXENGO?s4!6jXxd#e9l(YunHUKvs=8+ z6b7+f-Qp27X*L|SVZ50YQ#|o|-7mL*mUlGj_j*k01OHRLa>Yq&>t=tn_1$Y1*}Vj{ zSNcW=L%_MVE|p%>fgcp6i`cbIJJ0jg9m+I0W@;$T?szWVh}tshOZcFX|7Ztq_niEZ zx(G#_uyv(W@uvarzy<>v1!qq8Zpv)~#$(J`%v^ST)uq}iyK2r`Z5-Q`wD9xM%-vsb zRG-G|S+d%U|1=+3?!v%8P|^9ehk=_X$rj>R?H|wL`|K^sriU^sC#hWM82U!=sDQP~ zHa(mE!*hRw5rsKNrvy7Bw0q<9S2zM4b%c@kX$vuf3~F?4#?AL(LI%e}0wqNn6bE%E zLp;d~f8Pjbv1W>R_K2lW!*S|KG#Te7oJ!0#1L!*sW}DQo9Qsm*MXtu?X`%;as=wcWobt3>ILtu-?pOC=<753Y4jtR zxOf9!mvOgi1;|(OXvaUawN`#=v8|RxyKiiEzxEJ$0y#ZX^YqQe>|lQOdLoUK+D_Dl_sEH?5- z->gesw$g4hi0KZw*zLvh8Duwy+7<76Kc4us)(= zoFD*Kn0PKLdY=o@^kwydTsB5?xW_Rq^wEntUAb2x>vBFZy4`0U=-1wkhidOb^2Y`v zB*z1ow6|(d?n!J=*PB07a9yfp5-$#blN%2*B0>r2@t2g45-+Vc#~=sY43!qi z-63^e1Rs8;GoHJPddhVwA)&M2x~#xPha>yF;1_+k!ar(nV1Jjb55>F)U?3;(v7;+` z7OAlfAwD`eC;z!PJ{s9C3A{B(CqDhY9BL|vnbgNTJ$B?tLLYZCWLFF7MT(#>j-#Gp zXk%w3SzPo_Ml@wzkp&Dr*AKgYnimYeurv2RY2p3fEih|CdHsU$;!3t&uMl0lyqUe^ zQPlGjpNgQ_6$IZJ9^brS7T@Gf52}zNG^5$XJtI0ZxfI;BtOmU`#E++Itda7fsAXuP z+3;H!t7W$>CHAqIo1KX8j_QBO;(FHZD1GGOPr$l`##{p=OC4&nUdR!xz?4N)nAB4* zh8v1&=*FH{HYZz0U;Qcj$&Y>^N6j<~4~8b)CCi9zI3-@QlQnww^-@cIh1KAM6EpD^ z>;~y+C~AY-=FTeF~zeL46#+yE0sCOLq!CeC)80o_GqXI1vKtA`qf>cxj8lWx^kb?< z7aWzD0DOM!8Vio-b{z@U#90kohB&7=AZ(f%$cfS!1ks`jjxcAs`y^+9P1!YDVX>fw z=D>^~LXBuL7dZp((dznVIbLi`?%z+0yV{fU*}Qya-?Q!sS_*#)!*`!LGIar8r=ExP zi#B|T=2Za=ZW6AH=%p{TdmOzrucEgfn1Xe(El47nw!y*^lRN`B;%uL(c+LY z$SU*=`2Zzs98t>j#q|!9B^e!94sFg`x+Y(~ktC83pTSI5+o&r4!m8_;6lEqN+CFI8 z#-`NhhLv?02@;z#8zF4a)?8tPubyGfb>-&PZonEuVHTSe=WXQTUZ!Oi63LjUdamVD zT(5(AnuWQhs3^O5KFVPCOK(%I=5Hz5eUd_+S#4Id=)9F8UzX1jb3 zj^2Zh@Z8?TM!&ZDP1zIL^Pr*AYH`w#Nz1bNSj0veaP9wQ0lr#r@_8DF^?pH^UZol+ ze=I4b?zAtP)Ky@eF(d=Na6n4yfaf_sc(S}e}o_1H)2g+Y1R6MEidLR*pQdsh{O z9PT(V^sln8IKX|LZHn+)mEIox%E(QOk))Q}^zVPU=ENV!vR z_J-Fp4RqiS(WNWpPv8@vEd0m4%U@wRV1*csjZdjv7F)l| z=~eQ07sO?6XV3U%(d+(mF!&(}Sg_Ego}a*7Nr6SDE^b%v?nrpRqCr+uf37E5w`&Hz z*3P^2&2{CkD^FOOR2{gOo zGp-5~fRyXgx2c)Ur_p4>E5l)Z#mv0lnx!5$&A^#;7{x>x4?hu?GaVN5H~Nii>ysY4qvRz9a` zu(Zp*q-qN7Hzl>S;n3#vh8mTL|gx z1wpRGaC@#wv%yKnQvspM954<@m>r)}{q9jDv1DT=AczRuQXp}selT3W?sL5Ny}$c6E?FMO<~8qI_&>mlmLNn z*K?M@X5s|j5=fbXq<(I%Y2cQQS!uV<7TIEB&GCD*@@x~OB>P&}Rbb7_- z`T~lC4_<^LyH`F)1t@TX5s+U0J`y2w+N~$rrLPfIKl&93ne*iD!*xA;c?OD3npIIs zcDZm<5Yy9s9cv||ce%YJ7J>Ztf#j9$(mo6V1*wbJ`TD-=rvYeeF0DgEaU2n11QUPM zNCe(Z>?Ck-yqm^MeF|6fUBNSscgAMTH?>B`ulLqu{!YOS$38X>ftu$PDF9bs;TBXj zygK~0e9Le9Bg5PLfiD9YU$I|^h$VYy56f$d&L3^JX>kVp9y_^5`;a6ycXW=g#dZBL zp_zEI`}Tw0J_$hurJf*>`Mt~Q2G&96krXWYdj3gcQJ!ifWmi@Fu%Ff)8okfX#86ov zlNvifnYxT9`0FwmMcmK)HL z&q`Ar+MMT188q#cVb)io%|^xT3eU|+0u@V^xhb;nHzmmBfe!|rm_1(+FE8$P@Byp` zBmZ*jDU6BlciqU!d%D6gQbARcdsm6q8NBR3cCKwTv_N$DUmyj`gQKae-(S3 zOZL-L|CUD)l|i69(z5FRmXl;#?c7*t#XYrcs$_{52+UnQXJ|7D8LyI=4bpA^TuGr6 z^pHNA&?N;3inK2JEEpY|O}qm4Ey)i_&xfvdNQAZI8#UC@#+P-rnn6LP3+4k4gsC)1 z1w614h6xkN&x*7;{9;`G`uB|T?q@FoR1h|!`&0(?2O9&(AYC5^JtP!T#ORf(%9w!H z%`Vof4Liz2|AV&`c`R~4#J#4H&_r&ZXGDlzacM5v1jY{nOr<&g__GMXE9Y$saE0)v z+(^iJ!&?{ak7$&Xl0o{KJM!a!Yk}<>`U$cq49KfPo!&n9Yp(;}qC}G~GXDJt?B{W~ zdZZCF&_Ki`JE4RG1FDazA0YxwJ$a$`iN<2?7w`}3bvpW{O#vZR2Si=Q1VLGS26j56P?0?^~ z*_`Cs|Gx}hEDY0}NIDKp#9O-iGSyg^Jb;o}I#NZw521!rB!BPMaqglb0n>kiHP(3! zw90e&LtXKXl!}#Zn|WzA9PW=p9lFE`Ry54=VqEz9U@BUSpFkedyLONAlShJk2XlzwUHe zteF~$THL~({r|i<#lV7m<#Lq9&9^IpV_nvg;KgvK1PG27@LHCnQg8nO)52wY2h(vS zVP0*JG3Yohp(qnW6-)((CD1WVmtCBFMponnZ%+)HUs~LyDD0D`snA>%*T{H-WOf^4 z@$LH;L41cum`smGX><;1WO#a-Cf9v!Q{fs_RpK(}6s9H`h3jG{v5!~MvD6!(PqD;H zR>8>xcKPoUXAjoY7$~|HeNwHbB6n3#JfnJW<)hxFhqpB{F^2h7^g$@AP(lk73ar`L*I;lQFvV<(q z`^UxVATG`zc12wV4Ah32;dwH^5dWTSQNL@2BScuQa}Rhn!d`cRi1wD?CYyJU5o`A1 zMCEQAy7_!fl~xv&+=&^i;T!c%UVkm)nQMPPu6&f)&1NTH{Vzi({!Itp)E?{#FfxTg ze%wGwRQ(8$oD&8axIi^Nz;OVSXM1!cr7aqSdQKm1>O01-I$VJ+zWtAN=S0B>nE0T! zaTl?|OisIaOw9kL*zzq7wxV7RE#J^A4z!fSwK|$@2ugso0k_pDj8jQ7hwP32$Nqv; zA)2F*L#SZ|7Jy9J@v-qOuauFpqgO;um{o3c%7-i_)ExM^RY2;?D-ki zcFex8Wr%`7RwFc$-;hxK=M3Y%z<*84_CEzDf$gze8=JIUVTAZL6^|{usa! zoLz)t?%n%fER zf)Lz=0Q6Dckj1qdq61X3UmagO$-w)KEI9hddFy<+czF8`Qa4nX#2W-!(~ZCP?;8Ss zLk=BsW+21qn3G-?l;dQ`VtX01Hd1XAIhaP_Jfwfqgu-?_TRpIfej1stL|^{{#orS; zi9uaoQM#ErgG=Z?`*wWi_X*<%_~wuPf*wB15TCX9NtGURuE571x~p8k`_yf&Fk&5Quuy2y*)Q0W9sOh%NWdNY~YnVX5Gqa zm)&B67UI1kYPiJQEcP-eT+z~khcE!QvfvVE1W^dTuH5;vX*$m_{eL`RxK7dq)+t{W zGQ3aX{`_a!6b;`20X8u$69a~=L066Xiq=w;z>ialdQ&{k%MrEHEi~(VR-3HVc}&gZ z=1YMJw~anMV8e?^Q|1{@s|Z%9|6)U%`Kp!OpfO41*gO)?mfOf*iS!~A1-XgaN1l?| zd>U1`eC&9CsYLUZ$GrW6GWGYVlxpcmvhYjs2KaY-yXR%!mM!BpM-U?o=qVO!+O8Oy zD+53jeOaaUV__j{465hTw^zLOG^#5)sDf2I;6RN3aUj5K<+Xn}i}CX^kCt9x(`2BG&IJ|y*J$UOLM~)4K;s7GMs+;rK zNn6x691n=LynL^PMr0nHG+Xs>gv);~X*uEB#m8Rq4ydNt;-9j035=-2RhFbz$*&W#zS^B0%>iAr_`v-oKSnbzlKKN^ z`Bvq=6zTRHP-hl=p3RqtyM({RsK9J_GK!h#w#YZj?Te%nu!;9PnS9;gG$Eh4+vQ9V zqW=iM1cA-fW_d$$*eJ0zL)6>%sxabxoT{YMeyg)T`bU`d2KsX{=BfjZy(LHYKUW_c zr3Hh(iMbF_p~hE$$JIKDD8gjUN)G7E7EFo=FdUtTE>a1=hfHiaRVz<>5K)Nx z&8FjmHu>DqHs6ZlZAGx8-L5;0^FqO%Rzw^rVB*WV_>;|ZolC&|gUu^&h9ypx(NyPr zQd_t&ELfGd*vJ9^B|?AGQN5M+#p}NC%_sLV`!rg|!!7sf!pk3t&3TgbuYNAV#145T zp48}*HIy8v>3?|CEv)iV=Iy$GuTlrg9qYv<1YtI3{ zz$C5TiCcJuXngM3IO00D&4Z=uvTXZ8R+H>9iC$vDlJPV*61@JgU-|0SOBb5aWS|9U z^zhgNG=@jEKwk{heW0b>J?5F}SO+~{z(S15w6%iQ?H zs*xX(uuUM72nWGbeFu8^^5%3nDZ4AUxc!(;E4vUj{6QkrzpKL=4vZxrhLWR+UBRsuI9n6-(> z>_O;Kh!shY)z-zY9+{6owMEorE-axTLqwKm3fbfA#8NWpZmAMjKZyL^v{k z!^GTCm>`Lj86-Rsql4OECYJ$YV{XP`sbW)MRFmzflJWbcuMTJcRsnZa`??L0EWgBp z)CIl5%4e@IkS#bWAl4pX%ql2knDCVgg;6r7wpv!=+S2Is{GAgh?g#xUz4LnV%9g6;?|3-aAwb2XRrR1)(?ZPhKI)ZSg zXx_~lynr`Iju=Qy_paW0??@Wj7yR%7P}+V)r}5`S;tYAtK5o)jvZ)bGnvM%f%*$Eq zB}_eZ#1W3u^?;3i6MQ;KozlXo9%bsr<)$h}8CbLW5!kV;oQFW!)k~o0w-q@uQ*HsJ zkbBUvUyMBTb~^ot=!{|E#x>g=?3)FEMl-15k=!B`GgY6h+zWaK?}Dr41rbjKK^l0i$h$TF#gV} zXXu75MnIs^@WUju1}_EnVDI~g3O7*zwx6PFlI#RlnepFMvLSSA!L=lR_~tfj+DLG} zV5F!14(I{~$kIBv*=vk>4q|fvp0_?K^N>4V7i%cW7-aINq3sC(DOlz;eET*{_4wN> zW~5&2;C(=)$dzeZ%y5R#rMq>@qK7BWo*L>Z<9rJ$UA{fAM{moqpB=C22D;ZELv?^z zy$DcY9kK2eFj!{7V}R!sn6wDk{tAwDJ!xm|4N&KIm^2cbwVK8i2brS{%CtCO7xkrs zf4yt%UoOCMFZ6nsGG31BVo~O-rYdgv<2-|J9qRvDE!KP$NFo~VI-`X1MB-kj&f(3A%BpYO>U&BTa_ zKC{RlcrKazaemiwc&4(?j0edh7F(3LqOriXdQnWc5D=ZMq^+noSx_%GaAx{mmw-vF z_$J5|RUX_g*$**ionH%ld6o?w;z`=53>Ic;{i*r$PhJ1d{d$5C%)H71`5a1M!_o$w zV-&qyk9pGK!9`OIRT)g-BnA;Mzi#H!4C`Gxdf<_#ZYKYUk(aiL%vd1BUA}4?8$2}9 zP&ygG?<*79r3X%oqx__S&7agg>LZm^F3n%3yW?8RGeG--3Fq!-Q;eU}TU7^8cu>oD z|G{bWq-|V@Q@g^c8%OMqXDjMrBGb2-Z&ZhIReU~et2*jv?a5veSfyWBE3~{i|78cT ziaz`W)$>*DHR!+_foKKt;|k<^QVj!K`Angy!iaW5l*{<#lM|EH-|H>9OW(J(DXFFC z-)z{TM&J5=_8TqHmZqhl4B<6lUr4vn1t1w{X^MY!_yf=jIMDHvwLcgpE@uB9C-elP zlRD1e1rnDr9zS7cw=0rc&Gwr)YZ!7YT$L=~Xb$2g_AaTQs64crKy-5T&`fI%U6R}B zE|2rb{{>y$yDX*6$Yu5mbb2NQb%|Q5mNOS3HEzlyZuVMI+|v2cedgQYIRo8UKtaF8`oTgP|LcgmN5m2>mG*VoE3Oy(4R|MpfL16AlGlS@Z=70OcBC zPVMcK728d08l7n6s-;Qb@(brh5_H-sZZfqw?c0t;u-$raW zTKj>>2)GqJRxf`!;@#u*6@ILl{@6jMAz8cY@eELpvG}Spqp`pAAiGC9_zFI}^Wpa)muDU}TC#>GzYBYbof@mBS&F#`J z2i~98zU*1E%uVkGqCqldpN(lA--3e8UH_y! zi<%{|v5GQ2rn2H5?ooJ%bVyYxDE3+vKv%O5k+)UcjKgPo!~d3qk&`=_Phl6Y(poG_m&qEA zR$t9c=>ePv)$=aXGc@taZxhxV#0r0H#s8chtKp{TEq8+{7!OptPY?aQv3%_`#32h^$-8i<`FJ+)L~CJ`39 z{1c3^ZY@uhL>O|AC{a!Y<4oA*0P)q^yB43}>H*b#UZzco)sC%#)-{xJO?h+8@%*{y zi!S|*WeA=Z2*MfWo7ac;Ix~icL@D@HnrM#5f7O{r1oX3cxk|D_U;A4R-lnPc2LExx zWoN_zztju&ZZEn#D=elq?3&FI&h_ra>)i*Nvz)aB9VgZu6JeyQe@=D4(_+y}IC9bx zgpOd~5kXeyxB3!!*m#wJvp*B_-7n;M5=*vZISF&jmeHi=pfGnv6EKJ zzB?3))YSf)jQq(;=voRKdtfbE9%XO&;fvh2G#44s>jn#z1-GJhTCV>7>Dn&ZZxHyB z+bj3bjn?{2yI?-KQ39Y|3SOh#H`lJztpz~5W`bnl*t=Q}Yv>bOr2cfI>VXy+?o0CZ z!5kvy1&vNNrmd3745^iiL1nY;^+CDpPj((T1;NV)UQ-^K1<}q(I(o@h9-JDBt}&Yg zX{@7Um&q!_X*j5n>@}$cOQH9ixQy#53}@b`t@V1)ko6X+b4w5M2fz0~a_*oz9p{i? z8};jjm1n-OPLP#DTrx=T*&yk3@SDfqLN>(Z5z*P6lAEvk)z;NMXM+svpHwWHuVcy0 zE`9MQ+x<|8%AoS_+Wpl6JCp^$D9FARyk3Ul{#2yj zy(q)K(?84DX^~HTSySuNP#kFDfl&4Qr1!(y_>b*V34o`$UWyR9*S0n)8uRjWM_>Imb>-9ikimdzAj~*I*`9kWnQkHkC{y{cA%QHWrwcFn+S!c_BiDo6K)-Hc1l`&VH z(G3b^*w7HhPk*nB-pH?!XgoyRFdTXr?lBzEW7%-TI0`>X?=XjSDy@AS2&yknw^r_7 zHOyAE4Sme?e~N*jSS(!7NiDd97tZA>a>D%T;8Co8AI!-`@90uG9s}gv=J>v zDRyhh8>d;^Ur&0zKuAttk9uko-wNgY{o)CqCvc|E9+&w>kXM}0C0(q?+HdjuKl=BA z{B4uLviWiuYvE76r4F;ZmTycAEc-^}XhtCskTVmC05L&e=>h2f^a! z!%^KTENEdSvTK2}oo89aNdQyQiJf_LS*g-VDL`crI6Baw!uI<9)A!L3imX44y%il; zRycdo_k()i(CxM&0k~>$fc?_|s)QcSyWnSmxa|qX7?(%iFX!!{fuLl2V8uqhIXRaG zHVoQ@CwOCT7A$or)&*qbvD~$r(jv}vL_P9AL>!b1OfdU$T3|Ff=M@FWRccMD@>fBo zhE8{7mQGx!kfM|SiHcEO2uU@glhQ0$TW?VSmj$4SDIET`61=bK}wnecHm>bc(ygtS-7|*(Z(XEthQB}3U4zESH2Z7lu{SD zd&P4)#b7*i0Ov-J5g=&y%dlDJT?;({btt%#89N4``o`^U5l%=PdY$-g>10-0Fi_@R z--vQ&N9KNOt=~=|bFMV!p$Z^7btG|^q(UZ_J$sJ~j7H%JH}2How!>6nkK$jfML){t z5U#({4(OR%Tg!X@Y03TPoTAuw@|@;EN%|JY33~hH@P!C;?yZZzdUCF&3P0>ui$fP& zHqkW4`6zI-FkDpqWOeA{;J0jZ4wJ@$rnSuxlx*+=s_$IwlXpNO8^7(=0=%@cAN#!1 zh6Sd>jM)-^qR#8JYu18NbyP*_^MzwaRd9qOa#^l(wpv*h|LN10c}sCRZ)OG&=fP{R z+*~opNsPlBbb8V*CNuziiSOYCz>((Hl>SFo9@EW3zDFyg>Ti0uMXsx}5%1oD zMCoGeP){u^N$GVzdSBG614JW6;x$DGuVEJ^2mO0mV;NEOmLTgtm*GDT+9?ex&o0bu z!2L9I95SuwkBH3PbNY^={Zbtlnsa-f5abkDClI8rT57@0mrsfCoC&iZ_bfzb+jlnh zE(0{5`d8sj(+*6FP4By~d~P8bC|;EDLKk!v2;zt@k!lh?ScnQ=pa0ux|2;NLC0KqB zRIH!^_*HUH;MeYe*4D6zjbpuWDeEP}*>`kpCE6nN2$AQlm%_AT!JYVNjC0MA8L3IV z?}#js5qpW*RO8SFUMqyGrx=F z{c2szfaZsXTN}Ut=*~Q@1maPZ&&W7_)yflgRp`7RygX0^;?mf@Xh2u(bC%9W*p8i{ z1*L`_NKfxb*8}Jbu07KWiZH3e>RV*CC1S2e7{gzqx5bze?s~;$Ndi zDF{-OX@#Aa>ICOzCI#_f%dl_o4sXZ3-ZHQM-!ZMvN+8R*0T~qFXGN4mWh(X)o;#0G zmW3gyIZ62MGMXJz3%Eg@2x^>vqWX=YK=t39A4Z255ku1C@0O?5M*6| zJHZyv;ldh*je$s*Bqe@y9wW@*qW#FApm@~a%?9Mj99}kn^PhUov_n8l3gWLLt|jaD zY7PJLrKsb<)S1R5s>_&h(JZ4Zvu{#tKjK|MnfLQ@H3KIs$PbGq1Qd+xl7HfNkwfKw zC5teA*9thsVh|TL)4!kmtLP~4z>`PaJ>aC`;~R(LNWb0QWBV^OA-F}T27abTy>%)eQ*g+@+FBtlX9`{brtv2 zq5Fczd6AMGjm2L&ckzzH@+^oTAWRGKAm|dEM0#pV_ax`q0Z#m2>30Q_7}K&;l(j!*tbJ)#VdHs z0g$f1xg?+tE){DJ&lWbf1ddj(iUiyalf@*qE9TFh!4A?-RwvI_0o(?f7fX^}<~Dy4 z^SoKgEQwYO{Bb@daJw=fM{3j04%@KVS4_&2V@zIk?=vTnnGSQ}Rsk|ru8CGu;1ci}HLsqj< zx`v2;W?QZD_40-mydJ58)eje{a^K5$Ph$#Dpw4T-*{;^0YhUPax#6; zpWRr;MSrv~pzzG%mz>na=NDtJw89~I;cFek)u97x9&OK?@02sp)Ytc(zE-!_`Xi`b za6C#trh?r9ELeW-Nr*_C@5+R_UOTDCSLkB;KNT8R$B&LCOv?Qy>1{-5{~EKoVtKxv z`vX{UEu!Ayms>GdI(S*9d<-(Roevi^!8FVofHUKHD9CkNwQ+P5COjb_=uI8bJh}EK zLz&ZU9$TRYB*!;%TOVk2KHyn}x!f{tt|IwiL2%?R-;m62c1cr*>gT@xO4-Dcu<-pC zM$9zqN-c~xocPY)I0F}-NxKb6lM#v$AynE=7DNsR43*J-tuVdO(8VSuzbezW6(Tqt zv!OSOHqXN*F@&lwOU6hZsJvCwcq#fNacN`=nIj@A#O-G*6Y2d5 z5>Yu!>SsPwF5f#-SvyyxepDl1dr8cI^hb>}6F@`2f~s)pKMLdKP`IGC8;uXEydlse z3DVWAR2winx6k$TE+2={l-B_jB=D$GLpHG~mCBf5@vexXH7DTqH1}{DYG;aoj7Tx& zY*Yk+&%(k{DF8AKfZM)gX(tG1f!|5}C4%1Ykw$!E-UA`e?g{yY%anDcDH@l7v9HKO z<|riYUMd{h!Pljng`?e0NZp$Ip6Wr!$S60v3|KA;=j#N6l?t{jz~Ejsc(tRkF$SOw zpc$gaZgmohpcKaR5^=s^>P7p-ziQf;2}V27S2KRsh_~;3U!sb_>89E-aR*<{0P<1F z<0{>{%lzJ^kL=4Eff*N3+8-(PGT7vX^*(16d~zOG`KyEyshR)vg$6LfAltzq+F+ilfir$s8=5?8Y5wKx z1@OqT^=)#=8(@iQgcY1*kV0n&1E4wsdF}Sn{$B_i4t~6F^Z6Q48FdC8xwYA{Etn3S zNbs!_{+p2DcOHD)NGB0M3mpaHpk$Yg8XK%zedn*%v3eC_SpcGwbE(Z5SBK#+7G>H- zAcm2`$V**Q8O(?RWP`my82h-d&x5Czx-)nr;zN{bR}|1j_U{Ah z1FFHOgNYsDqweV1XnoltYfQi2p%Y&U>gO{(1ItIW#hhvKW1iex7rHfW^SWo#1K_(x zP!anR5QGU+A;bK=9fMN__nJ8Cf|!*gDmPxBJr%d1Y#NI>tYRQJ!7zFK0^NlDXK3 zV+h*qF?=mcY)$wYVJN@4AVKr#Muo4o)jsJhlW_0)IU6=PoXZ36L(=Rw452N?gt!OV zqqs`yfB0Su#>oy?GKHdBM=AL@3J^`8YU4NU2%nbYMHXhk!!|bx8mjT7o2-l)?P)B?_^?`5Crb^cuo`_glgqdnNA3 zJUZCsbgEf}Xg?sAdPkJmfB^z1S@`0+0{>^Bs0t}LcN611#_inissVfZgEC^(cWq8= z-rvK&OMQ{D5gJ^pOxiTqr=4qMj-}$1ea)s+x2`^T&(wn!fx`pMk?ZX=9|Wr zb;}mN9UJ;CuLUHoT^lBl{v@$zDuyUCA*_vGcU*3B`zJZ+RGQXnfg-R9*9F^YU74K* z0DUeUL0+LHA7C#-DdTA-Bb{za50?>U%M=`)8?wCdn9Y?b?Sos5yZ7iA|0a&FuV+?y zsaVWn@#zQ#@}SqMQoB8`xl7+)g_UPG^?_tQ@l;aDeJ%IgLOhUaC)^mLN4;m8)J z6{Q)myMV`qfeelzGM4wJhv2o!vlq?d?dL_zzvQk@CrfFj5UT%Om^#Ggzrh`?;XTsFGT^t{;h% ziyX_+Q`|AfuM{y4#t5yx-Ix{cwnh#?m-Rw-4P z^ICW9P#Xz~zQ@W5$*+-KKd;Ms36~!B$PMvJQ&RANbTDAq5~g^v+70!ox$8ewN#=f~ z>;=-IoFCU{As~{h%d>nU8d!FcY0k00DW8Hi5xYX}PzE$>@XOp@^t-wu@#Cwtf`2Ow$ ztVu%eS&9=KV95HV2_6deK2Jnc)*|EI^s`=_BSHnpFa`ae?<8#SRsVO6%y)rZEG&3T z1earp>ikF)2f~q(y);YeiX2#+xjjhBdUcMTyu%)!;Z}153Q&!wMH<;E#?&O=BZHni zyK(8r)m)5AEMpFoJ-Tm+yT7*q6szrY3fmbKP_x{9GT${_#%@*`@0ooI&Rw8tba)a@ zI3@Pc^1be>9*aUYe7ieHo_=Je?jhBzH6OdQ+g3ZVL;@bu6V=);zT5|4XGhQKED1Lxhs}LdS7eE>PcCg9i^Yz8}$&c6l2Q8pg4pSo=9GBgWERT1;_B*Wp~F^faR&7-a^V4te{)c))+H^8N_i|EPc2Q&jxQ zLz4fZWX+7$@gu>xT<)c-L;B2K-9&C~WgW6<&a8duwVhSy1f}k*)@D`qs z70M?%_l|hTvQ6H~B%CVyeU>G5nCUCkt`Yfo3wT-rD=8;e6h}lEoqR_S^cD0w*AnEb zNtlLJ^QD>)M2y4Zr!|{mc=P1aVjv%G|aJox*0T~TPfQnBxn)3>KHVuq829E}#UJcmwKR^R1i?74Y4aG@bdnt}yrWhyW^q5TLwLwADdW+Y)9wf`BU#iN82R%hU&$0PvoGGObuQ zET+E}lGe^k%X`z{KUx3{i3=S$ltTTQ^}q@`w^G;Rt4&{sNK9sY!0X=V^$+6{;>(+Q zF)BmPTz9i#Qq?^%CB|mJ^Wi%Xt@yW>7A?2|}e#&J!kum6*^>dq<;zIQjR|46|8b0$LlD2` zg)=W%rCcP2&dG+8mu3lOrW1Ss9ZAgd>`Zqj(&Qy+%(*5?zH)}zo1R?w%JOoOJNM9N za=c0q5HX`W({MP5v3u_;iDjRtr^8>PZ#&iq>Ll8Jm0xD1vUG)}xtsV(v)cD$m(y~$ zF*k&zPbH-%?_xuVMr!H`N|4+FVYd9O#ee7&GBOzgZ=h%mZlojeG~KTY|wiIb6p1q9OnJg=odpA3zoi|wxFYS#$Maphzj zZ+G75K2WRqkk5MRI#6E&FbvY#H$kqc<rkeyKQRV-~!P@Yhq6D z(94DO8^b%Fk1K4IpC4U(oC*(Z8$E>BO~atfCaD|yBH!8R0FOEHkXlc7oW=OdY8k2| zy%~GxcLLp(%gMP36gtVfLpfe)aA(P&6vRqddu!M@Sa;t6_nCwG^x$~}gV)I2>pEcU z02u|-*YhFfS`ptSJ$hoGjuKzgKmzEKOPYCmwBw;~$BdL0_2J9e8UHO5^UfCCto=iS z{BPJ_E-UyO25YP1hbQ%%>4a8MAA;e=>0_uwnwK4+qAJhu%&>!jZU_DC5)H?}S4-__ z8^|-%>M%U`KWm{X!*K{%afUlz;Hr<|b77~?}&MWFOa6dTk|kfFp!yKl`ogBd%e zsS*@vUIYUuh(&`_5pF9pt;fmKQWl*X8=jT{JijXEQgyiuBd>KmK;acbBmq2RK_^YUSFCt<~2#c<+7cfse6 zsplO*;1qLy_d~?m4acLSifzhQ)gFK*Ca|kK6*V-)`RplfV6T+Ky}wp-iukU0;>GN= zS~g|D)vdLSfTD^#u16(gQR%f8x*DLAF_EO*#rf{x8OO$8b2R~WdgQcnj(W#z>@83P z5PHF-zoKjzx1?2~r;o&YM*J8@;FCEI=lQ-|L{W^-5B{z(90922sx)ip=Eu&HAEdnY z?P8?WRlCqInis!8%4{?F`BQ1sER)j@FVPc$(sGD^BHzICYisnr9ksBzHXvg#pcK~f z1bhleKOAwlN4jc6ZXV}8U_98K-|?>4O~|)m1sVo|p{%FfK4oSCKP|SjhS_;v)tq` zrsivahPEU5pnvPfaINj+tl#x-@w}*qL?UZ;YLLpY#%wBE-z{|Li)sougV9kFF9Bk0 za@@93-xWF1M%iPt2ZJl_g~enZsh3| z>Vr9B)5IAi4Z_Gbsz5t*5psN^hB~)dntG6lcpmji8&_w>cPDnF5-@aH<@|Xxk5!@* zq*pbx*9v0p2l|G@7NWAlAvU7z76NU{U%o_AACHISd4~f6DWM`kv%QMKQoNuq4mIjB zYBA2wWlxvGCO3GaWU~1o5Qo(;+}7$6u=V#T${{hq8LQu5W`SM0%Sl)a{(;&vpvJGLRy#s=}W|z-@*wE&qxAQPIx~Ee~F3;_YP}V8v z&tbv26d!VH+;Y~hh|A}?sEIEw$+V-Dp+K^QMPWMv?>tiGzTZkT^0`XjiVVmag5i;f zY?kN#pD30yLZP0LQn2_|-Tit%boc_rd*0^n!7_EfEPct;t&Jaj@NiMXP56&nU4e6E zJy~<1(xHMT%aP0{O|^j{KrIX$fK*!f7-)!9MtkPXE<ZF zjMO5aeOqcZfSuC{tXGfpo_8*qSd2owq{wg;!$}b&OGEbP0#{3$F`<0_q*CC7IV1%VlfiXSN#hrG&z)ZLd>EY_XC@blR2F(XPWV(P#J36~2mFh%&353T=fJ7?t|(UE#>_Zi)D^im7R!n{zfHJwfSs=wE9^E}}kxu?3z zcY*Ye+};Qf!XPgJ!T}>7#0Ede-HD;7yw6LI*O`rbppMDFxnYLA1bWqUcw%LZptOqy z;8W$j$FEeo1$yYyOH?ECIzJ==9gk24Npih6(bhLThh}Q?(T)fnNxsY4B?bqT)Lx<@ zM&7Wc=J!IBOdXD)CVijh^X}`Kcz~CdP-{~eilO~%afw~o#idBAmI^eY!vbEfXNK1W+uLSB9R&wAFkWs;};V=={geLU^Sbp7vY z%X7uQ;kK(3smS&|>%P^5ZLrAF6En>klzdqS(ATpR>5KISm>wXzfzbmI(h9=!4Oh9~ z`6V-DBhZ^I5fz7DU_HTJ?&(I-U}qx8NiNk`&_bN&#h`oH>cs&wq8Ty&eYSrttPoW6 zW|!+Lh2vCfbIM&d57_(!n(^rL(q7X-2Q>!6-hEO?9X?&@62H|~>?_A1t60aW1;X+9 z?%?x588E@NjygkiL3Li8yRYDlInRuP4_J|r$U{qPapzI`7%-3Ps*vvN~w>{#YVtrwb4 zv;l(!$n>>M;cE-m#!;5&WUSY${cCjVKpimk6BUj3Ku!(}EBH7_Yg>A_5VfU_;lS=@ z2=+^)Z0+<7j4p-W{GfmWJ9(oj*Eh12#WlvGnvjJ-g9B(tb!1W0%l=tc=Bbtl`y6Wd+_)Y49J+gNg8G9nZbqu=_hsM)hsjjcZ#j8)uP0uBEn`7porO{Kt{5#nM4*0tH%w zar2uG2t6~RkFCNL3u=(W@68vfrp)7fU6>udn!rF%B=E)yuzw=QF~Pt2zZ2O2v(^XK zjGEDWowIn#wRU?g`Mr70Ir}?R*6SR4v=nv{TAP@RNA^4yoZSRm+gDI*D-RwY^g?gT z>%ZX49fbo)+=+_1FPl3q4m#cS+WLC^UOb~L8$J2%rw}Sm;YM0;0yU8fBbYAwt|5k4 zu;8_%T4V4dq4Wu@h<#x3BRp@g@*^U_SE_!vjHIHlXI|sG7%LhNQQ$p-teZB_GokNz z_x!3N(of%T31lA@nHdi@5>D9Al3e^ineT-fPgfmy&hLFm`kJ_Gb#sP)ADuJ7`#a@# zA4Q!S@nOC9(ivV)w?r+4TxZ&75JytGV;^J|^|T6Sy>3u&e{xa@wB6x6J$iiihjW3= z%R5S2p6zorhN_pw5qqw1J1o6 zO=A(XeMx(#$--;reS2(gv^^#Q0+oha;A11O!Kct*d26m06bJi7(HKqJDU-2z5)DZ-!%Z?{VvG z@|oSDE1*URb)mif@{<^jqN%Xd4WY(^!#LY+83)kM*eIQtTM40kb59R@t%z~t_8CdO z?C*NT$yP=oKkb}IT}=L^X!qA(95J$ZvFni$WZwkSc(8(~_T$&(W@|MfA7zLlPlA56 zGyKAhonU<1sQv(hj5NM@rN`0DW8kfLp^B+^#8>_F#EB(E5d^9+Iaziq zEY(uVcXo{5-ufou1{9qnaR1 znEpnj^nAS|8LfqgNtZ@j=uW-|lkYrST}WOcT{e3dH=I`BO?bRdXeCVZ;-p0Z?rV3k2Q$q~CecNECIHhiz=-A=FWZ_K!5eY|0 z-MaQfZW6EKv) z#qR%}&DtT{y?QO>jGiK`m&DN_Mb_*`c?+n&Fuw5U8%wFb@RvFneK z#N=z;7A!5gcqYkTC?gc@yJ{BR%rf*@h4%3#WJ78n_u5()b!7l(0)DLyLq%#=Jl<&9*1zgANnDM=g8Bm?)zp!-!G$cTeUEb%7fM#z2?^@3R}^# zCdms~+e`S2hpumX>*sUW^wQghU&bjFtK5$Iu)Z}r8e!4D-|-3)T{#Z1ozgI;o_*)& ztkOy?G|Mq^ITulZGZuo%PBV9I*T!hP2#Ab^nwCktUd|~bs~B-RaIrcV*&x^H*rhoO z{kSBaK3_8Y+kn4|EwksG6f5P29E}klF+SfqVIQZvPxP&1{U$-wEph=-(Rt~fHhs<6 zLXtwy#IKT%FX#$kg-qk!1bPQ(e<&q6b!opLnT z!xdgd;RObv01*_?@^{bAL zyR8a~2Ci}#H-eIKJdBlg7T^gnI8btztkaqsBl8;w%}6pNlQUlc7icb;p=!0q&GqGrWB~hoa!BJGthqo{$C}DiGcUy4xF~T7T=)Rp3J4$?uPUV6(V2^v zx9!=7HNO5jp^`vjkL60UYY8Qpf#VDxL@aZbqB59x5RPXvx-E_L}NZRI!I=gXJofTb!`9Z|pL_HUCXpTA(JXhuy6|2h&4tFP}$f@y@K#`(WB zWn}d9d^R+moST~$tg|v^tb((QtJp~nn>g}Vmtnl)Onm6^kd9M2Rf(G^S?Lx%JVeWe zX#e+$x?Z?a$8kunrXx_5r4EmB}spTMp9BHsFfB!%8dECDvyQr^5uZ$z{ z<(-RL#dAqf{DBZ-rU&1MBRlZgGpqQSNhS)M=6mTbV!_ApB5fp{%RU?y{XE~i*yL6I z(x`*OZ*y|%bO)$x5aG!T~^=PiG3@u;@tkR zTc*pv#bzZvv0M2dup>hulD+IlV(ZB{4!x!6lnmy5uWjB=w$5hccjMZ$-{rAtX}q;_ z&X=>s&ZK+dc;3HnaEG54%R)5cO=cQf@KE%Z2{XLy$pYKw2~d+ngitopj2b9(0Wl8- z_SZ)MLCDn*RcP6_E72NqJEJOQWVk63CHXz(M$F$~oGHjSPsZx>e7 zg(L;oWI6m*uoH2$7hS*dgq&~V3-TpNejlUYK2#<*~BJmWi zhZI3c4bRp$M%9<8Wgx3(8fNJOX@i=@tUqxCIJ2c?AYi{S-5AQce&d});L_;EOdM{* z#5OGg;Yu~Hx&GO5Z4RtA`ObHYxpMFYS;_}7F_dWpRbLv0Hn zonAQXwLj+mHn$mt$uV_4Sp4i?|8%NgFsdZ0z^;;->fp!hT=qxFo^ia>q{1S(zi( z6h!<1x)DHKEHs?hfFj(_WE&iA4(IKc4|WDEj*wDaOjbhjJ+M$4s{KbPM9Fg8EDCnW z^WRrCX|5?-oC(Oy9O>*g`{)v)33L*x+ z9dUd@I%g9G)%N`uNXwjAj>U#Eb>zOeqSx5oytW#voy9tdv~-X8v+|}>97yqm&J=Vv zMzwJ5cleT6Y1LhhcS|0`A2lAEM@<0_j;oFHAHnN?wT$r4NL3FsVPdV)eq5f+q0iCX zAa{H<>s6iS2txlFL#8Q~na86cj5=_6 zNDt3=G}FJ&&<-M(qMZ5lfLvtN>du$sG1CnMS7oXRxfB$mqsIk#h5?m%y)h>)l zaLX7X$B@GVV5IMxC6qelPni3e9{ha7Cg(ZXALbWsn21uDj)mc^&3_|F%dH^!3+Yy9 zyF^|Y7gy)O>~EhdNNuncI3rhrl#<6E6qyQ@MAp!vUY%>|6L1npcJ;h45&EeArO0hO zFJz+c(mZP6juL?tbrz5RElG48d3sE#^czk5p*NlKm|PWHE&tqqJ<=4YAKBzW-B3x|m> z;0>o%zPmj4=8IV72glxfmJa@z7Gsu6a zx$Qld7n|Te_T|AD7#)QYYLfE2!2rBJ_A9&#=V zb<`^k59#*P_fEa^J0dqcrkttNNgHuY(sKb0qun^iC{g(cB`R%yfL!|Bf9(?#;v}4T zGIhbEn%|~l9y9jVtZVY$8dkRo|t{E1AI`NT)rU=oIQ&2-Krh*c=$K{S;Wl@LR$~GE0f{r-tFj`c6qv<=j`{|V*mq(U~ zZNF2elitXr%VvRDM32UnEBJnWB>ztNAN3__lreMr zld6(@{OKLWqGbbBGn3=0H+em7MbX}-tC`W}M}4&Yw@5Au$Spn{#Bv7Gu`wQB_^xBA+S=BE&7JXyPkY;mD= zb{t1pI;TdHn&p;shYCt5f=43YM9X*GwK;xdvFX+`H2s~fw0Jx;3HC@y_DGAsYNv(k zy2^`>0|QJMLnw`?@sW{u>hcSFOJWx%;QLakbOm!Ur8u9ysaFg;!K4x5HM;BBe^p-f zPu;OS>j+sAK>IIV+REKNNYeOj_s0VnxNvnqQ?k&6lSPV@ZX|7EFZ)HubGC)?Y;avj zm<#%vt~|s#VT?YQ=XSSq+ZQmIL7db){PcbN@r9=pDd56H+0mVx~d z(Dq29VdEUf6gYXK#b7jHW)-EfxAN$=?lKOuqNr)AeZ5L&*ZSjYLuY?8Y*8#nwgD!O-$OIQbqPE>K$ArQ-^!X6@xLbh5Phk$CEW z<2?H#n>gQKc5lmaZkoSvxbOx-7+H!P+nd*@1R?Oidhs+xKY#zM@>AF3OtTE_|0%MH z7>!WME`L9BmmHKgp*ADTzqmfkEa{^kOb%5go2{-(BHf(-G@N;oMMhS=M^Rt4)`=2@ z&<>owaXWsPcSy7eBkGTZkN4c?SK715NS%oMK7Q}Q`bIiSnq4h~NRz$kWhgeE&pJiZ z9X|&=ahnc3&9}y~M1s7eY-^Y^x%B}y47(tv3-L>R$!|#Ii!tA9v+eQ@S$NZCrS%=T z6dLwPdGv5~m2OfoF=IQ7BDQK4fA1Kd_C^5$?|nGaL&uS0!rvW%cFt?p7+ouVbP@hP z4>Uhuaj2VT><25im)C8KkTk?Hw!d^w-kf_*_WJc99Nk@U~uXRrdK zhY|jsgLfP^aLBOxccUQqtf}UH4oAy#dwq|l!F{i3RR7TeXbOFe#aDzAGkTPqNp0va zy%Z;+d{d!=yDuFCOtYaWBu(Xw z3keD_uaeIk^wTX`FByTz^thxqLCLe|=g0!U9`H;>-9rmRs=Cw!Ts%@wE--;hEL4>yw`%iqPLdVOUdHvpZ^3Np9 zcl`3@zZcuom%8yFE4Ut=ojRy{?YH`o6w)gppTRg3{#W{I8)gL>FlhYH7GO85>_pkn z&1k8L{8`|lYq>#M!*0lx)LH1@tJ#>WpFPQIRjWUn3E3oe>WDr$)-2On^Cfuh2&!a} zXQFe3rA71IDYRs9Q#{pdKBJWgOnO&RMZC7}Z1~WTmU78ngd;Coe>vU%+7K3cgRr=> zK-1|OCUE6-&anZe(;O~$ui}x6N843w;$bN258Z9#kKK$Xn;aZPo?}W`$^f_n+&7iK zEinPpammI%x=#2ZM8t#Qo8ai4uG72tG&`8zmX=`}iLCrG%jkcODox40_!X`Pyr8iP z=WDxcXBPYX$#5&_3-dOe{*rE~0vAeJ(3evoRSZ^OmqB26;&7rBZa^41HR z)fXI&8G<#$$t-N=m+uRN{_D*jTI^ zc1O!QZ`M|*PcM#mF8(g!sj|_>I}pzNK+g!Er-Bry3UTiE7#u-~1gsGW{)QlLkS>Ss zt^Zq|Z2no>efPv(>V8$fE9E2`tB)Ql3ZP6(bnUf}E!w|R>-@ygpZzLHy_{Gt{iJB* z2|K>pDVo!DH!LFRmkvb-NvlaB8M^fRr!;s+_MdgY`H*>{S*FNf$3jW?;Oj~KJe)k6 zMHz5cwtzq3KdQG49txYA+gYK7b?gXNH_ELQBtOj_4h#{q-YhM+fdxV@ZmVIAApX_g zMMrJ0COmSclg}d8I=nBiU*Zj}=L5z}lDArG+#0x|w9388>$q>SM*=F=AjPqqLOEii z>Hn$2{etY{MD_6d>^2#DqI({((B92X@${W^IWSO};-T@>6S;0wrF`Sh91~yfraTvO zk^;APG%mXpK{$!>fH4!sY%6Qlo_xN^?-)2@MBE_P|K;?ebkl<7RS^ku<^^4Y3nb#O zicUSgE5=d+HHCiy(WS?1vDh#A5N`v+pbR-Cca{^o*T1}MsgUAPo?3Mo)M$AO)F=dZOTFj($7U+3~!H4Jn| ziT|op>G1$fk-}3@6CjiGBMI#x&48*=SmS#H4E^FfXtL>Di@X>(5I)GhmM1 z;0}rZG#Ws$P`%mO)w%JZmq`$y&Ak`%#k^=02_p~P3jn?h?5!_POMPLrftji+j$Rbb?U{%bg|0^7_*u|!)(e(n+&|HoOQLg)tqV5}T@6e!W zIS~3=x@fAt{p19pID!3iC`>v+h!aRu)zA*bdUo|>)K?J48r&v<{}$6k7HU$9OIf~e zACAVsyw&hV>ni6&WPqa2*z*OyQuu|e0r|rMy5vP+I|YAzQyhkuoyCq%_x5z8X=6mS@obNB(r!+l z-0dtl>JdYMPTPil*c+=cJd+cvvRBVp=zNsSLG%%oSK_+SAPh?8_ERlNf){!<&`-4r zd8@3QH*eTwbG}Z=ugevaPy9!N53Ls`iMn2i9We&z5$p{yT^D-dZ3MV(O5FCjPFcg2 z04-~&Ad0M#ATdhFlViK~*UMGpQOIfpdw@Sl`ycQR51^)mwqp052lR=$t@%0ZjJ$%} zgQzY&bIvSj-*?0s3;=AeRblVU+zVAXAx7AX*UG0k-&Khkw$G-Xe>T!0ducs|rj@o$ zcOA>Zftwg(BGL)l6}o*u=JNn&f!1dC7heS-6p}2ve^_|T-Uuc~^dw3%uy5v_F#)59 z6M0%`$UrqwpzJb~nX;$QgZZD$E>IL~OGj8M35nb7=swQn?^b*dUDwj6HLwq3q`^(v z!8g@5mf`KgZvl^0YU!t1*)7X5VqdK)&f=EJNfcSD)>7b<;y-gc-8AY7MWI$l4BD$r zt;E=qqvI|l0@qxUYy^Uo^8GRHyVc&e0>(zjl16+cpfp6@{!$pnaF@?NSG>FjcX2aP zb+_aAr6c@VRu3R6DJP88w$VN%&dh(bCWa1HSum6?CruW{f3;gZ4lgqh9!6Vt1}G^m zI7Q_Vr=P=Y_oXY=ev(fe6zy1-$4EP6__xE>pZ*T8D9g-*h0u0x<3v}A`bT_ffb9S<2j+?Ir(ey<}hotf^?hzDy@%Ht~f!b2&9~7^kc3TpqJRIva%c?)Z^vnKQKly z;op_Sa<>>eKD}}(Yj=hS;%G+gS)~n@o_#Dad4N=#70O5d+tiMPhHz}wr`FdusIGoa z4|rw{QB8JMae*k*A8K)oD8C&-`!Y{!Q~4+d-rTLfHSjKy>^@B{i?YOuPj#~32pUmC zn#ZT<_djLlay5S@&bE5K%o`2>@;Hj<=@-eB2Sg3+C7vAx-y){BcvRjLuDledz4yRb z33t4Y!iH~b$#vOgkyEBE9~r`g&@TFuOimn|N_ zkc}w)NZ8p|s+-sX2L>MhI?~vMbSVF{PKJysAlHf{C7XKYR4$*vo==HEgyX4?Sy4^1 zs#ywp3%Z?%Tn*aB=o_!Lr%`>?9u=B9A!HXm+x=tk^SZ7#E%9=05ShI5Str$dV#I;j z*F+Dbch64`b}*OAqPzL3*N6B^Hh4sw&Q{1%yuM0JhvY+U?myA~xIW?@igACqQYQy^nTvBP@l)LiX(3mnj08N%LEbo5?@yLwcswQSsd8Da@`F8;%wgaLa#TYeJ6Iz#I zi+fRlrsI4xo^+z~ZB2{LVszposd6ePmhr`x5D~}gK6WN_N_Y?gh`yj>z*F`*2>!5c zJ(FKtZsUI&++sL^*ibkhqs=h+y^g;lsbPir;cs`tAGaOJZ0C3xX@KnFviGO3@*)d(a1iIU6V$k_y1W~{BP)2`+_26=rfxXyo zaUeY-iz9gA+a`F^zWM$zEpoIBw&3Gu!9#zB)AFTm-LJ;$g7ibnmyF>J3L4ZaBwQ8j z8pSctT^F*s_+<^>;cAn69z#yjRyQVEly>)?T=m;dAj{ocL%d1Ux=oZxPMPa~g!6(w z?47u_{lu2_EvYV=(v@?VMl-B0(nxQ-r&d25a^w#Knb}Xeetnx(LYY@BY9sB1m#@@ZKP^OWYxf1?(IC)YJJC%)A%xn#dQfthizq#RWN#CG2a7-aD2Q7)(*( ze&Kk%6`r`SN`2X%g~{(hbSwc1cYD%yQZ(Ph<=3(8IBg_XaYW=H>av$UzxAzW>57l5 z(UUGcR}0kzTzwCx5xiGM>Cf=j=O6D-UJo}I8>>>1ll-&B=EsSx*YHcs0Z{-rFN%<6 z@IXlb^-SxcSoZAE>Nyh z!|U@moxLm7#WS#1DQ)90##^8;5%++KI4g7-W2`JE3| zzK;aYvN9>j5=WtsWg@vJf7Su#BNmA`J#?WI2If!d$L>TDtz@Hp!Ja4xhn59_A+3@E(ov7|z?zZDUWGmtA>%jlQ?Aye{lhYdMnh}+*Qh^#cc3ARPWSdRiiLnjiw z1zV~987xl*++mV|hv*|-OJbeb8mYex8RYhR{Z8uT^KfBQYJ2OZxORA&;$yOck`30& z$0XZ*bne%2a^x6Qs&Kjs89f%+_ML^Kq}lzkT1(&hLmG)HS9Bu`#$)uj>e+A>ijU~a zn2a_44z`~Ui)<8E&tXKnz{>U?e~M7ut*v9vndtnbe6@WQXHhx;0N^fYbNfB$BQ1Vp zM}c-pM*?4!d9jA+6Es7h+NA`+FL@y_V^c&Uf&OA`%a`N_$A|P1ME5CbCb<+Ep>YS) zZ`~2b`{RQGlMx$^IE*r1b3~0YCSON&x%2SPkxb_~-Hhu5q*;S9HApcC^3p zU*OvVYIf7?XsURxPNh${|9#@>5$~B+4&wTjVWQ*t&m; z=#tX&Wt80QXRPue|9&|*dWh3>HP{_Td=>oK{qfenKA(o!)U4jO6LoH#a!2N0V;7X{ zS}EpWK^^8oWM26G?}moI!!sfSsgtHHpAhS#}Xku~RrH-bg3u zoqv7yhX_><0D*IpF&LesW2YS%18|H>}bd6I|~1r2%$vRN+WgF1>b8f;*t!|Bmc^;UB184TXyj)or+dQ zbh1y9{ju&mpPvpbPs#Xx#o6-xNMQ%Pjo77#+e8QN(;DA7KtpHEQI+EBUuD<Pp0^)$y;7j6*Ao~&RJ@M*|{Kv6g-|^Ft+n}I6ixuZEU&6zScf^bD5WoF|#pxxN9an~j>kx7hDMa`8N`lVh zxX+};wHZEQruZ`Yz*n&;EPGYxO|N_75=K!oOLI42fp+_h%7r=g&MB$ zW?yFO5z*C<#78HH4~erlqqWbl8xxx9+tFW>!oDDNL~-nK-^qNqhL~~5m>ZwTTTb&; zPD4C;Oy;w`Xaau}&yei)eK{dJqk+gIU4-U+Lr6p+9u$MC++N5h8?xOYbtG7V-UEq) zB)|338tw-}M8<6OW&7QrC_@=xGxI$*XPkzyR-*pb$UYbU(pb!v$+x7-4w6U;m&(Jw zIei~@=Zo)oi8g4kB@a?|1J|{-c35@BhswB6znm_iX;^GiMo??`I6+bmn9?ZEE@8F!k4)#N+`<$lFSTz`z!J4>ZY$xlupYgdqdF*jPFblJCfXwoja_v zMg^Y~*1d;S9~0=p-Rbu9&NRW#M<+F35ZjNe_u9Z$BlHA1Q+jN79_wPzra9(CwKcQ# zGseY0&@&znZT#mav8fgh)wAP=lshl=QNLTe1KYX5XNNdL>kjOpb$-qsPmf*z9xAP@ z8E}JtNW!&g%TYXDE0p9Ag<#%mCvp$Pcb9*B zx`*zzT0)t+pa1C$s)IJ5WB<1_1>+EON0)-p{ogp70g(-;$*E&e(82`x$~%lO9ijKr zYIJ!u8ues7B^L~h^E*k0M7AuW-#qN0tX4zuc!a%w3?U$AHrs~uyFynQ?1Gm6^LzdQHAjTRs z6ZVK@VDDGc`=*eKEE@UG> zB4b&-%`G|GsuPcqL4k)E1qIq*w7d=bn;V|Elv;G{_mMuKArE;5W?(ECos~^0YbqdCR7&>o*%7ZK1m6iNF_H$Sr)e8S9Mt{Z+!sGT0+ne_mE*9(RkYHl&En)NAPUb6JIWz2)P zgu%|{#AR;cuuP@9?l~n#c-XB1jHB1$qAXatD3ZL9pO`YvuGhuLS%xnmrd`c?FT!BG z;0+CwP3n$~U^Tmg=WUgHU8lov5!CyRH(iGxFfO#ju_rd4_I++hJL*EGebi1QbxtL) zJq9QN6a9Mn0U@mbHb@e3b(n5_ueSS!GBYq2ph2$An~j^3Ksx>3w9|N-(HW9#x(P9_>?6H6u5{>e_q>uy-$%)hz34|Qrc zk7#(do`+}&K9WHFS(E93qpo^MqE7_}CC+eW=8=ee`d9C+30ZeGUScIzpf-*v@BALQ zLn%I5Wk*(?k2P&wD^XF1e67rC^FAzR3bbEo^JxHe+(qseqvcNNvTR%P;O!#gy zA3-H5{b-(BCTxqNQj}+Iq)uu9%9NB9SeC#F9JCYKc^Vb{r$VhpP;?*uF+kP}d(8CD zqx$!0xCVV~eE0W&KG0qZ@IsM0`1y;okGQ-)E%Gej63PxkDw46jyB&gkh<50_o04LX zS7qAel|K5yd*-`bwSJarou4LARZg7dtg;pD>FwS0Zv#05&?7~qhumTH*P5;z zszyjO80z+Wj9T?({jp$K|P!>d+ubgBguny4A)wif||EMRha|d7s3q zh6|==nzxm|=gFY5AMYJCPh{y|k2?XJI1nt7G%o=uvNP&&y+)}5Q0}#Z9q<4TS%o;6Mw_{yA{q|ziukyWf3&NT2F5l0U?{PeuCx%>G3uHfi z(a*{}D#rsPS-RcHv|m@uRM7r_y9$d)hP#~?trjoc>;Apb0pyy`x_kj?r?L4sDJ`4B z`>P-6pZf92Kie?hubIF_&Y1i2`~uZ~T7a($ArmEuMTD}%p&BfhX3!hP;t(w6 z+dh<*UGo4VB<8z+Mbp|E9}5B>hIW^@S>(!SK| zC}O|zFE1R}4RagiGRFDycumq{SVosF82QI5J^g|Lb-HHD5ClaoqVZ3?(e1mn@=Hcb zvnR-ku&D;)*_5;^-3^S8_n~{;CQ@=o?qC^97j98_qee9$HJhX1?k_6*BZl!&4chzV z>K9xtfv+2Ahd=LdR7x5}7{f0_lCmZi2l%Rqx39hwChq`k?;ts8g;;4SMA=NY@Y+FH zrNjGPLNGD-e0?#S?|HadUNtugwBj`x--^BueXJq$%_!oDeoQ>6| za4x=(J9p|tVD3+O)FOQdqzVXpN7pZ2tLV_<711I^8E?rmSG6vCv(cpCyjN!e2&bdb z&q{IaCc$p+%8DvmWwPCT*gKt#;=pQ~H9aakz@`^*XuMIan$Umj0)yr3&p8%5&d9tZ znATzv>JCbZx<>ULX^gTPJSw-Z1%5^U1FBvH?ntY`f0qL3ZKe#&LxxBKov!q47h@u$ z!4Sn$e(5U$pR1pmEYeqTf9%#&6meQ!KudRt`cC1Or{-dnV>%*On}n3k|K`d+ZHmcjX%5&}@saJAMdg9O3?3duLD_ zpzJwsbmpZc`B?Q^X#}(26^wU-6O?#CG%$tXjx3+=mm;nYtl|`JTnCcC68!emwiAE_ zXZS;^zdb@=iMxnABzh~c+X)YwDer64=3yNxEg7TfV;h^y%)1R?0oEe zE<2!k{=&Te3Dz=3X+$Db@M;P#L#?5LAcIUA@iZ z*>#wT9(S@bzRszC%C$NZ1Ss!n-*-o4fUCxCgK8<>TjXT3;}*w?VTX3IKBgdLa@-6M z4Zb!rNchRrj$_TjvRaqtlm=9*6%DDCgZ>r0WnK5Y1%pZfHL_jDk^SMwb%#EI9@KVj5{6tk3@^3AxvD93H6RtNm{9`>cpzzv55}^2VGEF)__lS=jMd zxakEf;S@jE(4aSpLHhDjdO^D&$i-z!<@-z7+k|Vvz6@OQDcmv0-)RuL|dbNz0w=9pnqu4diDpKgcl+PlbCs4CA@)3Q1Yl`lTQE}@tsr#|pRU<#6|4y76M{PY}We9=X z$5b!OFQiV#HiV`E+6(m9ll8P)(OFBGuKrY#)L0NMo$ z7e|sb%8lRn+G4{uRs3uuNEY{~YMjjiz9gnY{Th5wG;h7HUcNMKuV0wNI8^rf@nI94 zlqkK_t0C~+dwdo6f699rwz;M?NKNLLrpuKT26|&vf8E53^ig{(IUjNd;mWpVo(W*s z8)F2_IFNq*@}C<;zx`R`6K2RLGVq9jQ9KGXEXj%me*gF+1S9;~7dSrIm3v`wX0eX9 zGP^BB{%_uR*9yeb$n{VLgf$R1dBzuu7{LKX0L7@kk07VDjdXI)d$3z`Co7>6^uxJ} zp5S`eB1M+&o3C3<{SL!1W6Sk06>>F+oj*A&=dtB`uW;eFVn6% z^y;g|FG@N{Sspy9mtUVsKv`MvKA=6QYj(;`eYqvjke|RD1l)H=qGO|loyP$(Q9mo| zlNygccJ1zmU)5SMixuf>jf<>v(~J1CK6FnQmZ760W#XQuopW71DbEO=oWJ($1-g74 z>=|sxYKs)^MVG8M5q&a~3`zn42K|oU<%$Xs>x zpXMiZ$d!|%sX_3Is%f&k^8LMQfD<&=H}Njx0qp>7{aK%XjIVFaTetRSeS7p6bfztt zuIgj8UPR>l6H7(aoo%Y4IO3y$m+zv;=vH3A1^8<;`fz{lg%>|&J}8*bDEV<>4Bv`; zQ(3l_!mC^3o7ynrG@soFEjS*;7-~0FxRK8eIu586UQd*Dr94+a`G%)akZd2-B*(DR zjA`eb2om5fqooV{7pz#C!&hFTwco-TI)^-xQ|=&dJxQ{5Kddv$8jYrW=Idn>K1v5n zxxre*mRZa$;Z`TfPXX?V4ebSSla;H0UnYZ+=n$v7&*zC0TNQkvtC!gGAV2Zy)g$dl zFZ8|resK=V(0uQAY|;ITx~CGMW5Wx$5RO-WS@FyG`8gm4LmzRW(-TB^x2k+~sGZ$g ztsZkgFwVaJ9w?DLTj42vuXFFi>#pzj9QU!Syu-l(%b*=x(I?MRp6&T9A9#L>(tFp- z?-;&GIG~Z>MfHXirv7YRs#ITPxCKob zzN?;9Ek=?_pP*&*8QtlR&!L%Nv*~~j^s@qfn{{7XQ+AciN9H!{U=w*v&H<~1b^!;} zQHfSV(UsXdwyq_yk?-hD5It0XU%17zR2-0-A0mSW-ezW6YHtZC*N z?mc}15(c5!w?*@#QkJC z7Cewu4Kls`a7*AawvK664SpjCYqa-@hAr7Ge;b=rh8`nkuF@xnl%Ie+bVO& z8r)br!V_%$xZ6+@-aQH=Zg#F4C3+q(a0q_Ka(Cz_$ZBC#&DSm#b6>O3?P z^!PU7USnHvi=0*vo@Tr)@kTcLDmX%7lm>8Ec!br0chY*aCM))`#NKR)x>mUfVdyG- zk!jtbjJ~=UiSvf2`#0mFjQd2y}NwruI;e%e7&bj-~I5bf!0-3mAEn~pn z-BADw8|xwN^lH}TM-YU^Z75=UeQODl#w>~jJ_+8?WFi-@afIpwzNPLvr~qYw@=~fw zaZ2N3^+oPE{k_YRxFrLl%b(|CW8shAnS~VD$w({O_bQ*?0=P=uG`C?_mCM$EH6kA1 zv}z_wWLU4p@D&ExR=E*3C0%Cw%E-QAID$&UcfPE(fQ>0CXwZyU@PTLgl$M`njwRnI z59NY%49LP3FJjcqah5WWWZ-o`Pk(VEUG79@%U+8hlX<<>oh&>g<6Q<;e`AdFq}W5G zh*Pr8{=?x7jv+0>hnb|HO4$Kg8)=|%2 zdb;qeK&x0MA6Cpsrbes8a(e0#p2I2Ojb;PzcyiPv%hR8$@wqI6GmG>MIxW#kHZeuZ znONUf^Rb`<5YsFJI6csPH&qA!#{H&gU~Jm<7t{nuX(rLrTGLel+h?T`(}2As-Vu9K zS%$d2YZ%bDvnO>roOT|41T{<;8|Udo#N~{)=I=kvIf@f;bxTsW(YdVbVF&WscaM8O zL)U}0nNAhZ&L%)5Vd90>EzEikgxsGWwLB3V(Xe#Y+JKHr@x3W!@jy8iqIXp+jJv3j{XPm>`)}NQXer0|xV}&6BMg+Vx3T3=z77n|VL61?y!sII8 zUt;%A)>Zd7o)`KM#hx)uP95Nw;y1n%?_xyM69tGM7|TjDF^X@I(-u~GXjmQ)%^k@Y zO-dn<#sbt*xxABad8l%Lz^G~rtI1npj;v@NEqR8l=B0N69)`pW?yQ9*!of z{VLuH0YK-SUumLTPrT$A&+7^+SLe|F?>bL1IZEv-9o1Rqp{3d(G^-H|1iP*S_eX(c z;Cy|wV^KNv)QI^1K3Xs5br4TR$%fLI+7&X-;{TU)X%758a{dPBih>L%nEr zt2l}3|N48sjC|}O&VY8=6MWWi4h;UaSE_-$fH&YXtO0NnFtrr=nIXL7a#wB{8)WG+ zQwhal4}lkcFJ+al0?m+8R~Mv|UKU$pdT+Wd$XM*sE&@vUPj2y%dW<|@e$NpyfSO0K z*rctujGRFm`#}F5uoO2(w2$-4mm9H%-v@>tLz7KUSwHjRJ-fA~F@d0Q-7}$oHZ{N;GM#ruN0fxLV^a_B8uh-eOX5`4+j#+B2viT>LFXc6!-(muiWx!YbPg}o_yasa(e+XU<%*{|Wv zp;=w@`E_&yTJTcY#^2>Mu+(}EZS<@^_!pq08w}npM4ZB{AF&-^%TW*91d|2w*&!eW zm#`06dCpx1`KJX3R_}kgWp94(B&&D)*=Q*vcO&S{!_>ZrcjY-f$v^-2T2p-Y`+GIM z&5C`8ZVB1H7s9xZ><1@kK><107^6nhRG{~Q%k*uK{W)z5pf7sey$hq-SIG3}!ZCa{r%ST$aL?!NAwB4#;!dw~!2{U;`-@d7Qc2i>u}mA*}?=-5;HAY};0VyBeDmXnlwWb6vZW5)+Pw3Tq5Cpm%16gwFTv^hoyHtf=AT0^c3-qHH&_RIE)fw< zwpT!pj+mxTMA6HExR~mp>K+1ChIoLg-1*v)0X@wSzuKK<(wCB7d%U)2ir=E;JtjjN z)+&z-EOj;70~rsf78?4)0bvL@hT{2)+|eTf(jxZC*8Bt)7KMS>9ne~C51L`+o>8^6 z#a;wwz#F3gr3=&sMcc3)j?;*x_LCJW>im-gA9$EC+>hAj3MNx`HD`)GA2C3Ae?HRk zZUt$Wwv?oCJ5p42%IrHosV)`X=g*-x35>E{)-UbA>;e`0O}Nr=IcZ(QDT51^hdjqO zgqCUfS!n`PZ7X92*fH%{1lxYtp!TMi$SE@m3amnw)s6QuaS8tN*G2nJRS2frh8{ls zKi}(rsh<;6PYU5p)cApkdW^S9een#$0?SIUZvF3jr{$B*u;AHwZaxHyc4^Dsn>%l6 z<_7SDjWkkVJe)o?Sy<+jT6SN8TTlawuGN~gM;*5smV+?H{}R25zktD8PZ`@0$)!H; z5>>ETpv%lOl(f8UdwhE|?@s6%5v5PCQr7@)4k~O56@KKQ#;87@1sjnWvOa3$@4r;x zi^(@G$j3A}ZX@W{Be zB7Kl|A|*+Qjp2NYP$p@~16$poGk~7CKn1e?icuO7zbEg~lCi1Va{qjzvyFEYRhcyT zjuqkWQ|^Gu)|;qB((37b{8t-LB^}mpg~B6a6NCA0F4#&K-ObPdaqUs!4Q)A6e%_!y&JIiGHcyDr;5WO# z+gjdhXh!j~HN_3?(pRLinDS(k$bee`0ElNZ->((En!hB&J)=`P)5m5AX&H&R#bP3L zGWr2%QnP=NcBH_x>(P#}XzID((}LC2{n<#F&@brrlJ>2ZW76I8uUUq{WP@;jsz%g) zzZ|u#%juOX_(Id|aJ{d-cSH&s!(=;)0XMoX1<@6^%++4o%4D7Ty&=m9IL)a0priYs z&p^>4_X2(^?o*bmn`Nvkh;_mwP+M=yWX#kNXzp>cmNZ-bw&(lP5{4$a6sD!Wl@YZk zQbFzNrKwl=E|M|#9>GYT|KAjkdvsNmQx|LAl}AYdpUa)Eb;#vU(?+J>m-?IyMnJZ+ z$wj%8sBJ5WAPaRShr+t>Xw+hS>t{a9Ny$jKrb#{WAU${SGQOe-E2WUQv9Y+o{b?o) z+)$c&G=MM$!KQ=oz@oe(&A@Ytt{6>X_=wZx?IhU6`_;2wpqtKSJfsH6FT=MVLwEQyy98o?ep$5SS&ifm1Jk?!;k{z}jrB1gd0Vyv zW!Z>cWlW)3jfy{l!a1A19X@tzw4P~6n1P&=H<%IC{BfF*cvmo^;a;mlz4Ag~>OR*a zWE$A~k=zRrP{GVcfvX_9ixS)?p3DKZsnm$2!S5)vB-!}l&cXog=rp3qOejW$wr$u1 zpvcZA2#|b&_SA2H$O8g^H-MEo-JXpG{3VHgobIM<6qqKq=8oh0X$Mo|&_uZ6v2$qJ zo;{oJBf-U2t(a(&TGx-t_=3H-Tfg-szx^MBm)q=K`4%|7xkR(e=ki~-%zj-So3RK6 zeDESzs?cVqmFA4Xj$2dR49$Z7!ebGUYnpIQoU|8Dqi{}9`JCz5cry`IJuAv(5ED7% zfQV=qLwmz*M_f$Q+oM4p#0&8#5Z1nDd<1zq53Bmx?l%Yzu5&szpC4IL0RQp0R}|09 zw(iOOF^)3|mZ@keP-B}xB{Pfdx7Cb7e(DWdW93-{5gvgm6l35rjLq=xP=>E_FX(j~P& z!#N&#Am`rv4b39w8x4IO(cvlK?0*p2;KJ8h6(;<8rc|3*r?|)#{u!re>bs+M$(#eW zK}M7bM=4hEBe+tG)cB)tIeZ{c2kK}~L5qFlWxFe$TuBW}2M{MXIXilygA7m_8<1kj z;Qc~wwE5AuK{lB*FLsoz>RvHteo6V**ZET9h->e&sBbz~zdno+9lpjvlq@~|vAY8W zy7oEx>e4h^01Ghj_asVbgq2bp%=^M0RKtGwS<+6FvboYHc{ggczkht@#*Cwwn;@M; ziN`KG{gpPfa4T!kLs>&U1BKbAQVct}^uczY%S5~`t~Xb|lo13&lZY7v{o{|?hc0iN zu2ziO#XG|Ps8gHQYTWK%a6I_wr5HJi3Q>1J;b$!&962k5<>_nAq zmQYD`Se3E|hDiZAL+iHVy-TYhVv?3cA9PE_Pzw;Rz0sMM{iVRjGHe~wb8*H9^21@v zVgVqP;rGT~dd<)ikzt)_m3Qstaf-9(K`SgI1s6 z&h6;7NRYR3J*>m)kc&SyFM@@hjO?7%Wnt{fx|baKpB4aiZZ8lfOMwG!ykERL?gr z>u;TKOc#Wh7qb(52i(&{9m_Y~`K~WH5AU;D+hCD%$pYQAmx_8zyGl-IbY*Y1vYaKI z38nFiuW(F=dAivZFpZ)Hda~7dYj}eVQOK=me$~W}Vt&F;K5v3{T{dTQ2kB)gVj;dz z)kfnd<{SUAl23Mw?xUgyw3TY5?FAW3`w-cnA6x=BpAE*PbKDAjvrhNO^{>Xz^SitB z^bOlfUHmjGqP{xTBd6x-&2>#+6p>f2?FHUj#ej&?;UL`ktEnA)!?;su40U4OGnSnE zYs+(l+1my0CjnQXj=CGamzD)d4N{fFnjrjn!M@S@6pFu@wWzI}*}bWM`zc^hPh5*o z?oS8QM47#>vry7FpsS1!sw5ZBa_fTYL3{CiHDQZ7SAz?Q`-M9{{bv7d^+M})t+s46 zeZO)bZ(jceuq6me+5zwt{cRd0EY;+PgwN%Ft!Wi2sMtf9bYP6%3<4=Af;q3TDt0TcD7tRXrrUz@mNQ-PDFl? z5J4r6FT@}P<#CZl_Gk&3b&$K+8HcI(zTIm?t_Z(C>HqfVBVpu$Zao8qA7RIVOBKpn z4|SfNI-!XOalMnb8$oPk>$UmN=RvS7ARP`gnxN1M8fjAtkggMqA?j(Y55Wkd#p~Ml z3d>SnF>YHdOu!UR%ol?-|3(StY#75S-ciZk5wZoH4aAje^Dllxw&)x*Uq1^z1Zp`z zY5-2h%Zn_IzFgw6=se)opr;iAQzL4uK+eAcl@rPiiW6q!n~lTCmvw%NJ7iRay@{L3 z^Pr>7qk(Bb-@UsECCnhoz?-hdM^m3TT?swDMo>+tHQtUUVhHh)PczcBary;06mf(P zyHGt26`iEVdbxrlYl?5GUls|8KZEN(@F#=JnQ>;Q$uFGa0#K~mKf)t(aTCychT2o2 z!TF2~FL3A4^!YR|Xkv#{8Y}IIaaH#+DZuOs2{LHWaa}fa|O&$BbkJ6cfuVH+MlPAS_ZL()U;gr zbUv9o~nmRelWUxPAn^DI{ar~24L>{ zO`408j~KmM7fVi+uZwG+`+(_d$0uQO_<+UK_J)#!-WV{8f`#1o+i`RB*s4lEen73Z z@Mr^8y{mrIsgqjm43=z>QzQaC83(Pl#mzQeJo{`oL6p%RpPVmOrrol_sBg99VqF#n&z$OzbuN>JsE3g z>DkqgbC^h^QfD?ic`+SQShJY+>T^npgZi!BI?*tvbg>Y1*PFj_{XSIpLY*2ZoMt8a z4T=PXKfSz1iG@#>*9uj3IL=%e-(%W)0@qBuekrwqY+B1a+HGCS^stFeUEtMmX^$+d z8n_6nVMoW~e7P(<{|ts~YNONi(J>M43B!^X_hizK*i5AavONh1jqkiH&6z3ew)AWG z^aN6l@(tWnOebjzlSkS9JaNfFsGaw`WI5!E)od%GODvhLoIrBD>be^cs)Q!L6GhCC zJ?Rj(7#yIO#dfdIQpigqCkkRg(y{qr$AntR_p#@MzteQtCyUsQMR+*T2cMTB$4P(0 z-XTg`VW+3&#poPTVaRIE{QD*!=i$9N)MDIq=_OuGnPd;>)n^0?E%N0~ulT*SlKXE_ z(~GG29o3)SXRBAkKc4>DGJpI<(qs3`w}C3XyL0^%R?jF^Kp77amz0MxJ0r=0u%I9qy16h}v7j#-EuVSJHqkHpC;-rycxDw;@JxXWG z=M$;ep3~RkY@@%Wu!mTr4K)D5QweC_z}_3wXdzBMy}5J1_9@$@Q6}53G+&j;(T^kK{!?*--~=`ew0{HL$lBarWDgYicp2|p8GDh8Pb?7G+Gd)A+Sh@Z2QuJbWp`?!kd-y)i}gwkKc`jl#rifGm1 z@5{A7~NM(B%BrYPP&R|>xyFU#!ZHR zG4dc?_WO*j-U8n&AFc@FIq(vM1$pot>59Wf(#xJBg#; z2x_xAp5X0P?xIpzT`kp%=c2pv$$lJORp-RbA6}^{Up1>XE}2`dy5H4X zA{=b?ah#s&c?T&gMf$WN+?CLZcWT+%&SGMA!dhXPJ`^u6Gb%r+u_)D{X+BYvdi`36 z8ays!{u;Yme131y(`;t^t3EJ!y+{1{Zw^JVyQc$ku8PqKu>RG(A`jE5)MXjZoAUC; zs+T(x6EYTH&7yo}`OO3ASJk6ZEnIhZPT4-~?@X(aJOL9rF&FrWy+Wl4agpOsY9pej zr7q#*mK_a0W$H^~p+ZNJ(r~PIF}uYZL2u>O>Mk8-#x4(nPhY3gkEdpH(KfbfZi$W5 zG=qh5{Sjw`3U_Mzsitm^XkfmF4QnR<!y3p7b!blg=2X5mqU9k%JDr zfv+;$7q1Cp5By!=3Y)UoT>PjHM zYJDf|LcXAo zq-1m6i}P~+429*dCoOC5qg~{$KE$vo-u!rY5WC&iye~?b!plwcW+Yh+r^JV}HHh%b z(vEIFk9O^?5Sw+*uj95q#*uFQ87x#1ahaJz9$8*nyP-puw}}1TdYSWqpM8g$bEUq& zyzqcDf)rx$*T;y4U3G@6lgoX2P|^WiuUgJce*4wI)Sx~g^g|yHxPV&)_6NT07Euj( z|M}{nH+ob*-xuyPPnA)(VrMiobZ2<<#=XtGvouE}`^$-rZr(VFm5Zr*#`>La7LW|N z=|MjdHgo%*zzsYqstRXaPH=}<(1I=Y>FyTwd|lvwZ^z3dEjrm^+RmiG$5;i!t{}W4 ziF=zJ%S4_@O3g3 zoHKPL0{gDM)r!BsciPO7$zok+pRe0dDi?I-sD2~a93ov#7tIT8G#6C9A9{`4RJa6< zC(DbTA7n}(A>o!r?pAJSbkTB$T1F_?{F2FQyFFg-;`}7kh8t(RQE@X-}M6*g{jduUdZjX z1g=stXoSf)Vuw8S^~)Ge6k`aiFIv;M6xCQ$Il1LsoMo#}%;9`AH*-f zAs?e*gA|P=i=cR4B5lBJYtX(9OESkDJsg}Kl#W|nXeD1;8ik?pjwqljpXci8OPsB5 z%tQD3PSdR<(4RZmrY-g35FV2ku zO+Dy!sm7(3(M&(Bkp(vHIsaLRy5Rc=Gh)@g{_oDLtdhbE9eA_YF9oev7Q|g7uxjVv zdou5IhxxPNQUu@`@v_crpv@o7JP}E4NaxrtkDbB_7kyU*pVG04_(2 z&Y$Q;`$2lV68K*4Vx=O-zs4KEi3nIY;~tKjHuw?!`)(xnv3S-YWfZraRq>gND^E+0 znTVtfPdsR-kosTi(i^v->}#lV`7_q)!{JTQh5;mI0@)LLG zHHEif`5z=A&7S!tJCd}tit6m_f+(}HT^8oGS(Z3Tl5iS!C>nhg&;P@n?r(gddxEkQ z6BgHV{FVZJQCU(+EDaxTiLY*^mj%yDpXD#GrERx)A2BwdSa-`JjQ^i4eYrHZfBW`r zE&YiRlh+%UQYwMPWWB*F=M?7~(cM)O!M2aqHa3<_et12uM9sgeir}lS70_M_fBUw2 zBA8>Uc~>45KyH;A@Dl{@51CV<2UH;JKL`GL3#b)~ks8NmWZ(9nj|yP}w$A2EfW`|`Ni)2$48TXHiyszSXB3+9W+dVT0> z6KC~zv&r%9JyW7)H8g{q$t8rBKdbEA&)7_w@a2j8A~G^@kOFP_*sFJ{WL<@D)bkX+ zvpeV)tNelnm>so@YXx4xhR=5gz6TXa$jU^VD)x9t2bJ@wbn`bEGC6v2yV zYgBLoOqS(d1-IjMhv1Dfw7{*_D^yKjY=>6mBlZh~i^(2rZvJ?mmsaF5 zb+bgnS{)RJBX7h5yL^gLoCvFVhMIs5z?Bb0)!HhFuFqB)8`s}p!?(Y*XFky?Sf6ym z`N+*T@^&dzmB_abP0XjT)O}D)>&0OBr0^SEH`#s-LN??t^ejZB5sn`Ao27J!MR|Qu z#$Ar|wLLydK2)Mm%Kc@h6g6zgAoNTbSK&)NxBrgi*Xd8*xIH%xG$OlvZ%N!vQhwKa z<6wAZ^-RX=VjzEU)Gk4zVDn)odsr2ZsyH^$Xd#4IgwVo}{f%!7?La3%Y2s$=D3rG{t?c9F+MUxo-ZXRz#k8kN1+%k1n$M7EIU`b=sQBN+N z@Yr*khgn8#_)su}O%BO!9sb6KSFCEvy-_s5km`;55oZE-vhao%xx! zB_0Ce|1SVeKlHhN{ASjI%Qw9!uB~uEM%{jWW_2L-reEu;9=-%vD*PX2eZEcR9XR!CR1#$w4 zndAKS-8nBwetgfZJlT8X z2hDjPehoX;>#u_Q>hwP{aBJq1SIP-LKDqm#RD3iqY`j^n?SYg7`D(!x7dnC1xFJ0JBv)=QpC`;Y;AQ>CuuB%&lupES~xAm>& zsEN$4!O~$$)MwLHCLt#RUQ)1)I8_2XB=%XJqg8svA-;yVhDOOQnR<2~N9hEx;p3 z=2&RIj~Mb-Qn{?CA}^wuy8>!;VdfyKvbb17hj!7*R5qLQeTt)tbL!%So`E_$H~4uK zED3(p==^kQ7ivuvxdBq~pH7mi!@O$h89#zF)uv+Mu`DB1TmUzCBNcEH_`mq*pE&tr zR~ePJbZb9W$}CbQum@R3=I+qymn;0+`;*a!qRkkG`!AH;9)J@7U;8xAb88o+HHmNE zUGVB%x8=GInI-U1PeP}EZU`Ez8JWAI`zu&ENNjQJ{uk819Hjm{E91vhB|18rMqYV= zsJq-XTElsB?MqhQidtwsjQ4YPsN@Pxq#RnbN;k}I=DdM>NrdDj7U(IF^mHD7G*ekR120LAHp zMYKHq7hfW;NVNA<JJl-BropJ#8{#oHz0v}$Zy8>{KcssK4i3H8nx;E+;|cK z;4GDHC!5Ee>`%jXjr^mj?;N_0?}GR3Hz%kjc|ZT}B4}Pj|H|XLoeBwQo>bYOdVg`v zP-M$iNCvN;)mQ24>_RV^E&2&lPm9+-@a8G=4y35fngHXKrbAN~rfY9~{Ykf~zS}fq zN8Kn=N0R*?;3j0jg}jinFP+d*_G(nOtFQDZuR}WKzy8=F>PiGi3(;qy17Tx`RvLNYC z723roYpQjWzS@8oJc-8^|U9z&XoZ_bJm%?8P8&{%5z#dyHq zu)LXw-Uu2OVKEL)ZY~cfYrNwHF`v(8vaj}Xm$dn2yXC(J4CW@&kCB^hBNZpsSyt7~ z0;FVUgNuhq&h|2^JXWXowyh-ZuV3)j!wo-3JRO~ZZS8Jt1UGGWi>@xamw&~?>oeMx z=Unic#{ff5>Q$HBzQvTM*5|(a+iAVLnGj*X^EUrCJj!$VE3KK74QkBJ^N+GrY-q@a*0gDHGHAXNRrc!3wVvkve4KeDcrpiD+?# z5G8h+XbX^3Qrm|+%%Z1mmv*_x4y!|&5s3RD4WhO<8Y59iZ3kWBw2 zCYl8HE)or13brT_s=mMmnIG+pGJZz{bcK{VTL$?{RQIEf_=RaARZ<9;tzIV2FXCtI zLBjO0>Kqj4gcn;U9WoHGshL;|SYQM$C-+DL$RCiJ3#~K&$?|M99kfcQZC&9&nx9Kk zl2!1xN?UkKJ-P*cX=UmutTiCiJ_BlRdg9xVV478xdJ5P29ZMN&3phRuxf56p!JW=D7@kdKuTxd;dvuw^I#SeK)4X>lTbNj4^~Kb4&L-( zy;H*VgVk0>z54oeq32{z`(UlMIC%Oj59K#setbzw80VX%Y~9;bW~iEmd3ZDBXq|QF zaz$)ST*~?KntEkxr1h#~)iPOeP9Lw`bp)JXggoYu@ofz{=g(WYfsBxaL{zJm#YY=? z$uGLZLNz!chv1Q*16hZz8W-B9+c?`e2~sEUt03l-Q~Wf@0@zRTp7gyD1s?ge72@Z9 zBC94Gc`kPOSmg>)!zNs^=wENhU@hmRElXz%(R&?@BEFty$!XZfvE5mnai~M4#D;0v z4VlCd9P8UnAhKB7KJQD!_UuBy$*&{fElZ6alh@zHZcfMZiQ*NFH)9uuR+cpAaUgBA zVpsDaY}alvPu$1j%+9!WoV14~GZ=0&J`|8amAc9YS9eahSG`&K%8Nb;XG2R@e~;bz z-8#Wr`(2qLWU>Ywel=qEqECSKFXvP{TLp-2Qs)|`j{6Y0r_J}D)Pfa)Ii#*6^15oZ zk3Ro<|H7*@O;keAwOy3-Db}WpwI)(jm+AcjY2v;Nd-0E{RyaRMBfMovl~V|CEcJ8w z8CiQ2xk=VrRvk-?a`}^gmp?m?x_3!N`8ej!6;9oFhnEku*a`nRpHeTXg{vp>UhHjQ zB!|73(0EAj!I8B%gcp7L(ZV)q2Q-s!>uhsfhL9fF_k0)z?$3|eqXHjDlCebI95Af7 zSnP;{@6Jmf7^n#og^0WnlrhJTEo*KuZYRjB0;&j=06pqAS3SKQ`@UcU$866V^YEDL zCQ6z)zwQZ_CYB}B`jsFhsjP+y*I6}}^5V8UeXAgP=|`~4+@Jbt3zZkZ!HCqsD`#vd zl+@57XIEN%=B(Y~T<-7Qn=%=w)mTReL~3Z9MPLik}3cjn~IxGOP=J8MczqR>G zM8m?K-qQ?~$GidNMMSf%sB0RH%0KPLh%T0XRD!zBNz48XATf*hc?sb0Z7cTK-^Pzs zB^-o0;cJmev~LkZ+=m+)ha5ZgcQ>s&6Z)R7nHQZ%e%X{PB{pUfNsKZ$aI(#Sj~&E1 zP2Xm%lD;o+ce;g<7$Qnr)sJ`d!MEy|st@DbRuM7^;61{dEq4qeu2zYPU}K=aJzoQy$4HzmSg`$6ChO^b}J_L_=T8em@<3~cxE>2X(z zk3|3_gULTHNQKVjgTi~FY^zIC7^(h0=Ox32SPXeM<5Z*h`_?5ppl^f%n6gpYK|vuk z5C~J|6NN`Q06qQLz`@57t26Tq{VGThHZ1DQ+iPT1&-Vsg6c-<+1*9SO8}MbGfgH4f zs<=5Q+}cELxP1Nm(3I>qhb!aPL`QuUi6!u7>Vv4K+?SdV_M03LFJJV*1rq`*k}sVi zy7@${sXl8QhyPmL+uiG2UyqOIw!BHVhyxhA-O67me^y?1le_l|(FfsEorj109~mr` z$MFB3qOJm}$#4yCfP~aQKtPaC2|*2!8pWCJkL zm+ILOUbgeBU(=`>3*U~2Uk}720@4VN+(&WqNyMzZJBQL-WjUTPb9(YLITOJO+Ic6f z`}&S@w>dH?$=vY^G?9~sq*3g zijQ|y6iYTiEp-0)Y+}IJqd)8a8Pp-G963IHGZ0hgfvwR|a7H$lY==>txjtQ^ufncA zy~h&q?Rc!Lh!r*O^l(Pa4wn*dG=Wrk?y5r}9e>`jMQk=k^giIYj0#@0plp2hygmM! z2vieFHf(A*n`$FBz^w(FZaP)p9I5Nz87LlP9>D_`bBD*bd~C7w=hK|d+m$c@KzMMt z!BB=lIhjVA9G@96x??Ynfiq;!lo#eKL|r~hd7VwN(S>&lZ{Ig!$G0A?=mQh%IP*?~ zi7hhSwy}Q7@6B}O3MSdSQXYrta7%V+KC7NFbIp3#pcSEvTc8FC-QAK$kNG{TTqakW zx~FiqISC*{;l6MZkBXHwWDQOej2dEU)k0Gw;!-I8cO$$uYb(&WGLgSgy zbMp^jClC;(K$LyVrU+EuRnGu$lQnlH3R{4M^;&ku#d8+uC@+jUp|#;dY|5pAYhPyt z-x`v`AjFbvd}{_5v$q#EaEFPcjeB*xTW8(Xd`VEIzC z%jK5w#hM;$c$Vslo!#VUd*to&KT{~#G*QQdPxQ#EGJ#l_BY)pn@Qab!-{;x9D?!j% zE7E|K35gSx2%Q)ATxIh7eaHL*Hr|~RL1is*H5)|=`C6u~e;qSTWNy};3(n_PeY@w$ zX;>uG_W@>!+*+X`)=eStz^JQD^4Ux}QnpBOQW(4){cwu}rE^WhDi8Mkh2in{{kcH6 zT!ilQ3L-VBS!4E&v|QgO^A#(`)l>2%9Rl}eRjTHcx_mSatc=`(J>%;j_x$u7ja#zJ z0NZWdbx^ccnq_nu4$Eo2wHMy;>^B{(tqkn$DxK#h5xYDSdU)l!U4a}`QWsS5#Pu=& z^;^`qxutWy69Y~m*Np6~=~0Q`k?@x;w(j)Td8cGcbBjNSGm^gUJ*jDytjNwJ_L_*p zC1vhK9|K((Vr0CJHFoJYn{Bix4?qqbC%ne0R}|M`gap%y*$STCsLAm#e5))mHgduu zhIq#?YqsKV)w4GPUHXm5MvWBQj+)Cyq@frUXZ3;XSe4c6oReR#T!&8@RFS1``u4yP zBf}H}*g|OFY@E7u`m!x&A{AL6_zHKX5*#$Q6oG6!&n*_Qz0E_g0SjM>oBxMq(!NhD=2`q$i@UvrkJRKr3L_6!m0 z+hBYvTK6j_fb-%w^H6R-;Rz?d5IF z+m}Cg);La5m#f*XHkk;p0WUXWzu21>Gm_x{$**rDy}6?bSA~1fYWa?8n0(an+rDgg za=2ohiPJt;sV!vi^OpM2h3<4s?A3?Vs-6XAnjis+FCyE~gzft%^((vF6rrWIrDsZK z+<0d%;4e2&Z76uFo|mQx{|YYW4V1e&922I2S#Ppk4PeZ-m}%+os+n^CFb_b!}HZw|wwO zLFCuA*O9%f_ZS#saLg#u65kyvO6J`B``mnOG6GT@^&7lgkaF*f=o3bC6gMy3cJM%Pj*Z^s)jeiw8V>?K z&09?OXEndvh@YgEc+@_UUWHq(z%dYdC*{KusJ!x&ntr5FHrRA_p*-sf2enE<)xgQw zL2-ybWe#u!`<{EnOGJw5`ROC!xq>Wsdr9!Ocw)71ycX+CU2R#9+b`Xq^#K$HAcaP- zAG4osTeeKY1NMc7{m40ZdQNGSG<0U&05?|VpdV~g^$dY&+n(sMspLJ z>{e6Yye(R{j@W|aOT>#t!^!5vNeYNNBwu@I4VC+v40Hb+d?Y2Y)v(hk-?sg5cz-t> zQDs`RWp;SsgF2XbGxOq?cS*8NqbpZwn0aLOqB8Tn00`wATrY~pf&k~xy!K5gEzzo+ zua-0Bk5`}(T^zsP>Cq*PI(`O=9lt7Ks4_g!ldG$bO5!Vo@g^ZVmp& zY?91gcEwEl_Bt88gjI}+$Ku%2p4>$OG8O%(_3T0&zsBGnU4tEb8IpdpgsWf2XyVzf z)n&NzM>-!r(vgI+**CGxbt{KlZ{5w7)$iDP6>P|lj-9ygrU6o~+Fi0ATie4%0t}^g zxuNJ{uIgW2J_z%0IfK4%A8UnIs|X3hvU~KK>rn(*Duw&F_AytH>I(t@gPA@&&SpNw zx#?H**@NlkBK}L+Vz1He7ubJ0G{}O{h@U5BDBOSrcqQ6!v&ai{taE`Km2$U4?oot7 zh4Z#E{@Z8q;^1BW>89(&fDiYIeYSHZ6-oHDZ}rwXIFmg^lQ<1^C%)FD?S#m$kubPk zugvG!s17;P=ux66W9Is^lt5U=`mif)9>mku8Gb(0B?$frgC<1XNpicDcI52fZAM=o z2&)$@-Sqij$R@}hh~XRe9+jXXa5vwgXyJ4>>8G)QYt1WusqOqN)F{d;tD?gXOUBN7 z*4w)z>$$SsJr)xe+*8)MJ9QbDaaL`vZ%bJ0J1gtlMv_C@vjhP62&rJfya~8l`|C&b z0OAnLq}SqxYY(;_pyOm5vMV}F4?^p>kDmlL7Q{WbE;YMR^qZi{SY569J;d;Mt`6r= zb-Jsn)5V83>DS|fADdDg2c?fDk(0PL?5aa|3T%wBgTeY8%KZ`0X;w5I~TzR4wZc`(VHls%bwuUhxG-^4e44Z=eF)BEjk zDY12Xv2qQe=E4WUH6y+Wa;iWXQn&jDCofn~%yFw)9Q^6svo~K6blP2MiF$q4+;@8) z1-n5qI)$5WBl6!}j?I0NyC#8?m8L45wZPUP&)w;-Ty!0gyEe++vA8XjwvKh^a!B`| z(Qe*Gn8fYe?!x^@|GBB)^E;Sjj&?ybytu#TqmB+Cq~gETMOSW2^4mS?7{c*7U%dA@ z1q)cLOoXxU%7FoypKQL+Pv5|a2P=H;rodDt8)MB&b&_V3kplY9|Mr&-TG$**cOS^t zJQKC_GwC-8&qR3#pmd57s223)gLF+?-yGJR`8u~VhgOpJ$?IB;-R-H$#tUJmTnF2r z8Z?;jHKqh-FvYgI0Pzwt;`1l%xCOlu?j0?bpy$oOz+plzfwo_$h**r>U@n9mQ8r7;B7zZN5CE&Rb{|qNccegipH^R6HWjWuu@lrjLY$+ z>-B-_H&<_M`7I3WODcuLGF8t;F)dRv4?EK;Co1_TVf2eCm3`MhT-zx5E}tfIPyr|1d3 zoB{nx(+iifwHPwue)cEuiLqZ8mF3CeRHG<|hm=8IIG!9wNu1p{l&gQC-mDDA4c+D% z<}Gxow*)S809U;m!~4HKYMz8PCSJ%;hmO~#)bsc*wpvjjs7Mgw}aO93Lo<2E7&Ln znkFBYUyWSu*g@>29xVBuH+w_8cTkggp;^8%>cZbV_{aIoBquH|LI`5QiNKbzpgBB4 z?;%a!oVwT1xbF?RPV@IuYmh9g&ejMncbgC3z2M)O4UD@tDk)5W-FM@m zhR{XdHL1@S>c>65aU^Y+x!b{M#-5i-ao?VSNwML{shN zAdx)RHw!cY8azmC<77$AV8VRq0jMmf5^=eMINdjGFAp5|ES-YT3q<^5M~S86YXF(g z^RO}e(kS#pyNv1BQW^O^cgW3lACohz-fB3W+&{alGS7>-6YqX6_q{4X!n<{zO~~yf zad|GYUvAiSBCj-N?(GG-iJV5h`*Ol~x`& zl-Zy`T3=awaip9^kVTfU;j1qOHoG8aV3cFYLvaL`;&9iU^w7VvqNNH^N0q5RvQH-_ z(TL2`FpkAWny$_l0nDj4XD+tm9pb_|GOp%GD&WibT5M=!(3X>c7?%_|P_zb~yhQ6CS-7qG}$}cIBGVBkjo( z`jTl(DDXgRW%?PQn63pSex4ojO%#sG#|0j1EPFaT1e;<~pC>_KeAOjF zucngSKlDmG2#+El3ilvj*f{M;`>rtF9tbUX19>MKAiI8Vg(b@&FJEmzT)4pBAzzM6 zyIV~uC8aLz&cQ|wT0uJ&Rj_R=OV5HL#EjE@OePjdBowGc6xH;BOxM6PzM4TOkb-Az zRI+UH=mtCmQj?yZBOyfG{&?jb=(PYM(VDouKDwnuyAam1Gs9Xhc}UUa@4W#}0stsT z;ZBEP<{-WRCjT247^u9K>oJhOxF~gB^q~@E7=a~imnxlw@GNR`gl0S%UI#+{rHi-+8gpZio5CE`Q)Rw&ojV-!IfjN5gS$Qx;gNii?BXO0t96hB_yvl zc8*T6ItvP2fd92f)#WgfaXav_uXz#Y79UrRon)Vh<(^&q;r?Hh1PT$}n0TY|yl287 zG0CyW6(au$G|*N+cQb$NeZ+ zLg{pe^6Aa&COMsgJJ~@JkjgFOmY*kN+0NHX#_SX=i}D6*Vzw}0Fbj(xps#zKE+ ze@VxG2z#ldWbOY_RaHvRpoMFUcU}S%>Kdy`51B#u9`!&ZmYm$eQ2yyPbq>Q2QI8Gl z>&!yI)>p0I4_t{YrAT+2gIH1Pp=|+veB~JQiLMq3oDn3&K_#_$l77-4vlK~@=h(As zuK*%3oq(-K2{Oq_R13k~h zUO@g=ht3^5ZD2-<9W=>Nn4DDNRKc!lv8(?-U0-n0S}N~);dzx!A5MmPlc61$fFPET)hWzmEF^s!&x{95<*$mHlt$mm6JdSu-IUuM>S-yY)qEjr{7BU>`CL zuu!{mU<&Lpp2Xn;_4Y?AYm2xte*>nIO7nY*LXxy!Bx4O~@1ze?3ngcHjVnBy89`1W zWn}($>^O{2orov9P}yr5v*Y`oJ6h+vAV6EdfWMH(g=`$%?rSQ@38a>{U%!$|_4*Qo z=rKjiFq2tN3RXC~$p!;eo9SiZ>IcT>Y)wtxqCFB{ZkPGnxnNp32#Y;dJ>S75MiE`gQmYWpjt7e#g0E zi)t*YPSzg>_0b*gX-o~_b*=QXMjKKp)4FziI z0Tqh8Aq1kQ?o*&{f;4M@heq#jPFSKp`A%6ZTIMG;Eb$(s)IVv71x>z8SfF8<%xdsc z>1x6R2cn+HJ-NaBERi7+DYUawIaq4C4i0pj|Gzv$C=EBHhQR%FnH>AH*keZj{e}dq zAHDiYc<;D_JkFt~4{C08ea0?WMh$H7gER{s1csq2?jEH`yuHEh*bn`-6RGA9GI2L~ zDv*a~i7N-40+mQAX6f^4u4tR%{5M00v5I}WouLeW*F@9ZdO;3BNPrq~ zZ4T<(*U!4?qLtj2bkv`;b|R`#0lVBe-MzRuol6UD6r`=IO%xfQDoC6r%6}cd;3tOVx5SnO-ue{^x)|?$N*VqN9+UFs^eXfHZ6h4=$JZz?U8;~DNYP7 ztNr*E;SlF5p-BK+f<2~GQbX+R?EbKK;_iN!q>FogbCe#KTYofu9-0wN=@j6cMW5I( z+q$ItcEbfJGwz=GfEf@lo#yDz`ghz?&|^3ffJDlnF_Xe86tX>cVHK>fP5OIsjI>Y= z{s7q@$Aa)P3Nhlx>=I;t)a+Bj=`{3l2y3ZZmdP+}#3MMNTQp?Srh0$QlB`$7&ofdT zF^QA6f3N4wI>c!3tH|-LF3qQl&%r=`U<{lyFez53@e=H$&@a~wj$6OEnLI*%57PX| zOgphiGZ(9>0fR}e-|tXVU~INtf4P=6T*I)zt*6p9#etC_pdz(U8I7*scvh}Rw^mi9 z9e55F46}erT!{WWm)qL^(+U!F8aMdeAZk(e-4&Uwr;E|{LkSU&yL0;?Pg1B5(vdyY zr6hF6u99AD-X%*HiZJNSgh0=$>?{OB#H1la&e9khZD{xiH#G1@%zU6+AW(+c`_9&o zw)V5WeHrY|gaH|_C`*WKPNZwmr>g_+V3xr*{>YzcZ||7(d_cSNvWFCThBM(wKF=94 zJ1p3fP~0PDou0T&6v!BaZru`{E+$lZ<9LT1gq1gFbZaT+u|^2nCNkU7;zCTj)^UQH zRHXh6#eY@RiW3`MAOsJ@JsDvQ&K5F{IrgG`%!h{Qv~^Q`C^3h0FYn_@$G!4eZ;&$<|&Ms(iepL*PV1%K(B4oCX%PRnke zm*paRprx$22_zvtkEA%$QU299YU>2jB+RcOl`@oBWB&=DMc&1xKo_^pvxM=^MTN8s zGDJKq#owyfe1`p>-$lwH9}Ju`CWkBFdQ3&!zsq_Bv3&KHMRXc3#r^6<064&_HdJ%< zV_iqJj>x|sD$~SWNu~&>t!a|ZdD>B{)Hta>@7P|#T~mjy^6%OKkr}~_m-FqrR8ozJ zwJrr`U+QF`z$zx8AG7_<4$&}9Wu?CYd(x4lS0G9AjuzzFylyH9t)%}u={@=3-#VZ#EpT8Jgo@y5VSL(*9G*`GA2`BS4|JN6oe+x|NM!R!%MEatqM(6aI9Ks^XK<4yZzv1 z)#jSjd39zjY24l^6hZA2IUaR-l!hLX@A`Ms;JP%;&NoE9w`#kS{(P3m>S5X4wHzfy z-X*=7(7`2)_t!V`t^R+0ku<3wLy0rNkTWSqi*~oERBjel7RYw@Cj7Yzhp#(xk?5Tn zlnpO16oLHkzwc12-EILF6<>AoGPwHBsHB2J;twAYEZ&1q#hJK6U@uK{75`b07RUB8 Wqk4&Z-a-)q{*>g@WlN>aU;GE-4t}u! literal 0 HcmV?d00001 diff --git a/public/images/inputs/nswitch.json b/public/images/inputs/nswitch.json new file mode 100644 index 000000000..441e70d47 --- /dev/null +++ b/public/images/inputs/nswitch.json @@ -0,0 +1,356 @@ +{"frames": { + +"T_S_A_Alt.png": +{ + "frame": {"x":0,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_B_Alt.png": +{ + "frame": {"x":128,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_Con_Alt.png": +{ + "frame": {"x":256,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_Con_Left_Alt.png": +{ + "frame": {"x":384,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_Con_Right_Alt.png": +{ + "frame": {"x":512,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_Con_Separate_Alt.png": +{ + "frame": {"x":640,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_Down_Alt.png": +{ + "frame": {"x":768,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_Dpad_Alt.png": +{ + "frame": {"x":896,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_Dpad_Down_Alt.png": +{ + "frame": {"x":1024,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_Dpad_Left_Alt.png": +{ + "frame": {"x":1152,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_Dpad_Right_Alt.png": +{ + "frame": {"x":1280,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_Dpad_Up_Alt.png": +{ + "frame": {"x":0,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_Dpad_X_Alt.png": +{ + "frame": {"x":128,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_Dpad_Y_Alt.png": +{ + "frame": {"x":256,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_Home_Alt.png": +{ + "frame": {"x":384,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_L_2D_Alt.png": +{ + "frame": {"x":512,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_L_Alt.png": +{ + "frame": {"x":640,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_L_Down_Alt.png": +{ + "frame": {"x":768,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_L_Left_Alt.png": +{ + "frame": {"x":896,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_L_Right_Alt.png": +{ + "frame": {"x":1024,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_L_Up_Alt.png": +{ + "frame": {"x":1152,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_L_X_Alt.png": +{ + "frame": {"x":1280,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_L_Y_Alt.png": +{ + "frame": {"x":0,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_LB_Alt.png": +{ + "frame": {"x":128,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_Left_Alt.png": +{ + "frame": {"x":256,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_LT_Alt.png": +{ + "frame": {"x":384,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_Minus_Alt.png": +{ + "frame": {"x":512,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_Plus_Alt.png": +{ + "frame": {"x":640,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_R_2D_Alt.png": +{ + "frame": {"x":768,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_R_Alt.png": +{ + "frame": {"x":896,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_R_Down_Alt.png": +{ + "frame": {"x":1024,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_R_Left_Alt.png": +{ + "frame": {"x":1152,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_R_Right_Alt.png": +{ + "frame": {"x":1280,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_R_Up_Alt.png": +{ + "frame": {"x":0,"y":384,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_R_X_Alt.png": +{ + "frame": {"x":128,"y":384,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_R_Y_Alt.png": +{ + "frame": {"x":256,"y":384,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_RB_Alt.png": +{ + "frame": {"x":384,"y":384,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_Right_Alt.png": +{ + "frame": {"x":512,"y":384,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_RT_Alt.png": +{ + "frame": {"x":640,"y":384,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_Square_Alt.png": +{ + "frame": {"x":768,"y":384,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_Up_Alt.png": +{ + "frame": {"x":896,"y":384,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_X_Alt.png": +{ + "frame": {"x":1024,"y":384,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +"T_S_Y_Alt.png": +{ + "frame": {"x":1152,"y":384,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}}, +"meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "nswitch.png", + "format": "RGBA8888", + "size": {"w":1408,"h":512}, + "scale": "1", + "smartupdate": "$TexturePacker:SmartUpdate:9256c2fb9e9658d02a3c87f7803e4cf0:da5e7131e2452535087974b43fe1c89f:aeba0349fc8bd3742630ffd8f1f1df7a$" +} +} diff --git a/public/images/inputs/nswitch.png b/public/images/inputs/nswitch.png new file mode 100644 index 0000000000000000000000000000000000000000..49b54d46a76519aaee67787ede4c0914ac7c3025 GIT binary patch literal 31039 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%0A7^RghOpkiV;R4Vb)%-(5=?CK^Co$uN+0 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%0A6t3B>(c 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 mapping[key] === button.index); + const key = Object.keys(mapping).find(key => mapping[key] === button.index); + return [this.player[this.chosenGamepad]['type'], icons[key]]; } @@ -360,6 +369,7 @@ export class InputsController { * - If mapped, emits an 'input_down' event with the controller type and button action, and updates the interaction of this button. */ gamepadButtonDown(pad: Phaser.Input.Gamepad.Gamepad, button: Phaser.Input.Gamepad.Button, value: number): void { + if (!pad) return; if (!this.chosenGamepad) // at the very first input, if we have not yet a chosen gamepad, we set it this.setChosenGamepad(pad.id); if (!this.gamepadSupport || pad.id.toLowerCase() !== this.chosenGamepad.toLowerCase()) return; @@ -388,6 +398,7 @@ export class InputsController { * - If mapped, emits an 'input_up' event with the controller type and button action, and clears the interaction for this button. */ gamepadButtonUp(pad: Phaser.Input.Gamepad.Gamepad, button: Phaser.Input.Gamepad.Button, value: number): void { + if (!pad) return; if (!this.gamepadSupport || pad.id !== this.chosenGamepad) return; const actionMapping = this.getActionGamepadMapping(); const buttonUp = actionMapping.hasOwnProperty(button.index) && actionMapping[button.index]; diff --git a/src/loading-scene.ts b/src/loading-scene.ts index a7d68ef98..8e69ad63b 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -211,6 +211,12 @@ export class LoadingScene extends SceneBase { this.loadAtlas(`pokemon_icons_${i}v`, ''); } + // Free icons from: + // https://juliocacko.itch.io/free-input-prompts + this.loadAtlas('dualshock', 'inputs'); + this.loadAtlas('nswitch', 'inputs'); + this.loadAtlas('xbox', 'inputs'); + this.loadSe('select'); this.loadSe('menu_open'); this.loadSe('hit'); diff --git a/src/ui/gamepad-binding-ui-handler.ts b/src/ui/gamepad-binding-ui-handler.ts index 6f1422e67..e2dba78c7 100644 --- a/src/ui/gamepad-binding-ui-handler.ts +++ b/src/ui/gamepad-binding-ui-handler.ts @@ -12,8 +12,11 @@ export default class GamepadBindingUiHandler extends UiHandler { protected optionSelectBg: Phaser.GameObjects.NineSlice; private unlockText: Phaser.GameObjects.Text; private keyPressed: Phaser.GameObjects.Text; + private alreadyAssignedText: Phaser.GameObjects.Text; private listening: boolean = false; private buttonPressed = ''; + private iconXbox: Phaser.GameObjects.Sprite; + private iconDualshock: Phaser.GameObjects.Sprite; constructor(scene: BattleScene, mode: Mode = Mode.GAMEPAD_BINDING) { super(scene, mode); @@ -29,31 +32,58 @@ export default class GamepadBindingUiHandler extends UiHandler { this.optionSelectContainer.setVisible(false); ui.add(this.optionSelectContainer); - // this.optionSelectBg = addWindow(this.scene, this.scene.game.canvas.width / 12, -this.scene.game.canvas.height / 12, this.getWindowWidth(), -this.getWindowHeight()); + this.titleBg = addWindow(this.scene, (this.scene.game.canvas.width / 6) - this.getWindowWidth(), -(this.scene.game.canvas.height / 6) + 28 + 22, this.getWindowWidth(), 24); + this.titleBg.setOrigin(0.5); + this.optionSelectContainer.add(this.titleBg); + + this.unlockText = addTextObject(this.scene, 0, 0, 'Press a button...', TextStyle.WINDOW); + this.unlockText.setOrigin(0, 0); + this.unlockText.setPositionRelative(this.titleBg, 36, 4); + this.optionSelectContainer.add(this.unlockText); + this.optionSelectBg = addWindow(this.scene, (this.scene.game.canvas.width / 6) - this.getWindowWidth(), -(this.scene.game.canvas.height / 6) + this.getWindowHeight() + 28, this.getWindowWidth(), this.getWindowHeight()); this.optionSelectBg.setOrigin(0.5); this.optionSelectContainer.add(this.optionSelectBg); - this.unlockText = addTextObject(this.scene, 0, 0, 'Press a button...', TextStyle.WINDOW); - this.unlockText.setOrigin(0, 0); - this.unlockText.setPositionRelative(this.optionSelectBg, 36, 4); + this.iconXbox = this.scene.add.sprite(0, 0, 'xbox'); + this.iconXbox.setScale(0.2); + this.iconXbox.setPositionRelative(this.optionSelectBg, 0, 0); + this.iconXbox.setOrigin(0, 0); + this.iconXbox.setVisible(false); - this.keyPressed = addTextObject(this.scene, 0, 0, '', TextStyle.WINDOW); - this.keyPressed.setOrigin(0, 0); - this.keyPressed.setPositionRelative(this.unlockText, 0, 12); - this.keyPressed.setVisible(false); + this.iconDualshock = this.scene.add.sprite(0, 0, 'dualshock'); + this.iconDualshock.setScale(0.2); + this.iconDualshock.setPositionRelative(this.optionSelectBg, this.optionSelectBg.width / 2, 0); + this.iconDualshock.setOrigin(0, 0); + this.iconDualshock.setVisible(false); + + this.alreadyAssignedText = addTextObject(this.scene, 0, 0, 'already assigned to', TextStyle.WINDOW); + this.alreadyAssignedText.setOrigin(0, 0); + this.alreadyAssignedText.setPositionRelative(this.optionSelectBg, this.optionSelectBg.width / 2, 24); - this.optionSelectContainer.add(this.unlockText); - this.optionSelectContainer.add(this.keyPressed); + this.optionSelectContainer.add(this.iconXbox); + this.optionSelectContainer.add(this.iconDualshock); + this.optionSelectContainer.add(this.alreadyAssignedText); } gamepadButtonDown(pad: Phaser.Input.Gamepad.Gamepad, button: Phaser.Input.Gamepad.Button, value: number): void { - if (!this.listening) return; + if (!this.listening || pad.id !== this.scene.inputController?.chosenGamepad) return; this.buttonPressed = button.index; - const buttonLabel = this.scene.inputController.getButtonLabel(button); - this.keyPressed.setText(buttonLabel); - this.keyPressed.setVisible(true); + const [type, buttonIcon] = this.scene.inputController.getButtonLabel(button); + switch (type) { + case 'dualshock': + this.iconXbox.setVisible(false); + this.iconDualshock.setFrame(buttonIcon); + this.iconDualshock.setVisible(true); + break + case 'xbox': + default: + this.iconDualshock.setVisible(false); + this.iconXbox.setFrame(buttonIcon); + this.iconXbox.setVisible(true); + break + } } show(args: any[]): boolean { From ef4bb9c3b57593866f2ff71601d0f7b5b6eadc7d Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sat, 11 May 2024 01:26:06 +0200 Subject: [PATCH 19/81] added ui to confirm or cancel the new input --- src/inputs-controller.ts | 2 - src/system/settings-gamepad.ts | 40 ++++----- src/ui/gamepad-binding-ui-handler.ts | 118 +++++++++++++++++++++++---- 3 files changed, 124 insertions(+), 36 deletions(-) diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index de4b80eb1..af2d1929c 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -66,8 +66,6 @@ export class InputsController { private gamepadSupport: boolean = true; - private gamepadSupport: boolean = true; - public customGamepadMapping = new Map(); public chosenGamepad: String; private disconnectedGamepads: Array = new Array(); diff --git a/src/system/settings-gamepad.ts b/src/system/settings-gamepad.ts index ce4fa77ea..b9bdc535c 100644 --- a/src/system/settings-gamepad.ts +++ b/src/system/settings-gamepad.ts @@ -24,9 +24,9 @@ export enum SettingGamepad { } export const settingGamepadOptions: SettingOptions = { - [SettingGamepad.Default_Controller]: [ 'Default', 'Change' ], - [SettingGamepad.Gamepad_Support]: [ 'Auto', 'Disabled' ], - [SettingGamepad.Swap_A_and_B]: [ 'Enabled', 'Disabled' ], + [SettingGamepad.Default_Controller]: ['Default', 'Change'], + [SettingGamepad.Gamepad_Support]: ['Auto', 'Disabled'], + [SettingGamepad.Swap_A_and_B]: ['Enabled', 'Disabled'], [SettingGamepad.Button_Action]: [`KEY ${Button.ACTION.toString()}`, 'Change'], [SettingGamepad.Button_Cancel]: [`KEY ${Button.CANCEL.toString()}`, 'Change'], // [SettingGamepad.Button_Menu]: [`KEY ${Button.MENU.toString()}`, 'Change'], @@ -59,7 +59,7 @@ export const settingGamepadDefaults: SettingDefaults = { // [SettingGamepad.Button_Slow_Down]: Button.SLOW_DOWN, }; -export const noOptionsCursors : Array = [ +export const noOptionsCursors: Array = [ SettingGamepad.Button_Action, SettingGamepad.Button_Cancel, ]; @@ -76,28 +76,25 @@ export function setSettingGamepad(scene: BattleScene, setting: SettingGamepad, v break; case SettingGamepad.Button_Action: case SettingGamepad.Button_Cancel: - // case SettingGamepad.Button_Menu: - // case SettingGamepad.Button_Stats: - // case SettingGamepad.Button_Cycle_Shiny: - // case SettingGamepad.Button_Cycle_Form: - // case SettingGamepad.Button_Cycle_Gender: - // case SettingGamepad.Button_Cycle_Ability: - // case SettingGamepad.Button_Cycle_Nature: - // case SettingGamepad.Button_Cycle_Variant: - // case SettingGamepad.Button_Speed_Up: - // case SettingGamepad.Button_Slow_Down: + // case SettingGamepad.Button_Menu: + // case SettingGamepad.Button_Stats: + // case SettingGamepad.Button_Cycle_Shiny: + // case SettingGamepad.Button_Cycle_Form: + // case SettingGamepad.Button_Cycle_Gender: + // case SettingGamepad.Button_Cycle_Ability: + // case SettingGamepad.Button_Cycle_Nature: + // case SettingGamepad.Button_Cycle_Variant: + // case SettingGamepad.Button_Speed_Up: + // case SettingGamepad.Button_Slow_Down: if (value) { if (scene.ui) { const cancelHandler = () => { scene.ui.revertMode(); - (scene.ui.getHandler() as SettingsGamepadUiHandler).setOptionCursor(Object.values(SettingGamepad).indexOf(setting), 0, true); return false; }; scene.ui.setOverlayMode(Mode.GAMEPAD_BINDING, { - options: [{ - label: 'Press a button on your gamepad', - handler: cancelHandler, - }] + target: setting, + cancelHandler: cancelHandler, }); } } @@ -117,7 +114,10 @@ export function setSettingGamepad(scene: BattleScene, setting: SettingGamepad, v return true; }; scene.ui.setOverlayMode(Mode.OPTION_SELECT, { - options: [...gp.map((g) => ({label: truncateString(g, 30), handler: () => changeGamepadHandler(g)})), { + options: [...gp.map((g) => ({ + label: truncateString(g, 30), + handler: () => changeGamepadHandler(g) + })), { label: 'Cancel', handler: cancelHandler, }] diff --git a/src/ui/gamepad-binding-ui-handler.ts b/src/ui/gamepad-binding-ui-handler.ts index e2dba78c7..f0f79c1cb 100644 --- a/src/ui/gamepad-binding-ui-handler.ts +++ b/src/ui/gamepad-binding-ui-handler.ts @@ -9,14 +9,22 @@ import Phaser from "phaser"; export default class GamepadBindingUiHandler extends UiHandler { protected optionSelectContainer: Phaser.GameObjects.Container; + protected actionsContainer: Phaser.GameObjects.Container; + protected titleBg: Phaser.GameObjects.NineSlice; + protected actionBg: Phaser.GameObjects.NineSlice; protected optionSelectBg: Phaser.GameObjects.NineSlice; private unlockText: Phaser.GameObjects.Text; private keyPressed: Phaser.GameObjects.Text; private alreadyAssignedText: Phaser.GameObjects.Text; + private actionLabel: Phaser.GameObjects.Text; + private cancelLabel: Phaser.GameObjects.Text; private listening: boolean = false; - private buttonPressed = ''; + private buttonPressed = null; private iconXbox: Phaser.GameObjects.Sprite; + private previousIconXbox: Phaser.GameObjects.Sprite; private iconDualshock: Phaser.GameObjects.Sprite; + private cancelFn; + private target; constructor(scene: BattleScene, mode: Mode = Mode.GAMEPAD_BINDING) { super(scene, mode); @@ -29,13 +37,20 @@ export default class GamepadBindingUiHandler extends UiHandler { setup() { const ui = this.getUi(); this.optionSelectContainer = this.scene.add.container(0, 0); + this.actionsContainer = this.scene.add.container(0, 0); this.optionSelectContainer.setVisible(false); + this.actionsContainer.setVisible(false); ui.add(this.optionSelectContainer); + ui.add(this.actionsContainer); - this.titleBg = addWindow(this.scene, (this.scene.game.canvas.width / 6) - this.getWindowWidth(), -(this.scene.game.canvas.height / 6) + 28 + 22, this.getWindowWidth(), 24); + this.titleBg = addWindow(this.scene, (this.scene.game.canvas.width / 6) - this.getWindowWidth(), -(this.scene.game.canvas.height / 6) + 28 + 21, this.getWindowWidth(), 24); this.titleBg.setOrigin(0.5); this.optionSelectContainer.add(this.titleBg); + this.actionBg = addWindow(this.scene, (this.scene.game.canvas.width / 6) - this.getWindowWidth(), -(this.scene.game.canvas.height / 6) + this.getWindowHeight() + 28 + 21 + 21, this.getWindowWidth(), 24); + this.actionBg.setOrigin(0.5); + this.actionsContainer.add(this.actionBg); + this.unlockText = addTextObject(this.scene, 0, 0, 'Press a button...', TextStyle.WINDOW); this.unlockText.setOrigin(0, 0); this.unlockText.setPositionRelative(this.titleBg, 36, 4); @@ -46,29 +61,48 @@ export default class GamepadBindingUiHandler extends UiHandler { this.optionSelectContainer.add(this.optionSelectBg); this.iconXbox = this.scene.add.sprite(0, 0, 'xbox'); - this.iconXbox.setScale(0.2); - this.iconXbox.setPositionRelative(this.optionSelectBg, 0, 0); - this.iconXbox.setOrigin(0, 0); + this.iconXbox.setScale(0.15); + this.iconXbox.setPositionRelative(this.optionSelectBg, 78, 16); + this.iconXbox.setOrigin(0.5); this.iconXbox.setVisible(false); this.iconDualshock = this.scene.add.sprite(0, 0, 'dualshock'); - this.iconDualshock.setScale(0.2); - this.iconDualshock.setPositionRelative(this.optionSelectBg, this.optionSelectBg.width / 2, 0); - this.iconDualshock.setOrigin(0, 0); + this.iconDualshock.setScale(0.15); + this.iconDualshock.setPositionRelative(this.optionSelectBg, 78, 16); + this.iconDualshock.setOrigin(0.5); this.iconDualshock.setVisible(false); this.alreadyAssignedText = addTextObject(this.scene, 0, 0, 'already assigned to', TextStyle.WINDOW); - this.alreadyAssignedText.setOrigin(0, 0); - this.alreadyAssignedText.setPositionRelative(this.optionSelectBg, this.optionSelectBg.width / 2, 24); + this.alreadyAssignedText.setOrigin(0.5); + this.alreadyAssignedText.setPositionRelative(this.optionSelectBg, this.optionSelectBg.width / 2 - 2, this.optionSelectBg.height / 2 - 2); + this.alreadyAssignedText.setVisible(false); + + this.previousIconXbox = this.scene.add.sprite(0, 0, 'xbox'); + this.previousIconXbox.setScale(0.15); + this.previousIconXbox.setPositionRelative(this.optionSelectBg, 78, 48); + this.previousIconXbox.setOrigin(0.5); + this.previousIconXbox.setVisible(false); + + this.cancelLabel = addTextObject(this.scene, 0, 0, 'Cancel', TextStyle.SETTINGS_LABEL); + this.cancelLabel.setOrigin(0, 0.5); + this.cancelLabel.setPositionRelative(this.actionBg, 10, this.actionBg.height / 2); + + this.actionLabel = addTextObject(this.scene, 0, 0, 'Confirm', TextStyle.SETTINGS_LABEL); + this.actionLabel.setOrigin(0, 0.5); + this.actionLabel.setPositionRelative(this.actionBg, this.actionBg.width - 45, this.actionBg.height / 2); this.optionSelectContainer.add(this.iconXbox); this.optionSelectContainer.add(this.iconDualshock); this.optionSelectContainer.add(this.alreadyAssignedText); + this.optionSelectContainer.add(this.previousIconXbox); + this.actionsContainer.add(this.actionLabel); + this.actionsContainer.add(this.cancelLabel); } gamepadButtonDown(pad: Phaser.Input.Gamepad.Gamepad, button: Phaser.Input.Gamepad.Button, value: number): void { - if (!this.listening || pad.id !== this.scene.inputController?.chosenGamepad) return; + const blacklist = [12, 13, 14, 15]; + if (!this.listening || pad.id !== this.scene.inputController?.chosenGamepad || blacklist.includes(button.index) || this.buttonPressed !== null) return; this.buttonPressed = button.index; const [type, buttonIcon] = this.scene.inputController.getButtonLabel(button); switch (type) { @@ -76,21 +110,30 @@ export default class GamepadBindingUiHandler extends UiHandler { this.iconXbox.setVisible(false); this.iconDualshock.setFrame(buttonIcon); this.iconDualshock.setVisible(true); + this.previousIconXbox.setVisible(true); + this.alreadyAssignedText.setVisible(true); break case 'xbox': default: this.iconDualshock.setVisible(false); this.iconXbox.setFrame(buttonIcon); this.iconXbox.setVisible(true); + this.previousIconXbox.setVisible(true); + this.alreadyAssignedText.setVisible(true); break } + this.setCursor(0); + this.actionsContainer.setVisible(true); } show(args: any[]): boolean { - console.log('args', args); super.show(args); + this.buttonPressed = null; + this.cancelFn = args[0].cancelHandler; + this.target = args[0].target; this.getUi().bringToTop(this.optionSelectContainer); + this.getUi().bringToTop(this.actionsContainer); this.optionSelectContainer.setVisible(true); setTimeout(() => this.listening = true, 150); @@ -106,12 +149,59 @@ export default class GamepadBindingUiHandler extends UiHandler { } processInput(button: Button): boolean { + if (this.buttonPressed === null) return; const ui = this.getUi(); - return true; + let success = false; + switch (button) { + case Button.LEFT: + case Button.RIGHT: + const cursor = this.cursor ? 0 : 1; + success = this.setCursor(cursor); + break + case Button.ACTION: + if (this.cursor === 0) { + success = true; + // Reverts UI to its previous state on cancel. + // this.scene.ui.revertMode(); + this.cancelFn(); + } + break; + } + + // Plays a select sound effect if an action was successfully processed. + if (success) + ui.playSelect(); + + return success; + } + + setCursor(cursor: number): boolean { + this.cursor = cursor; + if (cursor === 1) { + this.actionLabel.setColor(this.getTextColor(TextStyle.SETTINGS_SELECTED)); + this.actionLabel.setShadowColor(this.getTextColor(TextStyle.SETTINGS_SELECTED, true)); + this.cancelLabel.setColor(this.getTextColor(TextStyle.WINDOW)); + this.cancelLabel.setShadowColor(this.getTextColor(TextStyle.WINDOW, true)); + return; + } + this.actionLabel.setColor(this.getTextColor(TextStyle.WINDOW)); + this.actionLabel.setShadowColor(this.getTextColor(TextStyle.WINDOW, true)); + this.cancelLabel.setColor(this.getTextColor(TextStyle.SETTINGS_SELECTED)); + this.cancelLabel.setShadowColor(this.getTextColor(TextStyle.SETTINGS_SELECTED, true)); + return; } clear() { super.clear(); + this.target = null; + this.cancelFn = null; + this.optionSelectContainer.setVisible(false); + this.actionsContainer.setVisible(false); + this.iconXbox.setVisible(false); + this.iconDualshock.setVisible(false); + this.previousIconXbox.setVisible(false); + this.alreadyAssignedText.setVisible(false); + this.iconXbox.setFrame(null); + this.iconDualshock.setFrame(null); } - } \ No newline at end of file From 022c4351d39ca4d990e294f63fcbdcd8bf446e0b Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sat, 11 May 2024 02:33:41 +0200 Subject: [PATCH 20/81] adding essential data into config file --- src/configs/pad_xbox360.ts | 27 ++++++++++++++++++++++++--- src/enums/buttons.ts | 4 ++-- src/inputs-controller.ts | 10 +++++----- src/ui-inputs.ts | 4 ++-- src/ui/gamepad-binding-ui-handler.ts | 6 +++--- src/ui/settings-gamepad-ui-handler.ts | 4 ++-- src/ui/settings-ui-handler.ts | 4 ++-- src/ui/starter-select-ui-handler.ts | 4 ++-- 8 files changed, 42 insertions(+), 21 deletions(-) diff --git a/src/configs/pad_xbox360.ts b/src/configs/pad_xbox360.ts index 4c52015ae..03667f6f3 100644 --- a/src/configs/pad_xbox360.ts +++ b/src/configs/pad_xbox360.ts @@ -1,3 +1,6 @@ +import {SettingGamepad} from "../system/settings-gamepad"; +import {Button} from "#app/enums/buttons"; + /** * Generic pad mapping */ @@ -20,8 +23,7 @@ const pad_xbox360 = { LC_N: 12, LC_S: 13, LC_W: 14, - LC_E: 15, - MENU: 16 + LC_E: 15 }, icons: { RC_S: "T_X_A_Color_Alt.png", @@ -40,7 +42,26 @@ const pad_xbox360 = { LC_S: "T_X_Dpad_Down_Alt.png", LC_W: "T_X_Dpad_Left_Alt.png", LC_E: "T_X_Dpad_Right_Alt.png", - MENU: "" + }, + setting: { + RC_S: SettingGamepad.Button_Action, + RC_E: SettingGamepad.Button_Cancel, + }, + default: { + RC_S: Button.ACTION, + RC_E: Button.CANCEL, + RC_W: Button.CYCLE_NATURE, + RC_N: Button.CYCLE_VARIANT, + START: Button.MENU, + SELECT: Button.STATS, + LB: Button.CYCLE_FORM, + RB: Button.CYCLE_SHINY, + LT: Button.CYCLE_GENDER, + RT: Button.CYCLE_ABILITY, + LC_N: Button.UP, + LC_S: Button.DOWN, + LC_W: Button.LEFT, + LC_E: Button.RIGHT, } }; diff --git a/src/enums/buttons.ts b/src/enums/buttons.ts index f4e92b2ae..0d90843c8 100644 --- a/src/enums/buttons.ts +++ b/src/enums/buttons.ts @@ -8,8 +8,8 @@ export enum Button { CANCEL, MENU, STATS, - RB, // CYCLE_SHINY - LB, // CYCLE_FORM + CYCLE_SHINY, // CYCLE_SHINY + CYCLE_FORM, // CYCLE_FORM CYCLE_GENDER, CYCLE_ABILITY, CYCLE_NATURE, diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index af2d1929c..c2b376109 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -66,7 +66,6 @@ export class InputsController { private gamepadSupport: boolean = true; - public customGamepadMapping = new Map(); public chosenGamepad: String; private disconnectedGamepads: Array = new Array(); @@ -287,6 +286,7 @@ export class InputsController { const mappedPad = this.mapGamepad(gamepadID); if (!this.player[gamepad]) this.player[gamepad] = {}; this.player[gamepad]['mapping'] = mappedPad.gamepadMapping; + this.player[gamepad]['custom'] = mappedPad.gamepadMapping; this.player[gamepad]['icons'] = mappedPad.icons; this.player[gamepad]['type'] = mappedPad.padType; } @@ -335,8 +335,8 @@ export class InputsController { gamepadMapping[this.player[this.chosenGamepad].mapping.RC_E] = this.scene.abSwapped ? Button.ACTION : Button.CANCEL; gamepadMapping[this.player[this.chosenGamepad].mapping.SELECT] = Button.STATS; gamepadMapping[this.player[this.chosenGamepad].mapping.START] = Button.MENU; - gamepadMapping[this.player[this.chosenGamepad].mapping.RB] = Button.RB; - gamepadMapping[this.player[this.chosenGamepad].mapping.LB] = Button.LB; + gamepadMapping[this.player[this.chosenGamepad].mapping.RB] = Button.CYCLE_SHINY; + gamepadMapping[this.player[this.chosenGamepad].mapping.LB] = Button.CYCLE_FORM; gamepadMapping[this.player[this.chosenGamepad].mapping.LT] = Button.CYCLE_GENDER; gamepadMapping[this.player[this.chosenGamepad].mapping.RT] = Button.CYCLE_ABILITY; gamepadMapping[this.player[this.chosenGamepad].mapping.RC_W] = Button.CYCLE_NATURE; @@ -440,8 +440,8 @@ export class InputsController { [Button.CANCEL]: [keyCodes.BACKSPACE, keyCodes.X], [Button.MENU]: [keyCodes.ESC, keyCodes.M], [Button.STATS]: [keyCodes.SHIFT, keyCodes.C], - [Button.RB]: [keyCodes.R], - [Button.LB]: [keyCodes.F], + [Button.CYCLE_SHINY]: [keyCodes.R], + [Button.CYCLE_FORM]: [keyCodes.F], [Button.CYCLE_GENDER]: [keyCodes.G], [Button.CYCLE_ABILITY]: [keyCodes.E], [Button.CYCLE_NATURE]: [keyCodes.N], diff --git a/src/ui-inputs.ts b/src/ui-inputs.ts index 9f484355e..e08f05d38 100644 --- a/src/ui-inputs.ts +++ b/src/ui-inputs.ts @@ -58,8 +58,8 @@ export class UiInputs { actions[Button.CANCEL] = () => this.buttonAb(Button.CANCEL); actions[Button.MENU] = () => this.buttonMenu(); actions[Button.STATS] = () => this.buttonStats(true); - actions[Button.RB] = () => this.buttonCycleOption(Button.RB); - actions[Button.LB] = () => this.buttonCycleOption(Button.LB); + actions[Button.CYCLE_SHINY] = () => this.buttonCycleOption(Button.CYCLE_SHINY); + actions[Button.CYCLE_FORM] = () => this.buttonCycleOption(Button.CYCLE_FORM); actions[Button.CYCLE_GENDER] = () => this.buttonCycleOption(Button.CYCLE_GENDER); actions[Button.CYCLE_ABILITY] = () => this.buttonCycleOption(Button.CYCLE_ABILITY); actions[Button.CYCLE_NATURE] = () => this.buttonCycleOption(Button.CYCLE_NATURE); diff --git a/src/ui/gamepad-binding-ui-handler.ts b/src/ui/gamepad-binding-ui-handler.ts index f0f79c1cb..fa4357798 100644 --- a/src/ui/gamepad-binding-ui-handler.ts +++ b/src/ui/gamepad-binding-ui-handler.ts @@ -72,7 +72,7 @@ export default class GamepadBindingUiHandler extends UiHandler { this.iconDualshock.setOrigin(0.5); this.iconDualshock.setVisible(false); - this.alreadyAssignedText = addTextObject(this.scene, 0, 0, 'already assigned to', TextStyle.WINDOW); + this.alreadyAssignedText = addTextObject(this.scene, 0, 0, 'will swap with', TextStyle.WINDOW); this.alreadyAssignedText.setOrigin(0.5); this.alreadyAssignedText.setPositionRelative(this.optionSelectBg, this.optionSelectBg.width / 2 - 2, this.optionSelectBg.height / 2 - 2); this.alreadyAssignedText.setVisible(false); @@ -87,9 +87,9 @@ export default class GamepadBindingUiHandler extends UiHandler { this.cancelLabel.setOrigin(0, 0.5); this.cancelLabel.setPositionRelative(this.actionBg, 10, this.actionBg.height / 2); - this.actionLabel = addTextObject(this.scene, 0, 0, 'Confirm', TextStyle.SETTINGS_LABEL); + this.actionLabel = addTextObject(this.scene, 0, 0, 'Confirm Swap', TextStyle.SETTINGS_LABEL); this.actionLabel.setOrigin(0, 0.5); - this.actionLabel.setPositionRelative(this.actionBg, this.actionBg.width - 45, this.actionBg.height / 2); + this.actionLabel.setPositionRelative(this.actionBg, this.actionBg.width - 75, this.actionBg.height / 2); this.optionSelectContainer.add(this.iconXbox); diff --git a/src/ui/settings-gamepad-ui-handler.ts b/src/ui/settings-gamepad-ui-handler.ts index b044dd6f7..d8de0dbe5 100644 --- a/src/ui/settings-gamepad-ui-handler.ts +++ b/src/ui/settings-gamepad-ui-handler.ts @@ -167,10 +167,10 @@ export default class SettingsGamepadUiHandler extends UiHandler { if (this.optionCursors[cursor] < this.optionValueLabels[cursor].length - 1) success = this.setOptionCursor(cursor, this.optionCursors[cursor] + 1, true); break; - case Button.LB: + case Button.CYCLE_FORM: this.scene.ui.setMode(Mode.SETTINGS) success = true; - case Button.RB: + case Button.CYCLE_SHINY: this.scene.ui.setMode(Mode.SETTINGS) success = true; break; diff --git a/src/ui/settings-ui-handler.ts b/src/ui/settings-ui-handler.ts index 7c045df4d..678b5bca8 100644 --- a/src/ui/settings-ui-handler.ts +++ b/src/ui/settings-ui-handler.ts @@ -187,11 +187,11 @@ export default class SettingsUiHandler extends UiHandler { if (this.optionCursors[cursor] < this.optionValueLabels[cursor].length - 1) success = this.setOptionCursor(cursor, this.optionCursors[cursor] + 1, true); break; - case Button.LB: + case Button.CYCLE_FORM: this.scene.ui.setMode(Mode.SETTINGS_GAMEPAD) success = true; break; - case Button.RB: + case Button.CYCLE_SHINY: this.scene.ui.setMode(Mode.SETTINGS_GAMEPAD) success = true; break; diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index 4bfc4e35b..514bb173a 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -969,7 +969,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { const row = Math.floor(this.cursor / 9); const props = this.scene.gameData.getSpeciesDexAttrProps(this.lastSpecies, this.dexAttrCursor); switch (button) { - case Button.RB: + case Button.CYCLE_SHINY: if (this.canCycleShiny) { this.setSpeciesDetails(this.lastSpecies, !props.shiny, undefined, undefined, props.shiny ? 0 : undefined, undefined, undefined); if (this.dexAttrCursor & DexAttr.SHINY) @@ -978,7 +978,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { success = true; } break; - case Button.LB: + case Button.CYCLE_FORM: if (this.canCycleForm) { const formCount = this.lastSpecies.forms.length; let newFormIndex = props.formIndex; From 043216aa7503eb7645ff3240663349f3cdb61f14 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sat, 11 May 2024 02:36:47 +0200 Subject: [PATCH 21/81] adding essential data into config file --- src/configs/pad_dualshock.ts | 26 ++++++++++++++++++++++++-- src/configs/pad_generic.ts | 24 ++++++++++++++++++++++++ src/configs/pad_unlicensedSNES.ts | 22 ++++++++++++++++++++++ src/inputs-controller.ts | 11 +++++++++++ 4 files changed, 81 insertions(+), 2 deletions(-) diff --git a/src/configs/pad_dualshock.ts b/src/configs/pad_dualshock.ts index e9cb844ed..427a70c4c 100644 --- a/src/configs/pad_dualshock.ts +++ b/src/configs/pad_dualshock.ts @@ -1,3 +1,6 @@ +import {SettingGamepad} from "../system/settings-gamepad"; +import {Button} from "../enums/buttons"; + /** * Dualshock mapping */ @@ -21,7 +24,6 @@ const pad_dualshock = { LC_S: 13, LC_W: 14, LC_E: 15, - MENU: 16, TOUCH: 17 }, icons: { @@ -41,8 +43,28 @@ const pad_dualshock = { LC_S: "T_P4_Dpad_Down_Default.png", LC_W: "T_P4_Dpad_Left_Default.png", LC_E: "T_P4_Dpad_Right_Default.png", - MENU: "", TOUCH: "T_P4_Touch_Pad_Default.png" + }, + setting: { + RC_S: SettingGamepad.Button_Action, + RC_E: SettingGamepad.Button_Cancel, + }, + default: { + RC_S: Button.ACTION, + RC_E: Button.CANCEL, + RC_W: Button.CYCLE_NATURE, + RC_N: Button.CYCLE_VARIANT, + START: Button.MENU, + SELECT: Button.STATS, + LB: Button.CYCLE_FORM, + RB: Button.CYCLE_SHINY, + LT: Button.CYCLE_GENDER, + RT: Button.CYCLE_ABILITY, + LC_N: Button.UP, + LC_S: Button.DOWN, + LC_W: Button.LEFT, + LC_E: Button.RIGHT, + TOUCH: Button.SUBMIT, } }; diff --git a/src/configs/pad_generic.ts b/src/configs/pad_generic.ts index 00346413d..f4f459aeb 100644 --- a/src/configs/pad_generic.ts +++ b/src/configs/pad_generic.ts @@ -1,3 +1,6 @@ +import {SettingGamepad} from "../system/settings-gamepad"; +import {Button} from "../enums/buttons"; + /** * Generic pad mapping */ @@ -40,6 +43,27 @@ const pad_generic = { LC_W: "T_X_Dpad_Left_Alt.png", LC_E: "T_X_Dpad_Right_Alt.png", MENU: "" + }, + setting: { + RC_S: SettingGamepad.Button_Action, + RC_E: SettingGamepad.Button_Cancel, + }, + default: { + RC_S: Button.ACTION, + RC_E: Button.CANCEL, + RC_W: Button.CYCLE_NATURE, + RC_N: Button.CYCLE_VARIANT, + START: Button.MENU, + SELECT: Button.STATS, + LB: Button.CYCLE_FORM, + RB: Button.CYCLE_SHINY, + LT: Button.CYCLE_GENDER, + RT: Button.CYCLE_ABILITY, + LC_N: Button.UP, + LC_S: Button.DOWN, + LC_W: Button.LEFT, + LC_E: Button.RIGHT, + TOUCH: Button.SUBMIT, } }; diff --git a/src/configs/pad_unlicensedSNES.ts b/src/configs/pad_unlicensedSNES.ts index 2da76b804..ca58d7649 100644 --- a/src/configs/pad_unlicensedSNES.ts +++ b/src/configs/pad_unlicensedSNES.ts @@ -1,3 +1,6 @@ +import {SettingGamepad} from "../system/settings-gamepad"; +import {Button} from "../enums/buttons"; + /** * 081f-e401 - UnlicensedSNES */ @@ -31,6 +34,25 @@ const pad_unlicensedSNES = { LC_S: "T_X_Dpad_Down_Alt.png", LC_W: "T_X_Dpad_Left_Alt.png", LC_E: "T_X_Dpad_Right_Alt.png", + }, + setting: { + RC_S: SettingGamepad.Button_Action, + RC_E: SettingGamepad.Button_Cancel, + }, + default: { + RC_S: Button.ACTION, + RC_E: Button.CANCEL, + RC_W: Button.CYCLE_NATURE, + RC_N: Button.CYCLE_VARIANT, + START: Button.MENU, + SELECT: Button.STATS, + LB: Button.CYCLE_FORM, + RB: Button.CYCLE_SHINY, + LC_N: Button.UP, + LC_S: Button.DOWN, + LC_W: Button.LEFT, + LC_E: Button.RIGHT, + TOUCH: Button.SUBMIT, } }; diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index c2b376109..2dba78f36 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -18,11 +18,21 @@ export interface IconsMapping { [key: string]: string; } +export interface SettingMapping { + [key: string]: string; +} + +export interface DefaultMapping { + [key: string]: Button; +} + export interface GamepadConfig { padID: string; padType: string; gamepadMapping: GamepadMapping; icons: IconsMapping; + setting: SettingMapping; + default: DefaultMapping; } export interface ActionGamepadMapping { @@ -289,6 +299,7 @@ export class InputsController { this.player[gamepad]['custom'] = mappedPad.gamepadMapping; this.player[gamepad]['icons'] = mappedPad.icons; this.player[gamepad]['type'] = mappedPad.padType; + this.player[gamepad]['default'] = mappedPad.default; } if (this.chosenGamepad === thisGamepad.id) this.initChosenGamepad(this.chosenGamepad) } From 635a2ea959087db819982d95a90a9b6e53b9c33c Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sat, 11 May 2024 02:59:52 +0200 Subject: [PATCH 22/81] first step to display button icon in the gamepad menu for current binding --- src/configs/pad_dualshock.ts | 2 + src/configs/pad_generic.ts | 2 + src/configs/pad_xbox360.ts | 12 ++++ src/system/settings-gamepad.ts | 90 +++++++++++++++------------ src/ui/settings-gamepad-ui-handler.ts | 12 +++- 5 files changed, 75 insertions(+), 43 deletions(-) diff --git a/src/configs/pad_dualshock.ts b/src/configs/pad_dualshock.ts index 427a70c4c..58fbf31b7 100644 --- a/src/configs/pad_dualshock.ts +++ b/src/configs/pad_dualshock.ts @@ -60,6 +60,8 @@ const pad_dualshock = { RB: Button.CYCLE_SHINY, LT: Button.CYCLE_GENDER, RT: Button.CYCLE_ABILITY, + LS: Button.SPEED_UP, + RS: Button.SLOW_DOWN, LC_N: Button.UP, LC_S: Button.DOWN, LC_W: Button.LEFT, diff --git a/src/configs/pad_generic.ts b/src/configs/pad_generic.ts index f4f459aeb..9a7751d25 100644 --- a/src/configs/pad_generic.ts +++ b/src/configs/pad_generic.ts @@ -59,6 +59,8 @@ const pad_generic = { RB: Button.CYCLE_SHINY, LT: Button.CYCLE_GENDER, RT: Button.CYCLE_ABILITY, + LS: Button.SPEED_UP, + RS: Button.SLOW_DOWN, LC_N: Button.UP, LC_S: Button.DOWN, LC_W: Button.LEFT, diff --git a/src/configs/pad_xbox360.ts b/src/configs/pad_xbox360.ts index 03667f6f3..28c379041 100644 --- a/src/configs/pad_xbox360.ts +++ b/src/configs/pad_xbox360.ts @@ -46,6 +46,16 @@ const pad_xbox360 = { setting: { RC_S: SettingGamepad.Button_Action, RC_E: SettingGamepad.Button_Cancel, + RC_W: SettingGamepad.Button_Cycle_Nature, + RC_N: SettingGamepad.Button_Cycle_Variant, + START: SettingGamepad.Button_Menu, + SELECT: SettingGamepad.Button_Stats, + LB: SettingGamepad.Button_Cycle_Form, + RB: SettingGamepad.Button_Cycle_Shiny, + LT: SettingGamepad.Button_Cycle_Gender, + RT: SettingGamepad.Button_Cycle_Ability, + LS: SettingGamepad.Button_Speed_Up, + RS: SettingGamepad.Button_Slow_Down, }, default: { RC_S: Button.ACTION, @@ -58,6 +68,8 @@ const pad_xbox360 = { RB: Button.CYCLE_SHINY, LT: Button.CYCLE_GENDER, RT: Button.CYCLE_ABILITY, + LS: Button.SPEED_UP, + RS: Button.SLOW_DOWN, LC_N: Button.UP, LC_S: Button.DOWN, LC_W: Button.LEFT, diff --git a/src/system/settings-gamepad.ts b/src/system/settings-gamepad.ts index b9bdc535c..5922fee7b 100644 --- a/src/system/settings-gamepad.ts +++ b/src/system/settings-gamepad.ts @@ -11,16 +11,16 @@ export enum SettingGamepad { Swap_A_and_B = "SWAP_A_B", // Swaps which gamepad button handles ACTION and CANCEL Button_Action = "BUTTON_ACTION", Button_Cancel = "BUTTON_CANCEL", - // Button_Menu = "BUTTON_MENU", - // Button_Stats = "BUTTON_STATS", - // Button_Cycle_Shiny = "BUTTON_CYCLE_SHINY", - // Button_Cycle_Form = "BUTTON_CYCLE_FORM", - // Button_Cycle_Gender = "BUTTON_CYCLE_GENDER", - // Button_Cycle_Ability = "BUTTON_CYCLE_ABILITY", - // Button_Cycle_Nature = "BUTTON_CYCLE_NATURE", - // Button_Cycle_Variant = "BUTTON_CYCLE_VARIANT", - // Button_Speed_Up = "BUTTON_SPEED_UP", - // Button_Slow_Down = "BUTTON_SLOW_DOWN", + Button_Menu = "BUTTON_MENU", + Button_Stats = "BUTTON_STATS", + Button_Cycle_Shiny = "BUTTON_CYCLE_SHINY", + Button_Cycle_Form = "BUTTON_CYCLE_FORM", + Button_Cycle_Gender = "BUTTON_CYCLE_GENDER", + Button_Cycle_Ability = "BUTTON_CYCLE_ABILITY", + Button_Cycle_Nature = "BUTTON_CYCLE_NATURE", + Button_Cycle_Variant = "BUTTON_CYCLE_VARIANT", + Button_Speed_Up = "BUTTON_SPEED_UP", + Button_Slow_Down = "BUTTON_SLOW_DOWN", } export const settingGamepadOptions: SettingOptions = { @@ -29,16 +29,16 @@ export const settingGamepadOptions: SettingOptions = { [SettingGamepad.Swap_A_and_B]: ['Enabled', 'Disabled'], [SettingGamepad.Button_Action]: [`KEY ${Button.ACTION.toString()}`, 'Change'], [SettingGamepad.Button_Cancel]: [`KEY ${Button.CANCEL.toString()}`, 'Change'], - // [SettingGamepad.Button_Menu]: [`KEY ${Button.MENU.toString()}`, 'Change'], - // [SettingGamepad.Button_Stats]: [`KEY ${Button.STATS.toString()}`, 'Change'], - // [SettingGamepad.Button_Cycle_Shiny]: [`KEY ${Button.RB.toString()}`, 'Change'], - // [SettingGamepad.Button_Cycle_Form]: [`KEY ${Button.LB.toString()}`, 'Change'], - // [SettingGamepad.Button_Cycle_Gender]: [`KEY ${Button.CYCLE_GENDER.toString()}`, 'Change'], - // [SettingGamepad.Button_Cycle_Ability]: [`KEY ${Button.CYCLE_ABILITY.toString()}`, 'Change'], - // [SettingGamepad.Button_Cycle_Nature]: [`KEY ${Button.CYCLE_NATURE.toString()}`, 'Change'], - // [SettingGamepad.Button_Cycle_Variant]: [`KEY ${Button.CYCLE_VARIANT.toString()}`, 'Change'], - // [SettingGamepad.Button_Speed_Up]: [`KEY ${Button.SPEED_UP.toString()}`, 'Change'], - // [SettingGamepad.Button_Slow_Down]: [`KEY ${Button.SLOW_DOWN.toString()}`, 'Change'] + [SettingGamepad.Button_Menu]: [`KEY ${Button.MENU.toString()}`, 'Change'], + [SettingGamepad.Button_Stats]: [`KEY ${Button.STATS.toString()}`, 'Change'], + [SettingGamepad.Button_Cycle_Shiny]: [`KEY ${Button.CYCLE_SHINY.toString()}`, 'Change'], + [SettingGamepad.Button_Cycle_Form]: [`KEY ${Button.CYCLE_FORM.toString()}`, 'Change'], + [SettingGamepad.Button_Cycle_Gender]: [`KEY ${Button.CYCLE_GENDER.toString()}`, 'Change'], + [SettingGamepad.Button_Cycle_Ability]: [`KEY ${Button.CYCLE_ABILITY.toString()}`, 'Change'], + [SettingGamepad.Button_Cycle_Nature]: [`KEY ${Button.CYCLE_NATURE.toString()}`, 'Change'], + [SettingGamepad.Button_Cycle_Variant]: [`KEY ${Button.CYCLE_VARIANT.toString()}`, 'Change'], + [SettingGamepad.Button_Speed_Up]: [`KEY ${Button.SPEED_UP.toString()}`, 'Change'], + [SettingGamepad.Button_Slow_Down]: [`KEY ${Button.SLOW_DOWN.toString()}`, 'Change'] }; export const settingGamepadDefaults: SettingDefaults = { @@ -47,21 +47,31 @@ export const settingGamepadDefaults: SettingDefaults = { [SettingGamepad.Swap_A_and_B]: 1, // Set to 'Disabled' by default [SettingGamepad.Button_Action]: 0, [SettingGamepad.Button_Cancel]: 0, - // [SettingGamepad.Button_Menu]: Button.MENU, - // [SettingGamepad.Button_Stats]: Button.STATS, - // [SettingGamepad.Button_Cycle_Shiny]: Button.RB, - // [SettingGamepad.Button_Cycle_Form]: Button.LB, - // [SettingGamepad.Button_Cycle_Gender]: Button.CYCLE_GENDER, - // [SettingGamepad.Button_Cycle_Ability]: Button.CYCLE_ABILITY, - // [SettingGamepad.Button_Cycle_Nature]: Button.CYCLE_NATURE, - // [SettingGamepad.Button_Cycle_Variant]: Button.CYCLE_VARIANT, - // [SettingGamepad.Button_Speed_Up]: Button.SPEED_UP, - // [SettingGamepad.Button_Slow_Down]: Button.SLOW_DOWN, + [SettingGamepad.Button_Menu]: 0, + [SettingGamepad.Button_Stats]: 0, + [SettingGamepad.Button_Cycle_Shiny]: 0, + [SettingGamepad.Button_Cycle_Form]: 0, + [SettingGamepad.Button_Cycle_Gender]: 0, + [SettingGamepad.Button_Cycle_Ability]: 0, + [SettingGamepad.Button_Cycle_Nature]: 0, + [SettingGamepad.Button_Cycle_Variant]: 0, + [SettingGamepad.Button_Speed_Up]: 0, + [SettingGamepad.Button_Slow_Down]: 0, }; export const noOptionsCursors: Array = [ SettingGamepad.Button_Action, SettingGamepad.Button_Cancel, + SettingGamepad.Button_Menu, + SettingGamepad.Button_Stats, + SettingGamepad.Button_Cycle_Shiny, + SettingGamepad.Button_Cycle_Form, + SettingGamepad.Button_Cycle_Gender, + SettingGamepad.Button_Cycle_Ability, + SettingGamepad.Button_Cycle_Nature, + SettingGamepad.Button_Cycle_Variant, + SettingGamepad.Button_Speed_Up, + SettingGamepad.Button_Slow_Down, ]; export function setSettingGamepad(scene: BattleScene, setting: SettingGamepad, value: integer): boolean { @@ -76,16 +86,16 @@ export function setSettingGamepad(scene: BattleScene, setting: SettingGamepad, v break; case SettingGamepad.Button_Action: case SettingGamepad.Button_Cancel: - // case SettingGamepad.Button_Menu: - // case SettingGamepad.Button_Stats: - // case SettingGamepad.Button_Cycle_Shiny: - // case SettingGamepad.Button_Cycle_Form: - // case SettingGamepad.Button_Cycle_Gender: - // case SettingGamepad.Button_Cycle_Ability: - // case SettingGamepad.Button_Cycle_Nature: - // case SettingGamepad.Button_Cycle_Variant: - // case SettingGamepad.Button_Speed_Up: - // case SettingGamepad.Button_Slow_Down: + case SettingGamepad.Button_Menu: + case SettingGamepad.Button_Stats: + case SettingGamepad.Button_Cycle_Shiny: + case SettingGamepad.Button_Cycle_Form: + case SettingGamepad.Button_Cycle_Gender: + case SettingGamepad.Button_Cycle_Ability: + case SettingGamepad.Button_Cycle_Nature: + case SettingGamepad.Button_Cycle_Variant: + case SettingGamepad.Button_Speed_Up: + case SettingGamepad.Button_Slow_Down: if (value) { if (scene.ui) { const cancelHandler = () => { diff --git a/src/ui/settings-gamepad-ui-handler.ts b/src/ui/settings-gamepad-ui-handler.ts index d8de0dbe5..be1db4dc5 100644 --- a/src/ui/settings-gamepad-ui-handler.ts +++ b/src/ui/settings-gamepad-ui-handler.ts @@ -74,14 +74,20 @@ export default class SettingsGamepadUiHandler extends UiHandler { this.optionsContainer.add(this.settingLabels[s]); - this.optionValueLabels.push(settingGamepadOptions[SettingGamepad[setting]].map((option, o) => { + const valueLabels = [] + for (const [o, option] of settingGamepadOptions[SettingGamepad[setting]].entries()) { + if (noOptionsCursors.includes(SettingGamepad[setting])) { + // need to find a way to fetch icons and display, maybe a placeholder. + continue; + } const valueLabel = addTextObject(this.scene, 0, 0, option, settingGamepadDefaults[SettingGamepad[setting]] === o ? TextStyle.SETTINGS_SELECTED : TextStyle.WINDOW); valueLabel.setOrigin(0, 0); this.optionsContainer.add(valueLabel); - return valueLabel; - })); + valueLabels.push(valueLabel); + } + this.optionValueLabels.push(valueLabels); const totalWidth = this.optionValueLabels[s].map(o => o.width).reduce((total, width) => total += width, 0); From 931ec61f879b3b09cc9df2c7bf784105b1ed4465 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sat, 11 May 2024 12:45:45 +0200 Subject: [PATCH 23/81] commented gamepad fallback on disconnect --- src/inputs-controller.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 2dba78f36..663d31a71 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -257,18 +257,22 @@ export class InputsController { // If we don't do that, we have no way to determine if the gamepad is connected or not. // We want to know that because we want to hide it in the selection menu of gamepad to use this.disconnectedGamepads.push(thisGamepad.id); - // we look for gamepads still connected by substracting the 2 arrays - const gamepadsLeft = this.gamepads.filter(g => !this.disconnectedGamepads.includes(g.id)).map(g => g); + /** commented for now this code because i don't know anymore if it's good to do that + * for example i'm playing with a wireless gamepad that shutdown after 5 min + * i don't want my game to use my second controller when i turn back on my main gamepad + * we look for gamepads still connected by substracting the 2 arrays + */ + // const gamepadsLeft = this.gamepads.filter(g => !this.disconnectedGamepads.includes(g.id)).map(g => g); // we check if the chosen gamepad is still connected - const chosenIsConnected = gamepadsLeft.some(g => g.id === this.chosenGamepad); + // const chosenIsConnected = gamepadsLeft.some(g => g.id === this.chosenGamepad); // if the chosen gamepad is disconnected, and we got others gamepad connected - if (!chosenIsConnected && gamepadsLeft?.length) { + // if (!chosenIsConnected && gamepadsLeft?.length) { // We remove the previously chosen gamepad - this.clearChosenGamepad(); + // this.clearChosenGamepad(); // and we set the first of the gamepad still connected as the chosen one. - this.setChosenGamepad(gamepadsLeft[0].id); - return; - } + // this.setChosenGamepad(gamepadsLeft[0].id); + // return; + // } } onReconnect(thisGamepad: Phaser.Input.Gamepad.Gamepad): void { From cffcf9afd0563082276f6d423cf6564fe14aecb0 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sat, 11 May 2024 13:33:13 +0200 Subject: [PATCH 24/81] cleaned up a bit the code + added some more config --- src/configs/pad_dualshock.ts | 11 +++++++++++ src/configs/pad_generic.ts | 12 ++++++++++-- src/configs/pad_unlicensedSNES.ts | 7 ++++++- src/inputs-controller.ts | 18 +++++++++--------- src/system/settings-gamepad.ts | 6 +++++- 5 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/configs/pad_dualshock.ts b/src/configs/pad_dualshock.ts index 58fbf31b7..353027426 100644 --- a/src/configs/pad_dualshock.ts +++ b/src/configs/pad_dualshock.ts @@ -48,6 +48,17 @@ const pad_dualshock = { setting: { RC_S: SettingGamepad.Button_Action, RC_E: SettingGamepad.Button_Cancel, + RC_W: SettingGamepad.Button_Cycle_Nature, + RC_N: SettingGamepad.Button_Cycle_Variant, + START: SettingGamepad.Button_Menu, + SELECT: SettingGamepad.Button_Stats, + LB: SettingGamepad.Button_Cycle_Form, + RB: SettingGamepad.Button_Cycle_Shiny, + LT: SettingGamepad.Button_Cycle_Gender, + RT: SettingGamepad.Button_Cycle_Ability, + LS: SettingGamepad.Button_Speed_Up, + RS: SettingGamepad.Button_Slow_Down, + TOUCH: SettingGamepad.Button_Submit, }, default: { RC_S: Button.ACTION, diff --git a/src/configs/pad_generic.ts b/src/configs/pad_generic.ts index 9a7751d25..f64d6c342 100644 --- a/src/configs/pad_generic.ts +++ b/src/configs/pad_generic.ts @@ -42,11 +42,20 @@ const pad_generic = { LC_S: "T_X_Dpad_Down_Alt.png", LC_W: "T_X_Dpad_Left_Alt.png", LC_E: "T_X_Dpad_Right_Alt.png", - MENU: "" }, setting: { RC_S: SettingGamepad.Button_Action, RC_E: SettingGamepad.Button_Cancel, + RC_W: SettingGamepad.Button_Cycle_Nature, + RC_N: SettingGamepad.Button_Cycle_Variant, + START: SettingGamepad.Button_Menu, + SELECT: SettingGamepad.Button_Stats, + LB: SettingGamepad.Button_Cycle_Form, + RB: SettingGamepad.Button_Cycle_Shiny, + LT: SettingGamepad.Button_Cycle_Gender, + RT: SettingGamepad.Button_Cycle_Ability, + LS: SettingGamepad.Button_Speed_Up, + RS: SettingGamepad.Button_Slow_Down, }, default: { RC_S: Button.ACTION, @@ -65,7 +74,6 @@ const pad_generic = { LC_S: Button.DOWN, LC_W: Button.LEFT, LC_E: Button.RIGHT, - TOUCH: Button.SUBMIT, } }; diff --git a/src/configs/pad_unlicensedSNES.ts b/src/configs/pad_unlicensedSNES.ts index ca58d7649..8321c364e 100644 --- a/src/configs/pad_unlicensedSNES.ts +++ b/src/configs/pad_unlicensedSNES.ts @@ -38,6 +38,12 @@ const pad_unlicensedSNES = { setting: { RC_S: SettingGamepad.Button_Action, RC_E: SettingGamepad.Button_Cancel, + RC_W: SettingGamepad.Button_Cycle_Nature, + RC_N: SettingGamepad.Button_Cycle_Variant, + START: SettingGamepad.Button_Menu, + SELECT: SettingGamepad.Button_Stats, + LB: SettingGamepad.Button_Cycle_Form, + RB: SettingGamepad.Button_Cycle_Shiny, }, default: { RC_S: Button.ACTION, @@ -52,7 +58,6 @@ const pad_unlicensedSNES = { LC_S: Button.DOWN, LC_W: Button.LEFT, LC_E: Button.RIGHT, - TOUCH: Button.SUBMIT, } }; diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 663d31a71..0ae3a592b 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -1,4 +1,3 @@ -import Phaser, {Time} from "phaser"; import * as Utils from "./utils"; import {initTouchControls} from './touch-controls'; import pad_generic from "./configs/pad_generic"; @@ -65,13 +64,14 @@ const repeatInputDelayMillis = 250; */ export class InputsController { private buttonKeys: Phaser.Input.Keyboard.Key[][]; - private gamepads: Array = new Array(); + private gamepads: Array = new Array(); private scene: Phaser.Scene; + private events: Phaser.Events.EventEmitter; private buttonLock: Button; private buttonLock2: Button; private interactions: Map> = new Map(); - private time: Time; + private time: Phaser.Time.Clock; private player; private gamepadSupport: boolean = true; @@ -118,7 +118,7 @@ export class InputsController { * Additionally, it manages the game's behavior when it loses focus to prevent unwanted game actions during this state. */ init(): void { - this.events = new Phaser.Events.EventEmitter(); + this.events = this.scene.game.events; if (localStorage.hasOwnProperty('chosenGamepad')) { this.chosenGamepad = localStorage.getItem('chosenGamepad'); @@ -206,16 +206,16 @@ export class InputsController { for (const b of Utils.getEnumValues(Button).reverse()) { if ( this.interactions.hasOwnProperty(b) && - this.repeatInputDurationJustPassed(b) && + this.repeatInputDurationJustPassed(b as Button) && this.interactions[b].isPressed ) { // Prevents repeating button interactions when gamepad support is disabled. if ( (!this.gamepadSupport && this.interactions[b].source === 'gamepad') || - (this.interactions[b].sourceName !== null && this.interactions[b].sourceName !== this.chosenGamepad) + (this.interactions[b].sourceName && this.interactions[b].sourceName !== this.chosenGamepad) ) { // Deletes the last interaction for a button if gamepad is disabled. - this.delLastProcessedMovementTime(b); + this.delLastProcessedMovementTime(b as Button); return; } // Emits an event for the button press. @@ -223,7 +223,7 @@ export class InputsController { controller_type: this.interactions[b].source, button: b, }); - this.setLastProcessedMovementTime(b, this.interactions[b].source, this.interactions[b].sourceName); + this.setLastProcessedMovementTime(b as Button, this.interactions[b].source, this.interactions[b].sourceName); } } } @@ -572,7 +572,7 @@ export class InputsController { * * Additionally, this method locks the button (by calling `setButtonLock`) to prevent it from being re-processed until it is released, ensuring that each press is handled distinctly. */ - setLastProcessedMovementTime(button: Button, source: String = 'keyboard', sourceName: String): void { + setLastProcessedMovementTime(button: Button, source: String = 'keyboard', sourceName?: String): void { if (!this.interactions.hasOwnProperty(button)) return; this.setButtonLock(button); this.interactions[button].pressTime = this.time.now; diff --git a/src/system/settings-gamepad.ts b/src/system/settings-gamepad.ts index 5922fee7b..ea71d0e12 100644 --- a/src/system/settings-gamepad.ts +++ b/src/system/settings-gamepad.ts @@ -21,6 +21,7 @@ export enum SettingGamepad { Button_Cycle_Variant = "BUTTON_CYCLE_VARIANT", Button_Speed_Up = "BUTTON_SPEED_UP", Button_Slow_Down = "BUTTON_SLOW_DOWN", + Button_Submit = "BUTTON_SUBMIT", } export const settingGamepadOptions: SettingOptions = { @@ -38,7 +39,8 @@ export const settingGamepadOptions: SettingOptions = { [SettingGamepad.Button_Cycle_Nature]: [`KEY ${Button.CYCLE_NATURE.toString()}`, 'Change'], [SettingGamepad.Button_Cycle_Variant]: [`KEY ${Button.CYCLE_VARIANT.toString()}`, 'Change'], [SettingGamepad.Button_Speed_Up]: [`KEY ${Button.SPEED_UP.toString()}`, 'Change'], - [SettingGamepad.Button_Slow_Down]: [`KEY ${Button.SLOW_DOWN.toString()}`, 'Change'] + [SettingGamepad.Button_Slow_Down]: [`KEY ${Button.SLOW_DOWN.toString()}`, 'Change'], + [SettingGamepad.Button_Submit]: [`KEY ${Button.SUBMIT.toString()}`, 'Change'] }; export const settingGamepadDefaults: SettingDefaults = { @@ -57,6 +59,7 @@ export const settingGamepadDefaults: SettingDefaults = { [SettingGamepad.Button_Cycle_Variant]: 0, [SettingGamepad.Button_Speed_Up]: 0, [SettingGamepad.Button_Slow_Down]: 0, + [SettingGamepad.Button_Submit]: 0, }; export const noOptionsCursors: Array = [ @@ -72,6 +75,7 @@ export const noOptionsCursors: Array = [ SettingGamepad.Button_Cycle_Variant, SettingGamepad.Button_Speed_Up, SettingGamepad.Button_Slow_Down, + SettingGamepad.Button_Submit, ]; export function setSettingGamepad(scene: BattleScene, setting: SettingGamepad, value: integer): boolean { From 7ced12e13b8b0754e4e5d7ba46ba3992f66ca376 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sat, 11 May 2024 14:31:48 +0200 Subject: [PATCH 25/81] refactored input-controller to use config --- src/configs/gamepad-utils.ts | 15 ++++++++ src/inputs-controller.ts | 66 +++++++----------------------------- 2 files changed, 27 insertions(+), 54 deletions(-) create mode 100644 src/configs/gamepad-utils.ts diff --git a/src/configs/gamepad-utils.ts b/src/configs/gamepad-utils.ts new file mode 100644 index 000000000..6bf99f28a --- /dev/null +++ b/src/configs/gamepad-utils.ts @@ -0,0 +1,15 @@ +import {GamepadConfig} from "../inputs-controller"; + + +export function getKeyForButtonIndex(config: GamepadConfig, index: integer): String { + for (const key of Object.keys(config.gamepadMapping)) { + const id = config.gamepadMapping[key]; + if (id === index) return key; + } + return null; +} + +export function getIconForCustomIndex(config: GamepadConfig, index: integer): String { + const key = getKeyForButtonIndex(config, index); + return config.icons[key]; +} \ No newline at end of file diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 0ae3a592b..df7320984 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -8,6 +8,7 @@ import {Button} from "./enums/buttons"; import {Mode} from "./ui/ui"; import SettingsGamepadUiHandler from "./ui/settings-gamepad-ui-handler"; import {SettingGamepad} from "./system/settings-gamepad"; +import {getIconForCustomIndex, getKeyForButtonIndex} from "#app/configs/gamepad-utils"; export interface GamepadMapping { [key: string]: number; @@ -72,7 +73,7 @@ export class InputsController { private buttonLock2: Button; private interactions: Map> = new Map(); private time: Phaser.Time.Clock; - private player; + private configs: Map = new Map(); private gamepadSupport: boolean = true; @@ -94,7 +95,6 @@ export class InputsController { this.scene = scene; this.time = this.scene.time; this.buttonKeys = []; - this.player = {}; for (const b of Utils.getEnumValues(Button)) { this.interactions[b] = { @@ -295,15 +295,11 @@ export class InputsController { // we fetch all the gamepads name const allGamepads = this.getGamepadsName(); for (const gamepad of allGamepads) { - // for each gamepad, we set its mapping in this.player + // for each gamepad, we set its mapping in this.configs const gamepadID = gamepad.toLowerCase(); - const mappedPad = this.mapGamepad(gamepadID); - if (!this.player[gamepad]) this.player[gamepad] = {}; - this.player[gamepad]['mapping'] = mappedPad.gamepadMapping; - this.player[gamepad]['custom'] = mappedPad.gamepadMapping; - this.player[gamepad]['icons'] = mappedPad.icons; - this.player[gamepad]['type'] = mappedPad.padType; - this.player[gamepad]['default'] = mappedPad.default; + const config = this.getConfig(gamepadID); + this.configs[gamepad] = config; + this.configs[gamepad].custom = {...config.default}; } if (this.chosenGamepad === thisGamepad.id) this.initChosenGamepad(this.chosenGamepad) } @@ -327,46 +323,8 @@ export class InputsController { } } - /** - * Retrieves the current gamepad mapping for in-game actions. - * - * @returns An object mapping gamepad buttons to in-game actions based on the player's current gamepad configuration. - * - * @remarks - * This method constructs a mapping of gamepad buttons to in-game action buttons according to the player's - * current gamepad configuration. If no configuration is available, it returns an empty mapping. - * The mapping includes directional controls, action buttons, and system commands among others, - * adjusted for any custom settings such as swapped action buttons. - */ - getActionGamepadMapping(): ActionGamepadMapping { - const gamepadMapping = {}; - if (!this.player[this.chosenGamepad] || !this.player[this.chosenGamepad]?.mapping || !this.chosenGamepad) return gamepadMapping; - gamepadMapping[this.player[this.chosenGamepad].mapping.LC_N] = Button.UP; - gamepadMapping[this.player[this.chosenGamepad].mapping.LC_S] = Button.DOWN; - gamepadMapping[this.player[this.chosenGamepad].mapping.LC_W] = Button.LEFT; - gamepadMapping[this.player[this.chosenGamepad].mapping.LC_E] = Button.RIGHT; - gamepadMapping[this.player[this.chosenGamepad].mapping.TOUCH] = Button.SUBMIT; - gamepadMapping[this.player[this.chosenGamepad].mapping.RC_S] = this.scene.abSwapped ? Button.CANCEL : Button.ACTION; - gamepadMapping[this.player[this.chosenGamepad].mapping.RC_E] = this.scene.abSwapped ? Button.ACTION : Button.CANCEL; - gamepadMapping[this.player[this.chosenGamepad].mapping.SELECT] = Button.STATS; - gamepadMapping[this.player[this.chosenGamepad].mapping.START] = Button.MENU; - gamepadMapping[this.player[this.chosenGamepad].mapping.RB] = Button.CYCLE_SHINY; - gamepadMapping[this.player[this.chosenGamepad].mapping.LB] = Button.CYCLE_FORM; - gamepadMapping[this.player[this.chosenGamepad].mapping.LT] = Button.CYCLE_GENDER; - gamepadMapping[this.player[this.chosenGamepad].mapping.RT] = Button.CYCLE_ABILITY; - gamepadMapping[this.player[this.chosenGamepad].mapping.RC_W] = Button.CYCLE_NATURE; - gamepadMapping[this.player[this.chosenGamepad].mapping.RC_N] = Button.CYCLE_VARIANT; - gamepadMapping[this.player[this.chosenGamepad].mapping.LS] = Button.SPEED_UP; - gamepadMapping[this.player[this.chosenGamepad].mapping.RS] = Button.SLOW_DOWN; - - return gamepadMapping; - } - getButtonLabel(button: Phaser.Input.Gamepad.Button) { - const icons = this.player[this.chosenGamepad]['icons']; - const mapping = this.player[this.chosenGamepad]['mapping']; - const key = Object.keys(mapping).find(key => mapping[key] === button.index); - return [this.player[this.chosenGamepad]['type'], icons[key]]; + return getIconForCustomIndex(this.configs[this.chosenGamepad], button.index); } /** @@ -387,8 +345,8 @@ export class InputsController { if (!this.chosenGamepad) // at the very first input, if we have not yet a chosen gamepad, we set it this.setChosenGamepad(pad.id); if (!this.gamepadSupport || pad.id.toLowerCase() !== this.chosenGamepad.toLowerCase()) return; - const actionMapping = this.getActionGamepadMapping(); - const buttonDown = actionMapping.hasOwnProperty(button.index) && actionMapping[button.index]; + const key = getKeyForButtonIndex(this.configs[pad.id], button.index); + const buttonDown = this.configs[pad.id].custom[key]; if (buttonDown !== undefined) { this.events.emit('input_down', { controller_type: 'gamepad', @@ -414,8 +372,8 @@ export class InputsController { gamepadButtonUp(pad: Phaser.Input.Gamepad.Gamepad, button: Phaser.Input.Gamepad.Button, value: number): void { if (!pad) return; if (!this.gamepadSupport || pad.id !== this.chosenGamepad) return; - const actionMapping = this.getActionGamepadMapping(); - const buttonUp = actionMapping.hasOwnProperty(button.index) && actionMapping[button.index]; + const key = getKeyForButtonIndex(this.configs[pad.id], button.index); + const buttonUp = this.configs[pad.id]?.custom[key]; if (buttonUp !== undefined) { this.events.emit('input_up', { controller_type: 'gamepad', @@ -531,7 +489,7 @@ export class InputsController { * - If the ID contains '054c', it is identified as a DualShock gamepad. * If no specific identifiers are recognized, a generic gamepad configuration is returned. */ - mapGamepad(id: string): GamepadConfig { + getConfig(id: string): GamepadConfig { id = id.toLowerCase(); if (id.includes('081f') && id.includes('e401')) { From cfa919b46c19d737727bc9edc07e32968ea8cf94 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sat, 11 May 2024 14:51:01 +0200 Subject: [PATCH 26/81] added already assigned key in the UI --- src/configs/gamepad-utils.ts | 9 +++++++++ src/inputs-controller.ts | 4 ++++ src/ui/settings-gamepad-ui-handler.ts | 21 ++++++++++++++++++++- 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/configs/gamepad-utils.ts b/src/configs/gamepad-utils.ts index 6bf99f28a..d8be247ef 100644 --- a/src/configs/gamepad-utils.ts +++ b/src/configs/gamepad-utils.ts @@ -1,4 +1,5 @@ import {GamepadConfig} from "../inputs-controller"; +import {SettingGamepad} from "#app/system/settings-gamepad"; export function getKeyForButtonIndex(config: GamepadConfig, index: integer): String { @@ -12,4 +13,12 @@ export function getKeyForButtonIndex(config: GamepadConfig, index: integer): Str export function getIconForCustomIndex(config: GamepadConfig, index: integer): String { const key = getKeyForButtonIndex(config, index); return config.icons[key]; +} + +export function getKeyForSettingName(config: GamepadConfig, settingName: SettingGamepad) { + for (const key of Object.keys(config.setting)) { + const name = config.setting[key]; + if (name === settingName) return key; + } + return null; } \ No newline at end of file diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index df7320984..2568f49ee 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -644,4 +644,8 @@ export class InputsController { setBind(setting: SettingGamepad, button: Button) { console.log('button,', button); } + + getActiveConfig() :GamepadConfig { + return this.configs[this.chosenGamepad] || pad_generic; + } } \ No newline at end of file diff --git a/src/ui/settings-gamepad-ui-handler.ts b/src/ui/settings-gamepad-ui-handler.ts index be1db4dc5..d14196db2 100644 --- a/src/ui/settings-gamepad-ui-handler.ts +++ b/src/ui/settings-gamepad-ui-handler.ts @@ -12,6 +12,7 @@ import { settingGamepadOptions } from "../system/settings-gamepad"; import {truncateString} from "../utils"; +import {getKeyForSettingName} from "#app/configs/gamepad-utils"; export default class SettingsGamepadUiHandler extends UiHandler { private settingsContainer: Phaser.GameObjects.Container; @@ -32,6 +33,8 @@ export default class SettingsGamepadUiHandler extends UiHandler { private reloadI18n: boolean; private gamepads: Array; + private inputsIcons; + constructor(scene: BattleScene, mode?: Mode) { super(scene, mode); @@ -65,6 +68,7 @@ export default class SettingsGamepadUiHandler extends UiHandler { this.settingLabels = []; this.optionValueLabels = []; + this.inputsIcons = {}; Object.keys(SettingGamepad).forEach((setting, s) => { let settingName = setting.replace(/\_/g, ' '); @@ -77,7 +81,22 @@ export default class SettingsGamepadUiHandler extends UiHandler { const valueLabels = [] for (const [o, option] of settingGamepadOptions[SettingGamepad[setting]].entries()) { if (noOptionsCursors.includes(SettingGamepad[setting])) { - // need to find a way to fetch icons and display, maybe a placeholder. + if (o) { + const valueLabel = addTextObject(this.scene, 0, 0, option, settingGamepadDefaults[SettingGamepad[setting]] === o ? TextStyle.SETTINGS_SELECTED : TextStyle.WINDOW); + valueLabel.setOrigin(0, 0); + this.optionsContainer.add(valueLabel); + valueLabels.push(valueLabel); + continue; + } + const key = getKeyForSettingName(this.scene.inputController.getActiveConfig(), SettingGamepad[setting]); + const frame = this.scene.inputController.getActiveConfig().icons[key]; + const icon = this.scene.add.sprite(0, 0, 'xbox'); + icon.setScale(0.1); + icon.setOrigin(0, 0); + icon.setFrame(frame); + this.inputsIcons[key] = icon; + this.optionsContainer.add(icon); + valueLabels.push(icon); continue; } const valueLabel = addTextObject(this.scene, 0, 0, option, settingGamepadDefaults[SettingGamepad[setting]] === o ? TextStyle.SETTINGS_SELECTED : TextStyle.WINDOW); From 5ff09389db5a5bf5ab047acb7f5ca4b70041c25f Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sat, 11 May 2024 15:43:38 +0200 Subject: [PATCH 27/81] swap is working + display current assignation on rebinding --- src/configs/gamepad-utils.ts | 12 +++++++--- src/inputs-controller.ts | 32 ++++++++++++++++++++++----- src/system/settings-gamepad.ts | 5 +++-- src/ui/gamepad-binding-ui-handler.ts | 11 +++++++-- src/ui/settings-gamepad-ui-handler.ts | 6 +++++ 5 files changed, 54 insertions(+), 12 deletions(-) diff --git a/src/configs/gamepad-utils.ts b/src/configs/gamepad-utils.ts index d8be247ef..df31ed827 100644 --- a/src/configs/gamepad-utils.ts +++ b/src/configs/gamepad-utils.ts @@ -2,15 +2,21 @@ import {GamepadConfig} from "../inputs-controller"; import {SettingGamepad} from "#app/system/settings-gamepad"; -export function getKeyForButtonIndex(config: GamepadConfig, index: integer): String { +export function getKeyForButtonIndex(config: GamepadConfig, index: number): String { for (const key of Object.keys(config.gamepadMapping)) { const id = config.gamepadMapping[key]; if (id === index) return key; } return null; } +export function getButtonIndexForKey(config: GamepadConfig, _key: string): number { + for (const key of Object.keys(config.gamepadMapping)) { + if (key === _key) return config.gamepadMapping[key]; + } + return null; +} -export function getIconForCustomIndex(config: GamepadConfig, index: integer): String { +export function getIconForCustomIndex(config: GamepadConfig, index: number): String { const key = getKeyForButtonIndex(config, index); return config.icons[key]; } @@ -21,4 +27,4 @@ export function getKeyForSettingName(config: GamepadConfig, settingName: Setting if (name === settingName) return key; } return null; -} \ No newline at end of file +} diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 2568f49ee..f4e0e8056 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -8,7 +8,12 @@ import {Button} from "./enums/buttons"; import {Mode} from "./ui/ui"; import SettingsGamepadUiHandler from "./ui/settings-gamepad-ui-handler"; import {SettingGamepad} from "./system/settings-gamepad"; -import {getIconForCustomIndex, getKeyForButtonIndex} from "#app/configs/gamepad-utils"; +import { + getButtonIndexForKey, + getIconForCustomIndex, + getKeyForButtonIndex, + getKeyForSettingName +} from "./configs/gamepad-utils"; export interface GamepadMapping { [key: string]: number; @@ -33,6 +38,7 @@ export interface GamepadConfig { icons: IconsMapping; setting: SettingMapping; default: DefaultMapping; + custom: DefaultMapping; } export interface ActionGamepadMapping { @@ -323,10 +329,6 @@ export class InputsController { } } - getButtonLabel(button: Phaser.Input.Gamepad.Button) { - return getIconForCustomIndex(this.configs[this.chosenGamepad], button.index); - } - /** * Handles the 'down' event for gamepad buttons, emitting appropriate events and updating the interaction state. * @@ -648,4 +650,24 @@ export class InputsController { getActiveConfig() :GamepadConfig { return this.configs[this.chosenGamepad] || pad_generic; } + + getPressedButtonLabel(button: Phaser.Input.Gamepad.Button) { + return [this.configs[this.chosenGamepad].padType, getIconForCustomIndex(this.configs[this.chosenGamepad], button.index)]; + } + + getCurrentButtonLabel(target: SettingGamepad) { + const key = getKeyForSettingName(this.configs[this.chosenGamepad], target); + const id = getButtonIndexForKey(this.configs[this.chosenGamepad], key); + return getIconForCustomIndex(this.configs[this.chosenGamepad], id); + } + + swapBinding(target, newBinding) { + this.deactivatePressedKey(); + const keyTarget = getKeyForSettingName(this.configs[this.chosenGamepad], target); + const keyNewBinding = getKeyForButtonIndex(this.configs[this.chosenGamepad], newBinding); + const previousActionForThisNewBinding = this.configs[this.chosenGamepad].custom[keyNewBinding]; + const ActionForThisNewBinding = this.configs[this.chosenGamepad].custom[keyTarget]; + this.configs[this.chosenGamepad].custom[keyTarget] = previousActionForThisNewBinding; + this.configs[this.chosenGamepad].custom[keyNewBinding] = ActionForThisNewBinding; + } } \ No newline at end of file diff --git a/src/system/settings-gamepad.ts b/src/system/settings-gamepad.ts index ea71d0e12..9d987e3df 100644 --- a/src/system/settings-gamepad.ts +++ b/src/system/settings-gamepad.ts @@ -102,9 +102,10 @@ export function setSettingGamepad(scene: BattleScene, setting: SettingGamepad, v case SettingGamepad.Button_Slow_Down: if (value) { if (scene.ui) { - const cancelHandler = () => { + const cancelHandler = (success: boolean = false) => { scene.ui.revertMode(); - return false; + (scene.ui.getHandler() as SettingsGamepadUiHandler).updateBindings(); + return success; }; scene.ui.setOverlayMode(Mode.GAMEPAD_BINDING, { target: setting, diff --git a/src/ui/gamepad-binding-ui-handler.ts b/src/ui/gamepad-binding-ui-handler.ts index fa4357798..1d34c5285 100644 --- a/src/ui/gamepad-binding-ui-handler.ts +++ b/src/ui/gamepad-binding-ui-handler.ts @@ -5,6 +5,7 @@ import {Button} from "../enums/buttons"; import {addWindow} from "./ui-theme"; import {addTextObject, TextStyle} from "#app/ui/text"; import Phaser from "phaser"; +import {SettingGamepad} from "../system/settings-gamepad"; export default class GamepadBindingUiHandler extends UiHandler { @@ -24,7 +25,7 @@ export default class GamepadBindingUiHandler extends UiHandler { private previousIconXbox: Phaser.GameObjects.Sprite; private iconDualshock: Phaser.GameObjects.Sprite; private cancelFn; - private target; + private target: SettingGamepad; constructor(scene: BattleScene, mode: Mode = Mode.GAMEPAD_BINDING) { super(scene, mode); @@ -104,7 +105,8 @@ export default class GamepadBindingUiHandler extends UiHandler { const blacklist = [12, 13, 14, 15]; if (!this.listening || pad.id !== this.scene.inputController?.chosenGamepad || blacklist.includes(button.index) || this.buttonPressed !== null) return; this.buttonPressed = button.index; - const [type, buttonIcon] = this.scene.inputController.getButtonLabel(button); + const [type, buttonIcon] = this.scene.inputController.getPressedButtonLabel(button); + const assignedButtonIcon = this.scene.inputController.getCurrentButtonLabel(this.target); switch (type) { case 'dualshock': this.iconXbox.setVisible(false); @@ -118,6 +120,7 @@ export default class GamepadBindingUiHandler extends UiHandler { this.iconDualshock.setVisible(false); this.iconXbox.setFrame(buttonIcon); this.iconXbox.setVisible(true); + this.previousIconXbox.setFrame(assignedButtonIcon); this.previousIconXbox.setVisible(true); this.alreadyAssignedText.setVisible(true); break @@ -164,6 +167,10 @@ export default class GamepadBindingUiHandler extends UiHandler { // Reverts UI to its previous state on cancel. // this.scene.ui.revertMode(); this.cancelFn(); + } else { + success = true; + this.scene.inputController.swapBinding(this.target, this.buttonPressed); + this.cancelFn(success); } break; } diff --git a/src/ui/settings-gamepad-ui-handler.ts b/src/ui/settings-gamepad-ui-handler.ts index d14196db2..b0be6b5b2 100644 --- a/src/ui/settings-gamepad-ui-handler.ts +++ b/src/ui/settings-gamepad-ui-handler.ts @@ -139,6 +139,12 @@ export default class SettingsGamepadUiHandler extends UiHandler { this.settingsContainer.setVisible(false); } + updateBindings(): void { + // for (const elm of noOptionsCursors) { + // console.log('elm:', elm); + // } + } + show(args: any[]): boolean { super.show(args); From e5e100990e9c6b658ce02c54c6b116b5a05260cd Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sat, 11 May 2024 17:54:34 +0200 Subject: [PATCH 28/81] key displayed are correctly updated when swap or controller change + custom mapping is effective everywhere --- src/configs/gamepad-utils.ts | 20 ++++++++++++++ src/configs/pad_xbox360.ts | 8 +++--- src/inputs-controller.ts | 40 ++++++++++++++++----------- src/system/game-data.ts | 21 ++++++++++++++ src/system/settings-gamepad.ts | 1 + src/ui/settings-gamepad-ui-handler.ts | 14 ++++++---- 6 files changed, 78 insertions(+), 26 deletions(-) diff --git a/src/configs/gamepad-utils.ts b/src/configs/gamepad-utils.ts index df31ed827..946db4d4e 100644 --- a/src/configs/gamepad-utils.ts +++ b/src/configs/gamepad-utils.ts @@ -1,5 +1,6 @@ import {GamepadConfig} from "../inputs-controller"; import {SettingGamepad} from "#app/system/settings-gamepad"; +import {Button} from "#app/enums/buttons"; export function getKeyForButtonIndex(config: GamepadConfig, index: number): String { @@ -21,6 +22,25 @@ export function getIconForCustomIndex(config: GamepadConfig, index: number): Str return config.icons[key]; } +export function getKeyForRebindedAction(config: GamepadConfig, action: Button): String { + for (const key of Object.keys(config.default)) { + if (config.default[key] === action) return key; + } + return null; +} + +export function getKeyForRebindedSettingName(config: GamepadConfig, settingName: SettingGamepad): String { + const oldKey = getKeyForSettingName(config, settingName) + const action = config.custom[oldKey]; + return getKeyForRebindedAction(config, action); +} + +export function getIconForRebindedKey(config: GamepadConfig, _key): String { + const action = config.custom[_key]; + const key = getKeyForRebindedAction(config, action); + return config.icons[key]; +} + export function getKeyForSettingName(config: GamepadConfig, settingName: SettingGamepad) { for (const key of Object.keys(config.setting)) { const name = config.setting[key]; diff --git a/src/configs/pad_xbox360.ts b/src/configs/pad_xbox360.ts index 28c379041..189c2a349 100644 --- a/src/configs/pad_xbox360.ts +++ b/src/configs/pad_xbox360.ts @@ -58,12 +58,12 @@ const pad_xbox360 = { RS: SettingGamepad.Button_Slow_Down, }, default: { - RC_S: Button.ACTION, + RC_S: Button.ACTION, //5 RC_E: Button.CANCEL, RC_W: Button.CYCLE_NATURE, - RC_N: Button.CYCLE_VARIANT, - START: Button.MENU, - SELECT: Button.STATS, + RC_N: Button.CYCLE_VARIANT, //14 + START: Button.MENU, //7 + SELECT: Button.STATS, //8 LB: Button.CYCLE_FORM, RB: Button.CYCLE_SHINY, LT: Button.CYCLE_GENDER, diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index f4e0e8056..f56f3e77f 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -10,8 +10,8 @@ import SettingsGamepadUiHandler from "./ui/settings-gamepad-ui-handler"; import {SettingGamepad} from "./system/settings-gamepad"; import { getButtonIndexForKey, - getIconForCustomIndex, - getKeyForButtonIndex, + getIconForCustomIndex, getIconForRebindedKey, + getKeyForButtonIndex, getKeyForRebindedSettingName, getKeyForSettingName } from "./configs/gamepad-utils"; @@ -27,7 +27,7 @@ export interface SettingMapping { [key: string]: string; } -export interface DefaultMapping { +export interface MappingLayout { [key: string]: Button; } @@ -37,8 +37,8 @@ export interface GamepadConfig { gamepadMapping: GamepadMapping; icons: IconsMapping; setting: SettingMapping; - default: DefaultMapping; - custom: DefaultMapping; + default: MappingLayout; + custom: MappingLayout; } export interface ActionGamepadMapping { @@ -86,6 +86,8 @@ export class InputsController { public chosenGamepad: String; private disconnectedGamepads: Array = new Array(); + private pauseUpdate: boolean = false; + /** * Initializes a new instance of the game control system, setting up initial state and configurations. * @@ -218,7 +220,8 @@ export class InputsController { // Prevents repeating button interactions when gamepad support is disabled. if ( (!this.gamepadSupport && this.interactions[b].source === 'gamepad') || - (this.interactions[b].sourceName && this.interactions[b].sourceName !== this.chosenGamepad) + (this.interactions[b].sourceName && this.interactions[b].sourceName !== this.chosenGamepad) || + this.pauseUpdate ) { // Deletes the last interaction for a button if gamepad is disabled. this.delLastProcessedMovementTime(b as Button); @@ -304,8 +307,8 @@ export class InputsController { // for each gamepad, we set its mapping in this.configs const gamepadID = gamepad.toLowerCase(); const config = this.getConfig(gamepadID); + config.custom = this.configs[gamepad]?.custom || config.default; this.configs[gamepad] = config; - this.configs[gamepad].custom = {...config.default}; } if (this.chosenGamepad === thisGamepad.id) this.initChosenGamepad(this.chosenGamepad) } @@ -583,6 +586,7 @@ export class InputsController { * This method is typically called when needing to ensure that all inputs are neutralized. */ deactivatePressedKey(): void { + this.pauseUpdate = true; this.releaseButtonLock(this.buttonLock); this.releaseButtonLock(this.buttonLock2); for (const b of Utils.getEnumValues(Button)) { @@ -593,6 +597,7 @@ export class InputsController { this.interactions[b].sourceName = null; } } + setTimeout(() => this.pauseUpdate = false, 500); } /** @@ -643,12 +648,9 @@ export class InputsController { else if (this.buttonLock2 === button) this.buttonLock2 = null; } - setBind(setting: SettingGamepad, button: Button) { - console.log('button,', button); - } - getActiveConfig() :GamepadConfig { - return this.configs[this.chosenGamepad] || pad_generic; + if (this.configs[this.chosenGamepad]?.padID) return this.configs[this.chosenGamepad] + return pad_generic as GamepadConfig; } getPressedButtonLabel(button: Phaser.Input.Gamepad.Button) { @@ -657,17 +659,23 @@ export class InputsController { getCurrentButtonLabel(target: SettingGamepad) { const key = getKeyForSettingName(this.configs[this.chosenGamepad], target); - const id = getButtonIndexForKey(this.configs[this.chosenGamepad], key); - return getIconForCustomIndex(this.configs[this.chosenGamepad], id); + return getIconForRebindedKey(this.configs[this.chosenGamepad], key); } swapBinding(target, newBinding) { - this.deactivatePressedKey(); - const keyTarget = getKeyForSettingName(this.configs[this.chosenGamepad], target); + this.pauseUpdate = true; + const keyTarget = getKeyForRebindedSettingName(this.configs[this.chosenGamepad], target) const keyNewBinding = getKeyForButtonIndex(this.configs[this.chosenGamepad], newBinding); const previousActionForThisNewBinding = this.configs[this.chosenGamepad].custom[keyNewBinding]; const ActionForThisNewBinding = this.configs[this.chosenGamepad].custom[keyTarget]; this.configs[this.chosenGamepad].custom[keyTarget] = previousActionForThisNewBinding; this.configs[this.chosenGamepad].custom[keyNewBinding] = ActionForThisNewBinding; + this.scene.gameData.saveCustomMapping(this.chosenGamepad, this.configs[this.chosenGamepad].custom); + setTimeout(() => this.pauseUpdate = false, 500); + } + + loadConfig(gamepadName: String, customMappings: MappingLayout): void { + if (!this.configs[gamepadName]) this.configs[gamepadName] = {}; + this.configs[gamepadName].custom = customMappings; } } \ No newline at end of file diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 37f506ad3..a131cd318 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -30,6 +30,7 @@ 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 {MappingLayout} from "#app/inputs-controller"; const saveKey = 'x0i2O7WRiANTqPmZ'; // Temporary; secure encryption is not yet necessary @@ -227,6 +228,7 @@ export class GameData { this.scene = scene; this.loadSettings(); this.loadGamepadSettings(); + this.loadCustomMapping(); this.trainerId = Utils.randInt(65536); this.secretId = Utils.randInt(65536); this.starterData = {}; @@ -482,6 +484,25 @@ export class GameData { return true; } + public saveCustomMapping(gamepadName: string, mapping: MappingLayout): boolean { + let customMappings: object = {}; + if (localStorage.hasOwnProperty('customMapping')) + customMappings = JSON.parse(localStorage.getItem('customMapping')); + customMappings[gamepadName] = mapping; + localStorage.setItem('customMapping', JSON.stringify(customMappings)); + return true; + } + + public loadCustomMapping(): boolean { + console.log('loadCustomMapping'); + if (!localStorage.hasOwnProperty('customMapping')) + return false; + const customMappings = JSON.parse(localStorage.getItem('customMapping')); + for (const key of Object.keys(customMappings)) + this.scene.inputController.loadConfig(key, customMappings[key]); + + } + public saveGamepadSetting(setting: SettingGamepad, valueIndex: integer): boolean { let settingsGamepad: object = {}; if (localStorage.hasOwnProperty('settingsGamepad')) diff --git a/src/system/settings-gamepad.ts b/src/system/settings-gamepad.ts index 9d987e3df..8b5f28e87 100644 --- a/src/system/settings-gamepad.ts +++ b/src/system/settings-gamepad.ts @@ -121,6 +121,7 @@ export function setSettingGamepad(scene: BattleScene, setting: SettingGamepad, v const cancelHandler = () => { scene.ui.revertMode(); (scene.ui.getHandler() as SettingsGamepadUiHandler).setOptionCursor(Object.values(SettingGamepad).indexOf(SettingGamepad.Default_Controller), 0, true); + (scene.ui.getHandler() as SettingsGamepadUiHandler).updateBindings(); return false; }; const changeGamepadHandler = (gamepad: string) => { diff --git a/src/ui/settings-gamepad-ui-handler.ts b/src/ui/settings-gamepad-ui-handler.ts index b0be6b5b2..2dad71523 100644 --- a/src/ui/settings-gamepad-ui-handler.ts +++ b/src/ui/settings-gamepad-ui-handler.ts @@ -12,7 +12,7 @@ import { settingGamepadOptions } from "../system/settings-gamepad"; import {truncateString} from "../utils"; -import {getKeyForSettingName} from "#app/configs/gamepad-utils"; +import {getIconForRebindedKey, getKeyForSettingName} from "#app/configs/gamepad-utils"; export default class SettingsGamepadUiHandler extends UiHandler { private settingsContainer: Phaser.GameObjects.Container; @@ -89,11 +89,9 @@ export default class SettingsGamepadUiHandler extends UiHandler { continue; } const key = getKeyForSettingName(this.scene.inputController.getActiveConfig(), SettingGamepad[setting]); - const frame = this.scene.inputController.getActiveConfig().icons[key]; const icon = this.scene.add.sprite(0, 0, 'xbox'); icon.setScale(0.1); icon.setOrigin(0, 0); - icon.setFrame(frame); this.inputsIcons[key] = icon; this.optionsContainer.add(icon); valueLabels.push(icon); @@ -140,13 +138,17 @@ export default class SettingsGamepadUiHandler extends UiHandler { } updateBindings(): void { - // for (const elm of noOptionsCursors) { - // console.log('elm:', elm); - // } + const activeConfig = this.scene.inputController.getActiveConfig(); + for (const elm of noOptionsCursors) { + const key = getKeyForSettingName(activeConfig, elm); + const icon = getIconForRebindedKey(activeConfig, key); + this.inputsIcons[key].setFrame(icon); + } } show(args: any[]): boolean { super.show(args); + this.updateBindings(); const settings: object = localStorage.hasOwnProperty('settingsGamepad') ? JSON.parse(localStorage.getItem('settingsGamepad')) : {}; // in the menu, for each line, we set the cursor position for each option, either on the previously selected, or the default value. From 83a26d8e58eec7d6c45fcd654bc61a7e61b92b75 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sat, 11 May 2024 18:42:32 +0200 Subject: [PATCH 29/81] get missmatch function call --- src/configs/gamepad-utils.ts | 17 ++++++++++++++++- src/configs/pad_generic.ts | 4 ++-- src/configs/pad_xbox360.ts | 2 +- src/inputs-controller.ts | 3 ++- src/ui/settings-gamepad-ui-handler.ts | 7 +++++-- 5 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/configs/gamepad-utils.ts b/src/configs/gamepad-utils.ts index 946db4d4e..a01813db2 100644 --- a/src/configs/gamepad-utils.ts +++ b/src/configs/gamepad-utils.ts @@ -29,10 +29,18 @@ export function getKeyForRebindedAction(config: GamepadConfig, action: Button): return null; } +export function getKeyForAction(config: GamepadConfig, action: Button): String { + for (const key of Object.keys(config.custom)) { + if (config.custom[key] === action) return key; + } + return null; +} + export function getKeyForRebindedSettingName(config: GamepadConfig, settingName: SettingGamepad): String { const oldKey = getKeyForSettingName(config, settingName) const action = config.custom[oldKey]; - return getKeyForRebindedAction(config, action); + const key = getKeyForRebindedAction(config, action); + return key; } export function getIconForRebindedKey(config: GamepadConfig, _key): String { @@ -48,3 +56,10 @@ export function getKeyForSettingName(config: GamepadConfig, settingName: Setting } return null; } + +export function getIconForSettingName(config: GamepadConfig, settingName: SettingGamepad) { + const key = getKeyForSettingName(config, settingName); + const action = config.default[key]; + const rebindedKey = getKeyForAction(config, action); + return config.icons[rebindedKey]; +} diff --git a/src/configs/pad_generic.ts b/src/configs/pad_generic.ts index f64d6c342..de07ef478 100644 --- a/src/configs/pad_generic.ts +++ b/src/configs/pad_generic.ts @@ -58,8 +58,8 @@ const pad_generic = { RS: SettingGamepad.Button_Slow_Down, }, default: { - RC_S: Button.ACTION, - RC_E: Button.CANCEL, + RC_S: Button.ACTION, //5 + RC_E: Button.CANCEL, // 6 RC_W: Button.CYCLE_NATURE, RC_N: Button.CYCLE_VARIANT, START: Button.MENU, diff --git a/src/configs/pad_xbox360.ts b/src/configs/pad_xbox360.ts index 189c2a349..08bbd71a5 100644 --- a/src/configs/pad_xbox360.ts +++ b/src/configs/pad_xbox360.ts @@ -59,7 +59,7 @@ const pad_xbox360 = { }, default: { RC_S: Button.ACTION, //5 - RC_E: Button.CANCEL, + RC_E: Button.CANCEL, // 6 RC_W: Button.CYCLE_NATURE, RC_N: Button.CYCLE_VARIANT, //14 START: Button.MENU, //7 diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index f56f3e77f..102824518 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -307,8 +307,9 @@ export class InputsController { // for each gamepad, we set its mapping in this.configs const gamepadID = gamepad.toLowerCase(); const config = this.getConfig(gamepadID); - config.custom = this.configs[gamepad]?.custom || config.default; + config.custom = this.configs[gamepad]?.custom || {...config.default}; this.configs[gamepad] = config; + this.scene.gameData?.saveCustomMapping(this.chosenGamepad, this.configs[gamepad]?.custom); } if (this.chosenGamepad === thisGamepad.id) this.initChosenGamepad(this.chosenGamepad) } diff --git a/src/ui/settings-gamepad-ui-handler.ts b/src/ui/settings-gamepad-ui-handler.ts index 2dad71523..e099190d3 100644 --- a/src/ui/settings-gamepad-ui-handler.ts +++ b/src/ui/settings-gamepad-ui-handler.ts @@ -12,7 +12,10 @@ import { settingGamepadOptions } from "../system/settings-gamepad"; import {truncateString} from "../utils"; -import {getIconForRebindedKey, getKeyForSettingName} from "#app/configs/gamepad-utils"; +import { + getIconForSettingName, + getKeyForSettingName +} from "#app/configs/gamepad-utils"; export default class SettingsGamepadUiHandler extends UiHandler { private settingsContainer: Phaser.GameObjects.Container; @@ -141,7 +144,7 @@ export default class SettingsGamepadUiHandler extends UiHandler { const activeConfig = this.scene.inputController.getActiveConfig(); for (const elm of noOptionsCursors) { const key = getKeyForSettingName(activeConfig, elm); - const icon = getIconForRebindedKey(activeConfig, key); + const icon = getIconForSettingName(activeConfig, elm); this.inputsIcons[key].setFrame(icon); } } From fc2a0e06ec59a4905b0f27160ffe93b8b2b0863f Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sat, 11 May 2024 23:33:24 +0200 Subject: [PATCH 30/81] better layout management --- src/configs/pad_unlicensedSNES.ts | 2 +- src/inputs-controller.ts | 1 + src/ui/settings-gamepad-ui-handler.ts | 189 ++++++++++++++++---------- 3 files changed, 117 insertions(+), 75 deletions(-) diff --git a/src/configs/pad_unlicensedSNES.ts b/src/configs/pad_unlicensedSNES.ts index 8321c364e..7da7553ff 100644 --- a/src/configs/pad_unlicensedSNES.ts +++ b/src/configs/pad_unlicensedSNES.ts @@ -6,7 +6,7 @@ import {Button} from "../enums/buttons"; */ const pad_unlicensedSNES = { padID: '081f-e401', - padType: 'xbox', + padType: 'snes', gamepadMapping : { RC_S: 2, RC_E: 1, diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 102824518..82b02e0c2 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -506,6 +506,7 @@ export class InputsController { return pad_dualshock; } + // return pad_dualshock; return pad_generic; } diff --git a/src/ui/settings-gamepad-ui-handler.ts b/src/ui/settings-gamepad-ui-handler.ts index e099190d3..441572a6d 100644 --- a/src/ui/settings-gamepad-ui-handler.ts +++ b/src/ui/settings-gamepad-ui-handler.ts @@ -16,6 +16,10 @@ import { getIconForSettingName, getKeyForSettingName } from "#app/configs/gamepad-utils"; +import pad_xbox360 from "#app/configs/pad_xbox360"; +import pad_dualshock from "#app/configs/pad_dualshock"; +import pad_unlicensedSNES from "#app/configs/pad_unlicensedSNES"; +import {GamepadConfig} from "#app/inputs-controller"; export default class SettingsGamepadUiHandler extends UiHandler { private settingsContainer: Phaser.GameObjects.Container; @@ -38,6 +42,9 @@ export default class SettingsGamepadUiHandler extends UiHandler { private inputsIcons; + private layout = new Map(); + private keys; + constructor(scene: BattleScene, mode?: Mode) { super(scene, mode); @@ -67,99 +74,126 @@ export default class SettingsGamepadUiHandler extends UiHandler { 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.optionsContainer = this.scene.add.container(0, 0); - - this.settingLabels = []; - this.optionValueLabels = []; - this.inputsIcons = {}; - - Object.keys(SettingGamepad).forEach((setting, s) => { - let settingName = setting.replace(/\_/g, ' '); - - 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]); - - const valueLabels = [] - for (const [o, option] of settingGamepadOptions[SettingGamepad[setting]].entries()) { - if (noOptionsCursors.includes(SettingGamepad[setting])) { - if (o) { - const valueLabel = addTextObject(this.scene, 0, 0, option, settingGamepadDefaults[SettingGamepad[setting]] === o ? TextStyle.SETTINGS_SELECTED : TextStyle.WINDOW); - valueLabel.setOrigin(0, 0); - this.optionsContainer.add(valueLabel); - valueLabels.push(valueLabel); - continue; - } - const key = getKeyForSettingName(this.scene.inputController.getActiveConfig(), SettingGamepad[setting]); - const icon = this.scene.add.sprite(0, 0, 'xbox'); - icon.setScale(0.1); - icon.setOrigin(0, 0); - this.inputsIcons[key] = icon; - this.optionsContainer.add(icon); - valueLabels.push(icon); - continue; - } - const valueLabel = addTextObject(this.scene, 0, 0, option, settingGamepadDefaults[SettingGamepad[setting]] === o ? TextStyle.SETTINGS_SELECTED : TextStyle.WINDOW); - valueLabel.setOrigin(0, 0); - - this.optionsContainer.add(valueLabel); - - valueLabels.push(valueLabel); - } - this.optionValueLabels.push(valueLabels); - - 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 (let value of this.optionValueLabels[s]) { - value.setPositionRelative(this.settingLabels[s], labelWidth + xOffset, 0); - xOffset += value.width / 6 + optionSpacing; - } - }); - - this.optionCursors = Object.values(settingGamepadDefaults); - this.settingsContainer.add(headerBg); this.settingsContainer.add(headerText); this.settingsContainer.add(gamepadText); this.settingsContainer.add(this.optionsBg); - this.settingsContainer.add(this.optionsContainer); + + for (const config of [pad_xbox360, pad_dualshock, pad_unlicensedSNES]) { + this.layout[config.padType] = new Map(); + const optionsContainer = this.scene.add.container(0, 0); + optionsContainer.setVisible(false); + + const bindingSettings = Object.keys(config.setting).map(k=>config.setting[k]); + + const settingLabels = []; + const optionValueLabels = []; + const inputsIcons = {}; + Object.keys(SettingGamepad).forEach((setting, s) => { + let settingName = setting.replace(/\_/g, ' '); + + settingLabels[s] = addTextObject(this.scene, 8, 28 + s * 16, settingName, TextStyle.SETTINGS_LABEL); + settingLabels[s].setOrigin(0, 0); + + optionsContainer.add(settingLabels[s]); + const valueLabels = [] + for (const [o, option] of settingGamepadOptions[SettingGamepad[setting]].entries()) { + if (bindingSettings.includes(SettingGamepad[setting])) { + if (o) { + const valueLabel = addTextObject(this.scene, 0, 0, option, settingGamepadDefaults[SettingGamepad[setting]] === o ? TextStyle.SETTINGS_SELECTED : TextStyle.WINDOW); + valueLabel.setOrigin(0, 0); + optionsContainer.add(valueLabel); + valueLabels.push(valueLabel); + continue; + } + const key = getKeyForSettingName(config as GamepadConfig, SettingGamepad[setting]); + const icon = this.scene.add.sprite(0, 0, config.padType); + icon.setScale(0.1); + icon.setOrigin(0, 0); + inputsIcons[key] = icon; + optionsContainer.add(icon); + valueLabels.push(icon); + continue; + } + const valueLabel = addTextObject(this.scene, 0, 0, option, settingGamepadDefaults[SettingGamepad[setting]] === o ? TextStyle.SETTINGS_SELECTED : TextStyle.WINDOW); + valueLabel.setOrigin(0, 0); + + optionsContainer.add(valueLabel); + + valueLabels.push(valueLabel); + } + optionValueLabels.push(valueLabels); + + const totalWidth = optionValueLabels[s].map(o => o.width).reduce((total, width) => total += width, 0); + + const labelWidth = Math.max(78, settingLabels[s].displayWidth + 8); + + const totalSpace = (300 - labelWidth) - totalWidth / 6; + const optionSpacing = Math.floor(totalSpace / (optionValueLabels[s].length - 1)); + + let xOffset = 0; + + for (let value of optionValueLabels[s]) { + value.setPositionRelative(settingLabels[s], labelWidth + xOffset, 0); + xOffset += value.width / 6 + optionSpacing; + } + }); + + const commonSettingKeys = Object.keys(SettingGamepad).slice(0, 3).map(key => SettingGamepad[key]); + const specificBindingKeys = [...commonSettingKeys, ...Object.keys(config.setting).map(k => config.setting[k])]; + const optionCursors = Object.values(Object.keys(settingGamepadDefaults).filter(s => specificBindingKeys.includes(s)).map(k => settingGamepadDefaults[k])); + + this.layout[config.padType].optionsContainer = optionsContainer; + this.layout[config.padType].inputsIcons = inputsIcons; + this.layout[config.padType].settingLabels = settingLabels; + this.layout[config.padType].optionValueLabels = optionValueLabels; + this.layout[config.padType].optionCursors = optionCursors + this.layout[config.padType].keys = specificBindingKeys + this.layout[config.padType].bindingSettings = bindingSettings + + this.settingsContainer.add(optionsContainer); + } ui.add(this.settingsContainer); - this.setCursor(0); - this.setScrollCursor(0); - this.settingsContainer.setVisible(false); } updateBindings(): void { + Object.keys(this.layout).forEach((key) => this.layout[key].optionsContainer.setVisible(false)); const activeConfig = this.scene.inputController.getActiveConfig(); - for (const elm of noOptionsCursors) { + const configType = activeConfig.padType; + const layout = this.layout[configType]; + 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; + const bindingSettings = layout.bindingSettings; + + const settings: object = localStorage.hasOwnProperty('settingsGamepad') ? JSON.parse(localStorage.getItem('settingsGamepad')) : {}; + this.keys.forEach((key, index) => { + this.setOptionCursor(index, settings.hasOwnProperty(key) ? settings[key] : this.optionCursors[index]) + }); + + if (!activeConfig.custom) return; + for (const elm of bindingSettings) { const key = getKeyForSettingName(activeConfig, elm); const icon = getIconForSettingName(activeConfig, elm); + if (!this.inputsIcons[key]) debugger; this.inputsIcons[key].setFrame(icon); } + this.setCursor(0, true); + this.setScrollCursor(0, true); } show(args: any[]): boolean { super.show(args); this.updateBindings(); - const settings: object = localStorage.hasOwnProperty('settingsGamepad') ? JSON.parse(localStorage.getItem('settingsGamepad')) : {}; - // in the menu, for each line, we set the cursor position for each option, either on the previously selected, or the default value. - // if it's the default gamepad, we always want it by default to be on the very first option. never on "Change" - Object.keys(settingGamepadDefaults).forEach((setting, s) => this.setOptionCursor(s, settings.hasOwnProperty(setting) ? ( setting === SettingGamepad.Default_Controller ? 0 : settings[setting]) : settingGamepadDefaults[setting])); - this.settingsContainer.setVisible(true); - this.setCursor(0); this.getUi().moveTo(this.settingsContainer, this.getUi().length - 1); @@ -219,9 +253,14 @@ export default class SettingsGamepadUiHandler extends UiHandler { return success; } - setCursor(cursor: integer): boolean { + setCursor(cursor: integer, override?: boolean): boolean { const ret = super.setCursor(cursor); + if (override) { + this.optionsContainer.remove(this.cursorObj); + this.cursorObj = null; + } + 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); @@ -237,7 +276,9 @@ export default class SettingsGamepadUiHandler extends UiHandler { for (const [index, key] of Object.keys(SettingGamepad).entries()) { const setting = SettingGamepad[key] if (setting === SettingGamepad.Default_Controller) { - this.optionValueLabels[index][0].setText(truncateString(this.scene.inputController.chosenGamepad, 30)); + for (const key of Object.keys(this.layout)) { + this.layout[key].optionValueLabels[index][0].setText(truncateString(this.scene.inputController.chosenGamepad, 30)); + } } } } @@ -266,8 +307,8 @@ export default class SettingsGamepadUiHandler extends UiHandler { return true; } - setScrollCursor(scrollCursor: integer): boolean { - if (scrollCursor === this.scrollCursor) + setScrollCursor(scrollCursor: integer, override?: boolean): boolean { + if (scrollCursor === this.scrollCursor && !override) return false; this.scrollCursor = scrollCursor; From 3b693d2231cecb6593d1586299769e3ff32e067d Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sat, 11 May 2024 23:58:47 +0200 Subject: [PATCH 31/81] fix a cursor bug + filter labels to display (to avoir submit with an xbox controller for example) --- src/ui/settings-gamepad-ui-handler.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/ui/settings-gamepad-ui-handler.ts b/src/ui/settings-gamepad-ui-handler.ts index 441572a6d..8f0f105e5 100644 --- a/src/ui/settings-gamepad-ui-handler.ts +++ b/src/ui/settings-gamepad-ui-handler.ts @@ -89,7 +89,13 @@ export default class SettingsGamepadUiHandler extends UiHandler { const settingLabels = []; const optionValueLabels = []; const inputsIcons = {}; - Object.keys(SettingGamepad).forEach((setting, s) => { + + const commonSettingKeys = Object.keys(SettingGamepad).slice(0, 3).map(key => SettingGamepad[key]); + const specificBindingKeys = [...commonSettingKeys, ...Object.keys(config.setting).map(k => config.setting[k])]; + const optionCursors = Object.values(Object.keys(settingGamepadDefaults).filter(s => specificBindingKeys.includes(s)).map(k => settingGamepadDefaults[k])); + + const settingGamepadFiltered = Object.keys(SettingGamepad).filter(_key => specificBindingKeys.includes(SettingGamepad[_key])); + settingGamepadFiltered.forEach((setting, s) => { let settingName = setting.replace(/\_/g, ' '); settingLabels[s] = addTextObject(this.scene, 8, 28 + s * 16, settingName, TextStyle.SETTINGS_LABEL); @@ -139,9 +145,6 @@ export default class SettingsGamepadUiHandler extends UiHandler { } }); - const commonSettingKeys = Object.keys(SettingGamepad).slice(0, 3).map(key => SettingGamepad[key]); - const specificBindingKeys = [...commonSettingKeys, ...Object.keys(config.setting).map(k => config.setting[k])]; - const optionCursors = Object.values(Object.keys(settingGamepadDefaults).filter(s => specificBindingKeys.includes(s)).map(k => settingGamepadDefaults[k])); this.layout[config.padType].optionsContainer = optionsContainer; this.layout[config.padType].inputsIcons = inputsIcons; @@ -194,6 +197,8 @@ export default class SettingsGamepadUiHandler extends UiHandler { this.updateBindings(); this.settingsContainer.setVisible(true); + this.setCursor(0); + this.setScrollCursor(0); this.getUi().moveTo(this.settingsContainer, this.getUi().length - 1); @@ -257,7 +262,7 @@ export default class SettingsGamepadUiHandler extends UiHandler { const ret = super.setCursor(cursor); if (override) { - this.optionsContainer.remove(this.cursorObj); + Object.keys(this.layout).forEach(k => this.layout[k].optionsContainer.remove(this.cursorObj)); this.cursorObj = null; } @@ -273,6 +278,7 @@ export default class SettingsGamepadUiHandler extends UiHandler { } updateChosenGamepadDisplay(): void { + this.updateBindings(); for (const [index, key] of Object.keys(SettingGamepad).entries()) { const setting = SettingGamepad[key] if (setting === SettingGamepad.Default_Controller) { From ae85f1b416983ea6e55b2eaced7f755109a4a0ff Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sun, 12 May 2024 00:20:47 +0200 Subject: [PATCH 32/81] fix a floating cursor not destroyed --- src/ui/settings-gamepad-ui-handler.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/ui/settings-gamepad-ui-handler.ts b/src/ui/settings-gamepad-ui-handler.ts index 8f0f105e5..278d5732b 100644 --- a/src/ui/settings-gamepad-ui-handler.ts +++ b/src/ui/settings-gamepad-ui-handler.ts @@ -166,6 +166,9 @@ export default class SettingsGamepadUiHandler extends UiHandler { Object.keys(this.layout).forEach((key) => this.layout[key].optionsContainer.setVisible(false)); const activeConfig = this.scene.inputController.getActiveConfig(); const configType = activeConfig.padType; + this.cursorObj?.destroy(); + this.cursorObj = null; + this.scrollCursor = null; const layout = this.layout[configType]; this.keys = layout.keys; this.optionsContainer = layout.optionsContainer; @@ -188,8 +191,8 @@ export default class SettingsGamepadUiHandler extends UiHandler { if (!this.inputsIcons[key]) debugger; this.inputsIcons[key].setFrame(icon); } - this.setCursor(0, true); - this.setScrollCursor(0, true); + this.setCursor(0); + this.setScrollCursor(0); } show(args: any[]): boolean { @@ -197,7 +200,6 @@ export default class SettingsGamepadUiHandler extends UiHandler { this.updateBindings(); this.settingsContainer.setVisible(true); - this.setCursor(0); this.setScrollCursor(0); this.getUi().moveTo(this.settingsContainer, this.getUi().length - 1); @@ -258,14 +260,9 @@ export default class SettingsGamepadUiHandler extends UiHandler { return success; } - setCursor(cursor: integer, override?: boolean): boolean { + setCursor(cursor: integer): boolean { const ret = super.setCursor(cursor); - if (override) { - Object.keys(this.layout).forEach(k => this.layout[k].optionsContainer.remove(this.cursorObj)); - this.cursorObj = null; - } - 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); @@ -313,8 +310,8 @@ export default class SettingsGamepadUiHandler extends UiHandler { return true; } - setScrollCursor(scrollCursor: integer, override?: boolean): boolean { - if (scrollCursor === this.scrollCursor && !override) + setScrollCursor(scrollCursor: integer): boolean { + if (scrollCursor === this.scrollCursor) return false; this.scrollCursor = scrollCursor; From 5076625259753d0ed093407cc16c3126b23a9c9a Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sun, 12 May 2024 01:27:17 +0200 Subject: [PATCH 33/81] added placeholder when nogamepads is detected --- src/inputs-controller.ts | 3 +-- src/ui/settings-gamepad-ui-handler.ts | 27 +++++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 82b02e0c2..a0f4ee271 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -506,7 +506,6 @@ export class InputsController { return pad_dualshock; } - // return pad_dualshock; return pad_generic; } @@ -652,7 +651,7 @@ export class InputsController { getActiveConfig() :GamepadConfig { if (this.configs[this.chosenGamepad]?.padID) return this.configs[this.chosenGamepad] - return pad_generic as GamepadConfig; + return null; } getPressedButtonLabel(button: Phaser.Input.Gamepad.Button) { diff --git a/src/ui/settings-gamepad-ui-handler.ts b/src/ui/settings-gamepad-ui-handler.ts index 278d5732b..2f78a5706 100644 --- a/src/ui/settings-gamepad-ui-handler.ts +++ b/src/ui/settings-gamepad-ui-handler.ts @@ -156,6 +156,17 @@ export default class SettingsGamepadUiHandler extends UiHandler { this.settingsContainer.add(optionsContainer); } + this.layout['noGamepads'] = new Map(); + const optionsContainer = this.scene.add.container(0, 0); + optionsContainer.setVisible(false); + const label = addTextObject(this.scene, 8, 28, 'Please plug a controller or press a button', TextStyle.SETTINGS_LABEL); + label.setOrigin(0, 0); + optionsContainer.add(label); + this.settingsContainer.add(optionsContainer); + + this.layout['noGamepads'].optionsContainer = optionsContainer; + this.layout['noGamepads'].label = label; + ui.add(this.settingsContainer); @@ -165,11 +176,18 @@ export default class SettingsGamepadUiHandler extends UiHandler { updateBindings(): void { Object.keys(this.layout).forEach((key) => this.layout[key].optionsContainer.setVisible(false)); const activeConfig = this.scene.inputController.getActiveConfig(); + if (!activeConfig) { + const layout = this.layout['noGamepads']; + layout.optionsContainer.setVisible(true); + return; + } const configType = activeConfig.padType; this.cursorObj?.destroy(); this.cursorObj = null; this.scrollCursor = null; const layout = this.layout[configType]; + console.log('activeConfig', activeConfig); + console.log('layout', layout); this.keys = layout.keys; this.optionsContainer = layout.optionsContainer; this.optionsContainer.setVisible(true); @@ -237,10 +255,12 @@ export default class SettingsGamepadUiHandler extends UiHandler { } break; case Button.LEFT: + if (!this.optionCursors) return; if (this.optionCursors[cursor]) success = this.setOptionCursor(cursor, this.optionCursors[cursor] - 1, true); break; case Button.RIGHT: + if (!this.optionCursors) return; if (this.optionCursors[cursor] < this.optionValueLabels[cursor].length - 1) success = this.setOptionCursor(cursor, this.optionCursors[cursor] + 1, true); break; @@ -262,6 +282,7 @@ export default class SettingsGamepadUiHandler extends UiHandler { setCursor(cursor: integer): boolean { const ret = super.setCursor(cursor); + if (!this.optionsContainer) return ret; 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); @@ -279,8 +300,9 @@ export default class SettingsGamepadUiHandler extends UiHandler { for (const [index, key] of Object.keys(SettingGamepad).entries()) { const setting = SettingGamepad[key] if (setting === SettingGamepad.Default_Controller) { - for (const key of Object.keys(this.layout)) { - this.layout[key].optionValueLabels[index][0].setText(truncateString(this.scene.inputController.chosenGamepad, 30)); + for (const _key of Object.keys(this.layout)) { + if (_key === 'noGamepads') continue; + this.layout[_key].optionValueLabels[index][0].setText(truncateString(this.scene.inputController.chosenGamepad, 30)); } } } @@ -324,6 +346,7 @@ export default class SettingsGamepadUiHandler extends UiHandler { } updateSettingsScroll(): void { + if (!this.optionsContainer) return; this.optionsContainer.setY(-16 * this.scrollCursor); for (let s = 0; s < this.settingLabels.length; s++) { From d80c0e3d347b36d3805070e07d7251711ba67735 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sun, 12 May 2024 01:28:56 +0200 Subject: [PATCH 34/81] removed Swap a/b --- src/battle-scene.ts | 1 - src/system/settings-gamepad.ts | 6 ------ src/system/settings.ts | 8 -------- src/ui/settings-gamepad-ui-handler.ts | 4 +--- 4 files changed, 1 insertion(+), 18 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 0171fb96c..4003e5cf9 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -118,7 +118,6 @@ export default class BattleScene extends SceneBase { public fusionPaletteSwaps: boolean = true; public enableTouchControls: boolean = false; public enableVibration: boolean = false; - public abSwapped: boolean = false; public disableMenu: boolean = false; diff --git a/src/system/settings-gamepad.ts b/src/system/settings-gamepad.ts index 8b5f28e87..94c2ad951 100644 --- a/src/system/settings-gamepad.ts +++ b/src/system/settings-gamepad.ts @@ -8,7 +8,6 @@ import {Button} from "../enums/buttons"; export enum SettingGamepad { Default_Controller = "DEFAULT_CONTROLLER", Gamepad_Support = "GAMEPAD_SUPPORT", - Swap_A_and_B = "SWAP_A_B", // Swaps which gamepad button handles ACTION and CANCEL Button_Action = "BUTTON_ACTION", Button_Cancel = "BUTTON_CANCEL", Button_Menu = "BUTTON_MENU", @@ -27,7 +26,6 @@ export enum SettingGamepad { export const settingGamepadOptions: SettingOptions = { [SettingGamepad.Default_Controller]: ['Default', 'Change'], [SettingGamepad.Gamepad_Support]: ['Auto', 'Disabled'], - [SettingGamepad.Swap_A_and_B]: ['Enabled', 'Disabled'], [SettingGamepad.Button_Action]: [`KEY ${Button.ACTION.toString()}`, 'Change'], [SettingGamepad.Button_Cancel]: [`KEY ${Button.CANCEL.toString()}`, 'Change'], [SettingGamepad.Button_Menu]: [`KEY ${Button.MENU.toString()}`, 'Change'], @@ -46,7 +44,6 @@ export const settingGamepadOptions: SettingOptions = { export const settingGamepadDefaults: SettingDefaults = { [SettingGamepad.Default_Controller]: 0, [SettingGamepad.Gamepad_Support]: 0, - [SettingGamepad.Swap_A_and_B]: 1, // Set to 'Disabled' by default [SettingGamepad.Button_Action]: 0, [SettingGamepad.Button_Cancel]: 0, [SettingGamepad.Button_Menu]: 0, @@ -85,9 +82,6 @@ export function setSettingGamepad(scene: BattleScene, setting: SettingGamepad, v // activate or deactivate the controller listener scene.inputController.setGamepadSupport(settingGamepadOptions[setting][value] !== 'Disabled'); break; - case SettingGamepad.Swap_A_and_B: - scene.abSwapped = settingGamepadOptions[setting][value] !== 'Disabled'; - break; case SettingGamepad.Button_Action: case SettingGamepad.Button_Cancel: case SettingGamepad.Button_Menu: diff --git a/src/system/settings.ts b/src/system/settings.ts index 03ba462b9..017ff2f40 100644 --- a/src/system/settings.ts +++ b/src/system/settings.ts @@ -148,14 +148,6 @@ export function setSetting(scene: BattleScene, setting: Setting, value: integer) } else return false; break; - case Setting.Gamepad_Support: - // if we change the value of the gamepad support, we call a method in the inputController to - // activate or deactivate the controller listener - scene.inputController.setGamepadSupport(settingOptions[setting][value] !== 'Disabled'); - break; - case Setting.Swap_A_and_B: - scene.abSwapped = settingOptions[setting][value] !== 'Disabled'; - break; case Setting.Touch_Controls: scene.enableTouchControls = settingOptions[setting][value] !== 'Disabled' && hasTouchscreen(); const touchControls = document.getElementById('touchControls'); diff --git a/src/ui/settings-gamepad-ui-handler.ts b/src/ui/settings-gamepad-ui-handler.ts index 2f78a5706..829acb0b9 100644 --- a/src/ui/settings-gamepad-ui-handler.ts +++ b/src/ui/settings-gamepad-ui-handler.ts @@ -90,7 +90,7 @@ export default class SettingsGamepadUiHandler extends UiHandler { const optionValueLabels = []; const inputsIcons = {}; - const commonSettingKeys = Object.keys(SettingGamepad).slice(0, 3).map(key => SettingGamepad[key]); + const commonSettingKeys = Object.keys(SettingGamepad).slice(0, 2).map(key => SettingGamepad[key]); const specificBindingKeys = [...commonSettingKeys, ...Object.keys(config.setting).map(k => config.setting[k])]; const optionCursors = Object.values(Object.keys(settingGamepadDefaults).filter(s => specificBindingKeys.includes(s)).map(k => settingGamepadDefaults[k])); @@ -186,8 +186,6 @@ export default class SettingsGamepadUiHandler extends UiHandler { this.cursorObj = null; this.scrollCursor = null; const layout = this.layout[configType]; - console.log('activeConfig', activeConfig); - console.log('layout', layout); this.keys = layout.keys; this.optionsContainer = layout.optionsContainer; this.optionsContainer.setVisible(true); From 4c39fd068cf7ba19fb6a5fa2e2e6d26cbc5fd072 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sun, 12 May 2024 01:31:57 +0200 Subject: [PATCH 35/81] added top/down support from legacy settings --- src/ui/settings-gamepad-ui-handler.ts | 35 +++++++++++++++++++-------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/ui/settings-gamepad-ui-handler.ts b/src/ui/settings-gamepad-ui-handler.ts index 829acb0b9..1ea51419f 100644 --- a/src/ui/settings-gamepad-ui-handler.ts +++ b/src/ui/settings-gamepad-ui-handler.ts @@ -1,9 +1,8 @@ import BattleScene from "../battle-scene"; -import { hasTouchscreen, isMobile } from "../touch-controls"; -import { TextStyle, addTextObject } from "./text"; -import { Mode } from "./ui"; +import {TextStyle, addTextObject} from "./text"; +import {Mode} from "./ui"; import UiHandler from "./ui-handler"; -import { addWindow } from "./ui-theme"; +import {addWindow} from "./ui-theme"; import {Button} from "../enums/buttons"; import { noOptionsCursors, @@ -84,7 +83,7 @@ export default class SettingsGamepadUiHandler extends UiHandler { const optionsContainer = this.scene.add.container(0, 0); optionsContainer.setVisible(false); - const bindingSettings = Object.keys(config.setting).map(k=>config.setting[k]); + const bindingSettings = Object.keys(config.setting).map(k => config.setting[k]); const settingLabels = []; const optionValueLabels = []; @@ -132,7 +131,7 @@ export default class SettingsGamepadUiHandler extends UiHandler { const totalWidth = optionValueLabels[s].map(o => o.width).reduce((total, width) => total += width, 0); - const labelWidth = Math.max(78, settingLabels[s].displayWidth + 8); + const labelWidth = Math.max(78, settingLabels[s].displayWidth + 8); const totalSpace = (300 - labelWidth) - totalWidth / 6; const optionSpacing = Math.floor(totalSpace / (optionValueLabels[s].length - 1)); @@ -197,7 +196,7 @@ export default class SettingsGamepadUiHandler extends UiHandler { const settings: object = localStorage.hasOwnProperty('settingsGamepad') ? JSON.parse(localStorage.getItem('settingsGamepad')) : {}; this.keys.forEach((key, index) => { - this.setOptionCursor(index, settings.hasOwnProperty(key) ? settings[key] : this.optionCursors[index]) + this.setOptionCursor(index, settings.hasOwnProperty(key) ? settings[key] : this.optionCursors[index]) }); if (!activeConfig.custom) return; @@ -227,6 +226,8 @@ export default class SettingsGamepadUiHandler extends UiHandler { processInput(button: Button): boolean { const ui = this.getUi(); + // Defines the maximum number of rows that can be displayed on the screen. + const rowsToDisplay = 9; let success = false; @@ -242,14 +243,28 @@ export default class SettingsGamepadUiHandler extends UiHandler { 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(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: - if (cursor < this.optionValueLabels.length) { - if (this.cursor < 8) + if (cursor < this.optionValueLabels.length - 1) { + if (this.cursor < rowsToDisplay - 1) success = this.setCursor(this.cursor + 1); - else if (this.scrollCursor < this.optionValueLabels.length - 9) + 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: From 1c17c193c593898970806f1c82c8bb343e31130a Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sun, 12 May 2024 01:43:27 +0200 Subject: [PATCH 36/81] fix icon position + added cancel sound --- src/ui/gamepad-binding-ui-handler.ts | 9 ++++----- src/ui/settings-gamepad-ui-handler.ts | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/ui/gamepad-binding-ui-handler.ts b/src/ui/gamepad-binding-ui-handler.ts index 1d34c5285..34d106707 100644 --- a/src/ui/gamepad-binding-ui-handler.ts +++ b/src/ui/gamepad-binding-ui-handler.ts @@ -163,9 +163,6 @@ export default class GamepadBindingUiHandler extends UiHandler { break case Button.ACTION: if (this.cursor === 0) { - success = true; - // Reverts UI to its previous state on cancel. - // this.scene.ui.revertMode(); this.cancelFn(); } else { success = true; @@ -178,6 +175,8 @@ export default class GamepadBindingUiHandler extends UiHandler { // Plays a select sound effect if an action was successfully processed. if (success) ui.playSelect(); + else + ui.playError(); return success; } @@ -189,13 +188,13 @@ export default class GamepadBindingUiHandler extends UiHandler { this.actionLabel.setShadowColor(this.getTextColor(TextStyle.SETTINGS_SELECTED, true)); this.cancelLabel.setColor(this.getTextColor(TextStyle.WINDOW)); this.cancelLabel.setShadowColor(this.getTextColor(TextStyle.WINDOW, true)); - return; + return true; } this.actionLabel.setColor(this.getTextColor(TextStyle.WINDOW)); this.actionLabel.setShadowColor(this.getTextColor(TextStyle.WINDOW, true)); this.cancelLabel.setColor(this.getTextColor(TextStyle.SETTINGS_SELECTED)); this.cancelLabel.setShadowColor(this.getTextColor(TextStyle.SETTINGS_SELECTED, true)); - return; + return true; } clear() { diff --git a/src/ui/settings-gamepad-ui-handler.ts b/src/ui/settings-gamepad-ui-handler.ts index 1ea51419f..22e736e11 100644 --- a/src/ui/settings-gamepad-ui-handler.ts +++ b/src/ui/settings-gamepad-ui-handler.ts @@ -105,7 +105,7 @@ export default class SettingsGamepadUiHandler extends UiHandler { for (const [o, option] of settingGamepadOptions[SettingGamepad[setting]].entries()) { if (bindingSettings.includes(SettingGamepad[setting])) { if (o) { - const valueLabel = addTextObject(this.scene, 0, 0, option, settingGamepadDefaults[SettingGamepad[setting]] === o ? TextStyle.SETTINGS_SELECTED : TextStyle.WINDOW); + const valueLabel = addTextObject(this.scene, 0, 0, option, TextStyle.WINDOW); valueLabel.setOrigin(0, 0); optionsContainer.add(valueLabel); valueLabels.push(valueLabel); @@ -114,7 +114,7 @@ export default class SettingsGamepadUiHandler extends UiHandler { const key = getKeyForSettingName(config as GamepadConfig, SettingGamepad[setting]); const icon = this.scene.add.sprite(0, 0, config.padType); icon.setScale(0.1); - icon.setOrigin(0, 0); + icon.setOrigin(0, -0.1); inputsIcons[key] = icon; optionsContainer.add(icon); valueLabels.push(icon); From bf8f62bc9e12b86bf6bc2bb5f6602301e8d560d0 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sun, 12 May 2024 01:59:38 +0200 Subject: [PATCH 37/81] renaming + fix a display bug --- src/inputs-controller.ts | 5 +- src/ui/gamepad-binding-ui-handler.ts | 83 ++++++++++------------------ 2 files changed, 32 insertions(+), 56 deletions(-) diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index a0f4ee271..95b0ab6df 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -10,7 +10,7 @@ import SettingsGamepadUiHandler from "./ui/settings-gamepad-ui-handler"; import {SettingGamepad} from "./system/settings-gamepad"; import { getButtonIndexForKey, - getIconForCustomIndex, getIconForRebindedKey, + getIconForCustomIndex, getIconForRebindedKey, getIconForSettingName, getKeyForButtonIndex, getKeyForRebindedSettingName, getKeyForSettingName } from "./configs/gamepad-utils"; @@ -659,8 +659,7 @@ export class InputsController { } getCurrentButtonLabel(target: SettingGamepad) { - const key = getKeyForSettingName(this.configs[this.chosenGamepad], target); - return getIconForRebindedKey(this.configs[this.chosenGamepad], key); + return getIconForSettingName(this.configs[this.chosenGamepad], target); } swapBinding(target, newBinding) { diff --git a/src/ui/gamepad-binding-ui-handler.ts b/src/ui/gamepad-binding-ui-handler.ts index 34d106707..9f35df36c 100644 --- a/src/ui/gamepad-binding-ui-handler.ts +++ b/src/ui/gamepad-binding-ui-handler.ts @@ -15,15 +15,13 @@ export default class GamepadBindingUiHandler extends UiHandler { protected actionBg: Phaser.GameObjects.NineSlice; protected optionSelectBg: Phaser.GameObjects.NineSlice; private unlockText: Phaser.GameObjects.Text; - private keyPressed: Phaser.GameObjects.Text; - private alreadyAssignedText: Phaser.GameObjects.Text; + private swapText: Phaser.GameObjects.Text; private actionLabel: Phaser.GameObjects.Text; private cancelLabel: Phaser.GameObjects.Text; private listening: boolean = false; private buttonPressed = null; - private iconXbox: Phaser.GameObjects.Sprite; - private previousIconXbox: Phaser.GameObjects.Sprite; - private iconDualshock: Phaser.GameObjects.Sprite; + private newButtonIcon: Phaser.GameObjects.Sprite; + private targetButtonIcon: Phaser.GameObjects.Sprite; private cancelFn; private target: SettingGamepad; @@ -61,28 +59,22 @@ export default class GamepadBindingUiHandler extends UiHandler { this.optionSelectBg.setOrigin(0.5); this.optionSelectContainer.add(this.optionSelectBg); - this.iconXbox = this.scene.add.sprite(0, 0, 'xbox'); - this.iconXbox.setScale(0.15); - this.iconXbox.setPositionRelative(this.optionSelectBg, 78, 16); - this.iconXbox.setOrigin(0.5); - this.iconXbox.setVisible(false); + this.newButtonIcon = this.scene.add.sprite(0, 0, 'xbox'); + this.newButtonIcon.setScale(0.15); + this.newButtonIcon.setPositionRelative(this.optionSelectBg, 78, 16); + this.newButtonIcon.setOrigin(0.5); + this.newButtonIcon.setVisible(false); - this.iconDualshock = this.scene.add.sprite(0, 0, 'dualshock'); - this.iconDualshock.setScale(0.15); - this.iconDualshock.setPositionRelative(this.optionSelectBg, 78, 16); - this.iconDualshock.setOrigin(0.5); - this.iconDualshock.setVisible(false); + this.swapText = addTextObject(this.scene, 0, 0, 'will swap with', TextStyle.WINDOW); + this.swapText.setOrigin(0.5); + this.swapText.setPositionRelative(this.optionSelectBg, this.optionSelectBg.width / 2 - 2, this.optionSelectBg.height / 2 - 2); + this.swapText.setVisible(false); - this.alreadyAssignedText = addTextObject(this.scene, 0, 0, 'will swap with', TextStyle.WINDOW); - this.alreadyAssignedText.setOrigin(0.5); - this.alreadyAssignedText.setPositionRelative(this.optionSelectBg, this.optionSelectBg.width / 2 - 2, this.optionSelectBg.height / 2 - 2); - this.alreadyAssignedText.setVisible(false); - - this.previousIconXbox = this.scene.add.sprite(0, 0, 'xbox'); - this.previousIconXbox.setScale(0.15); - this.previousIconXbox.setPositionRelative(this.optionSelectBg, 78, 48); - this.previousIconXbox.setOrigin(0.5); - this.previousIconXbox.setVisible(false); + this.targetButtonIcon = this.scene.add.sprite(0, 0, 'xbox'); + this.targetButtonIcon.setScale(0.15); + this.targetButtonIcon.setPositionRelative(this.optionSelectBg, 78, 48); + this.targetButtonIcon.setOrigin(0.5); + this.targetButtonIcon.setVisible(false); this.cancelLabel = addTextObject(this.scene, 0, 0, 'Cancel', TextStyle.SETTINGS_LABEL); this.cancelLabel.setOrigin(0, 0.5); @@ -93,10 +85,9 @@ export default class GamepadBindingUiHandler extends UiHandler { this.actionLabel.setPositionRelative(this.actionBg, this.actionBg.width - 75, this.actionBg.height / 2); - this.optionSelectContainer.add(this.iconXbox); - this.optionSelectContainer.add(this.iconDualshock); - this.optionSelectContainer.add(this.alreadyAssignedText); - this.optionSelectContainer.add(this.previousIconXbox); + this.optionSelectContainer.add(this.newButtonIcon); + this.optionSelectContainer.add(this.swapText); + this.optionSelectContainer.add(this.targetButtonIcon); this.actionsContainer.add(this.actionLabel); this.actionsContainer.add(this.cancelLabel); } @@ -107,24 +98,13 @@ export default class GamepadBindingUiHandler extends UiHandler { this.buttonPressed = button.index; const [type, buttonIcon] = this.scene.inputController.getPressedButtonLabel(button); const assignedButtonIcon = this.scene.inputController.getCurrentButtonLabel(this.target); - switch (type) { - case 'dualshock': - this.iconXbox.setVisible(false); - this.iconDualshock.setFrame(buttonIcon); - this.iconDualshock.setVisible(true); - this.previousIconXbox.setVisible(true); - this.alreadyAssignedText.setVisible(true); - break - case 'xbox': - default: - this.iconDualshock.setVisible(false); - this.iconXbox.setFrame(buttonIcon); - this.iconXbox.setVisible(true); - this.previousIconXbox.setFrame(assignedButtonIcon); - this.previousIconXbox.setVisible(true); - this.alreadyAssignedText.setVisible(true); - break - } + this.newButtonIcon.setTexture(type); + this.newButtonIcon.setFrame(buttonIcon); + this.targetButtonIcon.setTexture(type); + this.targetButtonIcon.setFrame(assignedButtonIcon); + this.newButtonIcon.setVisible(true); + this.targetButtonIcon.setVisible(true); + this.swapText.setVisible(true); this.setCursor(0); this.actionsContainer.setVisible(true); } @@ -203,11 +183,8 @@ export default class GamepadBindingUiHandler extends UiHandler { this.cancelFn = null; this.optionSelectContainer.setVisible(false); this.actionsContainer.setVisible(false); - this.iconXbox.setVisible(false); - this.iconDualshock.setVisible(false); - this.previousIconXbox.setVisible(false); - this.alreadyAssignedText.setVisible(false); - this.iconXbox.setFrame(null); - this.iconDualshock.setFrame(null); + this.newButtonIcon.setVisible(false); + this.targetButtonIcon.setVisible(false); + this.swapText.setVisible(false); } } \ No newline at end of file From 6f8601815f5b3584ba44e5823e12e3822f2aad8a Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sun, 12 May 2024 03:22:37 +0200 Subject: [PATCH 38/81] renaming + small cleanup --- src/configs/gamepad-utils.ts | 57 ++++++++++----------------- src/configs/pad_generic.ts | 6 +-- src/inputs-controller.ts | 41 ++++++++++--------- src/system/game-data.ts | 2 +- src/system/settings-gamepad.ts | 8 ++-- src/ui/gamepad-binding-ui-handler.ts | 2 +- src/ui/settings-gamepad-ui-handler.ts | 33 ++++++++++------ 7 files changed, 72 insertions(+), 77 deletions(-) diff --git a/src/configs/gamepad-utils.ts b/src/configs/gamepad-utils.ts index a01813db2..b0cd75e99 100644 --- a/src/configs/gamepad-utils.ts +++ b/src/configs/gamepad-utils.ts @@ -3,63 +3,46 @@ import {SettingGamepad} from "#app/system/settings-gamepad"; import {Button} from "#app/enums/buttons"; -export function getKeyForButtonIndex(config: GamepadConfig, index: number): String { +// Given a button index from an input event, return its naming from the mapping config +export function getKeyFromInputIndex(config: GamepadConfig, index: number): String | null { for (const key of Object.keys(config.gamepadMapping)) { - const id = config.gamepadMapping[key]; - if (id === index) return key; - } - return null; -} -export function getButtonIndexForKey(config: GamepadConfig, _key: string): number { - for (const key of Object.keys(config.gamepadMapping)) { - if (key === _key) return config.gamepadMapping[key]; + if (config.gamepadMapping[key] === index) return key; } return null; } -export function getIconForCustomIndex(config: GamepadConfig, index: number): String { - const key = getKeyForButtonIndex(config, index); - return config.icons[key]; -} - -export function getKeyForRebindedAction(config: GamepadConfig, action: Button): String { - for (const key of Object.keys(config.default)) { - if (config.default[key] === action) return key; +// Given a setting name, return the key assigned to it from the config file +export function getKeyForSettingName(config: GamepadConfig, settingName: SettingGamepad): String | null { + for (const key of Object.keys(config.setting)) { + if (config.setting[key] === settingName) return key; } return null; } -export function getKeyForAction(config: GamepadConfig, action: Button): String { +// Given a Button, return the custom key assigned to it from the config file +export function getCurrenlyAssignedKeyToAction(config: GamepadConfig, action: Button): String | null { for (const key of Object.keys(config.custom)) { if (config.custom[key] === action) return key; } return null; } -export function getKeyForRebindedSettingName(config: GamepadConfig, settingName: SettingGamepad): String { +// Given a setting name, return the custom key for the default action from the config file +export function getCurrentlyAssignedToSettingName(config: GamepadConfig, settingName: SettingGamepad): String { const oldKey = getKeyForSettingName(config, settingName) - const action = config.custom[oldKey]; - const key = getKeyForRebindedAction(config, action); + const action = config.default[oldKey]; + const key = getCurrenlyAssignedKeyToAction(config, action); return key; } -export function getIconForRebindedKey(config: GamepadConfig, _key): String { - const action = config.custom[_key]; - const key = getKeyForRebindedAction(config, action); +// Given a button index from an input event, return its icon from the config file +export function getCurrenlyAssignedIconFromInputIndex(config: GamepadConfig, index: number): String { + const key = getKeyFromInputIndex(config, index); return config.icons[key]; } -export function getKeyForSettingName(config: GamepadConfig, settingName: SettingGamepad) { - for (const key of Object.keys(config.setting)) { - const name = config.setting[key]; - if (name === settingName) return key; - } - return null; -} - -export function getIconForSettingName(config: GamepadConfig, settingName: SettingGamepad) { - const key = getKeyForSettingName(config, settingName); - const action = config.default[key]; - const rebindedKey = getKeyForAction(config, action); - return config.icons[rebindedKey]; +// Given a setting name, return the icon currently assigned to this setting name +export function getCurrentlyAssignedIconToSettingName(config: GamepadConfig, settingName: SettingGamepad) { + const key = getCurrentlyAssignedToSettingName(config, settingName); + return config.icons[key]; } diff --git a/src/configs/pad_generic.ts b/src/configs/pad_generic.ts index de07ef478..1e38c6aca 100644 --- a/src/configs/pad_generic.ts +++ b/src/configs/pad_generic.ts @@ -64,9 +64,9 @@ const pad_generic = { RC_N: Button.CYCLE_VARIANT, START: Button.MENU, SELECT: Button.STATS, - LB: Button.CYCLE_FORM, - RB: Button.CYCLE_SHINY, - LT: Button.CYCLE_GENDER, + LB: Button.CYCLE_FORM, //10 + RB: Button.CYCLE_SHINY, //9 + LT: Button.CYCLE_GENDER, //11 RT: Button.CYCLE_ABILITY, LS: Button.SPEED_UP, RS: Button.SLOW_DOWN, diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 95b0ab6df..35a5b45ff 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -9,10 +9,8 @@ import {Mode} from "./ui/ui"; import SettingsGamepadUiHandler from "./ui/settings-gamepad-ui-handler"; import {SettingGamepad} from "./system/settings-gamepad"; import { - getButtonIndexForKey, - getIconForCustomIndex, getIconForRebindedKey, getIconForSettingName, - getKeyForButtonIndex, getKeyForRebindedSettingName, - getKeyForSettingName + getCurrenlyAssignedIconFromInputIndex, getCurrentlyAssignedIconToSettingName, + getKeyFromInputIndex, getCurrentlyAssignedToSettingName } from "./configs/gamepad-utils"; export interface GamepadMapping { @@ -88,6 +86,8 @@ export class InputsController { private pauseUpdate: boolean = false; + public lastSource: string = 'keyboard'; + /** * Initializes a new instance of the game control system, setting up initial state and configurations. * @@ -276,11 +276,11 @@ export class InputsController { // const chosenIsConnected = gamepadsLeft.some(g => g.id === this.chosenGamepad); // if the chosen gamepad is disconnected, and we got others gamepad connected // if (!chosenIsConnected && gamepadsLeft?.length) { - // We remove the previously chosen gamepad - // this.clearChosenGamepad(); - // and we set the first of the gamepad still connected as the chosen one. - // this.setChosenGamepad(gamepadsLeft[0].id); - // return; + // We remove the previously chosen gamepad + // this.clearChosenGamepad(); + // and we set the first of the gamepad still connected as the chosen one. + // this.setChosenGamepad(gamepadsLeft[0].id); + // return; // } } @@ -351,8 +351,9 @@ export class InputsController { if (!this.chosenGamepad) // at the very first input, if we have not yet a chosen gamepad, we set it this.setChosenGamepad(pad.id); if (!this.gamepadSupport || pad.id.toLowerCase() !== this.chosenGamepad.toLowerCase()) return; - const key = getKeyForButtonIndex(this.configs[pad.id], button.index); + const key = getKeyFromInputIndex(this.configs[pad.id], button.index); const buttonDown = this.configs[pad.id].custom[key]; + this.lastSource = 'gamepad'; if (buttonDown !== undefined) { this.events.emit('input_down', { controller_type: 'gamepad', @@ -378,7 +379,7 @@ export class InputsController { gamepadButtonUp(pad: Phaser.Input.Gamepad.Gamepad, button: Phaser.Input.Gamepad.Button, value: number): void { if (!pad) return; if (!this.gamepadSupport || pad.id !== this.chosenGamepad) return; - const key = getKeyForButtonIndex(this.configs[pad.id], button.index); + const key = getKeyFromInputIndex(this.configs[pad.id], button.index); const buttonUp = this.configs[pad.id]?.custom[key]; if (buttonUp !== undefined) { this.events.emit('input_up', { @@ -465,6 +466,7 @@ export class InputsController { this.buttonKeys.forEach((row, index) => { for (const key of row) { key.on('down', () => { + this.lastSource = 'keyboard'; this.events.emit('input_down', { controller_type: 'keyboard', button: index, @@ -506,6 +508,7 @@ export class InputsController { return pad_dualshock; } + // return pad_dualshock; return pad_generic; } @@ -649,23 +652,23 @@ export class InputsController { else if (this.buttonLock2 === button) this.buttonLock2 = null; } - getActiveConfig() :GamepadConfig { + getActiveConfig(): GamepadConfig { if (this.configs[this.chosenGamepad]?.padID) return this.configs[this.chosenGamepad] return null; } getPressedButtonLabel(button: Phaser.Input.Gamepad.Button) { - return [this.configs[this.chosenGamepad].padType, getIconForCustomIndex(this.configs[this.chosenGamepad], button.index)]; + return [this.configs[this.chosenGamepad].padType, getCurrenlyAssignedIconFromInputIndex(this.configs[this.chosenGamepad], button.index)]; } - getCurrentButtonLabel(target: SettingGamepad) { - return getIconForSettingName(this.configs[this.chosenGamepad], target); + getCurrentlyAssignedIconToDisplay(target: SettingGamepad) { + return getCurrentlyAssignedIconToSettingName(this.configs[this.chosenGamepad], target); } - swapBinding(target, newBinding) { + swapBinding(settingName, pressedButton) { this.pauseUpdate = true; - const keyTarget = getKeyForRebindedSettingName(this.configs[this.chosenGamepad], target) - const keyNewBinding = getKeyForButtonIndex(this.configs[this.chosenGamepad], newBinding); + const keyTarget = getCurrentlyAssignedToSettingName(this.configs[this.chosenGamepad], settingName) + const keyNewBinding = getKeyFromInputIndex(this.configs[this.chosenGamepad], pressedButton); const previousActionForThisNewBinding = this.configs[this.chosenGamepad].custom[keyNewBinding]; const ActionForThisNewBinding = this.configs[this.chosenGamepad].custom[keyTarget]; this.configs[this.chosenGamepad].custom[keyTarget] = previousActionForThisNewBinding; @@ -674,7 +677,7 @@ export class InputsController { setTimeout(() => this.pauseUpdate = false, 500); } - loadConfig(gamepadName: String, customMappings: MappingLayout): void { + injectConfig(gamepadName: String, customMappings: MappingLayout): void { if (!this.configs[gamepadName]) this.configs[gamepadName] = {}; this.configs[gamepadName].custom = customMappings; } diff --git a/src/system/game-data.ts b/src/system/game-data.ts index a131cd318..0aedf14e1 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -499,7 +499,7 @@ export class GameData { return false; const customMappings = JSON.parse(localStorage.getItem('customMapping')); for (const key of Object.keys(customMappings)) - this.scene.inputController.loadConfig(key, customMappings[key]); + this.scene.inputController.injectConfig(key, customMappings[key]); } diff --git a/src/system/settings-gamepad.ts b/src/system/settings-gamepad.ts index 94c2ad951..bca9de689 100644 --- a/src/system/settings-gamepad.ts +++ b/src/system/settings-gamepad.ts @@ -12,8 +12,8 @@ export enum SettingGamepad { Button_Cancel = "BUTTON_CANCEL", Button_Menu = "BUTTON_MENU", Button_Stats = "BUTTON_STATS", - Button_Cycle_Shiny = "BUTTON_CYCLE_SHINY", Button_Cycle_Form = "BUTTON_CYCLE_FORM", + Button_Cycle_Shiny = "BUTTON_CYCLE_SHINY", Button_Cycle_Gender = "BUTTON_CYCLE_GENDER", Button_Cycle_Ability = "BUTTON_CYCLE_ABILITY", Button_Cycle_Nature = "BUTTON_CYCLE_NATURE", @@ -30,8 +30,8 @@ export const settingGamepadOptions: SettingOptions = { [SettingGamepad.Button_Cancel]: [`KEY ${Button.CANCEL.toString()}`, 'Change'], [SettingGamepad.Button_Menu]: [`KEY ${Button.MENU.toString()}`, 'Change'], [SettingGamepad.Button_Stats]: [`KEY ${Button.STATS.toString()}`, 'Change'], - [SettingGamepad.Button_Cycle_Shiny]: [`KEY ${Button.CYCLE_SHINY.toString()}`, 'Change'], [SettingGamepad.Button_Cycle_Form]: [`KEY ${Button.CYCLE_FORM.toString()}`, 'Change'], + [SettingGamepad.Button_Cycle_Shiny]: [`KEY ${Button.CYCLE_SHINY.toString()}`, 'Change'], [SettingGamepad.Button_Cycle_Gender]: [`KEY ${Button.CYCLE_GENDER.toString()}`, 'Change'], [SettingGamepad.Button_Cycle_Ability]: [`KEY ${Button.CYCLE_ABILITY.toString()}`, 'Change'], [SettingGamepad.Button_Cycle_Nature]: [`KEY ${Button.CYCLE_NATURE.toString()}`, 'Change'], @@ -48,8 +48,8 @@ export const settingGamepadDefaults: SettingDefaults = { [SettingGamepad.Button_Cancel]: 0, [SettingGamepad.Button_Menu]: 0, [SettingGamepad.Button_Stats]: 0, - [SettingGamepad.Button_Cycle_Shiny]: 0, [SettingGamepad.Button_Cycle_Form]: 0, + [SettingGamepad.Button_Cycle_Shiny]: 0, [SettingGamepad.Button_Cycle_Gender]: 0, [SettingGamepad.Button_Cycle_Ability]: 0, [SettingGamepad.Button_Cycle_Nature]: 0, @@ -64,8 +64,8 @@ export const noOptionsCursors: Array = [ SettingGamepad.Button_Cancel, SettingGamepad.Button_Menu, SettingGamepad.Button_Stats, - SettingGamepad.Button_Cycle_Shiny, SettingGamepad.Button_Cycle_Form, + SettingGamepad.Button_Cycle_Shiny, SettingGamepad.Button_Cycle_Gender, SettingGamepad.Button_Cycle_Ability, SettingGamepad.Button_Cycle_Nature, diff --git a/src/ui/gamepad-binding-ui-handler.ts b/src/ui/gamepad-binding-ui-handler.ts index 9f35df36c..8d3a93c12 100644 --- a/src/ui/gamepad-binding-ui-handler.ts +++ b/src/ui/gamepad-binding-ui-handler.ts @@ -97,7 +97,7 @@ export default class GamepadBindingUiHandler extends UiHandler { if (!this.listening || pad.id !== this.scene.inputController?.chosenGamepad || blacklist.includes(button.index) || this.buttonPressed !== null) return; this.buttonPressed = button.index; const [type, buttonIcon] = this.scene.inputController.getPressedButtonLabel(button); - const assignedButtonIcon = this.scene.inputController.getCurrentButtonLabel(this.target); + const assignedButtonIcon = this.scene.inputController.getCurrentlyAssignedIconToDisplay(this.target); this.newButtonIcon.setTexture(type); this.newButtonIcon.setFrame(buttonIcon); this.targetButtonIcon.setTexture(type); diff --git a/src/ui/settings-gamepad-ui-handler.ts b/src/ui/settings-gamepad-ui-handler.ts index 22e736e11..b21a38ef4 100644 --- a/src/ui/settings-gamepad-ui-handler.ts +++ b/src/ui/settings-gamepad-ui-handler.ts @@ -12,7 +12,7 @@ import { } from "../system/settings-gamepad"; import {truncateString} from "../utils"; import { - getIconForSettingName, + getCurrentlyAssignedIconToSettingName, getKeyForSettingName } from "#app/configs/gamepad-utils"; import pad_xbox360 from "#app/configs/pad_xbox360"; @@ -20,6 +20,20 @@ import pad_dualshock from "#app/configs/pad_dualshock"; import pad_unlicensedSNES from "#app/configs/pad_unlicensedSNES"; import {GamepadConfig} from "#app/inputs-controller"; +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; +} + export default class SettingsGamepadUiHandler extends UiHandler { private settingsContainer: Phaser.GameObjects.Container; private optionsContainer: Phaser.GameObjects.Container; @@ -39,10 +53,10 @@ export default class SettingsGamepadUiHandler extends UiHandler { private reloadI18n: boolean; private gamepads: Array; - private inputsIcons; + private inputsIcons: InputsIcons; - private layout = new Map(); - private keys; + private layout: Map = new Map(); + private keys: Array; constructor(scene: BattleScene, mode?: Mode) { super(scene, mode); @@ -202,8 +216,7 @@ export default class SettingsGamepadUiHandler extends UiHandler { if (!activeConfig.custom) return; for (const elm of bindingSettings) { const key = getKeyForSettingName(activeConfig, elm); - const icon = getIconForSettingName(activeConfig, elm); - if (!this.inputsIcons[key]) debugger; + const icon = getCurrentlyAssignedIconToSettingName(activeConfig, elm); this.inputsIcons[key].setFrame(icon); } this.setCursor(0); @@ -370,17 +383,13 @@ export default class SettingsGamepadUiHandler extends UiHandler { } } - clear() { + clear(): void { super.clear(); this.settingsContainer.setVisible(false); this.eraseCursor(); - if (this.reloadRequired) { - this.reloadRequired = false; - this.scene.reset(true, false, true); - } } - eraseCursor() { + eraseCursor(): void { if (this.cursorObj) this.cursorObj.destroy(); this.cursorObj = null; From 8f340509e58812312734c251ea8c003d7a478ad8 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sun, 12 May 2024 03:35:20 +0200 Subject: [PATCH 39/81] starting to add comments --- src/ui/settings-gamepad-ui-handler.ts | 31 +++++++++++++++------------ 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/ui/settings-gamepad-ui-handler.ts b/src/ui/settings-gamepad-ui-handler.ts index b21a38ef4..46d1b1b3c 100644 --- a/src/ui/settings-gamepad-ui-handler.ts +++ b/src/ui/settings-gamepad-ui-handler.ts @@ -39,31 +39,23 @@ export default class SettingsGamepadUiHandler extends UiHandler { private optionsContainer: Phaser.GameObjects.Container; private scrollCursor: integer; + private optionCursors: integer[]; + private cursorObj: Phaser.GameObjects.NineSlice; private optionsBg: Phaser.GameObjects.NineSlice; - private optionCursors: integer[]; - private settingLabels: Phaser.GameObjects.Text[]; private optionValueLabels: Phaser.GameObjects.Text[][]; - private cursorObj: Phaser.GameObjects.NineSlice; - - private reloadRequired: boolean; - private reloadI18n: boolean; - private gamepads: Array; - - private inputsIcons: InputsIcons; - + // 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; constructor(scene: BattleScene, mode?: Mode) { super(scene, mode); - - this.reloadRequired = false; - this.reloadI18n = false; - this.gamepads = null; } setup() { @@ -92,21 +84,32 @@ export default class SettingsGamepadUiHandler extends UiHandler { this.settingsContainer.add(gamepadText); this.settingsContainer.add(this.optionsBg); + // for every config, we create a new "screen" for (const config of [pad_xbox360, pad_dualshock, pad_unlicensedSNES]) { this.layout[config.padType] = new Map(); const optionsContainer = this.scene.add.container(0, 0); optionsContainer.setVisible(false); + // this is all the binding this specific config contains const bindingSettings = Object.keys(config.setting).map(k => config.setting[k]); + // the setting name - Default Controller, Gamepad Support, Button Action, ... const settingLabels = []; + + // the option for the setting - Auto, Disabled, ... const optionValueLabels = []; + + // the sprites for each button to display const inputsIcons = {}; + // Default Controller, Gamepad Support const commonSettingKeys = Object.keys(SettingGamepad).slice(0, 2).map(key => SettingGamepad[key]); + // All the binding of this specific config const specificBindingKeys = [...commonSettingKeys, ...Object.keys(config.setting).map(k => config.setting[k])]; + // we merge both keys, and we fetch their default values, to change the label color of the selected one const optionCursors = Object.values(Object.keys(settingGamepadDefaults).filter(s => specificBindingKeys.includes(s)).map(k => settingGamepadDefaults[k])); + // we filter the SettingGamepad setting to let only the specificBindingKeys, we need the keys from this setting to compute the name to display const settingGamepadFiltered = Object.keys(SettingGamepad).filter(_key => specificBindingKeys.includes(SettingGamepad[_key])); settingGamepadFiltered.forEach((setting, s) => { let settingName = setting.replace(/\_/g, ' '); From 956379bc3116d5c2f01bc3e33d92fcad8767f2d5 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sun, 12 May 2024 13:13:15 +0200 Subject: [PATCH 40/81] commented settings-gamepad-ui-handler.ts --- src/configs/gamepad-utils.ts | 6 +- src/ui/settings-gamepad-ui-handler.ts | 261 ++++++++++++++++++-------- 2 files changed, 189 insertions(+), 78 deletions(-) diff --git a/src/configs/gamepad-utils.ts b/src/configs/gamepad-utils.ts index b0cd75e99..707efd802 100644 --- a/src/configs/gamepad-utils.ts +++ b/src/configs/gamepad-utils.ts @@ -12,7 +12,7 @@ export function getKeyFromInputIndex(config: GamepadConfig, index: number): Stri } // Given a setting name, return the key assigned to it from the config file -export function getKeyForSettingName(config: GamepadConfig, settingName: SettingGamepad): String | null { +export function getKeyForSettingName(config: GamepadConfig, settingName: string): String | null { for (const key of Object.keys(config.setting)) { if (config.setting[key] === settingName) return key; } @@ -28,7 +28,7 @@ export function getCurrenlyAssignedKeyToAction(config: GamepadConfig, action: Bu } // Given a setting name, return the custom key for the default action from the config file -export function getCurrentlyAssignedToSettingName(config: GamepadConfig, settingName: SettingGamepad): String { +export function getCurrentlyAssignedToSettingName(config: GamepadConfig, settingName: string): String { const oldKey = getKeyForSettingName(config, settingName) const action = config.default[oldKey]; const key = getCurrenlyAssignedKeyToAction(config, action); @@ -42,7 +42,7 @@ export function getCurrenlyAssignedIconFromInputIndex(config: GamepadConfig, ind } // Given a setting name, return the icon currently assigned to this setting name -export function getCurrentlyAssignedIconToSettingName(config: GamepadConfig, settingName: SettingGamepad) { +export function getCurrentlyAssignedIconToSettingName(config: GamepadConfig, settingName: string) { const key = getCurrentlyAssignedToSettingName(config, settingName); return config.icons[key]; } diff --git a/src/ui/settings-gamepad-ui-handler.ts b/src/ui/settings-gamepad-ui-handler.ts index 46d1b1b3c..470411440 100644 --- a/src/ui/settings-gamepad-ui-handler.ts +++ b/src/ui/settings-gamepad-ui-handler.ts @@ -54,6 +54,9 @@ export default class SettingsGamepadUiHandler extends UiHandler { // 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); } @@ -84,43 +87,55 @@ export default class SettingsGamepadUiHandler extends UiHandler { this.settingsContainer.add(gamepadText); this.settingsContainer.add(this.optionsBg); - // for every config, we create a new "screen" + /// Initialize a new configuration "screen" for each type of gamepad. for (const config of [pad_xbox360, pad_dualshock, pad_unlicensedSNES]) { + // 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); - // this is all the binding this specific config contains + // Gather all gamepad binding settings from the configuration. const bindingSettings = Object.keys(config.setting).map(k => config.setting[k]); - // the setting name - Default Controller, Gamepad Support, Button Action, ... - const settingLabels = []; + // Array to hold labels for different settings such as 'Default Controller', 'Gamepad Support', etc. + const settingLabels: Phaser.GameObjects.Text[] = []; - // the option for the setting - Auto, Disabled, ... - const optionValueLabels = []; + // Array to hold options for each setting, e.g., 'Auto', 'Disabled'. + const optionValueLabels: Phaser.GameObjects.Text[][] = []; - // the sprites for each button to display - const inputsIcons = {}; + // Object to store sprites for each button configuration. + const inputsIcons: InputsIcons = {}; - // Default Controller, Gamepad Support + // Fetch common setting keys such as 'Default Controller' and 'Gamepad Support' from gamepad settings. const commonSettingKeys = Object.keys(SettingGamepad).slice(0, 2).map(key => SettingGamepad[key]); - // All the binding of this specific config + // Combine common and specific bindings into a single array. const specificBindingKeys = [...commonSettingKeys, ...Object.keys(config.setting).map(k => config.setting[k])]; - // we merge both keys, and we fetch their default values, to change the label color of the selected one + // Fetch default values for these settings and prepare to highlight selected options. const optionCursors = Object.values(Object.keys(settingGamepadDefaults).filter(s => specificBindingKeys.includes(s)).map(k => settingGamepadDefaults[k])); - // we filter the SettingGamepad setting to let only the specificBindingKeys, we need the keys from this setting to compute the name to display + // Filter out settings that are not relevant to the current gamepad configuration. const settingGamepadFiltered = Object.keys(SettingGamepad).filter(_key => specificBindingKeys.includes(SettingGamepad[_key])); + // Loop through the filtered settings to manage display and options. + settingGamepadFiltered.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]); - const valueLabels = [] + + // 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 settingGamepadOptions[SettingGamepad[setting]].entries()) { + // Check if the current setting is for binding keys. if (bindingSettings.includes(SettingGamepad[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); @@ -128,6 +143,7 @@ export default class SettingsGamepadUiHandler extends UiHandler { valueLabels.push(valueLabel); continue; } + // For null options, add an icon for the key. const key = getKeyForSettingName(config as GamepadConfig, SettingGamepad[setting]); const icon = this.scene.add.sprite(0, 0, config.padType); icon.setScale(0.1); @@ -137,71 +153,151 @@ export default class SettingsGamepadUiHandler extends UiHandler { 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, settingGamepadDefaults[SettingGamepad[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. - this.layout[config.padType].optionsContainer = optionsContainer; - this.layout[config.padType].inputsIcons = inputsIcons; - this.layout[config.padType].settingLabels = settingLabels; - this.layout[config.padType].optionValueLabels = optionValueLabels; - this.layout[config.padType].optionCursors = optionCursors - this.layout[config.padType].keys = specificBindingKeys - this.layout[config.padType].bindingSettings = bindingSettings - + // Add the options container to the overall settings container to be displayed in the UI. this.settingsContainer.add(optionsContainer); } + // If no gamepads are detected, set up a default UI prompt in the settings container. this.layout['noGamepads'] = new Map(); const optionsContainer = this.scene.add.container(0, 0); - optionsContainer.setVisible(false); + optionsContainer.setVisible(false); // Initially hide the container as no gamepads are connected. const label = addTextObject(this.scene, 8, 28, 'Please plug a controller or press a button', TextStyle.SETTINGS_LABEL); label.setOrigin(0, 0); optionsContainer.add(label); this.settingsContainer.add(optionsContainer); + // Map the 'noGamepads' layout options for easy access. this.layout['noGamepads'].optionsContainer = optionsContainer; this.layout['noGamepads'].label = label; - + // 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.getActiveConfig(); - if (!activeConfig) { - const layout = this.layout['noGamepads']; - layout.optionsContainer.setVisible(true); - return; + // 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('settingsGamepad') ? JSON.parse(localStorage.getItem('settingsGamepad')) : {}; + + // 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); + + // 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.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); + + // 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; + } + + setLayout(activeConfig: GamepadConfig): 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; + + // 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); @@ -209,35 +305,11 @@ export default class SettingsGamepadUiHandler extends UiHandler { this.optionValueLabels = layout.optionValueLabels; this.optionCursors = layout.optionCursors; this.inputsIcons = layout.inputsIcons; - const bindingSettings = layout.bindingSettings; - - const settings: object = localStorage.hasOwnProperty('settingsGamepad') ? JSON.parse(localStorage.getItem('settingsGamepad')) : {}; - this.keys.forEach((key, index) => { - this.setOptionCursor(index, settings.hasOwnProperty(key) ? settings[key] : this.optionCursors[index]) - }); - - if (!activeConfig.custom) return; - for (const elm of bindingSettings) { - const key = getKeyForSettingName(activeConfig, elm); - const icon = getCurrentlyAssignedIconToSettingName(activeConfig, elm); - this.inputsIcons[key].setFrame(icon); - } - this.setCursor(0); - this.setScrollCursor(0); - } - - show(args: any[]): boolean { - super.show(args); - this.updateBindings(); - - this.settingsContainer.setVisible(true); - this.setScrollCursor(0); - - this.getUi().moveTo(this.settingsContainer, this.getUi().length - 1); - - this.getUi().hideTooltip(); + this.bindingSettings = layout.bindingSettings; + // Return true indicating the layout was successfully applied. return true; + } processInput(button: Button): boolean { @@ -247,17 +319,19 @@ export default class SettingsGamepadUiHandler extends UiHandler { 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; + const cursor = this.cursor + this.scrollCursor; // Calculate the absolute cursor position. switch (button) { - case Button.UP: - if (cursor) { + 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 + 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. @@ -268,7 +342,7 @@ export default class SettingsGamepadUiHandler extends UiHandler { success = successA && successB; // success is just there to play the little validation sound effect } break; - case Button.DOWN: + 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); @@ -283,19 +357,17 @@ export default class SettingsGamepadUiHandler extends UiHandler { success = successA && successB; // Indicates a successful cursor and scroll adjustment. } break; - case Button.LEFT: + 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: + 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: - this.scene.ui.setMode(Mode.SETTINGS) - success = true; + case Button.CYCLE_FORM: // Change the UI mode to SETTINGS mode. case Button.CYCLE_SHINY: this.scene.ui.setMode(Mode.SETTINGS) success = true; @@ -303,34 +375,47 @@ export default class SettingsGamepadUiHandler extends UiHandler { } } + // If a change occurred, play the selection sound. if (success) ui.playSelect(); - return success; + return success; // Return whether the input resulted in a successful action. } 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); - this.optionsContainer.add(this.cursorObj); + 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 ret; // Return the result from the parent class's setCursor method. } updateChosenGamepadDisplay(): void { + // Update any bindings that might have changed since the last update. this.updateBindings(); + + // Iterate over the keys in the SettingGamepad enumeration. for (const [index, key] of Object.keys(SettingGamepad).entries()) { - const setting = SettingGamepad[key] + const setting = SettingGamepad[key] // Get the actual setting value using the key. + + // Check if the current setting corresponds to the default controller setting. if (setting === SettingGamepad.Default_Controller) { + // Iterate over all layouts excluding the 'noGamepads' special case. for (const _key of Object.keys(this.layout)) { - if (_key === 'noGamepads') continue; + if (_key === 'noGamepads') continue; // Skip updating the no gamepad 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.chosenGamepad, 30)); } } @@ -338,48 +423,67 @@ export default class SettingsGamepadUiHandler extends UiHandler { } setOptionCursor(settingIndex: integer, cursor: integer, save?: boolean): boolean { + // Retrieve the specific setting using the settingIndex from the SettingGamepad enumeration. const setting = SettingGamepad[Object.keys(SettingGamepad)[settingIndex]]; + // Get the current cursor position for this setting. const lastCursor = this.optionCursors[settingIndex]; - if (!noOptionsCursors.includes(setting)) { + // Check if the setting is not part of the bindings (i.e., it's a regular setting). + if (!this.bindingSettings.includes(setting)) { + // 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 (SettingGamepad[setting] !== SettingGamepad.Default_Controller) this.scene.gameData.saveGamepadSetting(setting, cursor) } - return true; + return true; // Return true to indicate the cursor was successfully updated. } 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; // 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); @@ -388,13 +492,20 @@ export default class SettingsGamepadUiHandler extends UiHandler { 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(); } eraseCursor(): void { + // Check if a cursor object exists. if (this.cursorObj) - this.cursorObj.destroy(); + 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 From 8f4669cab68b826a7800e658efa2d43e6f649ef3 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sun, 12 May 2024 13:26:00 +0200 Subject: [PATCH 41/81] added comment for gamepad-binding-ui-handler.ts --- src/inputs-controller.ts | 4 ---- src/system/settings-gamepad.ts | 2 +- src/ui/gamepad-binding-ui-handler.ts | 34 ++++++++++++++++++++++------ 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 35a5b45ff..42581fccb 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -39,10 +39,6 @@ export interface GamepadConfig { custom: MappingLayout; } -export interface ActionGamepadMapping { - [key: string]: Button; -} - const repeatInputDelayMillis = 250; /** diff --git a/src/system/settings-gamepad.ts b/src/system/settings-gamepad.ts index bca9de689..428d27181 100644 --- a/src/system/settings-gamepad.ts +++ b/src/system/settings-gamepad.ts @@ -96,7 +96,7 @@ export function setSettingGamepad(scene: BattleScene, setting: SettingGamepad, v case SettingGamepad.Button_Slow_Down: if (value) { if (scene.ui) { - const cancelHandler = (success: boolean = false) => { + const cancelHandler = (success: boolean = false) : boolean => { scene.ui.revertMode(); (scene.ui.getHandler() as SettingsGamepadUiHandler).updateBindings(); return success; diff --git a/src/ui/gamepad-binding-ui-handler.ts b/src/ui/gamepad-binding-ui-handler.ts index 8d3a93c12..864a63cba 100644 --- a/src/ui/gamepad-binding-ui-handler.ts +++ b/src/ui/gamepad-binding-ui-handler.ts @@ -9,39 +9,53 @@ import {SettingGamepad} from "../system/settings-gamepad"; export default class GamepadBindingUiHandler extends UiHandler { + // Containers for different segments of the UI. protected optionSelectContainer: Phaser.GameObjects.Container; protected actionsContainer: Phaser.GameObjects.Container; + + // Background elements for titles and action areas. protected titleBg: Phaser.GameObjects.NineSlice; protected actionBg: Phaser.GameObjects.NineSlice; protected optionSelectBg: Phaser.GameObjects.NineSlice; + + // Text elements for displaying instructions and actions. private unlockText: Phaser.GameObjects.Text; private swapText: Phaser.GameObjects.Text; private actionLabel: Phaser.GameObjects.Text; private cancelLabel: Phaser.GameObjects.Text; + private listening: boolean = false; - private buttonPressed = null; + private buttonPressed: number | null = null; + + // Icons for displaying current and new button assignments. private newButtonIcon: Phaser.GameObjects.Sprite; private targetButtonIcon: Phaser.GameObjects.Sprite; - private cancelFn; + + // Function to call on cancel or completion of binding. + private cancelFn: (boolean?) => boolean; + + // The specific setting being modified. private target: SettingGamepad; constructor(scene: BattleScene, mode: Mode = Mode.GAMEPAD_BINDING) { super(scene, mode); + // Listen to gamepad button down events to initiate binding. scene.input.gamepad.on('down', this.gamepadButtonDown, this); } - // const loadSessionBg = this.scene.add.rectangle(this.scene.game.canvas.width / 24, -this.scene.game.canvas.height / 24, this.scene.game.canvas.width / 12, -this.scene.game.canvas.height / 12, 0x006860); - // loadSessionBg.setOrigin(0, 0); - // this.optionSelectContainer.add(loadSessionBg); setup() { const ui = this.getUi(); this.optionSelectContainer = this.scene.add.container(0, 0); this.actionsContainer = this.scene.add.container(0, 0); + // Initially, containers are not visible. this.optionSelectContainer.setVisible(false); this.actionsContainer.setVisible(false); + + // Add containers to the UI. ui.add(this.optionSelectContainer); ui.add(this.actionsContainer); + // Setup backgrounds and text objects for UI. this.titleBg = addWindow(this.scene, (this.scene.game.canvas.width / 6) - this.getWindowWidth(), -(this.scene.game.canvas.height / 6) + 28 + 21, this.getWindowWidth(), 24); this.titleBg.setOrigin(0.5); this.optionSelectContainer.add(this.titleBg); @@ -50,6 +64,7 @@ export default class GamepadBindingUiHandler extends UiHandler { this.actionBg.setOrigin(0.5); this.actionsContainer.add(this.actionBg); + // Text prompts and instructions for the user. this.unlockText = addTextObject(this.scene, 0, 0, 'Press a button...', TextStyle.WINDOW); this.unlockText.setOrigin(0, 0); this.unlockText.setPositionRelative(this.titleBg, 36, 4); @@ -59,6 +74,7 @@ export default class GamepadBindingUiHandler extends UiHandler { this.optionSelectBg.setOrigin(0.5); this.optionSelectContainer.add(this.optionSelectBg); + // New button icon setup. this.newButtonIcon = this.scene.add.sprite(0, 0, 'xbox'); this.newButtonIcon.setScale(0.15); this.newButtonIcon.setPositionRelative(this.optionSelectBg, 78, 16); @@ -84,7 +100,7 @@ export default class GamepadBindingUiHandler extends UiHandler { this.actionLabel.setOrigin(0, 0.5); this.actionLabel.setPositionRelative(this.actionBg, this.actionBg.width - 75, this.actionBg.height / 2); - + // Add swap and cancel labels to the containers. this.optionSelectContainer.add(this.newButtonIcon); this.optionSelectContainer.add(this.swapText); this.optionSelectContainer.add(this.targetButtonIcon); @@ -93,7 +109,8 @@ export default class GamepadBindingUiHandler extends UiHandler { } gamepadButtonDown(pad: Phaser.Input.Gamepad.Gamepad, button: Phaser.Input.Gamepad.Button, value: number): void { - const blacklist = [12, 13, 14, 15]; + const blacklist = [12, 13, 14, 15]; // d-pad buttons are blacklisted. + // Check conditions before processing the button press. if (!this.listening || pad.id !== this.scene.inputController?.chosenGamepad || blacklist.includes(button.index) || this.buttonPressed !== null) return; this.buttonPressed = button.index; const [type, buttonIcon] = this.scene.inputController.getPressedButtonLabel(button); @@ -115,6 +132,7 @@ export default class GamepadBindingUiHandler extends UiHandler { this.cancelFn = args[0].cancelHandler; this.target = args[0].target; + // Bring the option and action containers to the front of the UI. this.getUi().bringToTop(this.optionSelectContainer); this.getUi().bringToTop(this.actionsContainer); @@ -138,10 +156,12 @@ export default class GamepadBindingUiHandler extends UiHandler { switch (button) { case Button.LEFT: case Button.RIGHT: + // Toggle between action and cancel options. const cursor = this.cursor ? 0 : 1; success = this.setCursor(cursor); break case Button.ACTION: + // Process actions based on current cursor position. if (this.cursor === 0) { this.cancelFn(); } else { From 32a9b7170c33e96054b8bb9fbcd58d4ad38a1787 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sun, 12 May 2024 13:26:33 +0200 Subject: [PATCH 42/81] removed useless array --- src/system/settings-gamepad.ts | 16 ---------------- src/ui/settings-gamepad-ui-handler.ts | 1 - 2 files changed, 17 deletions(-) diff --git a/src/system/settings-gamepad.ts b/src/system/settings-gamepad.ts index 428d27181..7c32d5763 100644 --- a/src/system/settings-gamepad.ts +++ b/src/system/settings-gamepad.ts @@ -59,22 +59,6 @@ export const settingGamepadDefaults: SettingDefaults = { [SettingGamepad.Button_Submit]: 0, }; -export const noOptionsCursors: Array = [ - SettingGamepad.Button_Action, - SettingGamepad.Button_Cancel, - SettingGamepad.Button_Menu, - SettingGamepad.Button_Stats, - SettingGamepad.Button_Cycle_Form, - SettingGamepad.Button_Cycle_Shiny, - SettingGamepad.Button_Cycle_Gender, - SettingGamepad.Button_Cycle_Ability, - SettingGamepad.Button_Cycle_Nature, - SettingGamepad.Button_Cycle_Variant, - SettingGamepad.Button_Speed_Up, - SettingGamepad.Button_Slow_Down, - SettingGamepad.Button_Submit, -]; - export function setSettingGamepad(scene: BattleScene, setting: SettingGamepad, value: integer): boolean { switch (setting) { case SettingGamepad.Gamepad_Support: diff --git a/src/ui/settings-gamepad-ui-handler.ts b/src/ui/settings-gamepad-ui-handler.ts index 470411440..c21bb60e7 100644 --- a/src/ui/settings-gamepad-ui-handler.ts +++ b/src/ui/settings-gamepad-ui-handler.ts @@ -5,7 +5,6 @@ import UiHandler from "./ui-handler"; import {addWindow} from "./ui-theme"; import {Button} from "../enums/buttons"; import { - noOptionsCursors, SettingGamepad, settingGamepadDefaults, settingGamepadOptions From ba42f072e1e4d2b64f48179357bc98ba6060dfe5 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sun, 12 May 2024 13:40:16 +0200 Subject: [PATCH 43/81] update comment in inputs-controller.ts --- src/configs/gamepad-utils.ts | 2 +- src/configs/pad_unlicensedSNES.ts | 2 +- src/inputs-controller.ts | 164 ++++++++++++++++++------------ 3 files changed, 99 insertions(+), 69 deletions(-) diff --git a/src/configs/gamepad-utils.ts b/src/configs/gamepad-utils.ts index 707efd802..9e55e35c2 100644 --- a/src/configs/gamepad-utils.ts +++ b/src/configs/gamepad-utils.ts @@ -42,7 +42,7 @@ export function getCurrenlyAssignedIconFromInputIndex(config: GamepadConfig, ind } // Given a setting name, return the icon currently assigned to this setting name -export function getCurrentlyAssignedIconToSettingName(config: GamepadConfig, settingName: string) { +export function getCurrentlyAssignedIconToSettingName(config: GamepadConfig, settingName: string): string { const key = getCurrentlyAssignedToSettingName(config, settingName); return config.icons[key]; } diff --git a/src/configs/pad_unlicensedSNES.ts b/src/configs/pad_unlicensedSNES.ts index 7da7553ff..8321c364e 100644 --- a/src/configs/pad_unlicensedSNES.ts +++ b/src/configs/pad_unlicensedSNES.ts @@ -6,7 +6,7 @@ import {Button} from "../enums/buttons"; */ const pad_unlicensedSNES = { padID: '081f-e401', - padType: 'snes', + padType: 'xbox', gamepadMapping : { RC_S: 2, RC_E: 1, diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 42581fccb..23e508507 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -188,6 +188,12 @@ export class InputsController { } } + /** + * Sets the currently chosen gamepad and initializes related settings. + * This method first deactivates any active key presses and then initializes the gamepad settings. + * + * @param gamepad - The identifier of the gamepad to set as chosen. + */ setChosenGamepad(gamepad: String): void { this.deactivatePressedKey(); this.initChosenGamepad(gamepad) @@ -196,15 +202,13 @@ export class InputsController { /** * Updates the interaction handling by processing input states. * This method gives priority to certain buttons by reversing the order in which they are checked. + * This method loops through all button values, checks for valid and timely interactions, and conditionally processes + * or ignores them based on the current state of gamepad support and other criteria. * - * @remarks - * The method iterates over all possible buttons, checking for specific conditions such as: - * - If the button is registered in the `interactions` dictionary. - * - If the button has been held down long enough. - * - If the button is currently pressed. + * It handles special conditions such as the absence of gamepad support or mismatches between the source of the input and + * the currently chosen gamepad. It also respects the paused state of updates to prevent unwanted input processing. * - * Special handling is applied if gamepad support is disabled but a gamepad source is still triggering inputs, - * preventing potential infinite loops by removing the last processed movement time for the button. + * If an interaction is valid and should be processed, it emits an 'input_down' event with details of the interaction. */ update(): void { for (const b of Utils.getEnumValues(Button).reverse()) { @@ -233,34 +237,39 @@ export class InputsController { } } + /** + * Retrieves the identifiers of all connected gamepads, excluding any that are currently marked as disconnected. + * @returns Array An array of strings representing the IDs of the connected gamepads. + */ getGamepadsName(): Array { return this.gamepads.filter(g => !this.disconnectedGamepads.includes(g.id)).map(g => g.id); } + /** + * Initializes the chosen gamepad by setting its identifier in the local storage and updating the UI to reflect the chosen gamepad. + * If a gamepad name is provided, it uses that as the chosen gamepad; otherwise, it defaults to the currently chosen gamepad. + * @param gamepadName Optional parameter to specify the name of the gamepad to initialize as chosen. + */ initChosenGamepad(gamepadName?: String): void { - // if we have a gamepad name in parameter, we set the chosen gamepad with this value let name = gamepadName; if (gamepadName) this.chosenGamepad = gamepadName; else - name = this.chosenGamepad; // otherwise we use the chosen gamepad's name + name = this.chosenGamepad; localStorage.setItem('chosenGamepad', name); - // we update the ui with the chosen gamepad const handler = this.scene.ui?.handlers[Mode.SETTINGS_GAMEPAD] as SettingsGamepadUiHandler; handler && handler.updateChosenGamepadDisplay() } - clearChosenGamepad() { - this.chosenGamepad = null; - if (localStorage.hasOwnProperty('chosenGamepad')) - localStorage.removeItem('chosenGamepad'); - } - + /** + * 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 + * disconnections, it would be impossible to determine the connection status of gamepads. This method ensures + * that disconnected gamepads are recognized and can be appropriately hidden in the gamepad selection menu. + * + * @param thisGamepad The gamepad that has been disconnected. + */ onDisconnect(thisGamepad: Phaser.Input.Gamepad.Gamepad): void { - // We need to add the disconnected gamepad into a local array - // Because Phaser keep in memory the previously connected gamepad - // If we don't do that, we have no way to determine if the gamepad is connected or not. - // We want to know that because we want to hide it in the selection menu of gamepad to use this.disconnectedGamepads.push(thisGamepad.id); /** commented for now this code because i don't know anymore if it's good to do that * for example i'm playing with a wireless gamepad that shutdown after 5 min @@ -280,27 +289,28 @@ export class InputsController { // } } + /** + * Updates the tracking of disconnected gamepads when a gamepad is reconnected. + * It removes the reconnected gamepad's identifier from the `disconnectedGamepads` array, + * effectively updating its status to connected. + * + * @param thisGamepad The gamepad that has been reconnected. + */ onReconnect(thisGamepad: Phaser.Input.Gamepad.Gamepad): void { - // We check if a gamepad reconnect by looking in the disconnectedGamepads array if is there - // If he is there, we remove it. this.disconnectedGamepads = this.disconnectedGamepads.filter(g => g !== thisGamepad.id); } /** - * Configures a gamepad for use based on its device ID. + * Initializes or updates configurations for connected gamepads. + * It retrieves the names of all connected gamepads, sets up their configurations according to stored or default settings, + * and ensures these configurations are saved. If the connected gamepad is the currently chosen one, + * it reinitializes the chosen gamepad settings. * - * @param thisGamepad - The gamepad to set up. - * - * @remarks - * This method initializes a gamepad by mapping its ID to a predefined configuration. - * It updates the player's gamepad mapping based on the identified configuration, ensuring - * that the gamepad controls are correctly mapped to in-game actions. + * @param thisGamepad The gamepad that is being set up. */ setupGamepad(thisGamepad: Phaser.Input.Gamepad.Gamepad): void { - // we fetch all the gamepads name const allGamepads = this.getGamepadsName(); for (const gamepad of allGamepads) { - // for each gamepad, we set its mapping in this.configs const gamepadID = gamepad.toLowerCase(); const config = this.getConfig(gamepadID); config.custom = this.configs[gamepad]?.custom || {...config.default}; @@ -330,21 +340,17 @@ export class InputsController { } /** - * Handles the 'down' event for gamepad buttons, emitting appropriate events and updating the interaction state. + * 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 + * action using a custom configuration, emits an event for the button press, and records the time of the action. * - * @param pad - The gamepad on which the button press occurred. - * @param button - The button that was pressed. - * @param value - The value associated with the button press, typically indicating pressure or degree of activation. - * - * @remarks - * This method is triggered when a gamepad button is pressed. If gamepad support is enabled, it: - * - Retrieves the current gamepad action mapping. - * - Checks if the pressed button is mapped to a game action. - * - If mapped, emits an 'input_down' event with the controller type and button action, and updates the interaction of this button. + * @param pad The gamepad on which the button was pressed. + * @param button The specific button that was pressed. + * @param value The intensity or value of the button press, if applicable. */ gamepadButtonDown(pad: Phaser.Input.Gamepad.Gamepad, button: Phaser.Input.Gamepad.Button, value: number): void { if (!pad) return; - if (!this.chosenGamepad) // at the very first input, if we have not yet a chosen gamepad, we set it + if (!this.chosenGamepad) this.setChosenGamepad(pad.id); if (!this.gamepadSupport || pad.id.toLowerCase() !== this.chosenGamepad.toLowerCase()) return; const key = getKeyFromInputIndex(this.configs[pad.id], button.index); @@ -360,17 +366,13 @@ export class InputsController { } /** - * Handles the 'up' event for gamepad buttons, emitting appropriate events and clearing the interaction state. + * Responds to a button release event on a gamepad by checking if the gamepad is supported and currently chosen. + * If conditions are met, it identifies the configured action for the button, emits an event signaling the button release, + * and clears the record of the button. * - * @param pad - The gamepad on which the button release occurred. - * @param button - The button that was released. - * @param value - The value associated with the button release, typically indicating pressure or degree of deactivation. - * - * @remarks - * This method is triggered when a gamepad button is released. If gamepad support is enabled, it: - * - Retrieves the current gamepad action mapping. - * - Checks if the released button is mapped to a game action. - * - If mapped, emits an 'input_up' event with the controller type and button action, and clears the interaction for this button. + * @param pad The gamepad from which the button was released. + * @param button The specific button that was released. + * @param value The intensity or value of the button release, if applicable. */ gamepadButtonUp(pad: Phaser.Input.Gamepad.Gamepad, button: Phaser.Input.Gamepad.Button, value: number): void { if (!pad) return; @@ -481,17 +483,12 @@ export class InputsController { } /** - * Maps a gamepad ID to a specific gamepad configuration based on the ID's characteristics. + * Retrieves the configuration object for a gamepad based on its identifier. The method identifies specific gamepad models + * based on substrings in the identifier and returns predefined configurations for recognized models. + * If no specific configuration matches, it defaults to a generic gamepad configuration. * - * @param id - The gamepad ID string, typically representing a unique identifier for a gamepad model or make. - * @returns A `GamepadConfig` object corresponding to the identified gamepad model. - * - * @remarks - * This function analyzes the provided gamepad ID and matches it to a predefined configuration based on known identifiers: - * - If the ID includes both '081f' and 'e401', it is identified as an unlicensed SNES gamepad. - * - If the ID contains 'xbox' and '360', it is identified as an Xbox 360 gamepad. - * - If the ID contains '054c', it is identified as a DualShock gamepad. - * If no specific identifiers are recognized, a generic gamepad configuration is returned. + * @param id The identifier string of the gamepad. + * @returns GamepadConfig The configuration object corresponding to the identified gamepad type. */ getConfig(id: string): GamepadConfig { id = id.toLowerCase(); @@ -504,7 +501,7 @@ export class InputsController { return pad_dualshock; } - // return pad_dualshock; + return pad_unlicensedSNES; return pad_generic; } @@ -648,20 +645,46 @@ export class InputsController { else if (this.buttonLock2 === button) this.buttonLock2 = null; } - getActiveConfig(): GamepadConfig { + /** + * 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. + */ + getActiveConfig(): GamepadConfig | null { if (this.configs[this.chosenGamepad]?.padID) return this.configs[this.chosenGamepad] return null; } - getPressedButtonLabel(button: Phaser.Input.Gamepad.Button) { + /** + * Determines icon for a button pressed on the currently chosen gamepad based on its configuration. + * + * @param button The button for which to retrieve the label and icon. + * @returns Array Tuple containing the pad type and the currently assigned icon for the button index. + */ + getPressedButtonLabel(button: Phaser.Input.Gamepad.Button): [string, string] { return [this.configs[this.chosenGamepad].padType, getCurrenlyAssignedIconFromInputIndex(this.configs[this.chosenGamepad], button.index)]; } - getCurrentlyAssignedIconToDisplay(target: SettingGamepad) { + /** + * Retrieves the currently assigned icon for a specific setting on the chosen gamepad. + * + * @param target The gamepad setting for which to retrieve the assigned icon. + * @returns string The icon assigned to the specified setting. + */ + getCurrentlyAssignedIconToDisplay(target: SettingGamepad): string { return getCurrentlyAssignedIconToSettingName(this.configs[this.chosenGamepad], target); } - swapBinding(settingName, pressedButton) { + /** + * Swaps the binding of two controls on the chosen gamepad configuration. + * It temporarily pauses updates, swaps the key bindings, saves the new configuration, + * and then resumes updates after a short delay. + * + * @param settingName The name of the setting for which to swap the binding. + * @param pressedButton The button index whose binding is to be swapped. + */ + swapBinding(settingName, pressedButton): void { this.pauseUpdate = true; const keyTarget = getCurrentlyAssignedToSettingName(this.configs[this.chosenGamepad], settingName) const keyNewBinding = getKeyFromInputIndex(this.configs[this.chosenGamepad], pressedButton); @@ -673,6 +696,13 @@ export class InputsController { setTimeout(() => this.pauseUpdate = false, 500); } + /** + * Injects a custom mapping configuration into the gamepad configuration for a specific gamepad. + * If the gamepad does not have an existing configuration, it initializes one first. + * + * @param gamepadName The identifier of the gamepad to configure. + * @param customMappings The custom mapping configuration to apply to the gamepad. + */ injectConfig(gamepadName: String, customMappings: MappingLayout): void { if (!this.configs[gamepadName]) this.configs[gamepadName] = {}; this.configs[gamepadName].custom = customMappings; From 39389757106b9463cd1974efa3de90af0985bc2f Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sun, 12 May 2024 13:54:49 +0200 Subject: [PATCH 44/81] fix snes controller support --- src/configs/pad_unlicensedSNES.ts | 2 +- src/inputs-controller.ts | 4 ++-- src/ui/settings-gamepad-ui-handler.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/configs/pad_unlicensedSNES.ts b/src/configs/pad_unlicensedSNES.ts index 8321c364e..7da7553ff 100644 --- a/src/configs/pad_unlicensedSNES.ts +++ b/src/configs/pad_unlicensedSNES.ts @@ -6,7 +6,7 @@ import {Button} from "../enums/buttons"; */ const pad_unlicensedSNES = { padID: '081f-e401', - padType: 'xbox', + padType: 'snes', gamepadMapping : { RC_S: 2, RC_E: 1, diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 23e508507..3e43d60ec 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -501,7 +501,6 @@ export class InputsController { return pad_dualshock; } - return pad_unlicensedSNES; return pad_generic; } @@ -663,7 +662,8 @@ export class InputsController { * @returns Array Tuple containing the pad type and the currently assigned icon for the button index. */ getPressedButtonLabel(button: Phaser.Input.Gamepad.Button): [string, string] { - return [this.configs[this.chosenGamepad].padType, getCurrenlyAssignedIconFromInputIndex(this.configs[this.chosenGamepad], button.index)]; + const type = this.configs[this.chosenGamepad].padType; + return [type === 'snes' ? 'xbox' : type, getCurrenlyAssignedIconFromInputIndex(this.configs[this.chosenGamepad], button.index)]; } /** diff --git a/src/ui/settings-gamepad-ui-handler.ts b/src/ui/settings-gamepad-ui-handler.ts index c21bb60e7..d0df227b3 100644 --- a/src/ui/settings-gamepad-ui-handler.ts +++ b/src/ui/settings-gamepad-ui-handler.ts @@ -144,7 +144,7 @@ export default class SettingsGamepadUiHandler extends UiHandler { } // For null options, add an icon for the key. const key = getKeyForSettingName(config as GamepadConfig, SettingGamepad[setting]); - const icon = this.scene.add.sprite(0, 0, config.padType); + const icon = this.scene.add.sprite(0, 0, config.padType === 'snes' ? 'xbox' : config.padType); icon.setScale(0.1); icon.setOrigin(0, -0.1); inputsIcons[key] = icon; @@ -429,7 +429,7 @@ export default class SettingsGamepadUiHandler extends UiHandler { 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)) { + 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)); From a2dd7047373d64c2cd9b71727465a682f2a563c3 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sun, 12 May 2024 14:18:48 +0200 Subject: [PATCH 45/81] added snes icons --- public/images/inputs/snes.json | 108 ++++++++++++++++++++++++++ public/images/inputs/snes.png | Bin 0 -> 7993 bytes src/configs/pad_unlicensedSNES.ts | 12 +-- src/inputs-controller.ts | 3 +- src/loading-scene.ts | 1 + src/ui/settings-gamepad-ui-handler.ts | 2 +- 6 files changed, 117 insertions(+), 9 deletions(-) create mode 100644 public/images/inputs/snes.json create mode 100644 public/images/inputs/snes.png diff --git a/public/images/inputs/snes.json b/public/images/inputs/snes.json new file mode 100644 index 000000000..2ffe08764 --- /dev/null +++ b/public/images/inputs/snes.json @@ -0,0 +1,108 @@ +{"frames": [ + +{ + "filename": "select.png", + "frame": {"x":0,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "start.png", + "frame": {"x":128,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_X_A_White_Alt.png", + "frame": {"x":256,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_X_B_White_Alt.png", + "frame": {"x":384,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_X_Dpad_Down_Alt.png", + "frame": {"x":512,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_X_Dpad_Left_Alt.png", + "frame": {"x":640,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_X_Dpad_Right_Alt.png", + "frame": {"x":768,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_X_Dpad_Up_Alt.png", + "frame": {"x":896,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_X_LB_Alt.png", + "frame": {"x":1024,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_X_RB_Alt.png", + "frame": {"x":1152,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_X_X_White_Alt.png", + "frame": {"x":1280,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_X_Y_White_Alt.png", + "frame": {"x":1408,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}], +"meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "snes.png", + "format": "RGBA8888", + "size": {"w":1536,"h":128}, + "scale": "1", + "smartupdate": "$TexturePacker:SmartUpdate:86d994650b80a3f876cc92e4d0928a65:60b6f706b30e87e11c3d01cec0eccf9c:7e443c950e063b6a235f0fd6295e2000$" +} +} diff --git a/public/images/inputs/snes.png b/public/images/inputs/snes.png new file mode 100644 index 0000000000000000000000000000000000000000..94b1d1ce72ca807dc3123a0ec81f7b50962ac6ad GIT binary patch literal 7993 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 Date: Sun, 12 May 2024 16:10:25 +0200 Subject: [PATCH 46/81] first step of keyboard rebinding --- public/images/inputs/keyboard.json | 644 +++++++++++++++++++++++++ public/images/inputs/keyboard.png | Bin 0 -> 51888 bytes src/configs/keyboard.ts | 176 +++++++ src/loading-scene.ts | 1 + src/system/settings-keyboard.ts | 87 ++++ src/ui/keyboard-binding-ui-handler.ts | 89 ++++ src/ui/settings-gamepad-ui-handler.ts | 10 +- src/ui/settings-keyboard-ui-handler.ts | 89 ++++ src/ui/settings-ui-handler.ts | 11 +- src/ui/ui.ts | 6 + 10 files changed, 1109 insertions(+), 4 deletions(-) create mode 100644 public/images/inputs/keyboard.json create mode 100644 public/images/inputs/keyboard.png create mode 100644 src/configs/keyboard.ts create mode 100644 src/system/settings-keyboard.ts create mode 100644 src/ui/keyboard-binding-ui-handler.ts create mode 100644 src/ui/settings-keyboard-ui-handler.ts diff --git a/public/images/inputs/keyboard.json b/public/images/inputs/keyboard.json new file mode 100644 index 000000000..e86dcc1d5 --- /dev/null +++ b/public/images/inputs/keyboard.json @@ -0,0 +1,644 @@ +{"frames": [ + +{ + "filename": "T_0_Key_Dark.png", + "frame": {"x":0,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_1_Key_Dark.png", + "frame": {"x":128,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_2_Key_Dark.png", + "frame": {"x":256,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_3_Key_Dark-1.png", + "frame": {"x":384,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_3_Key_Dark.png", + "frame": {"x":512,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_5_Key_Dark.png", + "frame": {"x":640,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_6_Key_Dark.png", + "frame": {"x":768,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_7_Key_Dark.png", + "frame": {"x":896,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_8_Key_Dark.png", + "frame": {"x":1024,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_9_Key_Dark.png", + "frame": {"x":1152,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_A_Key_Dark.png", + "frame": {"x":1280,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_Alt_Key_Dark.png", + "frame": {"x":1408,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_Asterisk_Key_Dark.png", + "frame": {"x":1536,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_B_Key_Dark.png", + "frame": {"x":1664,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_Backspace_Alt_Key_Dark.png", + "frame": {"x":1792,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_BackSpace_Key_Dark.png", + "frame": {"x":1920,"y":0,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_Brackets_L_Key_Dark.png", + "frame": {"x":0,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_Brackets_R_Key_Dark.png", + "frame": {"x":128,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_C_Key_Dark.png", + "frame": {"x":256,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_Crtl_Key_Dark.png", + "frame": {"x":384,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_D_Key_Dark.png", + "frame": {"x":512,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_Del_Key_Dark.png", + "frame": {"x":640,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_Down_Key_Dark.png", + "frame": {"x":768,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_E_Key_Dark.png", + "frame": {"x":896,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_End_Key_Dark.png", + "frame": {"x":1024,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_Enter_Alt_Key_Dark.png", + "frame": {"x":1152,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_Esc_Key_Dark.png", + "frame": {"x":1280,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_F1_Key_Dark.png", + "frame": {"x":1408,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_F2_Key_Dark.png", + "frame": {"x":1536,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_F3_Key_Dark.png", + "frame": {"x":1664,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_F4_Key_Dark.png", + "frame": {"x":1792,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_F5_Key_Dark.png", + "frame": {"x":1920,"y":128,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_F6_Key_Dark.png", + "frame": {"x":0,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_F7_Key_Dark.png", + "frame": {"x":128,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_F8_Key_Dark.png", + "frame": {"x":256,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_F9_Key_Dark.png", + "frame": {"x":384,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_F10_Key_Dark.png", + "frame": {"x":512,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_F11_Key_Dark.png", + "frame": {"x":640,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_F12_Key_Dark.png", + "frame": {"x":768,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_F_Key_Dark.png", + "frame": {"x":896,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_G_Key_Dark.png", + "frame": {"x":1024,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_H_Key_Dark.png", + "frame": {"x":1152,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_Home_Key_Dark.png", + "frame": {"x":1280,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_I_Key_Dark.png", + "frame": {"x":1408,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_Ins_Key_Dark.png", + "frame": {"x":1536,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_J_Key_Dark.png", + "frame": {"x":1664,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_K_Key_Dark.png", + "frame": {"x":1792,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_Keyboard_R_Key_Dark-1.png", + "frame": {"x":1920,"y":256,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_Keyboard_R_Key_Dark.png", + "frame": {"x":0,"y":384,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_L_Key_Dark.png", + "frame": {"x":128,"y":384,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_Left_Key_Dark.png", + "frame": {"x":256,"y":384,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_M_Key_Dark.png", + "frame": {"x":384,"y":384,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_Minus_Key_Dark.png", + "frame": {"x":512,"y":384,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_N_Key_Dark.png", + "frame": {"x":640,"y":384,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_O_Key_Dark.png", + "frame": {"x":768,"y":384,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_P_Key_Dark.png", + "frame": {"x":896,"y":384,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_PageDown_Key_Dark.png", + "frame": {"x":1024,"y":384,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_PageUp_Key_Dark.png", + "frame": {"x":1152,"y":384,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_Plus_Tall_Key_Dark.png", + "frame": {"x":1280,"y":384,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_Q_Key_Dark.png", + "frame": {"x":1408,"y":384,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_Quotation_Key_Dark.png", + "frame": {"x":1536,"y":384,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_R_Key_Dark.png", + "frame": {"x":1664,"y":384,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_Right_Key_Dark.png", + "frame": {"x":1792,"y":384,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_S_Key_Dark.png", + "frame": {"x":1920,"y":384,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_Semicolon_Key_Dark.png", + "frame": {"x":0,"y":512,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_Shift_Key_Dark.png", + "frame": {"x":128,"y":512,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_Shift_Super_Wide_Key_Dark.png", + "frame": {"x":256,"y":512,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_Slash_Key_Dark.png", + "frame": {"x":384,"y":512,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_Space_Key_Dark.png", + "frame": {"x":512,"y":512,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_T_Key_Dark.png", + "frame": {"x":640,"y":512,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_Tab_Key_Dark.png", + "frame": {"x":768,"y":512,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_Tilde_Key_Dark.png", + "frame": {"x":896,"y":512,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_U_Key_Dark.png", + "frame": {"x":1024,"y":512,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_Up_Key_Dark.png", + "frame": {"x":1152,"y":512,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_V_Key_Dark.png", + "frame": {"x":1280,"y":512,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_W_Key_Dark.png", + "frame": {"x":1408,"y":512,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_X_Key_Dark.png", + "frame": {"x":1536,"y":512,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_Y_Key_Dark.png", + "frame": {"x":1664,"y":512,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}, +{ + "filename": "T_Z_Key_Dark.png", + "frame": {"x":1792,"y":512,"w":128,"h":128}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, + "sourceSize": {"w":128,"h":128} +}], +"meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "keyboard.png", + "format": "RGBA8888", + "size": {"w":2048,"h":640}, + "scale": "1", + "smartupdate": "$TexturePacker:SmartUpdate:c63c48370eadc7845a8cc15895f925a0:5ddaf57801c3bd84e190cd9b4786d52d:bad03abb89ad027d879c383c13fd51bc$" +} +} diff --git a/public/images/inputs/keyboard.png b/public/images/inputs/keyboard.png new file mode 100644 index 0000000000000000000000000000000000000000..af019a60da1d842bd6f180290b5dd59a71db73c9 GIT binary patch literal 51888 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 { + scene.ui.revertMode(); + (scene.ui.getHandler() as SettingsKeyboardUiHandler).updateBindings(); + return success; + } + scene.ui.setOverlayMode(Mode.KEYBOARD_BINDING, { + target: setting, + cancelHandler: cancelHandler, + }) + } + } + break; + } + return true; + +} \ No newline at end of file diff --git a/src/ui/keyboard-binding-ui-handler.ts b/src/ui/keyboard-binding-ui-handler.ts new file mode 100644 index 000000000..ba237215b --- /dev/null +++ b/src/ui/keyboard-binding-ui-handler.ts @@ -0,0 +1,89 @@ +import UiHandler from "#app/ui/ui-handler"; +import BattleScene from "#app/battle-scene"; +import {Mode} from "#app/ui/ui"; +import {Button} from "#app/enums/buttons"; + + +export default class KeyboardBindingUiHandler extends UiHandler { + constructor(scene: BattleScene, mode?: Mode) { + super(scene, mode); + } + + setup() { + const ui = this.getUi(); + } + + updateBindings(): void { + + } + + show(args: any[]): boolean { + super.show(args); + + // 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; + } + + processInput(button: Button): boolean { + const ui = this.getUi(); + return false; + } + + setCursor(cursor: integer): boolean { + const ret = super.setCursor(cursor); + return ret; + } + + setOptionCursor(settingIndex: integer, cursor: integer, save?: boolean): boolean { + return true; + } + + setScrollCursor(scrollCursor: integer): boolean { + return true; + } + + // 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); + + // Remove the cursor from the UI. + this.eraseCursor(); + } + + 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; + } + +} \ No newline at end of file diff --git a/src/ui/settings-gamepad-ui-handler.ts b/src/ui/settings-gamepad-ui-handler.ts index 0e12d9250..f78888238 100644 --- a/src/ui/settings-gamepad-ui-handler.ts +++ b/src/ui/settings-gamepad-ui-handler.ts @@ -78,12 +78,17 @@ export default class SettingsGamepadUiHandler extends UiHandler { gamepadText.setOrigin(0, 0); gamepadText.setPositionRelative(headerBg, 50, 4); + const keyboardText = addTextObject(this.scene, 0, 0, 'Keyboard', TextStyle.SETTINGS_LABEL); + 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); /// Initialize a new configuration "screen" for each type of gamepad. @@ -367,10 +372,13 @@ export default class SettingsGamepadUiHandler extends UiHandler { success = this.setOptionCursor(cursor, this.optionCursors[cursor] + 1, true); break; case Button.CYCLE_FORM: // Change the UI mode to SETTINGS mode. - case Button.CYCLE_SHINY: this.scene.ui.setMode(Mode.SETTINGS) success = true; break; + case Button.CYCLE_SHINY: + this.scene.ui.setMode(Mode.SETTINGS_KEYBOARD) + success = true; + break; } } diff --git a/src/ui/settings-keyboard-ui-handler.ts b/src/ui/settings-keyboard-ui-handler.ts new file mode 100644 index 000000000..6b01be325 --- /dev/null +++ b/src/ui/settings-keyboard-ui-handler.ts @@ -0,0 +1,89 @@ +import UiHandler from "#app/ui/ui-handler"; +import BattleScene from "#app/battle-scene"; +import {Mode} from "#app/ui/ui"; +import {Button} from "#app/enums/buttons"; + + +export default class SettingsKeyboardUiHandler extends UiHandler { + constructor(scene: BattleScene, mode?: Mode) { + super(scene, mode); + } + + setup() { + const ui = this.getUi(); + } + + updateBindings(): void { + + } + + show(args: any[]): boolean { + super.show(args); + + // 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; + } + + processInput(button: Button): boolean { + const ui = this.getUi(); + return false; + } + + setCursor(cursor: integer): boolean { + const ret = super.setCursor(cursor); + return ret; + } + + setOptionCursor(settingIndex: integer, cursor: integer, save?: boolean): boolean { + return true; + } + + setScrollCursor(scrollCursor: integer): boolean { + return true; + } + + // 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); + + // Remove the cursor from the UI. + this.eraseCursor(); + } + + 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; + } + +} \ No newline at end of file diff --git a/src/ui/settings-ui-handler.ts b/src/ui/settings-ui-handler.ts index 678b5bca8..691e5cb8f 100644 --- a/src/ui/settings-ui-handler.ts +++ b/src/ui/settings-ui-handler.ts @@ -50,6 +50,10 @@ export default class SettingsUiHandler extends UiHandler { gamepadText.setOrigin(0, 0); gamepadText.setPositionRelative(headerBg, 50, 4); + const keyboardText = addTextObject(this.scene, 0, 0, 'Keyboard', TextStyle.SETTINGS_LABEL); + 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); @@ -97,6 +101,7 @@ export default class SettingsUiHandler extends UiHandler { this.settingsContainer.add(headerBg); this.settingsContainer.add(headerText); this.settingsContainer.add(gamepadText); + this.settingsContainer.add(keyboardText); this.settingsContainer.add(this.optionsBg); this.settingsContainer.add(this.optionsContainer); @@ -187,11 +192,11 @@ export default class SettingsUiHandler extends UiHandler { if (this.optionCursors[cursor] < this.optionValueLabels[cursor].length - 1) success = this.setOptionCursor(cursor, this.optionCursors[cursor] + 1, true); break; - case Button.CYCLE_FORM: - this.scene.ui.setMode(Mode.SETTINGS_GAMEPAD) + case Button.CYCLE_FORM: // to the left + this.scene.ui.setMode(Mode.SETTINGS_KEYBOARD) success = true; break; - case Button.CYCLE_SHINY: + case Button.CYCLE_SHINY: // to the right this.scene.ui.setMode(Mode.SETTINGS_GAMEPAD) success = true; break; diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 3e6ba9795..2f23a6035 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -38,6 +38,8 @@ import OutdatedModalUiHandler from './outdated-modal-ui-handler'; import SessionReloadModalUiHandler from './session-reload-modal-ui-handler'; import {Button} from "../enums/buttons"; import GamepadBindingUiHandler from "./gamepad-binding-ui-handler"; +import SettingsKeyboardUiHandler from "#app/ui/settings-keyboard-ui-handler"; +import KeyboardBindingUiHandler from "#app/ui/keyboard-binding-ui-handler"; export enum Mode { MESSAGE, @@ -60,6 +62,8 @@ export enum Mode { SETTINGS, SETTINGS_GAMEPAD, GAMEPAD_BINDING, + SETTINGS_KEYBOARD, + KEYBOARD_BINDING, ACHIEVEMENTS, GAME_STATS, VOUCHERS, @@ -145,6 +149,8 @@ export default class UI extends Phaser.GameObjects.Container { new SettingsUiHandler(scene), new SettingsGamepadUiHandler(scene), new GamepadBindingUiHandler(scene), + new SettingsKeyboardUiHandler(scene), + new KeyboardBindingUiHandler(scene), new AchvsUiHandler(scene), new GameStatsUiHandler(scene), new VouchersUiHandler(scene), From d9d455f3175c44e5543495f1087bfa67b3f6724a Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sun, 12 May 2024 16:51:18 +0200 Subject: [PATCH 47/81] create folder for settings in ui folder --- src/battle-scene.ts | 3 --- src/inputs-controller.ts | 2 +- src/system/settings-gamepad.ts | 4 ++-- src/system/settings.ts | 4 ++-- src/ui-inputs.ts | 4 ++-- .../{ => settings}/gamepad-binding-ui-handler.ts | 10 +++++----- .../keyboard-binding-ui-handler.ts | 8 ++++---- .../{ => settings}/option-select-ui-handler.ts | 6 +++--- .../settings-gamepad-ui-handler.ts | 16 ++++++++-------- .../settings-keyboard-ui-handler.ts | 8 ++++---- src/ui/{ => settings}/settings-ui-handler.ts | 16 ++++++++-------- src/ui/title-ui-handler.ts | 2 +- src/ui/ui.ts | 12 ++++++------ 13 files changed, 46 insertions(+), 49 deletions(-) rename src/ui/{ => settings}/gamepad-binding-ui-handler.ts (97%) rename src/ui/{ => settings}/keyboard-binding-ui-handler.ts (94%) rename src/ui/{ => settings}/option-select-ui-handler.ts (59%) rename src/ui/{ => settings}/settings-gamepad-ui-handler.ts (98%) rename src/ui/{ => settings}/settings-keyboard-ui-handler.ts (94%) rename src/ui/{ => settings}/settings-ui-handler.ts (96%) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index c62525595..8590ae479 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -33,9 +33,6 @@ import TrainerData from './system/trainer-data'; import SoundFade from 'phaser3-rex-plugins/plugins/soundfade'; import { pokemonPrevolutions } from './data/pokemon-evolutions'; import PokeballTray from './ui/pokeball-tray'; -import { Setting, settingOptions } from './system/settings'; -import SettingsUiHandler from './ui/settings-ui-handler'; -import MessageUiHandler from './ui/message-ui-handler'; import { Species } from './data/enums/species'; import InvertPostFX from './pipelines/invert'; import { Achv, ModifierAchv, MoneyAchv, achvs } from './system/achv'; diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 744a03447..0ea1ee5f3 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -6,7 +6,7 @@ import pad_xbox360 from "./configs/pad_xbox360"; import pad_dualshock from "./configs/pad_dualshock"; import {Button} from "./enums/buttons"; import {Mode} from "./ui/ui"; -import SettingsGamepadUiHandler from "./ui/settings-gamepad-ui-handler"; +import SettingsGamepadUiHandler from "./ui/settings/settings-gamepad-ui-handler"; import {SettingGamepad} from "./system/settings-gamepad"; import { getCurrenlyAssignedIconFromInputIndex, getCurrentlyAssignedIconToSettingName, diff --git a/src/system/settings-gamepad.ts b/src/system/settings-gamepad.ts index 7c32d5763..488d8ecfa 100644 --- a/src/system/settings-gamepad.ts +++ b/src/system/settings-gamepad.ts @@ -1,7 +1,7 @@ import BattleScene from "../battle-scene"; import {SettingDefaults, SettingOptions} from "./settings"; -import SettingsGamepadUiHandler from "../ui/settings-gamepad-ui-handler"; -import {Mode} from "#app/ui/ui"; +import SettingsGamepadUiHandler from "../ui/settings/settings-gamepad-ui-handler"; +import {Mode} from "../ui/ui"; import {truncateString} from "../utils"; import {Button} from "../enums/buttons"; diff --git a/src/system/settings.ts b/src/system/settings.ts index 017ff2f40..7fee9cc6b 100644 --- a/src/system/settings.ts +++ b/src/system/settings.ts @@ -3,8 +3,8 @@ import BattleScene from "../battle-scene"; import { hasTouchscreen } from "../touch-controls"; import { updateWindowType } from "../ui/ui-theme"; import { PlayerGender } from "./game-data"; -import { Mode } from "#app/ui/ui"; -import SettingsUiHandler from "#app/ui/settings-ui-handler"; +import { Mode } from "../ui/ui"; +import SettingsUiHandler from "../ui/settings/settings-ui-handler"; export enum Setting { Game_Speed = "GAME_SPEED", diff --git a/src/ui-inputs.ts b/src/ui-inputs.ts index e08f05d38..99210b8a4 100644 --- a/src/ui-inputs.ts +++ b/src/ui-inputs.ts @@ -4,9 +4,9 @@ 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 SettingsUiHandler from "./ui/settings-ui-handler"; +import SettingsUiHandler from "./ui/settings/settings-ui-handler"; import {Button} from "./enums/buttons"; -import SettingsGamepadUiHandler from "#app/ui/settings-gamepad-ui-handler"; +import SettingsGamepadUiHandler from "./ui/settings/settings-gamepad-ui-handler"; export interface ActionKeys { [key in Button]: () => void; diff --git a/src/ui/gamepad-binding-ui-handler.ts b/src/ui/settings/gamepad-binding-ui-handler.ts similarity index 97% rename from src/ui/gamepad-binding-ui-handler.ts rename to src/ui/settings/gamepad-binding-ui-handler.ts index 864a63cba..a84ea9cbb 100644 --- a/src/ui/gamepad-binding-ui-handler.ts +++ b/src/ui/settings/gamepad-binding-ui-handler.ts @@ -1,11 +1,11 @@ -import UiHandler from "./ui-handler"; +import UiHandler from "../ui-handler"; import BattleScene from "#app/battle-scene"; -import {Mode} from "./ui"; -import {Button} from "../enums/buttons"; -import {addWindow} from "./ui-theme"; +import {Mode} from "../ui"; +import {Button} from "../../enums/buttons"; +import {addWindow} from "../ui-theme"; import {addTextObject, TextStyle} from "#app/ui/text"; import Phaser from "phaser"; -import {SettingGamepad} from "../system/settings-gamepad"; +import {SettingGamepad} from "../../system/settings-gamepad"; export default class GamepadBindingUiHandler extends UiHandler { diff --git a/src/ui/keyboard-binding-ui-handler.ts b/src/ui/settings/keyboard-binding-ui-handler.ts similarity index 94% rename from src/ui/keyboard-binding-ui-handler.ts rename to src/ui/settings/keyboard-binding-ui-handler.ts index ba237215b..469de1605 100644 --- a/src/ui/keyboard-binding-ui-handler.ts +++ b/src/ui/settings/keyboard-binding-ui-handler.ts @@ -1,7 +1,7 @@ -import UiHandler from "#app/ui/ui-handler"; -import BattleScene from "#app/battle-scene"; -import {Mode} from "#app/ui/ui"; -import {Button} from "#app/enums/buttons"; +import UiHandler from "../ui-handler"; +import BattleScene from "../../battle-scene"; +import {Mode} from "../ui"; +import {Button} from "../../enums/buttons"; export default class KeyboardBindingUiHandler extends UiHandler { diff --git a/src/ui/option-select-ui-handler.ts b/src/ui/settings/option-select-ui-handler.ts similarity index 59% rename from src/ui/option-select-ui-handler.ts rename to src/ui/settings/option-select-ui-handler.ts index 824fa1535..18979dca1 100644 --- a/src/ui/option-select-ui-handler.ts +++ b/src/ui/settings/option-select-ui-handler.ts @@ -1,6 +1,6 @@ -import BattleScene from "../battle-scene"; -import AbstractOptionSelectUiHandler from "./abstact-option-select-ui-handler"; -import { Mode } from "./ui"; +import BattleScene from "../../battle-scene"; +import AbstractOptionSelectUiHandler from "../abstact-option-select-ui-handler"; +import { Mode } from "../ui"; export default class OptionSelectUiHandler extends AbstractOptionSelectUiHandler { constructor(scene: BattleScene, mode: Mode = Mode.OPTION_SELECT) { diff --git a/src/ui/settings-gamepad-ui-handler.ts b/src/ui/settings/settings-gamepad-ui-handler.ts similarity index 98% rename from src/ui/settings-gamepad-ui-handler.ts rename to src/ui/settings/settings-gamepad-ui-handler.ts index f78888238..f7cadc5d1 100644 --- a/src/ui/settings-gamepad-ui-handler.ts +++ b/src/ui/settings/settings-gamepad-ui-handler.ts @@ -1,15 +1,15 @@ -import BattleScene from "../battle-scene"; -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 BattleScene from "../../battle-scene"; +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 { SettingGamepad, settingGamepadDefaults, settingGamepadOptions -} from "../system/settings-gamepad"; -import {truncateString} from "../utils"; +} from "../../system/settings-gamepad"; +import {truncateString} from "../../utils"; import { getCurrentlyAssignedIconToSettingName, getKeyForSettingName diff --git a/src/ui/settings-keyboard-ui-handler.ts b/src/ui/settings/settings-keyboard-ui-handler.ts similarity index 94% rename from src/ui/settings-keyboard-ui-handler.ts rename to src/ui/settings/settings-keyboard-ui-handler.ts index 6b01be325..88bff862c 100644 --- a/src/ui/settings-keyboard-ui-handler.ts +++ b/src/ui/settings/settings-keyboard-ui-handler.ts @@ -1,7 +1,7 @@ -import UiHandler from "#app/ui/ui-handler"; -import BattleScene from "#app/battle-scene"; -import {Mode} from "#app/ui/ui"; -import {Button} from "#app/enums/buttons"; +import UiHandler from "../ui-handler"; +import BattleScene from "../../battle-scene"; +import {Mode} from "../ui"; +import {Button} from "../../enums/buttons"; export default class SettingsKeyboardUiHandler extends UiHandler { diff --git a/src/ui/settings-ui-handler.ts b/src/ui/settings/settings-ui-handler.ts similarity index 96% rename from src/ui/settings-ui-handler.ts rename to src/ui/settings/settings-ui-handler.ts index 691e5cb8f..fdc94724e 100644 --- a/src/ui/settings-ui-handler.ts +++ b/src/ui/settings/settings-ui-handler.ts @@ -1,11 +1,11 @@ -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 { Mode } from "./ui"; -import UiHandler from "./ui-handler"; -import { addWindow } from "./ui-theme"; -import {Button} from "../enums/buttons"; +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 { Mode } from "../ui"; +import UiHandler from "../ui-handler"; +import { addWindow } from "../ui-theme"; +import {Button} from "../../enums/buttons"; export default class SettingsUiHandler extends UiHandler { private settingsContainer: Phaser.GameObjects.Container; diff --git a/src/ui/title-ui-handler.ts b/src/ui/title-ui-handler.ts index c808611b0..eeba02d0f 100644 --- a/src/ui/title-ui-handler.ts +++ b/src/ui/title-ui-handler.ts @@ -1,6 +1,6 @@ import BattleScene from "../battle-scene"; import { DailyRunScoreboard } from "./daily-run-scoreboard"; -import OptionSelectUiHandler from "./option-select-ui-handler"; +import OptionSelectUiHandler from "./settings/option-select-ui-handler"; import { Mode } from "./ui"; import * as Utils from "../utils"; import { TextStyle, addTextObject } from "./text"; diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 2f23a6035..28acad9b5 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -12,13 +12,13 @@ import SummaryUiHandler from './summary-ui-handler'; import StarterSelectUiHandler from './starter-select-ui-handler'; import EvolutionSceneHandler from './evolution-scene-handler'; import TargetSelectUiHandler from './target-select-ui-handler'; -import SettingsUiHandler from './settings-ui-handler'; -import SettingsGamepadUiHandler from "./settings-gamepad-ui-handler"; +import SettingsUiHandler from './settings/settings-ui-handler'; +import SettingsGamepadUiHandler from "./settings/settings-gamepad-ui-handler"; import { TextStyle, addTextObject } from './text'; import AchvBar from './achv-bar'; import MenuUiHandler from './menu-ui-handler'; import AchvsUiHandler from './achvs-ui-handler'; -import OptionSelectUiHandler from './option-select-ui-handler'; +import OptionSelectUiHandler from './settings/option-select-ui-handler'; import EggHatchSceneHandler from './egg-hatch-scene-handler'; import EggListUiHandler from './egg-list-ui-handler'; import EggGachaUiHandler from './egg-gacha-ui-handler'; @@ -37,9 +37,9 @@ import UnavailableModalUiHandler from './unavailable-modal-ui-handler'; import OutdatedModalUiHandler from './outdated-modal-ui-handler'; import SessionReloadModalUiHandler from './session-reload-modal-ui-handler'; import {Button} from "../enums/buttons"; -import GamepadBindingUiHandler from "./gamepad-binding-ui-handler"; -import SettingsKeyboardUiHandler from "#app/ui/settings-keyboard-ui-handler"; -import KeyboardBindingUiHandler from "#app/ui/keyboard-binding-ui-handler"; +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"; export enum Mode { MESSAGE, From c963970adfcc4ae9774eff84a161f074ffd93fc0 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sun, 12 May 2024 21:03:48 +0200 Subject: [PATCH 48/81] added key list in keyboard setting binding --- .../{keyboard.ts => cfg_keyboard_azerty.ts} | 82 +++- src/inputs-controller.ts | 61 ++- src/system/game-data.ts | 24 + src/system/settings-keyboard.ts | 45 +- .../settings/settings-keyboard-ui-handler.ts | 440 ++++++++++++++++-- 5 files changed, 597 insertions(+), 55 deletions(-) rename src/configs/{keyboard.ts => cfg_keyboard_azerty.ts} (60%) 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 From c6d3f93b3f29c8b2a7350ffb648a51bf18ee77d9 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sun, 12 May 2024 21:22:32 +0200 Subject: [PATCH 49/81] first iteration of KeyboardBindingUiHandler --- src/configs/cfg_keyboard_azerty.ts | 1 + src/configs/gamepad-utils.ts | 11 + src/inputs-controller.ts | 14 +- .../settings/keyboard-binding-ui-handler.ts | 225 +++++++++++++----- 4 files changed, 197 insertions(+), 54 deletions(-) diff --git a/src/configs/cfg_keyboard_azerty.ts b/src/configs/cfg_keyboard_azerty.ts index 77dc8b02a..89871772d 100644 --- a/src/configs/cfg_keyboard_azerty.ts +++ b/src/configs/cfg_keyboard_azerty.ts @@ -5,6 +5,7 @@ const cfg_keyboard_azerty = { padID: 'keyboard', padType: 'azerty', gamepadMapping: { + NEED TO ADD KEYCODE FROM EVENT HERE KEY_A: 0, KEY_B: 0, KEY_C: 0, diff --git a/src/configs/gamepad-utils.ts b/src/configs/gamepad-utils.ts index 9e55e35c2..eac1e9598 100644 --- a/src/configs/gamepad-utils.ts +++ b/src/configs/gamepad-utils.ts @@ -10,6 +10,12 @@ export function getKeyFromInputIndex(config: GamepadConfig, index: number): Stri } return null; } +export function getKeyFromKeyboardKey(config: GamepadConfig, key): String | null { + for (const _key of Object.keys(config.gamepadMapping)) { + if (config.gamepadMapping[_key] === key) return _key; + } + return null; +} // Given a setting name, return the key assigned to it from the config file export function getKeyForSettingName(config: GamepadConfig, settingName: string): String | null { @@ -41,6 +47,11 @@ export function getCurrenlyAssignedIconFromInputIndex(config: GamepadConfig, ind return config.icons[key]; } +export function getCurrenlyAssignedIconFromKeyboardKey(config: GamepadConfig, key): String { + const _key = getKeyFromKeyboardKey(config, key); + return config.icons[_key]; +} + // Given a setting name, return the icon currently assigned to this setting name export function getCurrentlyAssignedIconToSettingName(config: GamepadConfig, settingName: string): string { const key = getCurrentlyAssignedToSettingName(config, settingName); diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 3aa61443d..0413953f7 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -10,7 +10,7 @@ import SettingsGamepadUiHandler from "./ui/settings/settings-gamepad-ui-handler" import {SettingGamepad} from "./system/settings-gamepad"; import { getCurrenlyAssignedIconFromInputIndex, getCurrentlyAssignedIconToSettingName, - getKeyFromInputIndex, getCurrentlyAssignedToSettingName + getKeyFromInputIndex, getCurrentlyAssignedToSettingName, getCurrenlyAssignedIconFromKeyboardKey } from "./configs/gamepad-utils"; import SettingsKeyboardUiHandler from "#app/ui/settings/settings-keyboard-ui-handler"; import cfg_keyboard_azerty from "#app/configs/cfg_keyboard_azerty"; @@ -724,6 +724,10 @@ export class InputsController { return [this.configs[this.chosenGamepad].padType, getCurrenlyAssignedIconFromInputIndex(this.configs[this.chosenGamepad], button.index)]; } + getPressedKeyLabel(key): string { + return getCurrenlyAssignedIconFromKeyboardKey(this.configs[this.chosenKeyboard], key); + } + /** * Retrieves the currently assigned icon for a specific setting on the chosen gamepad. * @@ -734,6 +738,10 @@ export class InputsController { return getCurrentlyAssignedIconToSettingName(this.configs[this.chosenGamepad], target); } + getKeyboardCurrentlyAssignedIconToDisplay(target: SettingGamepad): string { + return getCurrentlyAssignedIconToSettingName(this.configs[this.chosenKeyboard], target); + } + /** * Swaps the binding of two controls on the chosen gamepad configuration. * It temporarily pauses updates, swaps the key bindings, saves the new configuration, @@ -754,6 +762,10 @@ export class InputsController { setTimeout(() => this.pauseUpdate = false, 500); } + swapKeyboardBinding(settingName, pressedButton): void { + + } + /** * Injects a custom mapping configuration into the gamepad configuration for a specific gamepad. * If the gamepad does not have an existing configuration, it initializes one first. diff --git a/src/ui/settings/keyboard-binding-ui-handler.ts b/src/ui/settings/keyboard-binding-ui-handler.ts index 469de1605..061a1e983 100644 --- a/src/ui/settings/keyboard-binding-ui-handler.ts +++ b/src/ui/settings/keyboard-binding-ui-handler.ts @@ -2,88 +2,207 @@ import UiHandler from "../ui-handler"; import BattleScene from "../../battle-scene"; import {Mode} from "../ui"; import {Button} from "../../enums/buttons"; +import Phaser from "phaser"; +import {addWindow} from "#app/ui/ui-theme"; +import {addTextObject, TextStyle} from "#app/ui/text"; +import {SettingKeyboard} from "#app/system/settings-keyboard"; export default class KeyboardBindingUiHandler extends UiHandler { + // Containers for different segments of the UI. + protected optionSelectContainer: Phaser.GameObjects.Container; + protected actionsContainer: Phaser.GameObjects.Container; + + // Background elements for titles and action areas. + protected titleBg: Phaser.GameObjects.NineSlice; + protected actionBg: Phaser.GameObjects.NineSlice; + protected optionSelectBg: Phaser.GameObjects.NineSlice; + + // Text elements for displaying instructions and actions. + private unlockText: Phaser.GameObjects.Text; + private swapText: Phaser.GameObjects.Text; + private actionLabel: Phaser.GameObjects.Text; + private cancelLabel: Phaser.GameObjects.Text; + + private listening: boolean = false; + private buttonPressed: number | null = null; + + // Icons for displaying current and new button assignments. + private newButtonIcon: Phaser.GameObjects.Sprite; + private targetButtonIcon: Phaser.GameObjects.Sprite; + + // Function to call on cancel or completion of binding. + private cancelFn: (boolean?) => boolean; + + // The specific setting being modified. + private target: SettingKeyboard; constructor(scene: BattleScene, mode?: Mode) { super(scene, mode); + // Listen to gamepad button down events to initiate binding. + scene.input.keyboard.on('keydown', this.onKeyDown, this); } setup() { const ui = this.getUi(); + this.optionSelectContainer = this.scene.add.container(0, 0); + this.actionsContainer = this.scene.add.container(0, 0); + // Initially, containers are not visible. + this.optionSelectContainer.setVisible(false); + this.actionsContainer.setVisible(false); + + // Add containers to the UI. + ui.add(this.optionSelectContainer); + ui.add(this.actionsContainer); + + // Setup backgrounds and text objects for UI. + this.titleBg = addWindow(this.scene, (this.scene.game.canvas.width / 6) - this.getWindowWidth(), -(this.scene.game.canvas.height / 6) + 28 + 21, this.getWindowWidth(), 24); + this.titleBg.setOrigin(0.5); + this.optionSelectContainer.add(this.titleBg); + + this.actionBg = addWindow(this.scene, (this.scene.game.canvas.width / 6) - this.getWindowWidth(), -(this.scene.game.canvas.height / 6) + this.getWindowHeight() + 28 + 21 + 21, this.getWindowWidth(), 24); + this.actionBg.setOrigin(0.5); + this.actionsContainer.add(this.actionBg); + + // Text prompts and instructions for the user. + this.unlockText = addTextObject(this.scene, 0, 0, 'Press a button...', TextStyle.WINDOW); + this.unlockText.setOrigin(0, 0); + this.unlockText.setPositionRelative(this.titleBg, 36, 4); + this.optionSelectContainer.add(this.unlockText); + + this.optionSelectBg = addWindow(this.scene, (this.scene.game.canvas.width / 6) - this.getWindowWidth(), -(this.scene.game.canvas.height / 6) + this.getWindowHeight() + 28, this.getWindowWidth(), this.getWindowHeight()); + this.optionSelectBg.setOrigin(0.5); + this.optionSelectContainer.add(this.optionSelectBg); + + // New button icon setup. + this.newButtonIcon = this.scene.add.sprite(0, 0, 'keyboard'); + this.newButtonIcon.setScale(0.15); + this.newButtonIcon.setPositionRelative(this.optionSelectBg, 78, 16); + this.newButtonIcon.setOrigin(0.5); + this.newButtonIcon.setVisible(false); + + this.swapText = addTextObject(this.scene, 0, 0, 'will swap with', TextStyle.WINDOW); + this.swapText.setOrigin(0.5); + this.swapText.setPositionRelative(this.optionSelectBg, this.optionSelectBg.width / 2 - 2, this.optionSelectBg.height / 2 - 2); + this.swapText.setVisible(false); + + this.targetButtonIcon = this.scene.add.sprite(0, 0, 'keyboard'); + this.targetButtonIcon.setScale(0.15); + this.targetButtonIcon.setPositionRelative(this.optionSelectBg, 78, 48); + this.targetButtonIcon.setOrigin(0.5); + this.targetButtonIcon.setVisible(false); + + this.cancelLabel = addTextObject(this.scene, 0, 0, 'Cancel', TextStyle.SETTINGS_LABEL); + this.cancelLabel.setOrigin(0, 0.5); + this.cancelLabel.setPositionRelative(this.actionBg, 10, this.actionBg.height / 2); + + this.actionLabel = addTextObject(this.scene, 0, 0, 'Confirm Swap', TextStyle.SETTINGS_LABEL); + this.actionLabel.setOrigin(0, 0.5); + this.actionLabel.setPositionRelative(this.actionBg, this.actionBg.width - 75, this.actionBg.height / 2); + + // Add swap and cancel labels to the containers. + this.optionSelectContainer.add(this.newButtonIcon); + this.optionSelectContainer.add(this.swapText); + this.optionSelectContainer.add(this.targetButtonIcon); + this.actionsContainer.add(this.actionLabel); + this.actionsContainer.add(this.cancelLabel); } - updateBindings(): void { - + onKeyDown(event): void { + const key = event.keyCode; + // // Check conditions before processing the button press. + if (!this.listening || this.buttonPressed !== null) return; + this.buttonPressed = key; + const buttonIcon = this.scene.inputController.getPressedKeyLabel(key); + const assignedButtonIcon = this.scene.inputController.getKeyboardCurrentlyAssignedIconToDisplay(this.target); + this.newButtonIcon.setFrame(buttonIcon); + this.targetButtonIcon.setFrame(assignedButtonIcon); + this.newButtonIcon.setVisible(true); + this.targetButtonIcon.setVisible(true); + this.swapText.setVisible(true); + this.setCursor(0); + this.actionsContainer.setVisible(true); } show(args: any[]): boolean { super.show(args); + this.buttonPressed = null; + this.cancelFn = args[0].cancelHandler; + this.target = args[0].target; - // 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); + // Bring the option and action containers to the front of the UI. + this.getUi().bringToTop(this.optionSelectContainer); + this.getUi().bringToTop(this.actionsContainer); - // Hide any tooltips that might be visible before showing the settings container. - this.getUi().hideTooltip(); - - // Return true to indicate the UI was successfully shown. + this.optionSelectContainer.setVisible(true); + setTimeout(() => this.listening = true, 150); return true; } + getWindowWidth(): number { + return 160; + } + + getWindowHeight(): number { + return 64; + } + processInput(button: Button): boolean { + if (this.buttonPressed === null) return; const ui = this.getUi(); - return false; + let success = false; + switch (button) { + case Button.LEFT: + case Button.RIGHT: + // Toggle between action and cancel options. + const cursor = this.cursor ? 0 : 1; + success = this.setCursor(cursor); + break + case Button.ACTION: + // Process actions based on current cursor position. + if (this.cursor === 0) { + this.cancelFn(); + } else { + success = true; + this.scene.inputController.swapKeyboardBinding(this.target, this.buttonPressed); + this.cancelFn(success); + } + break; + } + + // Plays a select sound effect if an action was successfully processed. + if (success) + ui.playSelect(); + else + ui.playError(); + + return success; } setCursor(cursor: integer): boolean { - const ret = super.setCursor(cursor); - return ret; - } - - setOptionCursor(settingIndex: integer, cursor: integer, save?: boolean): boolean { + this.cursor = cursor; + if (cursor === 1) { + this.actionLabel.setColor(this.getTextColor(TextStyle.SETTINGS_SELECTED)); + this.actionLabel.setShadowColor(this.getTextColor(TextStyle.SETTINGS_SELECTED, true)); + this.cancelLabel.setColor(this.getTextColor(TextStyle.WINDOW)); + this.cancelLabel.setShadowColor(this.getTextColor(TextStyle.WINDOW, true)); + return true; + } + this.actionLabel.setColor(this.getTextColor(TextStyle.WINDOW)); + this.actionLabel.setShadowColor(this.getTextColor(TextStyle.WINDOW, true)); + this.cancelLabel.setColor(this.getTextColor(TextStyle.SETTINGS_SELECTED)); + this.cancelLabel.setShadowColor(this.getTextColor(TextStyle.SETTINGS_SELECTED, true)); return true; } - setScrollCursor(scrollCursor: integer): boolean { - return true; - } - - // 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 { + 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(); - } - - 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; + this.target = null; + this.cancelFn = null; + this.optionSelectContainer.setVisible(false); + this.actionsContainer.setVisible(false); + this.newButtonIcon.setVisible(false); + this.targetButtonIcon.setVisible(false); + this.swapText.setVisible(false); } } \ No newline at end of file From 164838c66953fcf50d6aa5d11c132505c311bfa5 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sun, 12 May 2024 23:11:20 +0200 Subject: [PATCH 50/81] input detection for keyboards is working --- src/configs/cfg_keyboard_azerty.ts | 155 +++++++++--------- src/inputs-controller.ts | 7 +- .../settings/keyboard-binding-ui-handler.ts | 1 + .../settings/settings-gamepad-ui-handler.ts | 6 +- 4 files changed, 85 insertions(+), 84 deletions(-) diff --git a/src/configs/cfg_keyboard_azerty.ts b/src/configs/cfg_keyboard_azerty.ts index 89871772d..a55f61acf 100644 --- a/src/configs/cfg_keyboard_azerty.ts +++ b/src/configs/cfg_keyboard_azerty.ts @@ -5,81 +5,79 @@ const cfg_keyboard_azerty = { padID: 'keyboard', padType: 'azerty', gamepadMapping: { - NEED TO ADD KEYCODE FROM EVENT HERE - KEY_A: 0, - KEY_B: 0, - KEY_C: 0, - KEY_D: 0, - KEY_E: 0, - KEY_F: 0, - KEY_G: 0, - KEY_H: 0, - KEY_I: 0, - KEY_J: 0, - KEY_K: 0, - KEY_L: 0, - KEY_M: 0, - KEY_N: 0, - KEY_O: 0, - KEY_P: 0, - KEY_Q: 0, - KEY_R: 0, - KEY_S: 0, - KEY_T: 0, - KEY_U: 0, - KEY_V: 0, - KEY_W: 0, - KEY_X: 0, - KEY_Y: 0, - KEY_Z: 0, - KEY_0: 0, - KEY_1: 0, - KEY_2: 0, - KEY_3: 0, - KEY_4: 0, - KEY_5: 0, - KEY_6: 0, - KEY_7: 0, - KEY_8: 0, - KEY_9: 0, - KEY_CTRL: 0, - KEY_DEL: 0, - KEY_END: 0, - KEY_ENTER: 0, - KEY_ESC: 0, - KEY_F1: 0, - KEY_F2: 0, - KEY_F3: 0, - KEY_F4: 0, - KEY_F5: 0, - KEY_F6: 0, - KEY_F7: 0, - KEY_F8: 0, - KEY_F9: 0, - KEY_F10: 0, - KEY_F11: 0, - KEY_F12: 0, - KEY_HOME: 0, - KEY_INSERT: 0, - KEY_PAGE_DOWN: 0, - KEY_PAGE_UP: 0, - KEY_PLUS: 0, - KEY_MINUS: 0, - KEY_QUOTATION: 0, - KEY_SHIFT: 0, - KEY_LEFT_SHIFT: 0, - KEY_SPACE: 0, - KEY_TAB: 0, - KEY_TILDE: 0, - KEY_ARROW_UP: 0, - KEY_ARROW_DOWN: 0, - KEY_ARROW_LEFT: 0, - KEY_ARROW_RIGHT: 0, - KEY_LEFT_BRACKET: 0, - KEY_RIGHT_BRACKET: 0, - KEY_SEMICOLON: 0, - KEY_ASTERISK: 0, - T_Backspace_Alt_Key_Dark: 0 + KEY_A: Phaser.Input.Keyboard.KeyCodes.A, + KEY_B: Phaser.Input.Keyboard.KeyCodes.B, + KEY_C: Phaser.Input.Keyboard.KeyCodes.C, + KEY_D: Phaser.Input.Keyboard.KeyCodes.D, + KEY_E: Phaser.Input.Keyboard.KeyCodes.E, + KEY_F: Phaser.Input.Keyboard.KeyCodes.F, + KEY_G: Phaser.Input.Keyboard.KeyCodes.G, + KEY_H: Phaser.Input.Keyboard.KeyCodes.H, + KEY_I: Phaser.Input.Keyboard.KeyCodes.I, + KEY_J: Phaser.Input.Keyboard.KeyCodes.J, + KEY_K: Phaser.Input.Keyboard.KeyCodes.K, + KEY_L: Phaser.Input.Keyboard.KeyCodes.L, + KEY_M: Phaser.Input.Keyboard.KeyCodes.M, + KEY_N: Phaser.Input.Keyboard.KeyCodes.N, + KEY_O: Phaser.Input.Keyboard.KeyCodes.O, + KEY_P: Phaser.Input.Keyboard.KeyCodes.P, + KEY_Q: Phaser.Input.Keyboard.KeyCodes.Q, + KEY_R: Phaser.Input.Keyboard.KeyCodes.R, + KEY_S: Phaser.Input.Keyboard.KeyCodes.S, + KEY_T: Phaser.Input.Keyboard.KeyCodes.T, + KEY_U: Phaser.Input.Keyboard.KeyCodes.U, + KEY_V: Phaser.Input.Keyboard.KeyCodes.V, + KEY_W: Phaser.Input.Keyboard.KeyCodes.W, + KEY_X: Phaser.Input.Keyboard.KeyCodes.X, + KEY_Y: Phaser.Input.Keyboard.KeyCodes.Y, + KEY_Z: Phaser.Input.Keyboard.KeyCodes.Z, + KEY_0: Phaser.Input.Keyboard.KeyCodes.ZERO, + KEY_1: Phaser.Input.Keyboard.KeyCodes.ONE, + KEY_2: Phaser.Input.Keyboard.KeyCodes.TWO, + KEY_3: Phaser.Input.Keyboard.KeyCodes.THREE, + KEY_4: Phaser.Input.Keyboard.KeyCodes.FOUR, + KEY_5: Phaser.Input.Keyboard.KeyCodes.FIVE, + KEY_6: Phaser.Input.Keyboard.KeyCodes.SIX, + KEY_7: Phaser.Input.Keyboard.KeyCodes.SEVEN, + KEY_8: Phaser.Input.Keyboard.KeyCodes.EIGHT, + KEY_9: Phaser.Input.Keyboard.KeyCodes.NINE, + KEY_CTRL: Phaser.Input.Keyboard.KeyCodes.CTRL, + KEY_DEL: Phaser.Input.Keyboard.KeyCodes.DELETE, + KEY_END: Phaser.Input.Keyboard.KeyCodes.END, + KEY_ENTER: Phaser.Input.Keyboard.KeyCodes.ENTER, + KEY_ESC: Phaser.Input.Keyboard.KeyCodes.ESC, + KEY_F1: Phaser.Input.Keyboard.KeyCodes.F1, + KEY_F2: Phaser.Input.Keyboard.KeyCodes.F2, + KEY_F3: Phaser.Input.Keyboard.KeyCodes.F3, + KEY_F4: Phaser.Input.Keyboard.KeyCodes.F4, + KEY_F5: Phaser.Input.Keyboard.KeyCodes.F5, + KEY_F6: Phaser.Input.Keyboard.KeyCodes.F6, + KEY_F7: Phaser.Input.Keyboard.KeyCodes.F7, + KEY_F8: Phaser.Input.Keyboard.KeyCodes.F8, + KEY_F9: Phaser.Input.Keyboard.KeyCodes.F9, + KEY_F10: Phaser.Input.Keyboard.KeyCodes.F10, + KEY_F11: Phaser.Input.Keyboard.KeyCodes.F11, + KEY_F12: Phaser.Input.Keyboard.KeyCodes.F12, + KEY_HOME: Phaser.Input.Keyboard.KeyCodes.HOME, + KEY_INSERT: Phaser.Input.Keyboard.KeyCodes.INSERT, + KEY_PAGE_DOWN: Phaser.Input.Keyboard.KeyCodes.PAGE_DOWN, + KEY_PAGE_UP: Phaser.Input.Keyboard.KeyCodes.PAGE_UP, + KEY_PLUS: Phaser.Input.Keyboard.KeyCodes.NUMPAD_ADD, // Assuming numpad plus + KEY_MINUS: Phaser.Input.Keyboard.KeyCodes.NUMPAD_SUBTRACT, // Assuming numpad minus + KEY_QUOTATION: Phaser.Input.Keyboard.KeyCodes.QUOTES, + KEY_SHIFT: Phaser.Input.Keyboard.KeyCodes.SHIFT, + KEY_SPACE: Phaser.Input.Keyboard.KeyCodes.SPACE, + KEY_TAB: Phaser.Input.Keyboard.KeyCodes.TAB, + KEY_TILDE: Phaser.Input.Keyboard.KeyCodes.BACKTICK, + KEY_ARROW_UP: Phaser.Input.Keyboard.KeyCodes.UP, + KEY_ARROW_DOWN: Phaser.Input.Keyboard.KeyCodes.DOWN, + KEY_ARROW_LEFT: Phaser.Input.Keyboard.KeyCodes.LEFT, + KEY_ARROW_RIGHT: Phaser.Input.Keyboard.KeyCodes.RIGHT, + KEY_LEFT_BRACKET: Phaser.Input.Keyboard.KeyCodes.OPEN_BRACKET, + KEY_RIGHT_BRACKET: Phaser.Input.Keyboard.KeyCodes.CLOSED_BRACKET, + KEY_SEMICOLON: Phaser.Input.Keyboard.KeyCodes.SEMICOLON, + KEY_BACKSPACE: Phaser.Input.Keyboard.KeyCodes.BACKSPACE, + KEY_ALT: Phaser.Input.Keyboard.KeyCodes.ALT }, icons: { KEY_A: "T_A_Key_Dark.png", @@ -143,13 +141,12 @@ const cfg_keyboard_azerty = { 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_INSERT: "T_Ins_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", @@ -164,9 +161,9 @@ const cfg_keyboard_azerty = { KEY_RIGHT_BRACKET: "T_Brackets_R_Key_Dark.png", KEY_SEMICOLON: "T_Semicolon_Key_Dark.png", - KEY_ASTERISK: "T_Asterisk_Key_Dark.png", - KEY_BACKSPACE: "T_Backspace_Alt_Key_Dark.png" + KEY_BACKSPACE: "T_Backspace_Alt_Key_Dark.png", + KEY_ALT: "T_Alt_Key_Dark.png" }, setting: { KEY_ARROW_UP: SettingKeyboard.Button_Up, diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 0413953f7..9e0611691 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -14,6 +14,7 @@ import { } from "./configs/gamepad-utils"; import SettingsKeyboardUiHandler from "#app/ui/settings/settings-keyboard-ui-handler"; import cfg_keyboard_azerty from "#app/configs/cfg_keyboard_azerty"; +import {SettingKeyboard} from "#app/system/settings-keyboard"; export interface GamepadMapping { [key: string]: number; @@ -725,7 +726,7 @@ export class InputsController { } getPressedKeyLabel(key): string { - return getCurrenlyAssignedIconFromKeyboardKey(this.configs[this.chosenKeyboard], key); + return getCurrenlyAssignedIconFromKeyboardKey(this.keyboardConfigs[this.chosenKeyboard], key); } /** @@ -738,8 +739,8 @@ export class InputsController { return getCurrentlyAssignedIconToSettingName(this.configs[this.chosenGamepad], target); } - getKeyboardCurrentlyAssignedIconToDisplay(target: SettingGamepad): string { - return getCurrentlyAssignedIconToSettingName(this.configs[this.chosenKeyboard], target); + getKeyboardCurrentlyAssignedIconToDisplay(target: SettingKeyboard): string { + return getCurrentlyAssignedIconToSettingName(this.keyboardConfigs[this.chosenKeyboard], target); } /** diff --git a/src/ui/settings/keyboard-binding-ui-handler.ts b/src/ui/settings/keyboard-binding-ui-handler.ts index 061a1e983..fb0edc094 100644 --- a/src/ui/settings/keyboard-binding-ui-handler.ts +++ b/src/ui/settings/keyboard-binding-ui-handler.ts @@ -113,6 +113,7 @@ export default class KeyboardBindingUiHandler extends UiHandler { if (!this.listening || this.buttonPressed !== null) return; this.buttonPressed = key; const buttonIcon = this.scene.inputController.getPressedKeyLabel(key); + if (!buttonIcon) return; const assignedButtonIcon = this.scene.inputController.getKeyboardCurrentlyAssignedIconToDisplay(this.target); this.newButtonIcon.setFrame(buttonIcon); this.targetButtonIcon.setFrame(assignedButtonIcon); diff --git a/src/ui/settings/settings-gamepad-ui-handler.ts b/src/ui/settings/settings-gamepad-ui-handler.ts index f7cadc5d1..8c81f651d 100644 --- a/src/ui/settings/settings-gamepad-ui-handler.ts +++ b/src/ui/settings/settings-gamepad-ui-handler.ts @@ -332,6 +332,7 @@ export default class SettingsGamepadUiHandler extends UiHandler { const cursor = this.cursor + this.scrollCursor; // Calculate the absolute cursor position. switch (button) { 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); @@ -347,6 +348,7 @@ export default class SettingsGamepadUiHandler extends UiHandler { } break; case Button.DOWN: // Move down in the menu. + if (!this.optionValueLabels) return false; if (cursor < this.optionValueLabels.length - 1) { if (this.cursor < rowsToDisplay - 1) success = this.setCursor(this.cursor + 1); @@ -362,12 +364,12 @@ export default class SettingsGamepadUiHandler extends UiHandler { } break; case Button.LEFT: // Move selection left within the current option set. - if (!this.optionCursors) return; + if (!this.optionCursors || !this.optionValueLabels) 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 || !this.optionValueLabels) return; if (this.optionCursors[cursor] < this.optionValueLabels[cursor].length - 1) success = this.setOptionCursor(cursor, this.optionCursors[cursor] + 1, true); break; From 8205df46df883e4c7ff2bf06fbbe2a9c1033467e Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sun, 12 May 2024 23:28:02 +0200 Subject: [PATCH 51/81] fix transition bug --- src/inputs-controller.ts | 2 +- src/ui/ui.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 9e0611691..f9dee0983 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -764,7 +764,7 @@ export class InputsController { } swapKeyboardBinding(settingName, pressedButton): void { - + console.log('swap'); } /** diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 28acad9b5..0f2ac2447 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -95,8 +95,10 @@ const noTransitionModes = [ Mode.MENU, Mode.MENU_OPTION_SELECT, Mode.GAMEPAD_BINDING, + Mode.KEYBOARD_BINDING, Mode.SETTINGS, Mode.SETTINGS_GAMEPAD, + Mode.SETTINGS_KEYBOARD, Mode.ACHIEVEMENTS, Mode.GAME_STATS, Mode.VOUCHERS, From f8da26f203c661b61e9626d61ab513ef57a24193 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Sun, 12 May 2024 23:36:06 +0200 Subject: [PATCH 52/81] fix menu keyboard F and R --- src/ui-inputs.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ui-inputs.ts b/src/ui-inputs.ts index 99210b8a4..b2d820eb5 100644 --- a/src/ui-inputs.ts +++ b/src/ui-inputs.ts @@ -7,6 +7,7 @@ import {Setting, settingOptions} from "./system/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"; export interface ActionKeys { [key in Button]: () => void; @@ -131,7 +132,7 @@ export class UiInputs { } buttonCycleOption(button: Button): void { - const whitelist = [StarterSelectUiHandler, SettingsUiHandler, SettingsGamepadUiHandler]; + const whitelist = [StarterSelectUiHandler, SettingsUiHandler, SettingsGamepadUiHandler, SettingsKeyboardUiHandler]; const uiHandler = this.scene.ui?.getHandler(); if (whitelist.some(handler => uiHandler instanceof handler)) { this.scene.ui.processInput(button); From f3d5a2a84e98a6165997d6d23ffd0e99600ccf91 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Mon, 13 May 2024 00:50:27 +0200 Subject: [PATCH 53/81] created abstract-settings-ui-handler to gather common code --- .../settings/abstract-settings-ui-handler.ts | 177 ++++++++++++++++++ .../settings/settings-gamepad-ui-handler.ts | 169 +---------------- .../settings/settings-keyboard-ui-handler.ts | 145 +------------- 3 files changed, 185 insertions(+), 306 deletions(-) create mode 100644 src/ui/settings/abstract-settings-ui-handler.ts diff --git a/src/ui/settings/abstract-settings-ui-handler.ts b/src/ui/settings/abstract-settings-ui-handler.ts new file mode 100644 index 000000000..ca2222e3b --- /dev/null +++ b/src/ui/settings/abstract-settings-ui-handler.ts @@ -0,0 +1,177 @@ +import UiHandler from "#app/ui/ui-handler"; +import BattleScene from "#app/battle-scene"; +import {Mode} from "#app/ui/ui"; +import {GamepadConfig} from "#app/inputs-controller"; + +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; +} + +export default abstract class AbstractSettingsUiUiHandler extends UiHandler { + protected settingsContainer: Phaser.GameObjects.Container; + protected optionsContainer: Phaser.GameObjects.Container; + + protected scrollCursor: integer; + protected optionCursors: integer[]; + protected cursorObj: Phaser.GameObjects.NineSlice; + + protected optionsBg: 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; + // 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; + + constructor(scene: BattleScene, mode?: Mode) { + super(scene, mode); + } + + show(args: any[]): boolean { + super.show(args); + + // 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.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); + + // 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; + } + + setLayout(activeConfig: GamepadConfig): 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; + + // 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; + } + + 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. + } + + 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. + } + + 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); + + // Remove the cursor from the UI. + this.eraseCursor(); + } + + 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; + } + +} \ No newline at end of file diff --git a/src/ui/settings/settings-gamepad-ui-handler.ts b/src/ui/settings/settings-gamepad-ui-handler.ts index 8c81f651d..792e68c1c 100644 --- a/src/ui/settings/settings-gamepad-ui-handler.ts +++ b/src/ui/settings/settings-gamepad-ui-handler.ts @@ -1,60 +1,18 @@ import BattleScene from "../../battle-scene"; -import {TextStyle, addTextObject} from "../text"; +import {addTextObject, TextStyle} from "../text"; import {Mode} from "../ui"; -import UiHandler from "../ui-handler"; import {addWindow} from "../ui-theme"; import {Button} from "../../enums/buttons"; -import { - SettingGamepad, - settingGamepadDefaults, - settingGamepadOptions -} from "../../system/settings-gamepad"; +import {SettingGamepad, settingGamepadDefaults, settingGamepadOptions} from "../../system/settings-gamepad"; import {truncateString} from "../../utils"; -import { - getCurrentlyAssignedIconToSettingName, - getKeyForSettingName -} from "#app/configs/gamepad-utils"; +import {getCurrentlyAssignedIconToSettingName, getKeyForSettingName} from "#app/configs/gamepad-utils"; import pad_xbox360 from "#app/configs/pad_xbox360"; import pad_dualshock from "#app/configs/pad_dualshock"; import pad_unlicensedSNES from "#app/configs/pad_unlicensedSNES"; import {GamepadConfig} from "#app/inputs-controller"; +import AbstractSettingsUiUiHandler, {InputsIcons} from "#app/ui/settings/abstract-settings-ui-handler"; -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; -} - -export default class SettingsGamepadUiHandler 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; +export default class SettingsGamepadUiHandler extends AbstractSettingsUiUiHandler { constructor(scene: BattleScene, mode?: Mode) { super(scene, mode); @@ -259,27 +217,6 @@ export default class SettingsGamepadUiHandler extends UiHandler { this.setScrollCursor(0); } - show(args: any[]): boolean { - super.show(args); - - // 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.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); - - // 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; - } - setLayout(activeConfig: GamepadConfig): boolean { // Check if there is no active configuration (e.g., no gamepad connected). if (!activeConfig) { @@ -290,30 +227,8 @@ export default class SettingsGamepadUiHandler extends UiHandler { // 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; - - // 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; + return super.setLayout(activeConfig); } processInput(button: Button): boolean { @@ -391,24 +306,6 @@ export default class SettingsGamepadUiHandler extends UiHandler { return success; // Return whether the input resulted in a successful action. } - 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. - } - updateChosenGamepadDisplay(): void { // Update any bindings that might have changed since the last update. this.updateBindings(); @@ -463,58 +360,4 @@ export default class SettingsGamepadUiHandler extends UiHandler { return true; // Return true to indicate the cursor was successfully updated. } - 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. - } - - 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); - - // Remove the cursor from the UI. - this.eraseCursor(); - } - - 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; - } } \ No newline at end of file diff --git a/src/ui/settings/settings-keyboard-ui-handler.ts b/src/ui/settings/settings-keyboard-ui-handler.ts index eb6a0a374..6441b6f54 100644 --- a/src/ui/settings/settings-keyboard-ui-handler.ts +++ b/src/ui/settings/settings-keyboard-ui-handler.ts @@ -1,40 +1,17 @@ -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"; +import AbstractSettingsUiUiHandler, {InputsIcons} from "#app/ui/settings/abstract-settings-ui-handler"; -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; - +export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandler { constructor(scene: BattleScene, mode?: Mode) { super(scene, mode); } @@ -224,51 +201,6 @@ export default class SettingsKeyboardUiHandler extends UiHandler { } - 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); - - // 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; - } - - 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(); // Defines the maximum number of rows that can be displayed on the screen. @@ -342,24 +274,6 @@ export default class SettingsKeyboardUiHandler extends UiHandler { return success; // Return whether the input resulted in a successful action. } - 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. - } - updateChosenKeyboardDisplay(): void { // Update any bindings that might have changed since the last update. this.updateBindings(); @@ -413,59 +327,4 @@ export default class SettingsKeyboardUiHandler extends UiHandler { return true; // Return true to indicate the cursor was successfully updated. } - 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. - } - - 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); - - // Remove the cursor from the UI. - this.eraseCursor(); - } - - 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; - } - } \ No newline at end of file From 43b459aba14a713a0489f3939feff143cd07df31 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Mon, 13 May 2024 01:37:33 +0200 Subject: [PATCH 54/81] finished the refactor to use abstract-settings-ui-handler --- src/inputs-controller.ts | 2 + .../settings/abstract-settings-ui-handler.ts | 310 ++++++++++++++++- .../settings/settings-gamepad-ui-handler.ts | 317 ++---------------- .../settings/settings-keyboard-ui-handler.ts | 317 ++---------------- 4 files changed, 366 insertions(+), 580 deletions(-) diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index f9dee0983..797e75132 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -391,6 +391,8 @@ export class InputsController { * @param value The intensity or value of the button press, if applicable. */ gamepadButtonDown(pad: Phaser.Input.Gamepad.Gamepad, button: Phaser.Input.Gamepad.Button, value: number): void { + if (!this.keyboardConfigs[this.chosenKeyboard]?.padID) + this.setupKeyboard(); if (!pad) return; if (!this.chosenGamepad) this.setChosenGamepad(pad.id); diff --git a/src/ui/settings/abstract-settings-ui-handler.ts b/src/ui/settings/abstract-settings-ui-handler.ts index ca2222e3b..c9392e060 100644 --- a/src/ui/settings/abstract-settings-ui-handler.ts +++ b/src/ui/settings/abstract-settings-ui-handler.ts @@ -2,6 +2,10 @@ import UiHandler from "#app/ui/ui-handler"; import BattleScene from "#app/battle-scene"; import {Mode} from "#app/ui/ui"; import {GamepadConfig} from "#app/inputs-controller"; +import {addWindow} from "#app/ui/ui-theme"; +import {addTextObject, TextStyle} from "#app/ui/text"; +import {getCurrentlyAssignedIconToSettingName, getKeyForSettingName} from "#app/configs/gamepad-utils"; +import {Button} from "#app/enums/buttons"; export interface InputsIcons { [key: string]: Phaser.GameObjects.Sprite; @@ -40,10 +44,211 @@ export default abstract class AbstractSettingsUiUiHandler extends UiHandler { // Store the specific settings related to key bindings for the current gamepad configuration. protected bindingSettings: Array; + protected settingDevice; + protected settingDeviceDefaults; + protected settingDeviceOptions; + protected configs; + protected commonSettingsCount; + protected textureOverride; + protected titleSelected; + protected localStoragePropertyName; + + abstract getLocalStorageSetting(): object; + abstract navigateMenuLeft(): boolean; + abstract navigateMenuRight(): boolean; + abstract saveSettingToLocalStorage(setting, cursor): void; + abstract getActiveConfig(): void; + 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', this.titleSelected === 'Gamepad' ? TextStyle.SETTINGS_SELECTED : TextStyle.SETTINGS_LABEL); + gamepadText.setOrigin(0, 0); + gamepadText.setPositionRelative(headerBg, 50, 4); + + const keyboardText = addTextObject(this.scene, 0, 0, 'Keyboard', this.titleSelected === 'Keyboard' ? TextStyle.SETTINGS_SELECTED : TextStyle.SETTINGS_LABEL); + 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); + + /// 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 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(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.setting).map(k => config.setting[k])]; + // 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. + 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 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, 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, this.settingDevice[setting]); + const icon = this.scene.add.sprite(0, 0, this.textureOverride ? this.textureOverride : config.padType); + 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, this.settingDeviceDefaults[this.settingDevice[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.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) ? 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); @@ -100,6 +305,79 @@ export default abstract class AbstractSettingsUiUiHandler extends UiHandler { return true; } + processInput(button: Button): boolean { + const ui = this.getUi(); + // 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 (!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(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 (!this.optionValueLabels) return false; + 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 || !this.optionValueLabels) 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 || !this.optionValueLabels) return; + if (this.optionCursors[cursor] < this.optionValueLabels[cursor].length - 1) + success = this.setOptionCursor(cursor, this.optionCursors[cursor] + 1, true); + break; + case Button.CYCLE_FORM: + success = this.navigateMenuLeft(); + break; + case Button.CYCLE_SHINY: + success = this.navigateMenuRight(); + 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); // If the optionsContainer is not initialized, return the result from the parent class directly. @@ -135,6 +413,37 @@ export default abstract class AbstractSettingsUiUiHandler extends UiHandler { return true; // Return true to indicate the scroll cursor was successfully updated. } + 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 and the setting is not the default controller setting, save the setting to local storage + if (save) { + this.saveSettingToLocalStorage(setting, cursor); + } + + return true; // Return true to indicate the cursor was successfully updated. + } + updateSettingsScroll(): void { // Return immediately if the options container is not initialized. if (!this.optionsContainer) return; @@ -154,7 +463,6 @@ export default abstract class AbstractSettingsUiUiHandler extends UiHandler { } } - clear(): void { super.clear(); diff --git a/src/ui/settings/settings-gamepad-ui-handler.ts b/src/ui/settings/settings-gamepad-ui-handler.ts index 792e68c1c..a63023bdc 100644 --- a/src/ui/settings/settings-gamepad-ui-handler.ts +++ b/src/ui/settings/settings-gamepad-ui-handler.ts @@ -1,169 +1,29 @@ import BattleScene from "../../battle-scene"; import {addTextObject, TextStyle} from "../text"; import {Mode} from "../ui"; -import {addWindow} from "../ui-theme"; -import {Button} from "../../enums/buttons"; import {SettingGamepad, settingGamepadDefaults, settingGamepadOptions} from "../../system/settings-gamepad"; import {truncateString} from "../../utils"; -import {getCurrentlyAssignedIconToSettingName, getKeyForSettingName} from "#app/configs/gamepad-utils"; import pad_xbox360 from "#app/configs/pad_xbox360"; import pad_dualshock from "#app/configs/pad_dualshock"; import pad_unlicensedSNES from "#app/configs/pad_unlicensedSNES"; import {GamepadConfig} from "#app/inputs-controller"; -import AbstractSettingsUiUiHandler, {InputsIcons} from "#app/ui/settings/abstract-settings-ui-handler"; +import AbstractSettingsUiUiHandler from "#app/ui/settings/abstract-settings-ui-handler"; export default class SettingsGamepadUiHandler extends AbstractSettingsUiUiHandler { constructor(scene: BattleScene, mode?: Mode) { super(scene, mode); + this.titleSelected = 'Gamepad'; + this.settingDevice = SettingGamepad; + this.settingDeviceDefaults = settingGamepadDefaults; + this.settingDeviceOptions = settingGamepadOptions; + this.configs = [pad_xbox360, pad_dualshock, pad_unlicensedSNES] + this.commonSettingsCount = 2; + this.localStoragePropertyName = 'settingsGamepad'; } 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_SELECTED); - gamepadText.setOrigin(0, 0); - gamepadText.setPositionRelative(headerBg, 50, 4); - - const keyboardText = addTextObject(this.scene, 0, 0, 'Keyboard', TextStyle.SETTINGS_LABEL); - 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); - - /// Initialize a new configuration "screen" for each type of gamepad. - for (const config of [pad_xbox360, pad_dualshock, pad_unlicensedSNES]) { - // 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(SettingGamepad).slice(0, 2).map(key => SettingGamepad[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(settingGamepadDefaults).filter(s => specificBindingKeys.includes(s)).map(k => settingGamepadDefaults[k])); - - // Filter out settings that are not relevant to the current gamepad configuration. - const settingGamepadFiltered = Object.keys(SettingGamepad).filter(_key => specificBindingKeys.includes(SettingGamepad[_key])); - // Loop through the filtered settings to manage display and options. - - settingGamepadFiltered.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 settingGamepadOptions[SettingGamepad[setting]].entries()) { - // Check if the current setting is for binding keys. - if (bindingSettings.includes(SettingGamepad[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, SettingGamepad[setting]); - const icon = this.scene.add.sprite(0, 0, config.padType); - 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, settingGamepadDefaults[SettingGamepad[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); - } + super.setup(); // If no gamepads are detected, set up a default UI prompt in the settings container. this.layout['noGamepads'] = new Map(); const optionsContainer = this.scene.add.container(0, 0); @@ -176,45 +36,16 @@ export default class SettingsGamepadUiHandler extends AbstractSettingsUiUiHandle // Map the 'noGamepads' layout options for easy access. this.layout['noGamepads'].optionsContainer = optionsContainer; this.layout['noGamepads'].label = label; - - // 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.getActiveConfig(); - // Set the UI layout for the active configuration. If unsuccessful, exit the function early. - if (!this.setLayout(activeConfig)) return; + getActiveConfig() { + return this.scene.inputController.getActiveConfig(); + } + 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')) : {}; - - // 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); + return settings; } setLayout(activeConfig: GamepadConfig): boolean { @@ -231,91 +62,27 @@ export default class SettingsGamepadUiHandler extends AbstractSettingsUiUiHandle return super.setLayout(activeConfig); } - processInput(button: Button): boolean { - const ui = this.getUi(); - // Defines the maximum number of rows that can be displayed on the screen. - const rowsToDisplay = 9; - let success = false; + navigateMenuLeft(): boolean { + this.scene.ui.setMode(Mode.SETTINGS) + return true; + } - // 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 (!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(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 (!this.optionValueLabels) return false; - 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 || !this.optionValueLabels) 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 || !this.optionValueLabels) 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) - success = true; - break; - case Button.CYCLE_SHINY: - this.scene.ui.setMode(Mode.SETTINGS_KEYBOARD) - 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. + navigateMenuRight(): boolean { + this.scene.ui.setMode(Mode.SETTINGS_KEYBOARD) + return true; } updateChosenGamepadDisplay(): void { // Update any bindings that might have changed since the last update. this.updateBindings(); - // Iterate over the keys in the SettingGamepad enumeration. - for (const [index, key] of Object.keys(SettingGamepad).entries()) { - const setting = SettingGamepad[key] // Get the actual setting value using the key. + // 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. // Check if the current setting corresponds to the default controller setting. - if (setting === SettingGamepad.Default_Controller) { + if (setting === this.settingDevice.Default_Controller) { // Iterate over all layouts excluding the 'noGamepads' special case. for (const _key of Object.keys(this.layout)) { if (_key === 'noGamepads') continue; // Skip updating the no gamepad layout. @@ -328,36 +95,8 @@ export default class SettingsGamepadUiHandler extends AbstractSettingsUiUiHandle } } - setOptionCursor(settingIndex: integer, cursor: integer, save?: boolean): boolean { - // Retrieve the specific setting using the settingIndex from the SettingGamepad enumeration. - const setting = SettingGamepad[Object.keys(SettingGamepad)[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 (SettingGamepad[setting] !== SettingGamepad.Default_Controller) - this.scene.gameData.saveGamepadSetting(setting, cursor) - } - - return true; // Return true to indicate the cursor was successfully updated. + saveSettingToLocalStorage(setting, cursor): void { + if (this.settingDevice[setting] !== this.settingDevice.Default_Controller) + this.scene.gameData.saveGamepadSetting(setting, cursor) } - } \ No newline at end of file diff --git a/src/ui/settings/settings-keyboard-ui-handler.ts b/src/ui/settings/settings-keyboard-ui-handler.ts index 6441b6f54..dbb342c5a 100644 --- a/src/ui/settings/settings-keyboard-ui-handler.ts +++ b/src/ui/settings/settings-keyboard-ui-handler.ts @@ -1,289 +1,54 @@ 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 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"; -import AbstractSettingsUiUiHandler, {InputsIcons} from "#app/ui/settings/abstract-settings-ui-handler"; +import AbstractSettingsUiUiHandler from "#app/ui/settings/abstract-settings-ui-handler"; export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandler { constructor(scene: BattleScene, mode?: Mode) { super(scene, mode); + this.titleSelected = 'Keyboard'; + this.settingDevice = SettingKeyboard; + this.settingDeviceDefaults = settingKeyboardDefaults; + this.settingDeviceOptions = settingKeyboardOptions; + this.configs = [cfg_keyboard_azerty]; + this.commonSettingsCount = 1; + this.textureOverride = 'keyboard'; + this.localStoragePropertyName = 'settingsKeyboard'; } - 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); - + getActiveConfig() { + return this.scene.inputController.getActiveKeyboardConfig(); } - 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; - + 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')) : {}; - - // 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); - + return settings; } - processInput(button: Button): boolean { - const ui = this.getUi(); - // Defines the maximum number of rows that can be displayed on the screen. - const rowsToDisplay = 9; + navigateMenuLeft(): boolean { + this.scene.ui.setMode(Mode.SETTINGS_GAMEPAD) + return true; + } - 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. + navigateMenuRight(): boolean { + this.scene.ui.setMode(Mode.SETTINGS) + return true; } 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. + // 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. // Check if the current setting corresponds to the default controller setting. - if (setting === SettingKeyboard.Default_Layout) { + if (setting === this.settingDevice.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, @@ -295,36 +60,8 @@ export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandl } - setOptionCursor(settingIndex: integer, cursor: integer, save?: boolean): boolean { - // 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. + saveSettingToLocalStorage(setting, cursor): void { + if (this.settingDevice[setting] !== this.settingDevice.Default_Layout) + this.scene.gameData.saveKeyboardSetting(setting, cursor) } - } \ No newline at end of file From 627a6cb66fc17e0c75d3bd658510bcf1cd6a4495 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Mon, 13 May 2024 01:51:44 +0200 Subject: [PATCH 55/81] refactor to use abstract-binding-ui-handler --- src/ui/settings/abrast-binding-ui-handler.ts | 200 ++++++++++++++++++ src/ui/settings/gamepad-binding-ui-handler.ts | 196 +---------------- .../settings/keyboard-binding-ui-handler.ts | 189 +---------------- 3 files changed, 213 insertions(+), 372 deletions(-) create mode 100644 src/ui/settings/abrast-binding-ui-handler.ts diff --git a/src/ui/settings/abrast-binding-ui-handler.ts b/src/ui/settings/abrast-binding-ui-handler.ts new file mode 100644 index 000000000..c4e75ea5f --- /dev/null +++ b/src/ui/settings/abrast-binding-ui-handler.ts @@ -0,0 +1,200 @@ +import UiHandler from "#app/ui/ui-handler"; +import Phaser from "phaser"; +import BattleScene from "#app/battle-scene"; +import {Mode} from "#app/ui/ui"; +import {addWindow} from "#app/ui/ui-theme"; +import {addTextObject, TextStyle} from "#app/ui/text"; +import {Button} from "#app/enums/buttons"; + + +export default abstract class AbstractBindingUiHandler extends UiHandler { + // Containers for different segments of the UI. + protected optionSelectContainer: Phaser.GameObjects.Container; + protected actionsContainer: Phaser.GameObjects.Container; + + // Background elements for titles and action areas. + protected titleBg: Phaser.GameObjects.NineSlice; + protected actionBg: Phaser.GameObjects.NineSlice; + protected optionSelectBg: Phaser.GameObjects.NineSlice; + + // Text elements for displaying instructions and actions. + protected unlockText: Phaser.GameObjects.Text; + protected swapText: Phaser.GameObjects.Text; + protected actionLabel: Phaser.GameObjects.Text; + protected cancelLabel: Phaser.GameObjects.Text; + + protected listening: boolean = false; + protected buttonPressed: number | null = null; + + // Icons for displaying current and new button assignments. + protected newButtonIcon: Phaser.GameObjects.Sprite; + protected targetButtonIcon: Phaser.GameObjects.Sprite; + + // Function to call on cancel or completion of binding. + protected cancelFn: (boolean?) => boolean; + + // The specific setting being modified. + protected target; + + constructor(scene: BattleScene, mode: Mode) { + super(scene, mode); + } + + setup() { + const ui = this.getUi(); + this.optionSelectContainer = this.scene.add.container(0, 0); + this.actionsContainer = this.scene.add.container(0, 0); + // Initially, containers are not visible. + this.optionSelectContainer.setVisible(false); + this.actionsContainer.setVisible(false); + + // Add containers to the UI. + ui.add(this.optionSelectContainer); + ui.add(this.actionsContainer); + + // Setup backgrounds and text objects for UI. + this.titleBg = addWindow(this.scene, (this.scene.game.canvas.width / 6) - this.getWindowWidth(), -(this.scene.game.canvas.height / 6) + 28 + 21, this.getWindowWidth(), 24); + this.titleBg.setOrigin(0.5); + this.optionSelectContainer.add(this.titleBg); + + this.actionBg = addWindow(this.scene, (this.scene.game.canvas.width / 6) - this.getWindowWidth(), -(this.scene.game.canvas.height / 6) + this.getWindowHeight() + 28 + 21 + 21, this.getWindowWidth(), 24); + this.actionBg.setOrigin(0.5); + this.actionsContainer.add(this.actionBg); + + // Text prompts and instructions for the user. + this.unlockText = addTextObject(this.scene, 0, 0, 'Press a button...', TextStyle.WINDOW); + this.unlockText.setOrigin(0, 0); + this.unlockText.setPositionRelative(this.titleBg, 36, 4); + this.optionSelectContainer.add(this.unlockText); + + this.optionSelectBg = addWindow(this.scene, (this.scene.game.canvas.width / 6) - this.getWindowWidth(), -(this.scene.game.canvas.height / 6) + this.getWindowHeight() + 28, this.getWindowWidth(), this.getWindowHeight()); + this.optionSelectBg.setOrigin(0.5); + this.optionSelectContainer.add(this.optionSelectBg); + + // New button icon setup. + this.newButtonIcon = this.scene.add.sprite(0, 0, 'keyboard'); + this.newButtonIcon.setScale(0.15); + this.newButtonIcon.setPositionRelative(this.optionSelectBg, 78, 16); + this.newButtonIcon.setOrigin(0.5); + this.newButtonIcon.setVisible(false); + + this.swapText = addTextObject(this.scene, 0, 0, 'will swap with', TextStyle.WINDOW); + this.swapText.setOrigin(0.5); + this.swapText.setPositionRelative(this.optionSelectBg, this.optionSelectBg.width / 2 - 2, this.optionSelectBg.height / 2 - 2); + this.swapText.setVisible(false); + + this.targetButtonIcon = this.scene.add.sprite(0, 0, 'keyboard'); + this.targetButtonIcon.setScale(0.15); + this.targetButtonIcon.setPositionRelative(this.optionSelectBg, 78, 48); + this.targetButtonIcon.setOrigin(0.5); + this.targetButtonIcon.setVisible(false); + + this.cancelLabel = addTextObject(this.scene, 0, 0, 'Cancel', TextStyle.SETTINGS_LABEL); + this.cancelLabel.setOrigin(0, 0.5); + this.cancelLabel.setPositionRelative(this.actionBg, 10, this.actionBg.height / 2); + + this.actionLabel = addTextObject(this.scene, 0, 0, 'Confirm Swap', TextStyle.SETTINGS_LABEL); + this.actionLabel.setOrigin(0, 0.5); + this.actionLabel.setPositionRelative(this.actionBg, this.actionBg.width - 75, this.actionBg.height / 2); + + // Add swap and cancel labels to the containers. + this.optionSelectContainer.add(this.newButtonIcon); + this.optionSelectContainer.add(this.swapText); + this.optionSelectContainer.add(this.targetButtonIcon); + this.actionsContainer.add(this.actionLabel); + this.actionsContainer.add(this.cancelLabel); + } + + show(args: any[]): boolean { + super.show(args); + this.buttonPressed = null; + this.cancelFn = args[0].cancelHandler; + this.target = args[0].target; + + // Bring the option and action containers to the front of the UI. + this.getUi().bringToTop(this.optionSelectContainer); + this.getUi().bringToTop(this.actionsContainer); + + this.optionSelectContainer.setVisible(true); + setTimeout(() => this.listening = true, 150); + return true; + } + + getWindowWidth(): number { + return 160; + } + + getWindowHeight(): number { + return 64; + } + + processInput(button: Button): boolean { + if (this.buttonPressed === null) return; + const ui = this.getUi(); + let success = false; + switch (button) { + case Button.LEFT: + case Button.RIGHT: + // Toggle between action and cancel options. + const cursor = this.cursor ? 0 : 1; + success = this.setCursor(cursor); + break + case Button.ACTION: + // Process actions based on current cursor position. + if (this.cursor === 0) { + this.cancelFn(); + } else { + success = this.swapAction(); + this.cancelFn(success); + } + break; + } + + // Plays a select sound effect if an action was successfully processed. + if (success) + ui.playSelect(); + else + ui.playError(); + + return success; + } + + setCursor(cursor: integer): boolean { + this.cursor = cursor; + if (cursor === 1) { + this.actionLabel.setColor(this.getTextColor(TextStyle.SETTINGS_SELECTED)); + this.actionLabel.setShadowColor(this.getTextColor(TextStyle.SETTINGS_SELECTED, true)); + this.cancelLabel.setColor(this.getTextColor(TextStyle.WINDOW)); + this.cancelLabel.setShadowColor(this.getTextColor(TextStyle.WINDOW, true)); + return true; + } + this.actionLabel.setColor(this.getTextColor(TextStyle.WINDOW)); + this.actionLabel.setShadowColor(this.getTextColor(TextStyle.WINDOW, true)); + this.cancelLabel.setColor(this.getTextColor(TextStyle.SETTINGS_SELECTED)); + this.cancelLabel.setShadowColor(this.getTextColor(TextStyle.SETTINGS_SELECTED, true)); + return true; + } + + clear() { + super.clear(); + this.target = null; + this.cancelFn = null; + this.optionSelectContainer.setVisible(false); + this.actionsContainer.setVisible(false); + this.newButtonIcon.setVisible(false); + this.targetButtonIcon.setVisible(false); + this.swapText.setVisible(false); + } + + onInputDown(buttonIcon: string, assignedButtonIcon: string, type: string): void { + this.newButtonIcon.setTexture(type); + this.newButtonIcon.setFrame(buttonIcon); + this.targetButtonIcon.setTexture(type); + this.targetButtonIcon.setFrame(assignedButtonIcon); + this.newButtonIcon.setVisible(true); + this.targetButtonIcon.setVisible(true); + this.swapText.setVisible(true); + this.setCursor(0); + this.actionsContainer.setVisible(true); + } +} \ No newline at end of file diff --git a/src/ui/settings/gamepad-binding-ui-handler.ts b/src/ui/settings/gamepad-binding-ui-handler.ts index a84ea9cbb..96ba73a17 100644 --- a/src/ui/settings/gamepad-binding-ui-handler.ts +++ b/src/ui/settings/gamepad-binding-ui-handler.ts @@ -1,210 +1,30 @@ -import UiHandler from "../ui-handler"; import BattleScene from "#app/battle-scene"; -import {Mode} from "../ui"; -import {Button} from "../../enums/buttons"; -import {addWindow} from "../ui-theme"; -import {addTextObject, TextStyle} from "#app/ui/text"; import Phaser from "phaser"; -import {SettingGamepad} from "../../system/settings-gamepad"; +import AbstractBindingUiHandler from "#app/ui/settings/abrast-binding-ui-handler"; +import {Mode} from "#app/ui/ui"; -export default class GamepadBindingUiHandler extends UiHandler { - // Containers for different segments of the UI. - protected optionSelectContainer: Phaser.GameObjects.Container; - protected actionsContainer: Phaser.GameObjects.Container; +export default class GamepadBindingUiHandler extends AbstractBindingUiHandler { - // Background elements for titles and action areas. - protected titleBg: Phaser.GameObjects.NineSlice; - protected actionBg: Phaser.GameObjects.NineSlice; - protected optionSelectBg: Phaser.GameObjects.NineSlice; - - // Text elements for displaying instructions and actions. - private unlockText: Phaser.GameObjects.Text; - private swapText: Phaser.GameObjects.Text; - private actionLabel: Phaser.GameObjects.Text; - private cancelLabel: Phaser.GameObjects.Text; - - private listening: boolean = false; - private buttonPressed: number | null = null; - - // Icons for displaying current and new button assignments. - private newButtonIcon: Phaser.GameObjects.Sprite; - private targetButtonIcon: Phaser.GameObjects.Sprite; - - // Function to call on cancel or completion of binding. - private cancelFn: (boolean?) => boolean; - - // The specific setting being modified. - private target: SettingGamepad; - - constructor(scene: BattleScene, mode: Mode = Mode.GAMEPAD_BINDING) { + constructor(scene: BattleScene, mode: Mode) { super(scene, mode); // Listen to gamepad button down events to initiate binding. scene.input.gamepad.on('down', this.gamepadButtonDown, this); } - setup() { - const ui = this.getUi(); - this.optionSelectContainer = this.scene.add.container(0, 0); - this.actionsContainer = this.scene.add.container(0, 0); - // Initially, containers are not visible. - this.optionSelectContainer.setVisible(false); - this.actionsContainer.setVisible(false); - - // Add containers to the UI. - ui.add(this.optionSelectContainer); - ui.add(this.actionsContainer); - - // Setup backgrounds and text objects for UI. - this.titleBg = addWindow(this.scene, (this.scene.game.canvas.width / 6) - this.getWindowWidth(), -(this.scene.game.canvas.height / 6) + 28 + 21, this.getWindowWidth(), 24); - this.titleBg.setOrigin(0.5); - this.optionSelectContainer.add(this.titleBg); - - this.actionBg = addWindow(this.scene, (this.scene.game.canvas.width / 6) - this.getWindowWidth(), -(this.scene.game.canvas.height / 6) + this.getWindowHeight() + 28 + 21 + 21, this.getWindowWidth(), 24); - this.actionBg.setOrigin(0.5); - this.actionsContainer.add(this.actionBg); - - // Text prompts and instructions for the user. - this.unlockText = addTextObject(this.scene, 0, 0, 'Press a button...', TextStyle.WINDOW); - this.unlockText.setOrigin(0, 0); - this.unlockText.setPositionRelative(this.titleBg, 36, 4); - this.optionSelectContainer.add(this.unlockText); - - this.optionSelectBg = addWindow(this.scene, (this.scene.game.canvas.width / 6) - this.getWindowWidth(), -(this.scene.game.canvas.height / 6) + this.getWindowHeight() + 28, this.getWindowWidth(), this.getWindowHeight()); - this.optionSelectBg.setOrigin(0.5); - this.optionSelectContainer.add(this.optionSelectBg); - - // New button icon setup. - this.newButtonIcon = this.scene.add.sprite(0, 0, 'xbox'); - this.newButtonIcon.setScale(0.15); - this.newButtonIcon.setPositionRelative(this.optionSelectBg, 78, 16); - this.newButtonIcon.setOrigin(0.5); - this.newButtonIcon.setVisible(false); - - this.swapText = addTextObject(this.scene, 0, 0, 'will swap with', TextStyle.WINDOW); - this.swapText.setOrigin(0.5); - this.swapText.setPositionRelative(this.optionSelectBg, this.optionSelectBg.width / 2 - 2, this.optionSelectBg.height / 2 - 2); - this.swapText.setVisible(false); - - this.targetButtonIcon = this.scene.add.sprite(0, 0, 'xbox'); - this.targetButtonIcon.setScale(0.15); - this.targetButtonIcon.setPositionRelative(this.optionSelectBg, 78, 48); - this.targetButtonIcon.setOrigin(0.5); - this.targetButtonIcon.setVisible(false); - - this.cancelLabel = addTextObject(this.scene, 0, 0, 'Cancel', TextStyle.SETTINGS_LABEL); - this.cancelLabel.setOrigin(0, 0.5); - this.cancelLabel.setPositionRelative(this.actionBg, 10, this.actionBg.height / 2); - - this.actionLabel = addTextObject(this.scene, 0, 0, 'Confirm Swap', TextStyle.SETTINGS_LABEL); - this.actionLabel.setOrigin(0, 0.5); - this.actionLabel.setPositionRelative(this.actionBg, this.actionBg.width - 75, this.actionBg.height / 2); - - // Add swap and cancel labels to the containers. - this.optionSelectContainer.add(this.newButtonIcon); - this.optionSelectContainer.add(this.swapText); - this.optionSelectContainer.add(this.targetButtonIcon); - this.actionsContainer.add(this.actionLabel); - this.actionsContainer.add(this.cancelLabel); - } - gamepadButtonDown(pad: Phaser.Input.Gamepad.Gamepad, button: Phaser.Input.Gamepad.Button, value: number): void { const blacklist = [12, 13, 14, 15]; // d-pad buttons are blacklisted. // Check conditions before processing the button press. if (!this.listening || pad.id !== this.scene.inputController?.chosenGamepad || blacklist.includes(button.index) || this.buttonPressed !== null) return; this.buttonPressed = button.index; const [type, buttonIcon] = this.scene.inputController.getPressedButtonLabel(button); + if (!buttonIcon) return; const assignedButtonIcon = this.scene.inputController.getCurrentlyAssignedIconToDisplay(this.target); - this.newButtonIcon.setTexture(type); - this.newButtonIcon.setFrame(buttonIcon); - this.targetButtonIcon.setTexture(type); - this.targetButtonIcon.setFrame(assignedButtonIcon); - this.newButtonIcon.setVisible(true); - this.targetButtonIcon.setVisible(true); - this.swapText.setVisible(true); - this.setCursor(0); - this.actionsContainer.setVisible(true); + this.onInputDown(buttonIcon, assignedButtonIcon, type); } - show(args: any[]): boolean { - super.show(args); - this.buttonPressed = null; - this.cancelFn = args[0].cancelHandler; - this.target = args[0].target; - - // Bring the option and action containers to the front of the UI. - this.getUi().bringToTop(this.optionSelectContainer); - this.getUi().bringToTop(this.actionsContainer); - - this.optionSelectContainer.setVisible(true); - setTimeout(() => this.listening = true, 150); + swapAction() { + this.scene.inputController.swapBinding(this.target, this.buttonPressed); return true; } - - getWindowWidth(): number { - return 160; - } - - getWindowHeight(): number { - return 64; - } - - processInput(button: Button): boolean { - if (this.buttonPressed === null) return; - const ui = this.getUi(); - let success = false; - switch (button) { - case Button.LEFT: - case Button.RIGHT: - // Toggle between action and cancel options. - const cursor = this.cursor ? 0 : 1; - success = this.setCursor(cursor); - break - case Button.ACTION: - // Process actions based on current cursor position. - if (this.cursor === 0) { - this.cancelFn(); - } else { - success = true; - this.scene.inputController.swapBinding(this.target, this.buttonPressed); - this.cancelFn(success); - } - break; - } - - // Plays a select sound effect if an action was successfully processed. - if (success) - ui.playSelect(); - else - ui.playError(); - - return success; - } - - setCursor(cursor: number): boolean { - this.cursor = cursor; - if (cursor === 1) { - this.actionLabel.setColor(this.getTextColor(TextStyle.SETTINGS_SELECTED)); - this.actionLabel.setShadowColor(this.getTextColor(TextStyle.SETTINGS_SELECTED, true)); - this.cancelLabel.setColor(this.getTextColor(TextStyle.WINDOW)); - this.cancelLabel.setShadowColor(this.getTextColor(TextStyle.WINDOW, true)); - return true; - } - this.actionLabel.setColor(this.getTextColor(TextStyle.WINDOW)); - this.actionLabel.setShadowColor(this.getTextColor(TextStyle.WINDOW, true)); - this.cancelLabel.setColor(this.getTextColor(TextStyle.SETTINGS_SELECTED)); - this.cancelLabel.setShadowColor(this.getTextColor(TextStyle.SETTINGS_SELECTED, true)); - return true; - } - - clear() { - super.clear(); - this.target = null; - this.cancelFn = null; - this.optionSelectContainer.setVisible(false); - this.actionsContainer.setVisible(false); - this.newButtonIcon.setVisible(false); - this.targetButtonIcon.setVisible(false); - this.swapText.setVisible(false); - } } \ No newline at end of file diff --git a/src/ui/settings/keyboard-binding-ui-handler.ts b/src/ui/settings/keyboard-binding-ui-handler.ts index fb0edc094..5d572708b 100644 --- a/src/ui/settings/keyboard-binding-ui-handler.ts +++ b/src/ui/settings/keyboard-binding-ui-handler.ts @@ -1,112 +1,16 @@ -import UiHandler from "../ui-handler"; import BattleScene from "../../battle-scene"; import {Mode} from "../ui"; -import {Button} from "../../enums/buttons"; -import Phaser from "phaser"; -import {addWindow} from "#app/ui/ui-theme"; -import {addTextObject, TextStyle} from "#app/ui/text"; -import {SettingKeyboard} from "#app/system/settings-keyboard"; +import AbstractBindingUiHandler from "#app/ui/settings/abrast-binding-ui-handler"; -export default class KeyboardBindingUiHandler extends UiHandler { - // Containers for different segments of the UI. - protected optionSelectContainer: Phaser.GameObjects.Container; - protected actionsContainer: Phaser.GameObjects.Container; +export default class KeyboardBindingUiHandler extends AbstractBindingUiHandler { - // Background elements for titles and action areas. - protected titleBg: Phaser.GameObjects.NineSlice; - protected actionBg: Phaser.GameObjects.NineSlice; - protected optionSelectBg: Phaser.GameObjects.NineSlice; - - // Text elements for displaying instructions and actions. - private unlockText: Phaser.GameObjects.Text; - private swapText: Phaser.GameObjects.Text; - private actionLabel: Phaser.GameObjects.Text; - private cancelLabel: Phaser.GameObjects.Text; - - private listening: boolean = false; - private buttonPressed: number | null = null; - - // Icons for displaying current and new button assignments. - private newButtonIcon: Phaser.GameObjects.Sprite; - private targetButtonIcon: Phaser.GameObjects.Sprite; - - // Function to call on cancel or completion of binding. - private cancelFn: (boolean?) => boolean; - - // The specific setting being modified. - private target: SettingKeyboard; constructor(scene: BattleScene, mode?: Mode) { super(scene, mode); // Listen to gamepad button down events to initiate binding. scene.input.keyboard.on('keydown', this.onKeyDown, this); } - setup() { - const ui = this.getUi(); - this.optionSelectContainer = this.scene.add.container(0, 0); - this.actionsContainer = this.scene.add.container(0, 0); - // Initially, containers are not visible. - this.optionSelectContainer.setVisible(false); - this.actionsContainer.setVisible(false); - - // Add containers to the UI. - ui.add(this.optionSelectContainer); - ui.add(this.actionsContainer); - - // Setup backgrounds and text objects for UI. - this.titleBg = addWindow(this.scene, (this.scene.game.canvas.width / 6) - this.getWindowWidth(), -(this.scene.game.canvas.height / 6) + 28 + 21, this.getWindowWidth(), 24); - this.titleBg.setOrigin(0.5); - this.optionSelectContainer.add(this.titleBg); - - this.actionBg = addWindow(this.scene, (this.scene.game.canvas.width / 6) - this.getWindowWidth(), -(this.scene.game.canvas.height / 6) + this.getWindowHeight() + 28 + 21 + 21, this.getWindowWidth(), 24); - this.actionBg.setOrigin(0.5); - this.actionsContainer.add(this.actionBg); - - // Text prompts and instructions for the user. - this.unlockText = addTextObject(this.scene, 0, 0, 'Press a button...', TextStyle.WINDOW); - this.unlockText.setOrigin(0, 0); - this.unlockText.setPositionRelative(this.titleBg, 36, 4); - this.optionSelectContainer.add(this.unlockText); - - this.optionSelectBg = addWindow(this.scene, (this.scene.game.canvas.width / 6) - this.getWindowWidth(), -(this.scene.game.canvas.height / 6) + this.getWindowHeight() + 28, this.getWindowWidth(), this.getWindowHeight()); - this.optionSelectBg.setOrigin(0.5); - this.optionSelectContainer.add(this.optionSelectBg); - - // New button icon setup. - this.newButtonIcon = this.scene.add.sprite(0, 0, 'keyboard'); - this.newButtonIcon.setScale(0.15); - this.newButtonIcon.setPositionRelative(this.optionSelectBg, 78, 16); - this.newButtonIcon.setOrigin(0.5); - this.newButtonIcon.setVisible(false); - - this.swapText = addTextObject(this.scene, 0, 0, 'will swap with', TextStyle.WINDOW); - this.swapText.setOrigin(0.5); - this.swapText.setPositionRelative(this.optionSelectBg, this.optionSelectBg.width / 2 - 2, this.optionSelectBg.height / 2 - 2); - this.swapText.setVisible(false); - - this.targetButtonIcon = this.scene.add.sprite(0, 0, 'keyboard'); - this.targetButtonIcon.setScale(0.15); - this.targetButtonIcon.setPositionRelative(this.optionSelectBg, 78, 48); - this.targetButtonIcon.setOrigin(0.5); - this.targetButtonIcon.setVisible(false); - - this.cancelLabel = addTextObject(this.scene, 0, 0, 'Cancel', TextStyle.SETTINGS_LABEL); - this.cancelLabel.setOrigin(0, 0.5); - this.cancelLabel.setPositionRelative(this.actionBg, 10, this.actionBg.height / 2); - - this.actionLabel = addTextObject(this.scene, 0, 0, 'Confirm Swap', TextStyle.SETTINGS_LABEL); - this.actionLabel.setOrigin(0, 0.5); - this.actionLabel.setPositionRelative(this.actionBg, this.actionBg.width - 75, this.actionBg.height / 2); - - // Add swap and cancel labels to the containers. - this.optionSelectContainer.add(this.newButtonIcon); - this.optionSelectContainer.add(this.swapText); - this.optionSelectContainer.add(this.targetButtonIcon); - this.actionsContainer.add(this.actionLabel); - this.actionsContainer.add(this.cancelLabel); - } - onKeyDown(event): void { const key = event.keyCode; // // Check conditions before processing the button press. @@ -115,95 +19,12 @@ export default class KeyboardBindingUiHandler extends UiHandler { const buttonIcon = this.scene.inputController.getPressedKeyLabel(key); if (!buttonIcon) return; const assignedButtonIcon = this.scene.inputController.getKeyboardCurrentlyAssignedIconToDisplay(this.target); - this.newButtonIcon.setFrame(buttonIcon); - this.targetButtonIcon.setFrame(assignedButtonIcon); - this.newButtonIcon.setVisible(true); - this.targetButtonIcon.setVisible(true); - this.swapText.setVisible(true); - this.setCursor(0); - this.actionsContainer.setVisible(true); + this.onInputDown(buttonIcon, assignedButtonIcon, 'keyboard'); } - show(args: any[]): boolean { - super.show(args); - this.buttonPressed = null; - this.cancelFn = args[0].cancelHandler; - this.target = args[0].target; - - // Bring the option and action containers to the front of the UI. - this.getUi().bringToTop(this.optionSelectContainer); - this.getUi().bringToTop(this.actionsContainer); - - this.optionSelectContainer.setVisible(true); - setTimeout(() => this.listening = true, 150); + swapAction() { + this.scene.inputController.swapKeyboardBinding(this.target, this.buttonPressed); return true; } - getWindowWidth(): number { - return 160; - } - - getWindowHeight(): number { - return 64; - } - - processInput(button: Button): boolean { - if (this.buttonPressed === null) return; - const ui = this.getUi(); - let success = false; - switch (button) { - case Button.LEFT: - case Button.RIGHT: - // Toggle between action and cancel options. - const cursor = this.cursor ? 0 : 1; - success = this.setCursor(cursor); - break - case Button.ACTION: - // Process actions based on current cursor position. - if (this.cursor === 0) { - this.cancelFn(); - } else { - success = true; - this.scene.inputController.swapKeyboardBinding(this.target, this.buttonPressed); - this.cancelFn(success); - } - break; - } - - // Plays a select sound effect if an action was successfully processed. - if (success) - ui.playSelect(); - else - ui.playError(); - - return success; - } - - setCursor(cursor: integer): boolean { - this.cursor = cursor; - if (cursor === 1) { - this.actionLabel.setColor(this.getTextColor(TextStyle.SETTINGS_SELECTED)); - this.actionLabel.setShadowColor(this.getTextColor(TextStyle.SETTINGS_SELECTED, true)); - this.cancelLabel.setColor(this.getTextColor(TextStyle.WINDOW)); - this.cancelLabel.setShadowColor(this.getTextColor(TextStyle.WINDOW, true)); - return true; - } - this.actionLabel.setColor(this.getTextColor(TextStyle.WINDOW)); - this.actionLabel.setShadowColor(this.getTextColor(TextStyle.WINDOW, true)); - this.cancelLabel.setColor(this.getTextColor(TextStyle.SETTINGS_SELECTED)); - this.cancelLabel.setShadowColor(this.getTextColor(TextStyle.SETTINGS_SELECTED, true)); - return true; - } - - clear() { - super.clear(); - this.target = null; - this.cancelFn = null; - this.optionSelectContainer.setVisible(false); - this.actionsContainer.setVisible(false); - this.newButtonIcon.setVisible(false); - this.targetButtonIcon.setVisible(false); - this.swapText.setVisible(false); - } - } \ No newline at end of file From e798f72a639dcb927c3929c626746743cd32dbe8 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Mon, 13 May 2024 02:03:06 +0200 Subject: [PATCH 56/81] fix imports --- src/configs/gamepad-utils.ts | 19 +++++++------ src/inputs-controller.ts | 27 ++++++++++--------- src/ui/settings/abrast-binding-ui-handler.ts | 16 +++++------ .../settings/abstract-settings-ui-handler.ts | 20 +++++++------- src/ui/settings/gamepad-binding-ui-handler.ts | 7 +++-- .../settings/keyboard-binding-ui-handler.ts | 2 +- .../settings/settings-gamepad-ui-handler.ts | 4 +-- 7 files changed, 47 insertions(+), 48 deletions(-) diff --git a/src/configs/gamepad-utils.ts b/src/configs/gamepad-utils.ts index eac1e9598..2e6da5fb4 100644 --- a/src/configs/gamepad-utils.ts +++ b/src/configs/gamepad-utils.ts @@ -1,16 +1,15 @@ -import {GamepadConfig} from "../inputs-controller"; -import {SettingGamepad} from "#app/system/settings-gamepad"; +import {InterfaceConfig} from "../inputs-controller"; import {Button} from "#app/enums/buttons"; // Given a button index from an input event, return its naming from the mapping config -export function getKeyFromInputIndex(config: GamepadConfig, index: number): String | null { +export function getKeyFromInputIndex(config: InterfaceConfig, index: number): String | null { for (const key of Object.keys(config.gamepadMapping)) { if (config.gamepadMapping[key] === index) return key; } return null; } -export function getKeyFromKeyboardKey(config: GamepadConfig, key): String | null { +export function getKeyFromKeyboardKey(config: InterfaceConfig, key): String | null { for (const _key of Object.keys(config.gamepadMapping)) { if (config.gamepadMapping[_key] === key) return _key; } @@ -18,7 +17,7 @@ export function getKeyFromKeyboardKey(config: GamepadConfig, key): String | null } // Given a setting name, return the key assigned to it from the config file -export function getKeyForSettingName(config: GamepadConfig, settingName: string): String | null { +export function getKeyForSettingName(config: InterfaceConfig, settingName: string): String | null { for (const key of Object.keys(config.setting)) { if (config.setting[key] === settingName) return key; } @@ -26,7 +25,7 @@ export function getKeyForSettingName(config: GamepadConfig, settingName: string) } // Given a Button, return the custom key assigned to it from the config file -export function getCurrenlyAssignedKeyToAction(config: GamepadConfig, action: Button): String | null { +export function getCurrenlyAssignedKeyToAction(config: InterfaceConfig, action: Button): String | null { for (const key of Object.keys(config.custom)) { if (config.custom[key] === action) return key; } @@ -34,7 +33,7 @@ export function getCurrenlyAssignedKeyToAction(config: GamepadConfig, action: Bu } // Given a setting name, return the custom key for the default action from the config file -export function getCurrentlyAssignedToSettingName(config: GamepadConfig, settingName: string): String { +export function getCurrentlyAssignedToSettingName(config: InterfaceConfig, settingName: string): String { const oldKey = getKeyForSettingName(config, settingName) const action = config.default[oldKey]; const key = getCurrenlyAssignedKeyToAction(config, action); @@ -42,18 +41,18 @@ export function getCurrentlyAssignedToSettingName(config: GamepadConfig, setting } // Given a button index from an input event, return its icon from the config file -export function getCurrenlyAssignedIconFromInputIndex(config: GamepadConfig, index: number): String { +export function getCurrenlyAssignedIconFromInputIndex(config: InterfaceConfig, index: number): String { const key = getKeyFromInputIndex(config, index); return config.icons[key]; } -export function getCurrenlyAssignedIconFromKeyboardKey(config: GamepadConfig, key): String { +export function getCurrenlyAssignedIconFromKeyboardKey(config: InterfaceConfig, key): String { const _key = getKeyFromKeyboardKey(config, key); return config.icons[_key]; } // Given a setting name, return the icon currently assigned to this setting name -export function getCurrentlyAssignedIconToSettingName(config: GamepadConfig, settingName: string): string { +export function getCurrentlyAssignedIconToSettingName(config: InterfaceConfig, settingName: string): string { const key = getCurrentlyAssignedToSettingName(config, settingName); return config.icons[key]; } diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 797e75132..e0fd172c8 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -12,9 +12,9 @@ import { getCurrenlyAssignedIconFromInputIndex, getCurrentlyAssignedIconToSettingName, getKeyFromInputIndex, getCurrentlyAssignedToSettingName, getCurrenlyAssignedIconFromKeyboardKey } from "./configs/gamepad-utils"; -import SettingsKeyboardUiHandler from "#app/ui/settings/settings-keyboard-ui-handler"; -import cfg_keyboard_azerty from "#app/configs/cfg_keyboard_azerty"; -import {SettingKeyboard} from "#app/system/settings-keyboard"; +import SettingsKeyboardUiHandler from "./ui/settings/settings-keyboard-ui-handler"; +import cfg_keyboard_azerty from "./configs/cfg_keyboard_azerty"; +import {SettingKeyboard} from "./system/settings-keyboard"; export interface GamepadMapping { [key: string]: number; @@ -32,7 +32,7 @@ export interface MappingLayout { [key: string]: Button; } -export interface GamepadConfig { +export interface InterfaceConfig { padID: string; padType: string; gamepadMapping: GamepadMapping; @@ -76,8 +76,8 @@ export class InputsController { private buttonLock2: Button; private interactions: Map> = new Map(); private time: Phaser.Time.Clock; - private configs: Map = new Map(); - private keyboardConfigs: Map = new Map(); + private configs: Map = new Map(); + private keyboardConfigs: Map = new Map(); private gamepadSupport: boolean = true; @@ -204,6 +204,7 @@ export class InputsController { this.deactivatePressedKey(); this.initChosenGamepad(gamepad) } + setChosenKeyboardLayout(layoutKeyboard: String): void { this.deactivatePressedKey(); this.initChosenLayoutKeyboard(layoutKeyboard) @@ -532,9 +533,9 @@ export class InputsController { * If no specific configuration matches, it defaults to a generic gamepad configuration. * * @param id The identifier string of the gamepad. - * @returns GamepadConfig The configuration object corresponding to the identified gamepad type. + * @returns InterfaceConfig The configuration object corresponding to the identified gamepad type. */ - getConfig(id: string): GamepadConfig { + getConfig(id: string): InterfaceConfig { id = id.toLowerCase(); if (id.includes('081f') && id.includes('e401')) { @@ -548,7 +549,7 @@ export class InputsController { return pad_generic; } - getConfigKeyboard(id: string): GamepadConfig { + getConfigKeyboard(id: string): InterfaceConfig { if (id === 'azerty') return cfg_keyboard_azerty; @@ -699,9 +700,9 @@ export class InputsController { * 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. + * @returns InterfaceConfig The configuration object for the active gamepad, or null if not set. */ - getActiveConfig(): GamepadConfig | null { + getActiveConfig(): InterfaceConfig | null { if (this.configs[this.chosenGamepad]?.padID) return this.configs[this.chosenGamepad] return null; } @@ -710,9 +711,9 @@ export class InputsController { * 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. + * @returns InterfaceConfig The configuration object for the active gamepad, or null if not set. */ - getActiveKeyboardConfig(): GamepadConfig | null { + getActiveKeyboardConfig(): InterfaceConfig | null { if (this.keyboardConfigs[this.chosenKeyboard]?.padID) return this.keyboardConfigs[this.chosenKeyboard] return null; } diff --git a/src/ui/settings/abrast-binding-ui-handler.ts b/src/ui/settings/abrast-binding-ui-handler.ts index c4e75ea5f..715b5a928 100644 --- a/src/ui/settings/abrast-binding-ui-handler.ts +++ b/src/ui/settings/abrast-binding-ui-handler.ts @@ -1,10 +1,9 @@ -import UiHandler from "#app/ui/ui-handler"; -import Phaser from "phaser"; -import BattleScene from "#app/battle-scene"; -import {Mode} from "#app/ui/ui"; -import {addWindow} from "#app/ui/ui-theme"; -import {addTextObject, TextStyle} from "#app/ui/text"; -import {Button} from "#app/enums/buttons"; +import UiHandler from "../ui-handler"; +import BattleScene from "../../battle-scene"; +import {Mode} from "../ui"; +import {addWindow} from "../ui-theme"; +import {addTextObject, TextStyle} from "../text"; +import {Button} from "../../enums/buttons"; export default abstract class AbstractBindingUiHandler extends UiHandler { @@ -116,7 +115,7 @@ export default abstract class AbstractBindingUiHandler extends UiHandler { this.getUi().bringToTop(this.actionsContainer); this.optionSelectContainer.setVisible(true); - setTimeout(() => this.listening = true, 150); + setTimeout(() => this.listening = true, 300); return true; } @@ -177,6 +176,7 @@ export default abstract class AbstractBindingUiHandler extends UiHandler { clear() { super.clear(); + this.listening = false; this.target = null; this.cancelFn = null; this.optionSelectContainer.setVisible(false); diff --git a/src/ui/settings/abstract-settings-ui-handler.ts b/src/ui/settings/abstract-settings-ui-handler.ts index c9392e060..b001495f5 100644 --- a/src/ui/settings/abstract-settings-ui-handler.ts +++ b/src/ui/settings/abstract-settings-ui-handler.ts @@ -1,11 +1,11 @@ -import UiHandler from "#app/ui/ui-handler"; -import BattleScene from "#app/battle-scene"; -import {Mode} from "#app/ui/ui"; -import {GamepadConfig} from "#app/inputs-controller"; -import {addWindow} from "#app/ui/ui-theme"; -import {addTextObject, TextStyle} from "#app/ui/text"; -import {getCurrentlyAssignedIconToSettingName, getKeyForSettingName} from "#app/configs/gamepad-utils"; -import {Button} from "#app/enums/buttons"; +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 {getCurrentlyAssignedIconToSettingName, getKeyForSettingName} from "../../configs/gamepad-utils"; +import {Button} from "../../enums/buttons"; export interface InputsIcons { [key: string]: Phaser.GameObjects.Sprite; @@ -151,7 +151,7 @@ export default abstract class AbstractSettingsUiUiHandler extends UiHandler { continue; } // For null options, add an icon for the key. - const key = getKeyForSettingName(config as GamepadConfig, this.settingDevice[setting]); + const key = getKeyForSettingName(config as InterfaceConfig, this.settingDevice[setting]); const icon = this.scene.add.sprite(0, 0, this.textureOverride ? this.textureOverride : config.padType); icon.setScale(0.1); icon.setOrigin(0, -0.1); @@ -270,7 +270,7 @@ export default abstract class AbstractSettingsUiUiHandler extends UiHandler { return true; } - setLayout(activeConfig: GamepadConfig): boolean { + 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. diff --git a/src/ui/settings/gamepad-binding-ui-handler.ts b/src/ui/settings/gamepad-binding-ui-handler.ts index 96ba73a17..684ce93b4 100644 --- a/src/ui/settings/gamepad-binding-ui-handler.ts +++ b/src/ui/settings/gamepad-binding-ui-handler.ts @@ -1,7 +1,6 @@ -import BattleScene from "#app/battle-scene"; -import Phaser from "phaser"; -import AbstractBindingUiHandler from "#app/ui/settings/abrast-binding-ui-handler"; -import {Mode} from "#app/ui/ui"; +import BattleScene from "../../battle-scene"; +import AbstractBindingUiHandler from "../settings/abrast-binding-ui-handler"; +import {Mode} from "../ui"; export default class GamepadBindingUiHandler extends AbstractBindingUiHandler { diff --git a/src/ui/settings/keyboard-binding-ui-handler.ts b/src/ui/settings/keyboard-binding-ui-handler.ts index 5d572708b..51dbb508c 100644 --- a/src/ui/settings/keyboard-binding-ui-handler.ts +++ b/src/ui/settings/keyboard-binding-ui-handler.ts @@ -1,6 +1,6 @@ import BattleScene from "../../battle-scene"; +import AbstractBindingUiHandler from "../settings/abrast-binding-ui-handler"; import {Mode} from "../ui"; -import AbstractBindingUiHandler from "#app/ui/settings/abrast-binding-ui-handler"; export default class KeyboardBindingUiHandler extends AbstractBindingUiHandler { diff --git a/src/ui/settings/settings-gamepad-ui-handler.ts b/src/ui/settings/settings-gamepad-ui-handler.ts index a63023bdc..f4d85cf0c 100644 --- a/src/ui/settings/settings-gamepad-ui-handler.ts +++ b/src/ui/settings/settings-gamepad-ui-handler.ts @@ -6,7 +6,7 @@ import {truncateString} from "../../utils"; import pad_xbox360 from "#app/configs/pad_xbox360"; import pad_dualshock from "#app/configs/pad_dualshock"; import pad_unlicensedSNES from "#app/configs/pad_unlicensedSNES"; -import {GamepadConfig} from "#app/inputs-controller"; +import {InterfaceConfig} from "#app/inputs-controller"; import AbstractSettingsUiUiHandler from "#app/ui/settings/abstract-settings-ui-handler"; export default class SettingsGamepadUiHandler extends AbstractSettingsUiUiHandler { @@ -48,7 +48,7 @@ export default class SettingsGamepadUiHandler extends AbstractSettingsUiUiHandle return settings; } - setLayout(activeConfig: GamepadConfig): boolean { + 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. From 769412e4524dc078ffbbd5943ae4d6bf08a43f32 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Mon, 13 May 2024 02:29:15 +0200 Subject: [PATCH 57/81] game is using the custom config with keyboard --- src/configs/gamepad-utils.ts | 6 +-- src/inputs-controller.ts | 45 ++++++++++++++++---- src/ui/settings/abrast-binding-ui-handler.ts | 1 + 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/src/configs/gamepad-utils.ts b/src/configs/gamepad-utils.ts index 2e6da5fb4..362bf95e8 100644 --- a/src/configs/gamepad-utils.ts +++ b/src/configs/gamepad-utils.ts @@ -9,7 +9,7 @@ export function getKeyFromInputIndex(config: InterfaceConfig, index: number): St } return null; } -export function getKeyFromKeyboardKey(config: InterfaceConfig, key): String | null { +export function getKeyFromKeyboardKeyCode(config: InterfaceConfig, key): String | null { for (const _key of Object.keys(config.gamepadMapping)) { if (config.gamepadMapping[_key] === key) return _key; } @@ -46,8 +46,8 @@ export function getCurrenlyAssignedIconFromInputIndex(config: InterfaceConfig, i return config.icons[key]; } -export function getCurrenlyAssignedIconFromKeyboardKey(config: InterfaceConfig, key): String { - const _key = getKeyFromKeyboardKey(config, key); +export function getCurrenlyAssignedIconFromKeyboardKeyCode(config: InterfaceConfig, key): String { + const _key = getKeyFromKeyboardKeyCode(config, key); return config.icons[_key]; } diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index e0fd172c8..c9d7d851d 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -9,8 +9,12 @@ import {Mode} from "./ui/ui"; import SettingsGamepadUiHandler from "./ui/settings/settings-gamepad-ui-handler"; import {SettingGamepad} from "./system/settings-gamepad"; import { - getCurrenlyAssignedIconFromInputIndex, getCurrentlyAssignedIconToSettingName, - getKeyFromInputIndex, getCurrentlyAssignedToSettingName, getCurrenlyAssignedIconFromKeyboardKey + getCurrenlyAssignedIconFromInputIndex, + getCurrentlyAssignedIconToSettingName, + getKeyFromInputIndex, + getCurrentlyAssignedToSettingName, + getCurrenlyAssignedIconFromKeyboardKeyCode, + getKeyFromKeyboardKeyCode } from "./configs/gamepad-utils"; import SettingsKeyboardUiHandler from "./ui/settings/settings-keyboard-ui-handler"; import cfg_keyboard_azerty from "./configs/cfg_keyboard_azerty"; @@ -88,6 +92,7 @@ export class InputsController { private pauseUpdate: boolean = false; public lastSource: string = 'keyboard'; + private keys: Array = []; /** * Initializes a new instance of the game control system, setting up initial state and configurations. @@ -163,7 +168,7 @@ export class InputsController { this.scene.input.keyboard.on('keyup', this.keyboardKeyUp, this); } // Keyboard - this.setupKeyboardControls(); + // this.setupKeyboardControls(); } /** @@ -231,7 +236,8 @@ export class InputsController { // Prevents repeating button interactions when gamepad support is disabled. if ( (!this.gamepadSupport && this.interactions[b].source === 'gamepad') || - (this.interactions[b].sourceName && this.interactions[b].sourceName !== this.chosenGamepad) || + (this.interactions[b].source === 'gamepad' && this.interactions[b].sourceName && this.interactions[b].sourceName !== this.chosenGamepad) || + (this.interactions[b].source === 'keyboard' && this.interactions[b].sourceName && this.interactions[b].sourceName !== this.chosenKeyboard) || this.pauseUpdate ) { // Deletes the last interaction for a button if gamepad is disabled. @@ -372,14 +378,37 @@ export class InputsController { } keyboardKeyDown(event): void { - const keyDown = event.key; - const keyCode = event.keyCode + const keyDown = event.keyCode; if (!this.keyboardConfigs[this.chosenKeyboard]?.padID) this.setupKeyboard(); + if (this.keys.includes(keyDown)) return; + this.keys.push(keyDown); + const key = getKeyFromKeyboardKeyCode(this.keyboardConfigs[this.chosenKeyboard], keyDown); + const buttonDown = this.keyboardConfigs[this.chosenKeyboard].custom[key]; + this.lastSource = 'keyboard'; + if (buttonDown !== undefined) { + this.events.emit('input_down', { + controller_type: 'keyboard', + button: buttonDown, + }); + this.setLastProcessedMovementTime(buttonDown, 'keyboard', this.chosenKeyboard); + } } keyboardKeyUp(event): void { - + const keyDown = event.keyCode; + this.keys = this.keys.filter(k => k !== keyDown); + if (!this.keyboardConfigs[this.chosenKeyboard]?.padID) + this.setupKeyboard(); + const key = getKeyFromKeyboardKeyCode(this.keyboardConfigs[this.chosenKeyboard], keyDown); + const buttonUp = this.keyboardConfigs[this.chosenKeyboard].custom[key]; + if (buttonUp !== undefined) { + this.events.emit('input_up', { + controller_type: 'keyboard', + button: buttonUp, + }); + this.delLastProcessedMovementTime(buttonUp); + } } /** @@ -729,7 +758,7 @@ export class InputsController { } getPressedKeyLabel(key): string { - return getCurrenlyAssignedIconFromKeyboardKey(this.keyboardConfigs[this.chosenKeyboard], key); + return getCurrenlyAssignedIconFromKeyboardKeyCode(this.keyboardConfigs[this.chosenKeyboard], key); } /** diff --git a/src/ui/settings/abrast-binding-ui-handler.ts b/src/ui/settings/abrast-binding-ui-handler.ts index 715b5a928..03139d71b 100644 --- a/src/ui/settings/abrast-binding-ui-handler.ts +++ b/src/ui/settings/abrast-binding-ui-handler.ts @@ -184,6 +184,7 @@ export default abstract class AbstractBindingUiHandler extends UiHandler { this.newButtonIcon.setVisible(false); this.targetButtonIcon.setVisible(false); this.swapText.setVisible(false); + this.buttonPressed = null; } onInputDown(buttonIcon: string, assignedButtonIcon: string, type: string): void { From b8856d246ebedfcbac7508821f213fee90a7d234 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Mon, 13 May 2024 02:32:25 +0200 Subject: [PATCH 58/81] renamed azerty to default since it's not usefull anymore --- src/configs/cfg_keyboard_azerty.ts | 2 +- src/inputs-controller.ts | 6 +++--- src/system/settings-keyboard.ts | 5 +---- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/configs/cfg_keyboard_azerty.ts b/src/configs/cfg_keyboard_azerty.ts index a55f61acf..576fa8dbb 100644 --- a/src/configs/cfg_keyboard_azerty.ts +++ b/src/configs/cfg_keyboard_azerty.ts @@ -3,7 +3,7 @@ import {SettingKeyboard} from "#app/system/settings-keyboard"; const cfg_keyboard_azerty = { padID: 'keyboard', - padType: 'azerty', + padType: 'default', gamepadMapping: { KEY_A: Phaser.Input.Keyboard.KeyCodes.A, KEY_B: Phaser.Input.Keyboard.KeyCodes.B, diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index c9d7d851d..88e17f92a 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -86,7 +86,7 @@ export class InputsController { private gamepadSupport: boolean = true; public chosenGamepad: String; - public chosenKeyboard: string = "azerty"; + public chosenKeyboard: string = "default"; private disconnectedGamepads: Array = new Array(); private pauseUpdate: boolean = false; @@ -349,7 +349,7 @@ export class InputsController { } setupKeyboard(): void { - for (const layout of ['azerty']) { + for (const layout of ['default']) { const config = this.getConfigKeyboard(layout); config.custom = this.keyboardConfigs[layout]?.custom || {...config.default}; this.keyboardConfigs[layout] = config; @@ -579,7 +579,7 @@ export class InputsController { } getConfigKeyboard(id: string): InterfaceConfig { - if (id === 'azerty') + if (id === 'default') return cfg_keyboard_azerty; return cfg_keyboard_azerty; diff --git a/src/system/settings-keyboard.ts b/src/system/settings-keyboard.ts index 167c170b8..62bcbbfd2 100644 --- a/src/system/settings-keyboard.ts +++ b/src/system/settings-keyboard.ts @@ -115,10 +115,7 @@ export function setSettingKeyboard(scene: BattleScene, setting: SettingKeyboard, }; scene.ui.setOverlayMode(Mode.OPTION_SELECT, { options: [{ - label: 'azerty', - handler: changeKeyboardHandler, - }, { - label: 'qwerty', + label: 'Default', handler: changeKeyboardHandler, }] }); From 665ecc0e4b41b183677ff93cd0bdfd886dcfbbf1 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Mon, 13 May 2024 02:51:59 +0200 Subject: [PATCH 59/81] effective keyboard key swap --- src/configs/cfg_keyboard_azerty.ts | 56 ++++++++++++++++++++++++++++++ src/inputs-controller.ts | 14 +++++++- src/system/game-data.ts | 11 +++++- 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/src/configs/cfg_keyboard_azerty.ts b/src/configs/cfg_keyboard_azerty.ts index 576fa8dbb..b7d1b09a8 100644 --- a/src/configs/cfg_keyboard_azerty.ts +++ b/src/configs/cfg_keyboard_azerty.ts @@ -202,6 +202,62 @@ const cfg_keyboard_azerty = { KEY_V: Button.CYCLE_VARIANT, KEY_PLUS: Button.SPEED_UP, KEY_MINUS: Button.SLOW_DOWN, + KEY_A: -1, + KEY_B: -1, + KEY_D: -1, + KEY_H: -1, + KEY_I: -1, + KEY_J: -1, + KEY_K: -1, + KEY_L: -1, + KEY_M: -1, + KEY_O: -1, + KEY_P: -1, + KEY_Q: -1, + KEY_S: -1, + KEY_T: -1, + KEY_U: -1, + KEY_W: -1, + KEY_X: -1, + KEY_Y: -1, + KEY_Z: -1, + KEY_0: -1, + KEY_1: -1, + KEY_2: -1, + KEY_3: -1, + KEY_4: -1, + KEY_5: -1, + KEY_6: -1, + KEY_7: -1, + KEY_8: -1, + KEY_9: -1, + KEY_CTRL: -1, + KEY_DEL: -1, + KEY_END: -1, + KEY_F1: -1, + KEY_F2: -1, + KEY_F3: -1, + KEY_F4: -1, + KEY_F5: -1, + KEY_F6: -1, + KEY_F7: -1, + KEY_F8: -1, + KEY_F9: -1, + KEY_F10: -1, + KEY_F11: -1, + KEY_F12: -1, + KEY_HOME: -1, + KEY_INSERT: -1, + KEY_PAGE_DOWN: -1, + KEY_PAGE_UP: -1, + KEY_QUOTATION: -1, + KEY_SHIFT: -1, + KEY_TAB: -1, + KEY_TILDE: -1, + KEY_LEFT_BRACKET: -1, + KEY_RIGHT_BRACKET: -1, + KEY_SEMICOLON: -1, + KEY_ALT: -1 } }; diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 88e17f92a..f5ff95e51 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -796,7 +796,15 @@ export class InputsController { } swapKeyboardBinding(settingName, pressedButton): void { - console.log('swap'); + this.pauseUpdate = true; + const keyTarget = getCurrentlyAssignedToSettingName(this.keyboardConfigs[this.chosenKeyboard], settingName) + const keyNewBinding = getKeyFromKeyboardKeyCode(this.keyboardConfigs[this.chosenKeyboard], pressedButton); + const previousActionForThisNewBinding = this.keyboardConfigs[this.chosenKeyboard].custom[keyNewBinding]; + const ActionForThisNewBinding = this.keyboardConfigs[this.chosenKeyboard].custom[keyTarget]; + this.keyboardConfigs[this.chosenKeyboard].custom[keyTarget] = previousActionForThisNewBinding; + this.keyboardConfigs[this.chosenKeyboard].custom[keyNewBinding] = ActionForThisNewBinding; + this.scene.gameData.saveCustomKeyboardMapping(this.chosenKeyboard, this.keyboardConfigs[this.chosenKeyboard].custom); + setTimeout(() => this.pauseUpdate = false, 500); } /** @@ -810,4 +818,8 @@ export class InputsController { if (!this.configs[gamepadName]) this.configs[gamepadName] = {}; this.configs[gamepadName].custom = customMappings; } + injectKeyboardConfig(layout: string, customMappings: MappingLayout): void { + if (!this.keyboardConfigs[layout]) this.keyboardConfigs[layout] = {}; + this.keyboardConfigs[layout].custom = customMappings; + } } \ No newline at end of file diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 9e1c97ae7..fa35d0f28 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -233,6 +233,7 @@ export class GameData { this.loadSettings(); this.loadGamepadSettings(); this.loadCustomMapping(); + this.loadCustomKeyboardMapping(); this.trainerId = Utils.randInt(65536); this.secretId = Utils.randInt(65536); this.starterData = {}; @@ -510,8 +511,16 @@ export class GameData { return true; } + public loadCustomKeyboardMapping(): boolean { + if (!localStorage.hasOwnProperty('customKeyboardMappings')) + return false; + const customKeyboardMappings = JSON.parse(localStorage.getItem('customKeyboardMappings')); + for (const key of Object.keys(customKeyboardMappings)) + this.scene.inputController.injectKeyboardConfig(key, customKeyboardMappings[key]); + + } + public loadCustomMapping(): boolean { - console.log('loadCustomMapping'); if (!localStorage.hasOwnProperty('customMapping')) return false; const customMappings = JSON.parse(localStorage.getItem('customMapping')); From 6eab6f192008379162403b197d5c0a76d6c63702 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Mon, 13 May 2024 03:12:58 +0200 Subject: [PATCH 60/81] added alt keys for keyboard, need to find a way to differentiate main to alt --- src/configs/cfg_keyboard_azerty.ts | 52 +++++++++----- src/configs/gamepad-utils.ts | 5 +- src/system/settings-keyboard.ts | 69 ++++++++++++++++++- .../settings/abstract-settings-ui-handler.ts | 2 +- .../settings/settings-gamepad-ui-handler.ts | 2 +- .../settings/settings-keyboard-ui-handler.ts | 3 +- 6 files changed, 110 insertions(+), 23 deletions(-) diff --git a/src/configs/cfg_keyboard_azerty.ts b/src/configs/cfg_keyboard_azerty.ts index b7d1b09a8..e0a1fac29 100644 --- a/src/configs/cfg_keyboard_azerty.ts +++ b/src/configs/cfg_keyboard_azerty.ts @@ -183,6 +183,24 @@ const cfg_keyboard_azerty = { KEY_V: SettingKeyboard.Button_Cycle_Variant, KEY_PLUS: SettingKeyboard.Button_Speed_Up, KEY_MINUS: SettingKeyboard.Button_Slow_Down, + + KEY_Z: SettingKeyboard.Alt_Button_Up, + KEY_S: SettingKeyboard.Alt_Button_Down, + KEY_Q: SettingKeyboard.Alt_Button_Left, + KEY_D: SettingKeyboard.Alt_Button_Right, + KEY_CTRL: SettingKeyboard.Alt_Button_Submit, + KEY_W: SettingKeyboard.Alt_Button_Action, + KEY_X: SettingKeyboard.Alt_Button_Cancel, + KEY_TAB: SettingKeyboard.Alt_Button_Menu, + KEY_SHIFT: SettingKeyboard.Alt_Button_Stats, + KEY_P: SettingKeyboard.Alt_Button_Cycle_Shiny, + KEY_M: SettingKeyboard.Alt_Button_Cycle_Form, + KEY_O: SettingKeyboard.Alt_Button_Cycle_Gender, + KEY_L: SettingKeyboard.Alt_Button_Cycle_Ability, + KEY_I: SettingKeyboard.Alt_Button_Cycle_Nature, + KEY_K: SettingKeyboard.Alt_Button_Cycle_Variant, + KEY_PAGE_UP: SettingKeyboard.Alt_Button_Speed_Up, + KEY_PAGE_DOWN: SettingKeyboard.Alt_Button_Slow_Down, }, default: { KEY_ARROW_UP: Button.UP, @@ -204,23 +222,23 @@ const cfg_keyboard_azerty = { KEY_MINUS: Button.SLOW_DOWN, KEY_A: -1, KEY_B: -1, - KEY_D: -1, + KEY_D: Button.RIGHT, KEY_H: -1, - KEY_I: -1, + KEY_I: Button.CYCLE_NATURE, KEY_J: -1, - KEY_K: -1, - KEY_L: -1, - KEY_M: -1, - KEY_O: -1, - KEY_P: -1, - KEY_Q: -1, - KEY_S: -1, + KEY_K: Button.CYCLE_VARIANT, + KEY_L: Button.CYCLE_ABILITY, + KEY_M: Button.CYCLE_FORM, + KEY_O: Button.CYCLE_GENDER, + KEY_P: Button.CYCLE_SHINY, + KEY_Q: Button.LEFT, + KEY_S: Button.DOWN, KEY_T: -1, KEY_U: -1, - KEY_W: -1, - KEY_X: -1, + KEY_W: Button.ACTION, + KEY_X: Button.CANCEL, KEY_Y: -1, - KEY_Z: -1, + KEY_Z: Button.UP, KEY_0: -1, KEY_1: -1, KEY_2: -1, @@ -231,7 +249,7 @@ const cfg_keyboard_azerty = { KEY_7: -1, KEY_8: -1, KEY_9: -1, - KEY_CTRL: -1, + KEY_CTRL: Button.SUBMIT, KEY_DEL: -1, KEY_END: -1, KEY_F1: -1, @@ -248,11 +266,11 @@ const cfg_keyboard_azerty = { KEY_F12: -1, KEY_HOME: -1, KEY_INSERT: -1, - KEY_PAGE_DOWN: -1, - KEY_PAGE_UP: -1, + KEY_PAGE_DOWN: Button.SLOW_DOWN, + KEY_PAGE_UP: Button.SPEED_UP, KEY_QUOTATION: -1, - KEY_SHIFT: -1, - KEY_TAB: -1, + KEY_SHIFT: Button.STATS, + KEY_TAB: Button.MENU, KEY_TILDE: -1, KEY_LEFT_BRACKET: -1, KEY_RIGHT_BRACKET: -1, diff --git a/src/configs/gamepad-utils.ts b/src/configs/gamepad-utils.ts index 362bf95e8..5142dcdec 100644 --- a/src/configs/gamepad-utils.ts +++ b/src/configs/gamepad-utils.ts @@ -25,7 +25,8 @@ export function getKeyForSettingName(config: InterfaceConfig, settingName: strin } // Given a Button, return the custom key assigned to it from the config file -export function getCurrenlyAssignedKeyToAction(config: InterfaceConfig, action: Button): String | null { +export function getCurrenlyAssignedKeyToAction(config: InterfaceConfig, action: Button, alt: boolean = false): String | null { + // need to find a way to differentiate main/alt button for (const key of Object.keys(config.custom)) { if (config.custom[key] === action) return key; } @@ -36,7 +37,7 @@ export function getCurrenlyAssignedKeyToAction(config: InterfaceConfig, action: export function getCurrentlyAssignedToSettingName(config: InterfaceConfig, settingName: string): String { const oldKey = getKeyForSettingName(config, settingName) const action = config.default[oldKey]; - const key = getCurrenlyAssignedKeyToAction(config, action); + const key = getCurrenlyAssignedKeyToAction(config, action, settingName.includes("ALT_BUTTON_")); return key; } diff --git a/src/system/settings-keyboard.ts b/src/system/settings-keyboard.ts index 62bcbbfd2..b308153fb 100644 --- a/src/system/settings-keyboard.ts +++ b/src/system/settings-keyboard.ts @@ -7,64 +7,115 @@ import SettingsKeyboardUiHandler from "#app/ui/settings/settings-keyboard-ui-han export enum SettingKeyboard { Default_Layout = "DEFAULT_LAYOUT", Button_Up = "BUTTON_UP", + Alt_Button_Up = "ALT_BUTTON_UP", Button_Down = "BUTTON_DOWN", + Alt_Button_Down = "ALT_BUTTON_DOWN", Button_Left = "BUTTON_LEFT", + Alt_Button_Left = "ALT_BUTTON_LEFT", Button_Right = "BUTTON_RIGHT", + Alt_Button_Right = "ALT_BUTTON_RIGHT", Button_Action = "BUTTON_ACTION", + Alt_Button_Action = "ALT_BUTTON_ACTION", Button_Cancel = "BUTTON_CANCEL", + Alt_Button_Cancel = "ALT_BUTTON_CANCEL", Button_Menu = "BUTTON_MENU", + Alt_Button_Menu = "ALT_BUTTON_MENU", Button_Stats = "BUTTON_STATS", + Alt_Button_Stats = "ALT_BUTTON_STATS", Button_Cycle_Form = "BUTTON_CYCLE_FORM", + Alt_Button_Cycle_Form = "ALT_BUTTON_CYCLE_FORM", Button_Cycle_Shiny = "BUTTON_CYCLE_SHINY", + Alt_Button_Cycle_Shiny = "ALT_BUTTON_CYCLE_SHINY", Button_Cycle_Gender = "BUTTON_CYCLE_GENDER", + Alt_Button_Cycle_Gender = "ALT_BUTTON_CYCLE_GENDER", Button_Cycle_Ability = "BUTTON_CYCLE_ABILITY", + Alt_Button_Cycle_Ability = "ALT_BUTTON_CYCLE_ABILITY", Button_Cycle_Nature = "BUTTON_CYCLE_NATURE", + Alt_Button_Cycle_Nature = "ALT_BUTTON_CYCLE_NATURE", Button_Cycle_Variant = "BUTTON_CYCLE_VARIANT", + Alt_Button_Cycle_Variant = "ALT_BUTTON_CYCLE_VARIANT", Button_Speed_Up = "BUTTON_SPEED_UP", + Alt_Button_Speed_Up = "ALT_BUTTON_SPEED_UP", Button_Slow_Down = "BUTTON_SLOW_DOWN", + Alt_Button_Slow_Down = "ALT_BUTTON_SLOW_DOWN", Button_Submit = "BUTTON_SUBMIT", + Alt_Button_Submit = "ALT_BUTTON_SUBMIT", } export const settingKeyboardOptions: SettingOptions = { [SettingKeyboard.Default_Layout]: ['Default', 'Change'], [SettingKeyboard.Button_Up]: [`KEY ${Button.UP.toString()}`, 'Change'], + [SettingKeyboard.Alt_Button_Up]: [`KEY ${Button.UP.toString()}`, 'Change'], [SettingKeyboard.Button_Down]: [`KEY ${Button.DOWN.toString()}`, 'Change'], + [SettingKeyboard.Alt_Button_Down]: [`KEY ${Button.DOWN.toString()}`, 'Change'], [SettingKeyboard.Button_Left]: [`KEY ${Button.LEFT.toString()}`, 'Change'], + [SettingKeyboard.Alt_Button_Left]: [`KEY ${Button.LEFT.toString()}`, 'Change'], [SettingKeyboard.Button_Right]: [`KEY ${Button.RIGHT.toString()}`, 'Change'], + [SettingKeyboard.Alt_Button_Right]: [`KEY ${Button.RIGHT.toString()}`, 'Change'], [SettingKeyboard.Button_Action]: [`KEY ${Button.ACTION.toString()}`, 'Change'], + [SettingKeyboard.Alt_Button_Action]: [`KEY ${Button.ACTION.toString()}`, 'Change'], [SettingKeyboard.Button_Cancel]: [`KEY ${Button.CANCEL.toString()}`, 'Change'], + [SettingKeyboard.Alt_Button_Cancel]: [`KEY ${Button.CANCEL.toString()}`, 'Change'], [SettingKeyboard.Button_Menu]: [`KEY ${Button.MENU.toString()}`, 'Change'], + [SettingKeyboard.Alt_Button_Menu]: [`KEY ${Button.MENU.toString()}`, 'Change'], [SettingKeyboard.Button_Stats]: [`KEY ${Button.STATS.toString()}`, 'Change'], + [SettingKeyboard.Alt_Button_Stats]: [`KEY ${Button.STATS.toString()}`, 'Change'], [SettingKeyboard.Button_Cycle_Form]: [`KEY ${Button.CYCLE_FORM.toString()}`, 'Change'], + [SettingKeyboard.Alt_Button_Cycle_Form]: [`KEY ${Button.CYCLE_FORM.toString()}`, 'Change'], [SettingKeyboard.Button_Cycle_Shiny]: [`KEY ${Button.CYCLE_SHINY.toString()}`, 'Change'], + [SettingKeyboard.Alt_Button_Cycle_Shiny]: [`KEY ${Button.CYCLE_SHINY.toString()}`, 'Change'], [SettingKeyboard.Button_Cycle_Gender]: [`KEY ${Button.CYCLE_GENDER.toString()}`, 'Change'], + [SettingKeyboard.Alt_Button_Cycle_Gender]: [`KEY ${Button.CYCLE_GENDER.toString()}`, 'Change'], [SettingKeyboard.Button_Cycle_Ability]: [`KEY ${Button.CYCLE_ABILITY.toString()}`, 'Change'], + [SettingKeyboard.Alt_Button_Cycle_Ability]: [`KEY ${Button.CYCLE_ABILITY.toString()}`, 'Change'], [SettingKeyboard.Button_Cycle_Nature]: [`KEY ${Button.CYCLE_NATURE.toString()}`, 'Change'], + [SettingKeyboard.Alt_Button_Cycle_Nature]: [`KEY ${Button.CYCLE_NATURE.toString()}`, 'Change'], [SettingKeyboard.Button_Cycle_Variant]: [`KEY ${Button.CYCLE_VARIANT.toString()}`, 'Change'], + [SettingKeyboard.Alt_Button_Cycle_Variant]: [`KEY ${Button.CYCLE_VARIANT.toString()}`, 'Change'], [SettingKeyboard.Button_Speed_Up]: [`KEY ${Button.SPEED_UP.toString()}`, 'Change'], + [SettingKeyboard.Alt_Button_Speed_Up]: [`KEY ${Button.SPEED_UP.toString()}`, 'Change'], [SettingKeyboard.Button_Slow_Down]: [`KEY ${Button.SLOW_DOWN.toString()}`, 'Change'], - [SettingKeyboard.Button_Submit]: [`KEY ${Button.SUBMIT.toString()}`, 'Change'] + [SettingKeyboard.Alt_Button_Slow_Down]: [`KEY ${Button.SLOW_DOWN.toString()}`, 'Change'], + [SettingKeyboard.Button_Submit]: [`KEY ${Button.SUBMIT.toString()}`, 'Change'], + [SettingKeyboard.Alt_Button_Submit]: [`KEY ${Button.SUBMIT.toString()}`, 'Change'] }; export const settingKeyboardDefaults: SettingDefaults = { [SettingKeyboard.Default_Layout]: 0, [SettingKeyboard.Button_Up]: 0, + [SettingKeyboard.Alt_Button_Up]: 0, [SettingKeyboard.Button_Down]: 0, + [SettingKeyboard.Alt_Button_Down]: 0, [SettingKeyboard.Button_Left]: 0, + [SettingKeyboard.Alt_Button_Left]: 0, [SettingKeyboard.Button_Right]: 0, + [SettingKeyboard.Alt_Button_Right]: 0, [SettingKeyboard.Button_Action]: 0, + [SettingKeyboard.Alt_Button_Action]: 0, [SettingKeyboard.Button_Cancel]: 0, + [SettingKeyboard.Alt_Button_Cancel]: 0, [SettingKeyboard.Button_Menu]: 0, + [SettingKeyboard.Alt_Button_Menu]: 0, [SettingKeyboard.Button_Stats]: 0, + [SettingKeyboard.Alt_Button_Stats]: 0, [SettingKeyboard.Button_Cycle_Form]: 0, + [SettingKeyboard.Alt_Button_Cycle_Form]: 0, [SettingKeyboard.Button_Cycle_Shiny]: 0, + [SettingKeyboard.Alt_Button_Cycle_Shiny]: 0, [SettingKeyboard.Button_Cycle_Gender]: 0, + [SettingKeyboard.Alt_Button_Cycle_Gender]: 0, [SettingKeyboard.Button_Cycle_Ability]: 0, + [SettingKeyboard.Alt_Button_Cycle_Ability]: 0, [SettingKeyboard.Button_Cycle_Nature]: 0, + [SettingKeyboard.Alt_Button_Cycle_Nature]: 0, [SettingKeyboard.Button_Cycle_Variant]: 0, + [SettingKeyboard.Alt_Button_Cycle_Variant]: 0, [SettingKeyboard.Button_Speed_Up]: 0, + [SettingKeyboard.Alt_Button_Speed_Up]: 0, [SettingKeyboard.Button_Slow_Down]: 0, + [SettingKeyboard.Alt_Button_Slow_Down]: 0, [SettingKeyboard.Button_Submit]: 0, + [SettingKeyboard.Alt_Button_Submit]: 0, }; @@ -86,6 +137,22 @@ export function setSettingKeyboard(scene: BattleScene, setting: SettingKeyboard, case SettingKeyboard.Button_Cycle_Variant: case SettingKeyboard.Button_Speed_Up: case SettingKeyboard.Button_Slow_Down: + case SettingKeyboard.Alt_Button_Up: + case SettingKeyboard.Alt_Button_Down: + case SettingKeyboard.Alt_Button_Left: + case SettingKeyboard.Alt_Button_Right: + case SettingKeyboard.Alt_Button_Action: + case SettingKeyboard.Alt_Button_Cancel: + case SettingKeyboard.Alt_Button_Menu: + case SettingKeyboard.Alt_Button_Stats: + case SettingKeyboard.Alt_Button_Cycle_Shiny: + case SettingKeyboard.Alt_Button_Cycle_Form: + case SettingKeyboard.Alt_Button_Cycle_Gender: + case SettingKeyboard.Alt_Button_Cycle_Ability: + case SettingKeyboard.Alt_Button_Cycle_Nature: + case SettingKeyboard.Alt_Button_Cycle_Variant: + case SettingKeyboard.Alt_Button_Speed_Up: + case SettingKeyboard.Alt_Button_Slow_Down: if (value) { if (scene.ui) { const cancelHandler = (success: boolean = false) : boolean => { diff --git a/src/ui/settings/abstract-settings-ui-handler.ts b/src/ui/settings/abstract-settings-ui-handler.ts index b001495f5..4b87a7214 100644 --- a/src/ui/settings/abstract-settings-ui-handler.ts +++ b/src/ui/settings/abstract-settings-ui-handler.ts @@ -57,7 +57,7 @@ export default abstract class AbstractSettingsUiUiHandler extends UiHandler { abstract navigateMenuLeft(): boolean; abstract navigateMenuRight(): boolean; abstract saveSettingToLocalStorage(setting, cursor): void; - abstract getActiveConfig(): void; + abstract getActiveConfig(): InterfaceConfig; constructor(scene: BattleScene, mode?: Mode) { super(scene, mode); diff --git a/src/ui/settings/settings-gamepad-ui-handler.ts b/src/ui/settings/settings-gamepad-ui-handler.ts index f4d85cf0c..450a328d8 100644 --- a/src/ui/settings/settings-gamepad-ui-handler.ts +++ b/src/ui/settings/settings-gamepad-ui-handler.ts @@ -38,7 +38,7 @@ export default class SettingsGamepadUiHandler extends AbstractSettingsUiUiHandle this.layout['noGamepads'].label = label; } - getActiveConfig() { + getActiveConfig(): InterfaceConfig { return this.scene.inputController.getActiveConfig(); } diff --git a/src/ui/settings/settings-keyboard-ui-handler.ts b/src/ui/settings/settings-keyboard-ui-handler.ts index dbb342c5a..fdbbb5c07 100644 --- a/src/ui/settings/settings-keyboard-ui-handler.ts +++ b/src/ui/settings/settings-keyboard-ui-handler.ts @@ -4,6 +4,7 @@ import cfg_keyboard_azerty from "#app/configs/cfg_keyboard_azerty"; import {SettingKeyboard, settingKeyboardDefaults, settingKeyboardOptions} from "#app/system/settings-keyboard"; import {truncateString} from "#app/utils"; import AbstractSettingsUiUiHandler from "#app/ui/settings/abstract-settings-ui-handler"; +import {InterfaceConfig} from "#app/inputs-controller"; export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandler { @@ -19,7 +20,7 @@ export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandl this.localStoragePropertyName = 'settingsKeyboard'; } - getActiveConfig() { + getActiveConfig(): InterfaceConfig { return this.scene.inputController.getActiveKeyboardConfig(); } From e3685d2d19724490c77cbfcf9fe2625f88708a95 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Tue, 14 May 2024 00:21:51 +0200 Subject: [PATCH 61/81] refactor + cleanup + alt keyboard + tests --- src/configs/gamepad-utils.ts | 96 ++++-- src/inputs-controller.ts | 96 ++---- src/test/cfg_gamepad_example.ts | 97 ++++++ src/test/cfg_keyboard_example.ts | 319 ++++++++++++++++++ src/test/gamepad_remaping.test.ts | 166 +++++++++ src/test/keyboard_remaping.test.ts | 268 +++++++++++++++ src/ui/settings/abrast-binding-ui-handler.ts | 2 +- .../settings/abstract-settings-ui-handler.ts | 10 +- src/ui/settings/gamepad-binding-ui-handler.ts | 14 +- .../settings/keyboard-binding-ui-handler.ts | 13 +- src/utils.ts | 4 + 11 files changed, 969 insertions(+), 116 deletions(-) create mode 100644 src/test/cfg_gamepad_example.ts create mode 100644 src/test/cfg_keyboard_example.ts create mode 100644 src/test/gamepad_remaping.test.ts create mode 100644 src/test/keyboard_remaping.test.ts diff --git a/src/configs/gamepad-utils.ts b/src/configs/gamepad-utils.ts index 5142dcdec..b6968d369 100644 --- a/src/configs/gamepad-utils.ts +++ b/src/configs/gamepad-utils.ts @@ -1,20 +1,13 @@ import {InterfaceConfig} from "../inputs-controller"; import {Button} from "#app/enums/buttons"; - // Given a button index from an input event, return its naming from the mapping config -export function getKeyFromInputIndex(config: InterfaceConfig, index: number): String | null { +export function getKeyFromMapping(config: InterfaceConfig, index: number): String | null { for (const key of Object.keys(config.gamepadMapping)) { if (config.gamepadMapping[key] === index) return key; } return null; } -export function getKeyFromKeyboardKeyCode(config: InterfaceConfig, key): String | null { - for (const _key of Object.keys(config.gamepadMapping)) { - if (config.gamepadMapping[_key] === key) return _key; - } - return null; -} // Given a setting name, return the key assigned to it from the config file export function getKeyForSettingName(config: InterfaceConfig, settingName: string): String | null { @@ -24,36 +17,79 @@ export function getKeyForSettingName(config: InterfaceConfig, settingName: strin return null; } -// Given a Button, return the custom key assigned to it from the config file -export function getCurrenlyAssignedKeyToAction(config: InterfaceConfig, action: Button, alt: boolean = false): String | null { - // need to find a way to differentiate main/alt button - for (const key of Object.keys(config.custom)) { - if (config.custom[key] === action) return key; - } - return null; +// Given a setting name, return the key assigned to it from the config file +export function getIconForSettingName(config: InterfaceConfig, settingName: string): String | null { + const key = getKeyForSettingName(config, settingName); + return config.icons[key]; } -// Given a setting name, return the custom key for the default action from the config file -export function getCurrentlyAssignedToSettingName(config: InterfaceConfig, settingName: string): String { - const oldKey = getKeyForSettingName(config, settingName) - const action = config.default[oldKey]; - const key = getCurrenlyAssignedKeyToAction(config, action, settingName.includes("ALT_BUTTON_")); +// Given a Button, return the custom key assigned to it from the config file +export function getKeyWithAction(config: InterfaceConfig, action: Button, alt: boolean = false): String | null { + // need to find a way to differentiate main/alt button + const { key } = getKeyAndSettingNameFromCurrentKeysWithAction(config, action, alt); return key; } // Given a button index from an input event, return its icon from the config file -export function getCurrenlyAssignedIconFromInputIndex(config: InterfaceConfig, index: number): String { - const key = getKeyFromInputIndex(config, index); +export function getIconWithPressedButton(config: InterfaceConfig, pressedButton: number): String { + const key = getKeyFromMapping(config, pressedButton); return config.icons[key]; } -export function getCurrenlyAssignedIconFromKeyboardKeyCode(config: InterfaceConfig, key): String { - const _key = getKeyFromKeyboardKeyCode(config, key); - return config.icons[_key]; -} - // Given a setting name, return the icon currently assigned to this setting name -export function getCurrentlyAssignedIconToSettingName(config: InterfaceConfig, settingName: string): string { - const key = getCurrentlyAssignedToSettingName(config, settingName); - return config.icons[key]; +export function getIconWithSettingName(config: InterfaceConfig, settingName: string): string { + const { icon } = getKeyAndActionFromCurrentKeysWithSettingName(config, settingName) + return icon; } + +function getKeyAndSettingNameFromCurrentKeysWithAction(config, _action, alt: boolean = false) { + for (const _settingName of Object.keys(config.currentKeys)) { + if (alt && !_settingName.includes("ALT_")) continue; + if (config.currentKeys[_settingName].action === _action) return { + settingName: _settingName, + key: config.currentKeys[_settingName].key, + }; + } + return null; +} + +export function getKeyAndActionFromCurrentKeysWithSettingName(config, settingName) { + for (const _settingName of Object.keys(config.currentKeys)) { + if (_settingName === settingName) return config.currentKeys[_settingName]; + } + return null; +} + +export function getKeyAndActionFromCurrentKeysWithPressedButton(config, pressedButton) { + const key = getKeyFromMapping(config, pressedButton); + const settingName = Object.keys(config.currentKeys).find(_s => config.currentKeys[_s].key === key); + return getKeyAndActionFromCurrentKeysWithSettingName(config, settingName); +} + +export function swapCurrentKeys(config: InterfaceConfig, settingName, pressedButton): void { + const previousBind = getKeyAndActionFromCurrentKeysWithSettingName(config, settingName); + const newBind = getKeyAndActionFromCurrentKeysWithPressedButton(config, pressedButton); + config.custom[previousBind.key] = newBind.action; + config.custom[newBind.key] = previousBind.action; + config.icons[previousBind.key] = newBind.icon; + config.icons[newBind.key] = previousBind.icon; + reloadCurrentKeys(config); +} + + +export function reloadCurrentKeys(config): void { + // need to rework this to include keys that were not there at the begining + const currentKeys = {}; + debugger; + for (const key of Object.keys(config.setting)) { + const settingName = config.setting[key]; + const action = config.custom[key]; + const icon = config.icons[key]; + currentKeys[settingName] = { + key, + action, + icon, + } + } + config.currentKeys = currentKeys; +} \ No newline at end of file diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index f5ff95e51..93010f668 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -7,18 +7,14 @@ import pad_dualshock from "./configs/pad_dualshock"; import {Button} from "./enums/buttons"; import {Mode} from "./ui/ui"; import SettingsGamepadUiHandler from "./ui/settings/settings-gamepad-ui-handler"; -import {SettingGamepad} from "./system/settings-gamepad"; -import { - getCurrenlyAssignedIconFromInputIndex, - getCurrentlyAssignedIconToSettingName, - getKeyFromInputIndex, - getCurrentlyAssignedToSettingName, - getCurrenlyAssignedIconFromKeyboardKeyCode, - getKeyFromKeyboardKeyCode -} from "./configs/gamepad-utils"; import SettingsKeyboardUiHandler from "./ui/settings/settings-keyboard-ui-handler"; import cfg_keyboard_azerty from "./configs/cfg_keyboard_azerty"; -import {SettingKeyboard} from "./system/settings-keyboard"; +import { + getKeyAndActionFromCurrentKeysWithPressedButton, + getKeyFromMapping, + reloadCurrentKeys, swapCurrentKeys +} from "#app/configs/gamepad-utils"; +import {deepCopy} from "./utils"; export interface GamepadMapping { [key: string]: number; @@ -340,8 +336,9 @@ export class InputsController { const allGamepads = this.getGamepadsName(); for (const gamepad of allGamepads) { const gamepadID = gamepad.toLowerCase(); - const config = this.getConfig(gamepadID); + const config = deepCopy(this.getConfig(gamepadID)); config.custom = this.configs[gamepad]?.custom || {...config.default}; + reloadCurrentKeys(config); this.configs[gamepad] = config; this.scene.gameData?.saveCustomMapping(this.chosenGamepad, this.configs[gamepad]?.custom); } @@ -350,8 +347,9 @@ export class InputsController { setupKeyboard(): void { for (const layout of ['default']) { - const config = this.getConfigKeyboard(layout); + const config = deepCopy(this.getConfigKeyboard(layout)); config.custom = this.keyboardConfigs[layout]?.custom || {...config.default}; + reloadCurrentKeys(config); this.keyboardConfigs[layout] = config; this.scene.gameData?.saveCustomKeyboardMapping(this.chosenKeyboard, this.keyboardConfigs[layout]?.custom); } @@ -383,7 +381,7 @@ export class InputsController { this.setupKeyboard(); if (this.keys.includes(keyDown)) return; this.keys.push(keyDown); - const key = getKeyFromKeyboardKeyCode(this.keyboardConfigs[this.chosenKeyboard], keyDown); + const key = getKeyFromMapping(this.keyboardConfigs[this.chosenKeyboard], keyDown); const buttonDown = this.keyboardConfigs[this.chosenKeyboard].custom[key]; this.lastSource = 'keyboard'; if (buttonDown !== undefined) { @@ -400,7 +398,7 @@ export class InputsController { this.keys = this.keys.filter(k => k !== keyDown); if (!this.keyboardConfigs[this.chosenKeyboard]?.padID) this.setupKeyboard(); - const key = getKeyFromKeyboardKeyCode(this.keyboardConfigs[this.chosenKeyboard], keyDown); + const key = getKeyFromMapping(this.keyboardConfigs[this.chosenKeyboard], keyDown); const buttonUp = this.keyboardConfigs[this.chosenKeyboard].custom[key]; if (buttonUp !== undefined) { this.events.emit('input_up', { @@ -427,7 +425,7 @@ export class InputsController { if (!this.chosenGamepad) this.setChosenGamepad(pad.id); if (!this.gamepadSupport || pad.id.toLowerCase() !== this.chosenGamepad.toLowerCase()) return; - const key = getKeyFromInputIndex(this.configs[pad.id], button.index); + const key = getKeyFromMapping(this.configs[pad.id], button.index); const buttonDown = this.configs[pad.id].custom[key]; this.lastSource = 'gamepad'; if (buttonDown !== undefined) { @@ -451,7 +449,7 @@ export class InputsController { gamepadButtonUp(pad: Phaser.Input.Gamepad.Gamepad, button: Phaser.Input.Gamepad.Button, value: number): void { if (!pad) return; if (!this.gamepadSupport || pad.id !== this.chosenGamepad) return; - const key = getKeyFromInputIndex(this.configs[pad.id], button.index); + const key = getKeyFromMapping(this.configs[pad.id], button.index); const buttonUp = this.configs[pad.id]?.custom[key]; if (buttonUp !== undefined) { this.events.emit('input_up', { @@ -747,66 +745,6 @@ export class InputsController { return null; } - /** - * Determines icon for a button pressed on the currently chosen gamepad based on its configuration. - * - * @param button The button for which to retrieve the label and icon. - * @returns Array Tuple containing the pad type and the currently assigned icon for the button index. - */ - getPressedButtonLabel(button: Phaser.Input.Gamepad.Button): [string, string] { - return [this.configs[this.chosenGamepad].padType, getCurrenlyAssignedIconFromInputIndex(this.configs[this.chosenGamepad], button.index)]; - } - - getPressedKeyLabel(key): string { - return getCurrenlyAssignedIconFromKeyboardKeyCode(this.keyboardConfigs[this.chosenKeyboard], key); - } - - /** - * Retrieves the currently assigned icon for a specific setting on the chosen gamepad. - * - * @param target The gamepad setting for which to retrieve the assigned icon. - * @returns string The icon assigned to the specified setting. - */ - getCurrentlyAssignedIconToDisplay(target: SettingGamepad): string { - return getCurrentlyAssignedIconToSettingName(this.configs[this.chosenGamepad], target); - } - - getKeyboardCurrentlyAssignedIconToDisplay(target: SettingKeyboard): string { - return getCurrentlyAssignedIconToSettingName(this.keyboardConfigs[this.chosenKeyboard], target); - } - - /** - * Swaps the binding of two controls on the chosen gamepad configuration. - * It temporarily pauses updates, swaps the key bindings, saves the new configuration, - * and then resumes updates after a short delay. - * - * @param settingName The name of the setting for which to swap the binding. - * @param pressedButton The button index whose binding is to be swapped. - */ - swapBinding(settingName, pressedButton): void { - this.pauseUpdate = true; - const keyTarget = getCurrentlyAssignedToSettingName(this.configs[this.chosenGamepad], settingName) - const keyNewBinding = getKeyFromInputIndex(this.configs[this.chosenGamepad], pressedButton); - const previousActionForThisNewBinding = this.configs[this.chosenGamepad].custom[keyNewBinding]; - const ActionForThisNewBinding = this.configs[this.chosenGamepad].custom[keyTarget]; - this.configs[this.chosenGamepad].custom[keyTarget] = previousActionForThisNewBinding; - this.configs[this.chosenGamepad].custom[keyNewBinding] = ActionForThisNewBinding; - this.scene.gameData.saveCustomMapping(this.chosenGamepad, this.configs[this.chosenGamepad].custom); - setTimeout(() => this.pauseUpdate = false, 500); - } - - swapKeyboardBinding(settingName, pressedButton): void { - this.pauseUpdate = true; - const keyTarget = getCurrentlyAssignedToSettingName(this.keyboardConfigs[this.chosenKeyboard], settingName) - const keyNewBinding = getKeyFromKeyboardKeyCode(this.keyboardConfigs[this.chosenKeyboard], pressedButton); - const previousActionForThisNewBinding = this.keyboardConfigs[this.chosenKeyboard].custom[keyNewBinding]; - const ActionForThisNewBinding = this.keyboardConfigs[this.chosenKeyboard].custom[keyTarget]; - this.keyboardConfigs[this.chosenKeyboard].custom[keyTarget] = previousActionForThisNewBinding; - this.keyboardConfigs[this.chosenKeyboard].custom[keyNewBinding] = ActionForThisNewBinding; - this.scene.gameData.saveCustomKeyboardMapping(this.chosenKeyboard, this.keyboardConfigs[this.chosenKeyboard].custom); - setTimeout(() => this.pauseUpdate = false, 500); - } - /** * Injects a custom mapping configuration into the gamepad configuration for a specific gamepad. * If the gamepad does not have an existing configuration, it initializes one first. @@ -822,4 +760,10 @@ export class InputsController { if (!this.keyboardConfigs[layout]) this.keyboardConfigs[layout] = {}; this.keyboardConfigs[layout].custom = customMappings; } + + swapBinding(config, settingName, pressedButton): void { + this.pauseUpdate = true; + swapCurrentKeys(config, settingName, pressedButton) + setTimeout(() => this.pauseUpdate = false, 500); + } } \ No newline at end of file diff --git a/src/test/cfg_gamepad_example.ts b/src/test/cfg_gamepad_example.ts new file mode 100644 index 000000000..739111288 --- /dev/null +++ b/src/test/cfg_gamepad_example.ts @@ -0,0 +1,97 @@ +import {Button} from "#app/enums/buttons"; + +export enum SettingInterfaceGamepad { + Default_Controller = "DEFAULT_CONTROLLER", + Gamepad_Support = "GAMEPAD_SUPPORT", + Button_Action = "BUTTON_ACTION", + Button_Cancel = "BUTTON_CANCEL", + Button_Menu = "BUTTON_MENU", + Button_Stats = "BUTTON_STATS", + Button_Cycle_Form = "BUTTON_CYCLE_FORM", + Button_Cycle_Shiny = "BUTTON_CYCLE_SHINY", + Button_Cycle_Gender = "BUTTON_CYCLE_GENDER", + Button_Cycle_Ability = "BUTTON_CYCLE_ABILITY", + Button_Cycle_Nature = "BUTTON_CYCLE_NATURE", + Button_Cycle_Variant = "BUTTON_CYCLE_VARIANT", + Button_Speed_Up = "BUTTON_SPEED_UP", + Button_Slow_Down = "BUTTON_SLOW_DOWN", + Button_Submit = "BUTTON_SUBMIT", +} + +/** + * Generic pad mapping + */ +const pad_xbox360 = { + padID: 'Xbox 360 controller (XInput STANDARD GAMEPAD)', + padType: 'xbox', + gamepadMapping: { + RC_S: 0, + RC_E: 1, + RC_W: 2, + RC_N: 3, + START: 9, + SELECT: 8, + LB: 4, + RB: 5, + LT: 6, + RT: 7, + LS: 10, + RS: 11, + LC_N: 12, + LC_S: 13, + LC_W: 14, + LC_E: 15 + }, + icons: { + RC_S: "T_X_A_Color_Alt.png", + RC_E: "T_X_B_Color_Alt.png", + RC_W: "T_X_X_Color_Alt.png", + RC_N: "T_X_Y_Color_Alt.png", + START: "T_X_X_Alt.png", + SELECT: "T_X_Share_Alt.png", + LB: "T_X_LB_Alt.png", + RB: "T_X_RB_Alt.png", + LT: "T_X_LT_Alt.png", + RT: "T_X_RT_Alt.png", + LS: "T_X_Left_Stick_Click_Alt_Alt.png", + RS: "T_X_Right_Stick_Click_Alt_Alt.png", + LC_N: "T_X_Dpad_Up_Alt.png", + LC_S: "T_X_Dpad_Down_Alt.png", + LC_W: "T_X_Dpad_Left_Alt.png", + LC_E: "T_X_Dpad_Right_Alt.png", + }, + setting: { + RC_S: SettingInterfaceGamepad.Button_Action, + RC_E: SettingInterfaceGamepad.Button_Cancel, + RC_W: SettingInterfaceGamepad.Button_Cycle_Nature, + RC_N: SettingInterfaceGamepad.Button_Cycle_Variant, + START: SettingInterfaceGamepad.Button_Menu, + SELECT: SettingInterfaceGamepad.Button_Stats, + LB: SettingInterfaceGamepad.Button_Cycle_Form, + RB: SettingInterfaceGamepad.Button_Cycle_Shiny, + LT: SettingInterfaceGamepad.Button_Cycle_Gender, + RT: SettingInterfaceGamepad.Button_Cycle_Ability, + LS: SettingInterfaceGamepad.Button_Speed_Up, + RS: SettingInterfaceGamepad.Button_Slow_Down, + }, + default: { + RC_S: Button.ACTION, //5 + RC_E: Button.CANCEL, // 6 + RC_W: Button.CYCLE_NATURE, + RC_N: Button.CYCLE_VARIANT, //14 + START: Button.MENU, //7 + SELECT: Button.STATS, //8 + LB: Button.CYCLE_FORM, + RB: Button.CYCLE_SHINY, + LT: Button.CYCLE_GENDER, + RT: Button.CYCLE_ABILITY, + LS: Button.SPEED_UP, + RS: Button.SLOW_DOWN, + LC_N: Button.UP, + LC_S: Button.DOWN, + LC_W: Button.LEFT, + LC_E: Button.RIGHT, + } +}; + +export default pad_xbox360; diff --git a/src/test/cfg_keyboard_example.ts b/src/test/cfg_keyboard_example.ts new file mode 100644 index 000000000..c97594013 --- /dev/null +++ b/src/test/cfg_keyboard_example.ts @@ -0,0 +1,319 @@ +import {Button} from "#app/enums/buttons"; + +export enum SettingInterfaceKeyboard { + Default_Layout = "DEFAULT_LAYOUT", + Button_Up = "BUTTON_UP", + Alt_Button_Up = "ALT_BUTTON_UP", + Button_Down = "BUTTON_DOWN", + Alt_Button_Down = "ALT_BUTTON_DOWN", + Button_Left = "BUTTON_LEFT", + Alt_Button_Left = "ALT_BUTTON_LEFT", + Button_Right = "BUTTON_RIGHT", + Alt_Button_Right = "ALT_BUTTON_RIGHT", + Button_Action = "BUTTON_ACTION", + Alt_Button_Action = "ALT_BUTTON_ACTION", + Button_Cancel = "BUTTON_CANCEL", + Alt_Button_Cancel = "ALT_BUTTON_CANCEL", + Button_Menu = "BUTTON_MENU", + Alt_Button_Menu = "ALT_BUTTON_MENU", + Button_Stats = "BUTTON_STATS", + Alt_Button_Stats = "ALT_BUTTON_STATS", + Button_Cycle_Form = "BUTTON_CYCLE_FORM", + Alt_Button_Cycle_Form = "ALT_BUTTON_CYCLE_FORM", + Button_Cycle_Shiny = "BUTTON_CYCLE_SHINY", + Alt_Button_Cycle_Shiny = "ALT_BUTTON_CYCLE_SHINY", + Button_Cycle_Gender = "BUTTON_CYCLE_GENDER", + Alt_Button_Cycle_Gender = "ALT_BUTTON_CYCLE_GENDER", + Button_Cycle_Ability = "BUTTON_CYCLE_ABILITY", + Alt_Button_Cycle_Ability = "ALT_BUTTON_CYCLE_ABILITY", + Button_Cycle_Nature = "BUTTON_CYCLE_NATURE", + Alt_Button_Cycle_Nature = "ALT_BUTTON_CYCLE_NATURE", + Button_Cycle_Variant = "BUTTON_CYCLE_VARIANT", + Alt_Button_Cycle_Variant = "ALT_BUTTON_CYCLE_VARIANT", + Button_Speed_Up = "BUTTON_SPEED_UP", + Alt_Button_Speed_Up = "ALT_BUTTON_SPEED_UP", + Button_Slow_Down = "BUTTON_SLOW_DOWN", + Alt_Button_Slow_Down = "ALT_BUTTON_SLOW_DOWN", + Button_Submit = "BUTTON_SUBMIT", + Alt_Button_Submit = "ALT_BUTTON_SUBMIT", +} + +const cfg_keyboard_azerty = { + padID: 'keyboard', + padType: 'default', + gamepadMapping: { + KEY_A: Phaser.Input.Keyboard.KeyCodes.A, + KEY_B: Phaser.Input.Keyboard.KeyCodes.B, + KEY_C: Phaser.Input.Keyboard.KeyCodes.C, + KEY_D: Phaser.Input.Keyboard.KeyCodes.D, + KEY_E: Phaser.Input.Keyboard.KeyCodes.E, + KEY_F: Phaser.Input.Keyboard.KeyCodes.F, + KEY_G: Phaser.Input.Keyboard.KeyCodes.G, + KEY_H: Phaser.Input.Keyboard.KeyCodes.H, + KEY_I: Phaser.Input.Keyboard.KeyCodes.I, + KEY_J: Phaser.Input.Keyboard.KeyCodes.J, + KEY_K: Phaser.Input.Keyboard.KeyCodes.K, + KEY_L: Phaser.Input.Keyboard.KeyCodes.L, + KEY_M: Phaser.Input.Keyboard.KeyCodes.M, + KEY_N: Phaser.Input.Keyboard.KeyCodes.N, + KEY_O: Phaser.Input.Keyboard.KeyCodes.O, + KEY_P: Phaser.Input.Keyboard.KeyCodes.P, + KEY_Q: Phaser.Input.Keyboard.KeyCodes.Q, + KEY_R: Phaser.Input.Keyboard.KeyCodes.R, + KEY_S: Phaser.Input.Keyboard.KeyCodes.S, + KEY_T: Phaser.Input.Keyboard.KeyCodes.T, + KEY_U: Phaser.Input.Keyboard.KeyCodes.U, + KEY_V: Phaser.Input.Keyboard.KeyCodes.V, + KEY_W: Phaser.Input.Keyboard.KeyCodes.W, + KEY_X: Phaser.Input.Keyboard.KeyCodes.X, + KEY_Y: Phaser.Input.Keyboard.KeyCodes.Y, + KEY_Z: Phaser.Input.Keyboard.KeyCodes.Z, + KEY_0: Phaser.Input.Keyboard.KeyCodes.ZERO, + KEY_1: Phaser.Input.Keyboard.KeyCodes.ONE, + KEY_2: Phaser.Input.Keyboard.KeyCodes.TWO, + KEY_3: Phaser.Input.Keyboard.KeyCodes.THREE, + KEY_4: Phaser.Input.Keyboard.KeyCodes.FOUR, + KEY_5: Phaser.Input.Keyboard.KeyCodes.FIVE, + KEY_6: Phaser.Input.Keyboard.KeyCodes.SIX, + KEY_7: Phaser.Input.Keyboard.KeyCodes.SEVEN, + KEY_8: Phaser.Input.Keyboard.KeyCodes.EIGHT, + KEY_9: Phaser.Input.Keyboard.KeyCodes.NINE, + KEY_CTRL: Phaser.Input.Keyboard.KeyCodes.CTRL, + KEY_DEL: Phaser.Input.Keyboard.KeyCodes.DELETE, + KEY_END: Phaser.Input.Keyboard.KeyCodes.END, + KEY_ENTER: Phaser.Input.Keyboard.KeyCodes.ENTER, + KEY_ESC: Phaser.Input.Keyboard.KeyCodes.ESC, + KEY_F1: Phaser.Input.Keyboard.KeyCodes.F1, + KEY_F2: Phaser.Input.Keyboard.KeyCodes.F2, + KEY_F3: Phaser.Input.Keyboard.KeyCodes.F3, + KEY_F4: Phaser.Input.Keyboard.KeyCodes.F4, + KEY_F5: Phaser.Input.Keyboard.KeyCodes.F5, + KEY_F6: Phaser.Input.Keyboard.KeyCodes.F6, + KEY_F7: Phaser.Input.Keyboard.KeyCodes.F7, + KEY_F8: Phaser.Input.Keyboard.KeyCodes.F8, + KEY_F9: Phaser.Input.Keyboard.KeyCodes.F9, + KEY_F10: Phaser.Input.Keyboard.KeyCodes.F10, + KEY_F11: Phaser.Input.Keyboard.KeyCodes.F11, + KEY_F12: Phaser.Input.Keyboard.KeyCodes.F12, + KEY_HOME: Phaser.Input.Keyboard.KeyCodes.HOME, + KEY_INSERT: Phaser.Input.Keyboard.KeyCodes.INSERT, + KEY_PAGE_DOWN: Phaser.Input.Keyboard.KeyCodes.PAGE_DOWN, + KEY_PAGE_UP: Phaser.Input.Keyboard.KeyCodes.PAGE_UP, + KEY_PLUS: Phaser.Input.Keyboard.KeyCodes.NUMPAD_ADD, // Assuming numpad plus + KEY_MINUS: Phaser.Input.Keyboard.KeyCodes.NUMPAD_SUBTRACT, // Assuming numpad minus + KEY_QUOTATION: Phaser.Input.Keyboard.KeyCodes.QUOTES, + KEY_SHIFT: Phaser.Input.Keyboard.KeyCodes.SHIFT, + KEY_SPACE: Phaser.Input.Keyboard.KeyCodes.SPACE, + KEY_TAB: Phaser.Input.Keyboard.KeyCodes.TAB, + KEY_TILDE: Phaser.Input.Keyboard.KeyCodes.BACKTICK, + KEY_ARROW_UP: Phaser.Input.Keyboard.KeyCodes.UP, + KEY_ARROW_DOWN: Phaser.Input.Keyboard.KeyCodes.DOWN, + KEY_ARROW_LEFT: Phaser.Input.Keyboard.KeyCodes.LEFT, + KEY_ARROW_RIGHT: Phaser.Input.Keyboard.KeyCodes.RIGHT, + KEY_LEFT_BRACKET: Phaser.Input.Keyboard.KeyCodes.OPEN_BRACKET, + KEY_RIGHT_BRACKET: Phaser.Input.Keyboard.KeyCodes.CLOSED_BRACKET, + KEY_SEMICOLON: Phaser.Input.Keyboard.KeyCodes.SEMICOLON, + KEY_BACKSPACE: Phaser.Input.Keyboard.KeyCodes.BACKSPACE, + KEY_ALT: Phaser.Input.Keyboard.KeyCodes.ALT + }, + icons: { + KEY_A: "T_A_Key_Dark.png", + KEY_B: "T_B_Key_Dark.png", + KEY_C: "T_C_Key_Dark.png", + KEY_D: "T_D_Key_Dark.png", + KEY_E: "T_E_Key_Dark.png", + KEY_F: "T_F_Key_Dark.png", + KEY_G: "T_G_Key_Dark.png", + KEY_H: "T_H_Key_Dark.png", + KEY_I: "T_I_Key_Dark.png", + KEY_J: "T_J_Key_Dark.png", + KEY_K: "T_K_Key_Dark.png", + KEY_L: "T_L_Key_Dark.png", + KEY_M: "T_M_Key_Dark.png", + KEY_N: "T_N_Key_Dark.png", + KEY_O: "T_O_Key_Dark.png", + KEY_P: "T_P_Key_Dark.png", + KEY_Q: "T_Q_Key_Dark.png", + KEY_R: "T_R_Key_Dark.png", + KEY_S: "T_S_Key_Dark.png", + KEY_T: "T_T_Key_Dark.png", + KEY_U: "T_U_Key_Dark.png", + KEY_V: "T_V_Key_Dark.png", + KEY_W: "T_W_Key_Dark.png", + KEY_X: "T_X_Key_Dark.png", + KEY_Y: "T_Y_Key_Dark.png", + KEY_Z: "T_Z_Key_Dark.png", + + KEY_0: "T_0_Key_Dark.png", + KEY_1: "T_1_Key_Dark.png", + KEY_2: "T_2_Key_Dark.png", + KEY_3: "T_3_Key_Dark.png", + KEY_4: "T_4_Key_Dark.png", + KEY_5: "T_5_Key_Dark.png", + KEY_6: "T_6_Key_Dark.png", + KEY_7: "T_7_Key_Dark.png", + KEY_8: "T_8_Key_Dark.png", + KEY_9: "T_9_Key_Dark.png", + + KEY_F1: "T_F1_Key_Dark.png", + KEY_F2: "T_F2_Key_Dark.png", + KEY_F3: "T_F3_Key_Dark.png", + KEY_F4: "T_F4_Key_Dark.png", + KEY_F5: "T_F5_Key_Dark.png", + KEY_F6: "T_F6_Key_Dark.png", + KEY_F7: "T_F7_Key_Dark.png", + KEY_F8: "T_F8_Key_Dark.png", + KEY_F9: "T_F9_Key_Dark.png", + KEY_F10: "T_F10_Key_Dark.png", + KEY_F11: "T_F11_Key_Dark.png", + KEY_F12: "T_F12_Key_Dark.png", + + + KEY_PAGE_DOWN: "T_PageDown_Key_Dark.png", + KEY_PAGE_UP: "T_PageUp_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_Ins_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_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", + KEY_ARROW_LEFT: "T_Left_Key_Dark.png", + KEY_ARROW_RIGHT: "T_Right_Key_Dark.png", + + KEY_LEFT_BRACKET: "T_Brackets_L_Key_Dark.png", + KEY_RIGHT_BRACKET: "T_Brackets_R_Key_Dark.png", + + KEY_SEMICOLON: "T_Semicolon_Key_Dark.png", + + KEY_BACKSPACE: "T_Backspace_Alt_Key_Dark.png", + KEY_ALT: "T_Alt_Key_Dark.png" + }, + setting: { + KEY_ARROW_UP: SettingInterfaceKeyboard.Button_Up, + KEY_ARROW_DOWN: SettingInterfaceKeyboard.Button_Down, + KEY_ARROW_LEFT: SettingInterfaceKeyboard.Button_Left, + KEY_ARROW_RIGHT: SettingInterfaceKeyboard.Button_Right, + KEY_ENTER: SettingInterfaceKeyboard.Button_Submit, + KEY_SPACE: SettingInterfaceKeyboard.Button_Action, + KEY_BACKSPACE: SettingInterfaceKeyboard.Button_Cancel, + KEY_ESC: SettingInterfaceKeyboard.Button_Menu, + KEY_C: SettingInterfaceKeyboard.Button_Stats, + KEY_R: SettingInterfaceKeyboard.Button_Cycle_Shiny, + KEY_F: SettingInterfaceKeyboard.Button_Cycle_Form, + KEY_G: SettingInterfaceKeyboard.Button_Cycle_Gender, + KEY_E: SettingInterfaceKeyboard.Button_Cycle_Ability, + KEY_N: SettingInterfaceKeyboard.Button_Cycle_Nature, + KEY_V: SettingInterfaceKeyboard.Button_Cycle_Variant, + KEY_PLUS: SettingInterfaceKeyboard.Button_Speed_Up, + KEY_MINUS: SettingInterfaceKeyboard.Button_Slow_Down, + + KEY_Z: SettingInterfaceKeyboard.Alt_Button_Up, + KEY_S: SettingInterfaceKeyboard.Alt_Button_Down, + KEY_Q: SettingInterfaceKeyboard.Alt_Button_Left, + KEY_D: SettingInterfaceKeyboard.Alt_Button_Right, + KEY_CTRL: SettingInterfaceKeyboard.Alt_Button_Submit, + KEY_W: SettingInterfaceKeyboard.Alt_Button_Action, + KEY_X: SettingInterfaceKeyboard.Alt_Button_Cancel, + KEY_TAB: SettingInterfaceKeyboard.Alt_Button_Menu, + KEY_SHIFT: SettingInterfaceKeyboard.Alt_Button_Stats, + KEY_P: SettingInterfaceKeyboard.Alt_Button_Cycle_Shiny, + KEY_M: SettingInterfaceKeyboard.Alt_Button_Cycle_Form, + KEY_O: SettingInterfaceKeyboard.Alt_Button_Cycle_Gender, + KEY_L: SettingInterfaceKeyboard.Alt_Button_Cycle_Ability, + KEY_I: SettingInterfaceKeyboard.Alt_Button_Cycle_Nature, + KEY_K: SettingInterfaceKeyboard.Alt_Button_Cycle_Variant, + KEY_PAGE_UP: SettingInterfaceKeyboard.Alt_Button_Speed_Up, + KEY_PAGE_DOWN: SettingInterfaceKeyboard.Alt_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, + KEY_A: -1, + KEY_B: -1, + KEY_D: Button.RIGHT, + KEY_H: -1, + KEY_I: Button.CYCLE_NATURE, + KEY_J: -1, + KEY_K: Button.CYCLE_VARIANT, + KEY_L: Button.CYCLE_ABILITY, + KEY_M: Button.CYCLE_FORM, + KEY_O: Button.CYCLE_GENDER, + KEY_P: Button.CYCLE_SHINY, + KEY_Q: Button.LEFT, + KEY_S: Button.DOWN, + KEY_T: -1, + KEY_U: -1, + KEY_W: Button.ACTION, + KEY_X: Button.CANCEL, + KEY_Y: -1, + KEY_Z: Button.UP, + KEY_0: -1, + KEY_1: -1, + KEY_2: -1, + KEY_3: -1, + KEY_4: -1, + KEY_5: -1, + KEY_6: -1, + KEY_7: -1, + KEY_8: -1, + KEY_9: -1, + KEY_CTRL: Button.SUBMIT, + KEY_DEL: -1, + KEY_END: -1, + KEY_F1: -1, + KEY_F2: -1, + KEY_F3: -1, + KEY_F4: -1, + KEY_F5: -1, + KEY_F6: -1, + KEY_F7: -1, + KEY_F8: -1, + KEY_F9: -1, + KEY_F10: -1, + KEY_F11: -1, + KEY_F12: -1, + KEY_HOME: -1, + KEY_INSERT: -1, + KEY_PAGE_DOWN: Button.SLOW_DOWN, + KEY_PAGE_UP: Button.SPEED_UP, + KEY_QUOTATION: -1, + KEY_SHIFT: Button.STATS, + KEY_TAB: Button.MENU, + KEY_TILDE: -1, + KEY_LEFT_BRACKET: -1, + KEY_RIGHT_BRACKET: -1, + KEY_SEMICOLON: -1, + KEY_ALT: -1 + } +}; + +export default cfg_keyboard_azerty; diff --git a/src/test/gamepad_remaping.test.ts b/src/test/gamepad_remaping.test.ts new file mode 100644 index 000000000..5e717f6d3 --- /dev/null +++ b/src/test/gamepad_remaping.test.ts @@ -0,0 +1,166 @@ +import {beforeEach, expect, describe, it} from "vitest"; +import cfg_gamepad_example, {SettingInterfaceGamepad} from "./cfg_gamepad_example"; +import { + getIconWithPressedButton, + getIconWithSettingName, + getKeyAndActionFromCurrentKeysWithSettingName, + getKeyForSettingName, + getKeyFromMapping, + getKeyWithAction, initCurrentKeys, + reloadCurrentKeys, + swapCurrentKeys, +} from "#app/configs/gamepad-utils"; +import {Button} from "#app/enums/buttons"; + + +function deepCopy(config) { + return JSON.parse(JSON.stringify(config)); +} + +describe('Test Keyboard', () => { + let config; + beforeEach(() => { + const temp = {...cfg_gamepad_example} + config = deepCopy(temp); + config.custom = {...config.default} + reloadCurrentKeys(config); + }); + + it('Check if config is loaded', () => { + expect(config).not.toBeNull(); + }); + it('Check key for setting name', () => { + const settingName = SettingInterfaceGamepad.Button_Action; + const key = getKeyForSettingName(config, settingName); + expect(config.custom[key]).toEqual(Button.ACTION); + }); + it('Check key for Keyboard KeyCode', () => { + const key = getKeyFromMapping(config, 0); + expect(config.custom[key]).toEqual(Button.ACTION); + }); + it('Check key for currenly Assigned to action not alt', () => { + const key = getKeyWithAction(config, Button.ACTION, false); + expect(key).toEqual('RC_S'); + }); + it('Check key for currenly Assigned to setting name', () => { + const settingName = SettingInterfaceGamepad.Button_Action; + const { key } = getKeyAndActionFromCurrentKeysWithSettingName(config, settingName); + expect(key).toEqual('RC_S'); + }); + it('Check icon for currenly Assigned to key code', () => { + const icon = getIconWithPressedButton(config, 0); + expect(icon).toEqual('T_X_A_Color_Alt.png'); + }); + it('Check icon for currenly Assigned to setting name', () => { + const settingName = SettingInterfaceGamepad.Button_Action; + const icon = getIconWithSettingName(config, settingName); + expect(icon).toEqual('T_X_A_Color_Alt.png'); + }); + + + it('Check if current keys return the same', () => { + const settingNameA = SettingInterfaceGamepad.Button_Action; + const keyA = getKeyForSettingName(config, settingNameA); + const action = config.custom[keyA]; + expect(keyA).toEqual("RC_S"); + expect(action).toEqual(Button.ACTION); + + expect(config.currentKeys[settingNameA].key).toEqual(keyA); + expect(config.currentKeys[settingNameA].action).toEqual(action); + }); + + it('Check if new swap is working', () => { + const settingNameA = SettingInterfaceGamepad.Button_Action; + swapCurrentKeys(config, settingNameA, 1); + expect(config.currentKeys[settingNameA].key).toEqual("RC_S"); + expect(config.currentKeys[settingNameA].action).toEqual(Button.CANCEL); + }); + + it('Check if new double swap is working', () => { + const settingNameA = SettingInterfaceGamepad.Button_Action; + + swapCurrentKeys(config, settingNameA, 1); + expect(config.currentKeys[settingNameA].key).toEqual("RC_S"); + expect(config.currentKeys[settingNameA].action).toEqual(Button.CANCEL); + + swapCurrentKeys(config, settingNameA, 2); + expect(config.currentKeys[settingNameA].key).toEqual("RC_S"); + expect(config.currentKeys[settingNameA].action).toEqual(Button.CYCLE_NATURE); + }); + + it('Check if new triple swap is working', () => { + const settingNameA = SettingInterfaceGamepad.Button_Action; + const settingNameB = SettingInterfaceGamepad.Button_Cancel; + const settingNameC = SettingInterfaceGamepad.Button_Cycle_Nature; + + expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].key).toEqual("RC_S"); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].action).toEqual(Button.ACTION); + + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].key).toEqual("RC_E"); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].action).toEqual(Button.CANCEL); + + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cycle_Nature].key).toEqual("RC_W"); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cycle_Nature].action).toEqual(Button.CYCLE_NATURE); + + let iconA = getIconWithSettingName(config, settingNameA); + let iconB = getIconWithSettingName(config, settingNameB); + let iconC = getIconWithSettingName(config, settingNameC); + expect(iconA).toEqual('T_X_A_Color_Alt.png'); + expect(iconB).toEqual('T_X_B_Color_Alt.png'); + expect(iconC).toEqual('T_X_X_Color_Alt.png'); + + swapCurrentKeys(config, SettingInterfaceGamepad.Button_Action, 1); // cancel + + expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].key).toEqual("RC_S"); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].action).toEqual(Button.CANCEL); + + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].key).toEqual("RC_E"); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].action).toEqual(Button.ACTION); + + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cycle_Nature].key).toEqual("RC_W"); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cycle_Nature].action).toEqual(Button.CYCLE_NATURE); + + iconA = getIconWithSettingName(config, settingNameA); + iconB = getIconWithSettingName(config, settingNameB); + iconC = getIconWithSettingName(config, settingNameC); + expect(iconA).toEqual('T_X_B_Color_Alt.png'); + expect(iconB).toEqual('T_X_A_Color_Alt.png'); + expect(iconC).toEqual('T_X_X_Color_Alt.png'); + + swapCurrentKeys(config, SettingInterfaceGamepad.Button_Cancel, 2); // cycle nature + + expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].key).toEqual("RC_S"); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].action).toEqual(Button.CANCEL); // 6 + + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].key).toEqual("RC_E"); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].action).toEqual(Button.CYCLE_NATURE); // 13 + + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cycle_Nature].key).toEqual("RC_W"); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cycle_Nature].action).toEqual(Button.ACTION); // 5 + + iconA = getIconWithSettingName(config, settingNameA); + iconB = getIconWithSettingName(config, settingNameB); + iconC = getIconWithSettingName(config, settingNameC); + expect(iconA).toEqual('T_X_B_Color_Alt.png'); + expect(iconB).toEqual('T_X_X_Color_Alt.png'); + expect(iconC).toEqual('T_X_A_Color_Alt.png'); + + swapCurrentKeys(config, SettingInterfaceGamepad.Button_Cycle_Nature, 1); // cancel + + expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].key).toEqual("RC_S"); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].action).toEqual(Button.CANCEL); + + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].key).toEqual("RC_E"); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].action).toEqual(Button.ACTION); + + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cycle_Nature].key).toEqual("RC_W"); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cycle_Nature].action).toEqual(Button.CYCLE_NATURE); + + iconA = getIconWithSettingName(config, settingNameA); + iconB = getIconWithSettingName(config, settingNameB); + iconC = getIconWithSettingName(config, settingNameC); + expect(iconA).toEqual('T_X_B_Color_Alt.png'); + expect(iconB).toEqual('T_X_A_Color_Alt.png'); + expect(iconC).toEqual('T_X_X_Color_Alt.png'); + }); +}); \ No newline at end of file diff --git a/src/test/keyboard_remaping.test.ts b/src/test/keyboard_remaping.test.ts new file mode 100644 index 000000000..7c2705ca7 --- /dev/null +++ b/src/test/keyboard_remaping.test.ts @@ -0,0 +1,268 @@ +import {beforeEach, expect, describe, it} from "vitest"; +import cfg_keyboard_example, {SettingInterfaceKeyboard} from "#app/test/cfg_keyboard_example"; +import { + getIconWithPressedButton, + getIconWithSettingName, + getKeyAndActionFromCurrentKeysWithSettingName, + getKeyForSettingName, + getKeyFromMapping, + getKeyWithAction, + reloadCurrentKeys, + swapCurrentKeys, +} from "#app/configs/gamepad-utils"; +import {Button} from "#app/enums/buttons"; + + +function deepCopy(config) { + return JSON.parse(JSON.stringify(config)); +} + + +describe('Test Keyboard', () => { + let config; + beforeEach(() => { + config = deepCopy(cfg_keyboard_example); + config.custom = {...config.default} + reloadCurrentKeys(config); + }); + + it('Check if config is loaded', () => { + expect(config).not.toBeNull(); + }); + it('Check key for setting name', () => { + const settingName = SettingInterfaceKeyboard.Button_Left; + const key = getKeyForSettingName(config, settingName); + expect(config.custom[key]).toEqual(Button.LEFT); + }); + it('Check key for Keyboard KeyCode', () => { + const key = getKeyFromMapping(config, Phaser.Input.Keyboard.KeyCodes.LEFT); + expect(config.custom[key]).toEqual(Button.LEFT); + }); + it('Check key for currenly Assigned to action not alt', () => { + const key = getKeyWithAction(config, Button.LEFT, false); + expect(key).toEqual('KEY_ARROW_LEFT'); + }); + it('Check key for currenly Assigned to action alt', () => { + const key = getKeyWithAction(config, Button.LEFT, true); + expect(key).toEqual('KEY_Q'); + }); + it('Check key for currenly Assigned to setting name', () => { + const settingName = SettingInterfaceKeyboard.Button_Left; + const { key } = getKeyAndActionFromCurrentKeysWithSettingName(config, settingName); + expect(key).toEqual('KEY_ARROW_LEFT'); + }); + it('Check key for currenly Assigned to setting name alt', () => { + const settingName = SettingInterfaceKeyboard.Alt_Button_Left; + const { key } = getKeyAndActionFromCurrentKeysWithSettingName(config, settingName); + expect(key).toEqual('KEY_Q'); + }); + it('Check icon for currenly Assigned to key code', () => { + const icon = getIconWithPressedButton(config, Phaser.Input.Keyboard.KeyCodes.LEFT); + expect(icon).toEqual('T_Left_Key_Dark.png'); + }); + it('Check icon for currenly Assigned to key code alt', () => { + const icon = getIconWithPressedButton(config, Phaser.Input.Keyboard.KeyCodes.Q); + expect(icon).toEqual('T_Q_Key_Dark.png'); + }); + it('Check icon for currenly Assigned to setting name', () => { + const settingName = SettingInterfaceKeyboard.Button_Left; + const icon = getIconWithSettingName(config, settingName); + expect(icon).toEqual('T_Left_Key_Dark.png'); + }); + it('Check icon for currenly Assigned to setting name alt', () => { + const settingName = SettingInterfaceKeyboard.Alt_Button_Left; + const icon = getIconWithSettingName(config, settingName); + expect(icon).toEqual('T_Q_Key_Dark.png'); + }); + + + it('Check if current keys return the same', () => { + const settingNameA = SettingInterfaceKeyboard.Button_Left; + const keyA = getKeyForSettingName(config, settingNameA); + const action = config.custom[keyA]; + expect(keyA).toEqual("KEY_ARROW_LEFT"); + expect(action).toEqual(Button.LEFT); + + expect(config.currentKeys[settingNameA].key).toEqual(keyA); + expect(config.currentKeys[settingNameA].action).toEqual(action); + }); + + it('Check if new swap is working', () => { + const settingNameA = SettingInterfaceKeyboard.Button_Left; + swapCurrentKeys(config, settingNameA, Phaser.Input.Keyboard.KeyCodes.RIGHT); + expect(config.currentKeys[settingNameA].key).toEqual("KEY_ARROW_LEFT"); + expect(config.currentKeys[settingNameA].action).toEqual(Button.RIGHT); + }); + + it('Check if new double swap is working', () => { + const settingNameA = SettingInterfaceKeyboard.Button_Left; + + swapCurrentKeys(config, settingNameA, Phaser.Input.Keyboard.KeyCodes.RIGHT); + expect(config.currentKeys[settingNameA].key).toEqual("KEY_ARROW_LEFT"); + expect(config.currentKeys[settingNameA].action).toEqual(Button.RIGHT); + + swapCurrentKeys(config, settingNameA, Phaser.Input.Keyboard.KeyCodes.UP); + expect(config.currentKeys[settingNameA].key).toEqual("KEY_ARROW_LEFT"); + expect(config.currentKeys[settingNameA].action).toEqual(Button.UP); + }); + + it('Check if new triple swap is working', () => { + const settingNameA = SettingInterfaceKeyboard.Button_Left; + const settingNameB = SettingInterfaceKeyboard.Button_Action; + const settingNameC = SettingInterfaceKeyboard.Button_Right; + const settingNameD = SettingInterfaceKeyboard.Button_Up; + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].key).toEqual("KEY_ARROW_LEFT"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].action).toEqual(Button.LEFT); + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Action].key).toEqual("KEY_SPACE"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Action].action).toEqual(Button.ACTION); + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].key).toEqual("KEY_ARROW_RIGHT"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].action).toEqual(Button.RIGHT); + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].key).toEqual("KEY_ARROW_UP"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].action).toEqual(Button.UP); + + let iconA = getIconWithSettingName(config, settingNameA); + let iconB = getIconWithSettingName(config, settingNameB); + let iconC = getIconWithSettingName(config, settingNameC); + let iconD = getIconWithSettingName(config, settingNameD); + expect(iconA).toEqual('T_Left_Key_Dark.png'); + expect(iconB).toEqual('T_Space_Key_Dark.png'); + expect(iconC).toEqual('T_Right_Key_Dark.png'); + expect(iconD).toEqual('T_Up_Key_Dark.png'); + + swapCurrentKeys(config, SettingInterfaceKeyboard.Button_Left, Phaser.Input.Keyboard.KeyCodes.RIGHT); // left->RIGHT, right->LEFT + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].key).toEqual("KEY_ARROW_LEFT"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].action).toEqual(Button.RIGHT); + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Action].key).toEqual("KEY_SPACE"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Action].action).toEqual(Button.ACTION); + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].key).toEqual("KEY_ARROW_RIGHT"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].action).toEqual(Button.LEFT); + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].key).toEqual("KEY_ARROW_UP"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].action).toEqual(Button.UP); + + iconA = getIconWithSettingName(config, settingNameA); + iconB = getIconWithSettingName(config, settingNameB); + iconC = getIconWithSettingName(config, settingNameC); + iconD = getIconWithSettingName(config, settingNameD); + expect(iconA).toEqual('T_Right_Key_Dark.png'); + expect(iconB).toEqual('T_Space_Key_Dark.png'); + expect(iconC).toEqual('T_Left_Key_Dark.png'); + expect(iconD).toEqual('T_Up_Key_Dark.png'); + + swapCurrentKeys(config, SettingInterfaceKeyboard.Button_Action, Phaser.Input.Keyboard.KeyCodes.UP); // action->UP + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].key).toEqual("KEY_ARROW_LEFT"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].action).toEqual(Button.RIGHT); + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Action].key).toEqual("KEY_SPACE"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Action].action).toEqual(Button.UP) + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].key).toEqual("KEY_ARROW_RIGHT"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].action).toEqual(Button.LEFT); + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].key).toEqual("KEY_ARROW_UP"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].action).toEqual(Button.ACTION); + + iconA = getIconWithSettingName(config, settingNameA); + iconB = getIconWithSettingName(config, settingNameB); + iconC = getIconWithSettingName(config, settingNameC); + iconD = getIconWithSettingName(config, settingNameD); + expect(iconA).toEqual('T_Right_Key_Dark.png'); + expect(iconB).toEqual('T_Up_Key_Dark.png'); + expect(iconC).toEqual('T_Left_Key_Dark.png'); + expect(iconD).toEqual('T_Space_Key_Dark.png'); + + swapCurrentKeys(config, SettingInterfaceKeyboard.Button_Right, Phaser.Input.Keyboard.KeyCodes.UP); // right->UP, action->LEFT + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].key).toEqual("KEY_ARROW_LEFT"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].action).toEqual(Button.RIGHT); + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Action].key).toEqual("KEY_SPACE"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Action].action).toEqual(Button.UP) + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].key).toEqual("KEY_ARROW_RIGHT"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].action).toEqual(Button.ACTION); + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].key).toEqual("KEY_ARROW_UP"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].action).toEqual(Button.LEFT); + + iconA = getIconWithSettingName(config, settingNameA); + iconB = getIconWithSettingName(config, settingNameB); + iconC = getIconWithSettingName(config, settingNameC); + iconD = getIconWithSettingName(config, settingNameD); + expect(iconA).toEqual('T_Right_Key_Dark.png'); + expect(iconB).toEqual('T_Up_Key_Dark.png'); + expect(iconC).toEqual('T_Space_Key_Dark.png'); + expect(iconD).toEqual('T_Left_Key_Dark.png'); + }); + + + it('Swap alt with another main', () => { + const settingNameA = SettingInterfaceKeyboard.Button_Left; + const settingNameB = SettingInterfaceKeyboard.Alt_Button_Right; + const settingNameC = SettingInterfaceKeyboard.Button_Up; + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].key).toEqual("KEY_ARROW_LEFT"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].action).toEqual(Button.LEFT); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].icon).toEqual("T_Left_Key_Dark.png"); + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].key).toEqual("KEY_D"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].action).toEqual(Button.RIGHT); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].icon).toEqual("T_D_Key_Dark.png"); + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].key).toEqual("KEY_ARROW_UP"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].icon).toEqual("T_Up_Key_Dark.png"); + + swapCurrentKeys(config, SettingInterfaceKeyboard.Button_Left, Phaser.Input.Keyboard.KeyCodes.D); + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].key).toEqual("KEY_ARROW_LEFT"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].action).toEqual(Button.RIGHT); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].icon).toEqual("T_D_Key_Dark.png"); + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Left].key).toEqual("KEY_Q"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Left].action).toEqual(Button.LEFT); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Left].icon).toEqual("T_Q_Key_Dark.png"); + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].key).toEqual("KEY_ARROW_RIGHT"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].action).toEqual(Button.RIGHT); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].icon).toEqual("T_Right_Key_Dark.png"); + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].key).toEqual("KEY_D"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].action).toEqual(Button.LEFT); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].icon).toEqual("T_Left_Key_Dark.png"); + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].key).toEqual("KEY_ARROW_UP"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].icon).toEqual("T_Up_Key_Dark.png"); + + swapCurrentKeys(config, SettingInterfaceKeyboard.Button_Up, Phaser.Input.Keyboard.KeyCodes.LEFT); + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].key).toEqual("KEY_ARROW_LEFT"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].icon).toEqual("T_Up_Key_Dark.png"); + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Left].key).toEqual("KEY_Q"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Left].action).toEqual(Button.LEFT); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Left].icon).toEqual("T_Q_Key_Dark.png"); + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].key).toEqual("KEY_ARROW_RIGHT"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].action).toEqual(Button.RIGHT); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].icon).toEqual("T_Right_Key_Dark.png"); + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].key).toEqual("KEY_D"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].action).toEqual(Button.LEFT); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].icon).toEqual("T_Left_Key_Dark.png"); + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].key).toEqual("KEY_ARROW_UP"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].action).toEqual(Button.RIGHT); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].icon).toEqual("T_D_Key_Dark.png"); + }) +}); \ No newline at end of file diff --git a/src/ui/settings/abrast-binding-ui-handler.ts b/src/ui/settings/abrast-binding-ui-handler.ts index 03139d71b..4d73bea18 100644 --- a/src/ui/settings/abrast-binding-ui-handler.ts +++ b/src/ui/settings/abrast-binding-ui-handler.ts @@ -115,7 +115,7 @@ export default abstract class AbstractBindingUiHandler extends UiHandler { this.getUi().bringToTop(this.actionsContainer); this.optionSelectContainer.setVisible(true); - setTimeout(() => this.listening = true, 300); + setTimeout(() => this.listening = true, 100); return true; } diff --git a/src/ui/settings/abstract-settings-ui-handler.ts b/src/ui/settings/abstract-settings-ui-handler.ts index 4b87a7214..e92d3dc68 100644 --- a/src/ui/settings/abstract-settings-ui-handler.ts +++ b/src/ui/settings/abstract-settings-ui-handler.ts @@ -4,8 +4,11 @@ import {Mode} from "../ui"; import {InterfaceConfig} from "../../inputs-controller"; import {addWindow} from "../ui-theme"; import {addTextObject, TextStyle} from "../text"; -import {getCurrentlyAssignedIconToSettingName, getKeyForSettingName} from "../../configs/gamepad-utils"; import {Button} from "../../enums/buttons"; +import { + getKeyAndActionFromCurrentKeysWithSettingName, + getKeyForSettingName +} from "#app/configs/gamepad-utils"; export interface InputsIcons { [key: string]: Phaser.GameObjects.Sprite; @@ -238,9 +241,10 @@ export default abstract class AbstractSettingsUiUiHandler extends UiHandler { if (!activeConfig.custom) return; // For each element in the binding settings, update the icon according to the current assignment. + debugger; 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. + // const key = getKeyForSettingName(activeConfig, elm); // Get the key for the setting name. + const {key, icon} = getKeyAndActionFromCurrentKeysWithSettingName(activeConfig, elm); this.inputsIcons[key].setFrame(icon); // Set the icon frame to the inputs icon object. } diff --git a/src/ui/settings/gamepad-binding-ui-handler.ts b/src/ui/settings/gamepad-binding-ui-handler.ts index 684ce93b4..0519e7e44 100644 --- a/src/ui/settings/gamepad-binding-ui-handler.ts +++ b/src/ui/settings/gamepad-binding-ui-handler.ts @@ -1,6 +1,10 @@ import BattleScene from "../../battle-scene"; import AbstractBindingUiHandler from "../settings/abrast-binding-ui-handler"; import {Mode} from "../ui"; +import { + getKeyAndActionFromCurrentKeysWithPressedButton, + getKeyAndActionFromCurrentKeysWithSettingName, +} from "#app/configs/gamepad-utils"; export default class GamepadBindingUiHandler extends AbstractBindingUiHandler { @@ -16,14 +20,18 @@ export default class GamepadBindingUiHandler extends AbstractBindingUiHandler { // Check conditions before processing the button press. if (!this.listening || pad.id !== this.scene.inputController?.chosenGamepad || blacklist.includes(button.index) || this.buttonPressed !== null) return; this.buttonPressed = button.index; - const [type, buttonIcon] = this.scene.inputController.getPressedButtonLabel(button); + const activeConfig = this.scene.inputController.getActiveConfig(); + const type = activeConfig.padType + const buttonIcon = getKeyAndActionFromCurrentKeysWithPressedButton(activeConfig, this.buttonPressed)?.icon if (!buttonIcon) return; - const assignedButtonIcon = this.scene.inputController.getCurrentlyAssignedIconToDisplay(this.target); + const assignedButtonIcon = getKeyAndActionFromCurrentKeysWithSettingName(activeConfig, this.target)?.icon; this.onInputDown(buttonIcon, assignedButtonIcon, type); } swapAction() { - this.scene.inputController.swapBinding(this.target, this.buttonPressed); + const activeConfig = this.scene.inputController.getActiveConfig(); + this.scene.inputController.swapBinding(activeConfig, this.target, this.buttonPressed) + this.scene.gameData.saveCustomMapping(this.scene.inputController?.chosenGamepad, activeConfig.custom); return true; } } \ No newline at end of file diff --git a/src/ui/settings/keyboard-binding-ui-handler.ts b/src/ui/settings/keyboard-binding-ui-handler.ts index 51dbb508c..cd13757d8 100644 --- a/src/ui/settings/keyboard-binding-ui-handler.ts +++ b/src/ui/settings/keyboard-binding-ui-handler.ts @@ -1,6 +1,10 @@ import BattleScene from "../../battle-scene"; import AbstractBindingUiHandler from "../settings/abrast-binding-ui-handler"; import {Mode} from "../ui"; +import { + getKeyAndActionFromCurrentKeysWithPressedButton, + getKeyAndActionFromCurrentKeysWithSettingName, +} from "#app/configs/gamepad-utils"; export default class KeyboardBindingUiHandler extends AbstractBindingUiHandler { @@ -16,14 +20,17 @@ export default class KeyboardBindingUiHandler extends AbstractBindingUiHandler { // // Check conditions before processing the button press. if (!this.listening || this.buttonPressed !== null) return; this.buttonPressed = key; - const buttonIcon = this.scene.inputController.getPressedKeyLabel(key); + const activeConfig = this.scene.inputController.getActiveKeyboardConfig(); + const buttonIcon = getKeyAndActionFromCurrentKeysWithPressedButton(activeConfig, key)?.icon if (!buttonIcon) return; - const assignedButtonIcon = this.scene.inputController.getKeyboardCurrentlyAssignedIconToDisplay(this.target); + const assignedButtonIcon = getKeyAndActionFromCurrentKeysWithSettingName(activeConfig, this.target)?.icon; this.onInputDown(buttonIcon, assignedButtonIcon, 'keyboard'); } swapAction() { - this.scene.inputController.swapKeyboardBinding(this.target, this.buttonPressed); + const activeConfig = this.scene.inputController.getActiveKeyboardConfig(); + this.scene.inputController.swapBinding(activeConfig, this.target, this.buttonPressed) + this.scene.gameData.saveCustomKeyboardMapping(this.scene.inputController?.chosenKeyboard, activeConfig.custom); return true; } diff --git a/src/utils.ts b/src/utils.ts index 25391a890..9ffb02474 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -349,3 +349,7 @@ export function truncateString(str: String, maxLength: number = 10) { } return str; } + +export function deepCopy(values: object): object { + return JSON.parse(JSON.stringify(values)); +} \ No newline at end of file From 61a527c72188cd6f36a89fc0fd75b8f0b12b57a2 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Tue, 14 May 2024 00:23:03 +0200 Subject: [PATCH 62/81] removed breakpoint --- src/configs/gamepad-utils.ts | 1 - src/ui/settings/abstract-settings-ui-handler.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/configs/gamepad-utils.ts b/src/configs/gamepad-utils.ts index b6968d369..43c6d0920 100644 --- a/src/configs/gamepad-utils.ts +++ b/src/configs/gamepad-utils.ts @@ -80,7 +80,6 @@ export function swapCurrentKeys(config: InterfaceConfig, settingName, pressedBut export function reloadCurrentKeys(config): void { // need to rework this to include keys that were not there at the begining const currentKeys = {}; - debugger; for (const key of Object.keys(config.setting)) { const settingName = config.setting[key]; const action = config.custom[key]; diff --git a/src/ui/settings/abstract-settings-ui-handler.ts b/src/ui/settings/abstract-settings-ui-handler.ts index e92d3dc68..81ddacb18 100644 --- a/src/ui/settings/abstract-settings-ui-handler.ts +++ b/src/ui/settings/abstract-settings-ui-handler.ts @@ -241,7 +241,6 @@ export default abstract class AbstractSettingsUiUiHandler extends UiHandler { if (!activeConfig.custom) return; // For each element in the binding settings, update the icon according to the current assignment. - debugger; for (const elm of this.bindingSettings) { // const key = getKeyForSettingName(activeConfig, elm); // Get the key for the setting name. const {key, icon} = getKeyAndActionFromCurrentKeysWithSettingName(activeConfig, elm); From 66414e9bbc915822d1a4946dbdad4b7f7d89ee5c Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Tue, 14 May 2024 00:59:06 +0200 Subject: [PATCH 63/81] fix bug save bindings --- src/configs/gamepad-utils.ts | 12 ++++++++- src/inputs-controller.ts | 26 ++++++++++++------- src/system/game-data.ts | 8 +++--- src/ui/settings/gamepad-binding-ui-handler.ts | 2 +- .../settings/keyboard-binding-ui-handler.ts | 2 +- 5 files changed, 34 insertions(+), 16 deletions(-) diff --git a/src/configs/gamepad-utils.ts b/src/configs/gamepad-utils.ts index 43c6d0920..76a90d564 100644 --- a/src/configs/gamepad-utils.ts +++ b/src/configs/gamepad-utils.ts @@ -1,5 +1,6 @@ import {InterfaceConfig} from "../inputs-controller"; import {Button} from "#app/enums/buttons"; +import {deepCopy} from "#app/utils"; // Given a button index from an input event, return its naming from the mapping config export function getKeyFromMapping(config: InterfaceConfig, index: number): String | null { @@ -90,5 +91,14 @@ export function reloadCurrentKeys(config): void { icon, } } - config.currentKeys = currentKeys; + config.currentKeys = deepCopy(currentKeys); +} + +export function regenerateCustom(config): void { + const custom = deepCopy(config.custom); + for (const settingName of Object.keys(config.currentKeys)) { + const {key, action} = config.currentKeys[settingName]; + custom[key] = action; + } + config.custom = deepCopy(custom); } \ No newline at end of file diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 93010f668..889f5bf8b 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -11,7 +11,7 @@ import SettingsKeyboardUiHandler from "./ui/settings/settings-keyboard-ui-handle import cfg_keyboard_azerty from "./configs/cfg_keyboard_azerty"; import { getKeyAndActionFromCurrentKeysWithPressedButton, - getKeyFromMapping, + getKeyFromMapping, regenerateCustom, reloadCurrentKeys, swapCurrentKeys } from "#app/configs/gamepad-utils"; import {deepCopy} from "./utils"; @@ -338,9 +338,13 @@ export class InputsController { const gamepadID = gamepad.toLowerCase(); const config = deepCopy(this.getConfig(gamepadID)); config.custom = this.configs[gamepad]?.custom || {...config.default}; - reloadCurrentKeys(config); + config.currentKeys = this.configs[gamepad]?.currentKeys; + if (!this.configs[gamepad]?.currentKeys) + reloadCurrentKeys(config); + else + regenerateCustom(config); this.configs[gamepad] = config; - this.scene.gameData?.saveCustomMapping(this.chosenGamepad, this.configs[gamepad]?.custom); + this.scene.gameData?.saveCustomMapping(this.chosenGamepad, this.configs[gamepad]?.currentKeys); } if (this.chosenGamepad === thisGamepad.id) this.initChosenGamepad(this.chosenGamepad) } @@ -349,9 +353,13 @@ export class InputsController { for (const layout of ['default']) { const config = deepCopy(this.getConfigKeyboard(layout)); config.custom = this.keyboardConfigs[layout]?.custom || {...config.default}; - reloadCurrentKeys(config); + config.currentKeys = this.keyboardConfigs[layout]?.currentKeys; + if (!this.keyboardConfigs[layout]?.currentKeys) + reloadCurrentKeys(config); + else + regenerateCustom(config); this.keyboardConfigs[layout] = config; - this.scene.gameData?.saveCustomKeyboardMapping(this.chosenKeyboard, this.keyboardConfigs[layout]?.custom); + this.scene.gameData?.saveCustomKeyboardMapping(this.chosenKeyboard, this.keyboardConfigs[layout]?.currentKeys); } this.initChosenLayoutKeyboard(this.chosenKeyboard) } @@ -752,13 +760,13 @@ export class InputsController { * @param gamepadName The identifier of the gamepad to configure. * @param customMappings The custom mapping configuration to apply to the gamepad. */ - injectConfig(gamepadName: String, customMappings: MappingLayout): void { + injectConfig(gamepadName: String, customMappings): void { if (!this.configs[gamepadName]) this.configs[gamepadName] = {}; - this.configs[gamepadName].custom = customMappings; + this.configs[gamepadName].currentKeys = customMappings; } - injectKeyboardConfig(layout: string, customMappings: MappingLayout): void { + injectKeyboardConfig(layout: string, customMappings): void { if (!this.keyboardConfigs[layout]) this.keyboardConfigs[layout] = {}; - this.keyboardConfigs[layout].custom = customMappings; + this.keyboardConfigs[layout].currentKeys = customMappings; } swapBinding(config, settingName, pressedButton): void { diff --git a/src/system/game-data.ts b/src/system/game-data.ts index ac2ae9d38..31da8c084 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -498,20 +498,20 @@ export class GameData { return true; } - public saveCustomMapping(gamepadName: string, mapping: MappingLayout): boolean { + public saveCustomMapping(gamepadName: string, currentKeys): boolean { let customMappings: object = {}; if (localStorage.hasOwnProperty('customMapping')) customMappings = JSON.parse(localStorage.getItem('customMapping')); - customMappings[gamepadName] = mapping; + customMappings[gamepadName] = currentKeys; localStorage.setItem('customMapping', JSON.stringify(customMappings)); return true; } - public saveCustomKeyboardMapping(keyboardLayout: string, mapping: MappingLayout): boolean { + public saveCustomKeyboardMapping(keyboardLayout: string, currentKeys): boolean { let customKeyboardMappings: object = {}; if (localStorage.hasOwnProperty('customKeyboardMappings')) customKeyboardMappings = JSON.parse(localStorage.getItem('customKeyboardMappings')); - customKeyboardMappings[keyboardLayout] = mapping; + customKeyboardMappings[keyboardLayout] = currentKeys; localStorage.setItem('customKeyboardMappings', JSON.stringify(customKeyboardMappings)); return true; } diff --git a/src/ui/settings/gamepad-binding-ui-handler.ts b/src/ui/settings/gamepad-binding-ui-handler.ts index 0519e7e44..3bece0542 100644 --- a/src/ui/settings/gamepad-binding-ui-handler.ts +++ b/src/ui/settings/gamepad-binding-ui-handler.ts @@ -31,7 +31,7 @@ export default class GamepadBindingUiHandler extends AbstractBindingUiHandler { swapAction() { const activeConfig = this.scene.inputController.getActiveConfig(); this.scene.inputController.swapBinding(activeConfig, this.target, this.buttonPressed) - this.scene.gameData.saveCustomMapping(this.scene.inputController?.chosenGamepad, activeConfig.custom); + this.scene.gameData.saveCustomMapping(this.scene.inputController?.chosenGamepad, activeConfig.currentKeys); return true; } } \ No newline at end of file diff --git a/src/ui/settings/keyboard-binding-ui-handler.ts b/src/ui/settings/keyboard-binding-ui-handler.ts index cd13757d8..f1057bb92 100644 --- a/src/ui/settings/keyboard-binding-ui-handler.ts +++ b/src/ui/settings/keyboard-binding-ui-handler.ts @@ -30,7 +30,7 @@ export default class KeyboardBindingUiHandler extends AbstractBindingUiHandler { swapAction() { const activeConfig = this.scene.inputController.getActiveKeyboardConfig(); this.scene.inputController.swapBinding(activeConfig, this.target, this.buttonPressed) - this.scene.gameData.saveCustomKeyboardMapping(this.scene.inputController?.chosenKeyboard, activeConfig.custom); + this.scene.gameData.saveCustomKeyboardMapping(this.scene.inputController?.chosenKeyboard, activeConfig.currentKeys); return true; } From b5841471d2eb31349fc08dc3581217324bd20107 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Tue, 14 May 2024 01:09:53 +0200 Subject: [PATCH 64/81] added ogIcons --- src/inputs-controller.ts | 2 ++ src/ui/settings/gamepad-binding-ui-handler.ts | 5 +++-- src/ui/settings/keyboard-binding-ui-handler.ts | 5 +++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 889f5bf8b..30072dc00 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -338,6 +338,7 @@ export class InputsController { const gamepadID = gamepad.toLowerCase(); const config = deepCopy(this.getConfig(gamepadID)); config.custom = this.configs[gamepad]?.custom || {...config.default}; + config.ogIcons = {...config.icons}; config.currentKeys = this.configs[gamepad]?.currentKeys; if (!this.configs[gamepad]?.currentKeys) reloadCurrentKeys(config); @@ -354,6 +355,7 @@ export class InputsController { const config = deepCopy(this.getConfigKeyboard(layout)); config.custom = this.keyboardConfigs[layout]?.custom || {...config.default}; config.currentKeys = this.keyboardConfigs[layout]?.currentKeys; + config.ogIcons = {...config.icons}; if (!this.keyboardConfigs[layout]?.currentKeys) reloadCurrentKeys(config); else diff --git a/src/ui/settings/gamepad-binding-ui-handler.ts b/src/ui/settings/gamepad-binding-ui-handler.ts index 3bece0542..e9345e825 100644 --- a/src/ui/settings/gamepad-binding-ui-handler.ts +++ b/src/ui/settings/gamepad-binding-ui-handler.ts @@ -3,7 +3,7 @@ import AbstractBindingUiHandler from "../settings/abrast-binding-ui-handler"; import {Mode} from "../ui"; import { getKeyAndActionFromCurrentKeysWithPressedButton, - getKeyAndActionFromCurrentKeysWithSettingName, + getKeyAndActionFromCurrentKeysWithSettingName, getKeyFromMapping, } from "#app/configs/gamepad-utils"; @@ -22,7 +22,8 @@ export default class GamepadBindingUiHandler extends AbstractBindingUiHandler { this.buttonPressed = button.index; const activeConfig = this.scene.inputController.getActiveConfig(); const type = activeConfig.padType - const buttonIcon = getKeyAndActionFromCurrentKeysWithPressedButton(activeConfig, this.buttonPressed)?.icon + const key = getKeyFromMapping(activeConfig, this.buttonPressed); + const buttonIcon = activeConfig.ogIcons[key]; if (!buttonIcon) return; const assignedButtonIcon = getKeyAndActionFromCurrentKeysWithSettingName(activeConfig, this.target)?.icon; this.onInputDown(buttonIcon, assignedButtonIcon, type); diff --git a/src/ui/settings/keyboard-binding-ui-handler.ts b/src/ui/settings/keyboard-binding-ui-handler.ts index f1057bb92..242d0dfef 100644 --- a/src/ui/settings/keyboard-binding-ui-handler.ts +++ b/src/ui/settings/keyboard-binding-ui-handler.ts @@ -3,7 +3,7 @@ import AbstractBindingUiHandler from "../settings/abrast-binding-ui-handler"; import {Mode} from "../ui"; import { getKeyAndActionFromCurrentKeysWithPressedButton, - getKeyAndActionFromCurrentKeysWithSettingName, + getKeyAndActionFromCurrentKeysWithSettingName, getKeyFromMapping, } from "#app/configs/gamepad-utils"; @@ -21,7 +21,8 @@ export default class KeyboardBindingUiHandler extends AbstractBindingUiHandler { if (!this.listening || this.buttonPressed !== null) return; this.buttonPressed = key; const activeConfig = this.scene.inputController.getActiveKeyboardConfig(); - const buttonIcon = getKeyAndActionFromCurrentKeysWithPressedButton(activeConfig, key)?.icon + const _key = getKeyFromMapping(activeConfig, key); + const buttonIcon = activeConfig.ogIcons[_key]; if (!buttonIcon) return; const assignedButtonIcon = getKeyAndActionFromCurrentKeysWithSettingName(activeConfig, this.target)?.icon; this.onInputDown(buttonIcon, assignedButtonIcon, 'keyboard'); From ea4372e1f37a7fa4fa5829d69bdf9974a9294b36 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Tue, 14 May 2024 02:18:39 +0200 Subject: [PATCH 65/81] fix back to back button binding --- src/configs/gamepad-utils.ts | 47 ++++++++++++++++++++++--------- src/test/gamepad_remaping.test.ts | 22 ++++++++++++++- 2 files changed, 54 insertions(+), 15 deletions(-) diff --git a/src/configs/gamepad-utils.ts b/src/configs/gamepad-utils.ts index 76a90d564..473336ef2 100644 --- a/src/configs/gamepad-utils.ts +++ b/src/configs/gamepad-utils.ts @@ -55,10 +55,7 @@ function getKeyAndSettingNameFromCurrentKeysWithAction(config, _action, alt: boo } export function getKeyAndActionFromCurrentKeysWithSettingName(config, settingName) { - for (const _settingName of Object.keys(config.currentKeys)) { - if (_settingName === settingName) return config.currentKeys[_settingName]; - } - return null; + return config.currentKeys[settingName]; } export function getKeyAndActionFromCurrentKeysWithPressedButton(config, pressedButton) { @@ -69,27 +66,49 @@ export function getKeyAndActionFromCurrentKeysWithPressedButton(config, pressedB export function swapCurrentKeys(config: InterfaceConfig, settingName, pressedButton): void { const previousBind = getKeyAndActionFromCurrentKeysWithSettingName(config, settingName); + const prevKey = deepCopy(previousBind); const newBind = getKeyAndActionFromCurrentKeysWithPressedButton(config, pressedButton); - config.custom[previousBind.key] = newBind.action; - config.custom[newBind.key] = previousBind.action; - config.icons[previousBind.key] = newBind.icon; - config.icons[newBind.key] = previousBind.icon; + const nextKey = deepCopy(newBind); + if (prevKey.key === nextKey.key) { + // special case when back to back and not enough info to get back to previous button + const toRestore = getKeyAndSettingNameFromCurrentKeysWithAction(config, prevKey.from.action); + const iconToRestore = config.icons[toRestore.key]; + config.custom[prevKey.key] = prevKey.from.action; + config.icons[prevKey.key] = iconToRestore; + + config.custom[toRestore.key] = prevKey.action; + config.icons[toRestore.key] = prevKey.icon; + + config.currentKeys[settingName].from = prevKey; + config.currentKeys[toRestore.settingName].from = { + key: toRestore.key, + action: prevKey.from.action, + icon: iconToRestore, + }; + } else { + config.custom[previousBind.key] = newBind.action; + config.custom[newBind.key] = previousBind.action; + config.icons[previousBind.key] = newBind.icon; + config.icons[newBind.key] = previousBind.icon; + const nextSettingName = getKeyAndSettingNameFromCurrentKeysWithAction(config, newBind.action).settingName; + config.currentKeys[settingName].from = prevKey; + config.currentKeys[nextSettingName].from = nextKey; + } reloadCurrentKeys(config); } export function reloadCurrentKeys(config): void { // need to rework this to include keys that were not there at the begining - const currentKeys = {}; + const currentKeys = config.currentKeys ? deepCopy(config.currentKeys) : {}; for (const key of Object.keys(config.setting)) { const settingName = config.setting[key]; const action = config.custom[key]; const icon = config.icons[key]; - currentKeys[settingName] = { - key, - action, - icon, - } + if (!currentKeys[settingName]) currentKeys[settingName] = {}; + currentKeys[settingName].key = key; + currentKeys[settingName].action = action; + currentKeys[settingName].icon = icon; } config.currentKeys = deepCopy(currentKeys); } diff --git a/src/test/gamepad_remaping.test.ts b/src/test/gamepad_remaping.test.ts index 5e717f6d3..8bd683519 100644 --- a/src/test/gamepad_remaping.test.ts +++ b/src/test/gamepad_remaping.test.ts @@ -6,7 +6,7 @@ import { getKeyAndActionFromCurrentKeysWithSettingName, getKeyForSettingName, getKeyFromMapping, - getKeyWithAction, initCurrentKeys, + getKeyWithAction, reloadCurrentKeys, swapCurrentKeys, } from "#app/configs/gamepad-utils"; @@ -23,6 +23,7 @@ describe('Test Keyboard', () => { const temp = {...cfg_gamepad_example} config = deepCopy(temp); config.custom = {...config.default} + config.ogIcons = {...config.icons} reloadCurrentKeys(config); }); @@ -162,5 +163,24 @@ describe('Test Keyboard', () => { expect(iconA).toEqual('T_X_B_Color_Alt.png'); expect(iconB).toEqual('T_X_A_Color_Alt.png'); expect(iconC).toEqual('T_X_X_Color_Alt.png'); + + expect(config.ogIcons["RC_S"]).toEqual("T_X_A_Color_Alt.png") + expect(config.ogIcons["RC_E"]).toEqual("T_X_B_Color_Alt.png") + expect(config.ogIcons["RC_N"]).toEqual("T_X_Y_Color_Alt.png") + expect(config.ogIcons["RC_W"]).toEqual("T_X_X_Color_Alt.png") + }); + + it('Check 2 swap back to back', () => { + swapCurrentKeys(config, SettingInterfaceGamepad.Button_Action, 1); // cancel + expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].key).toEqual("RC_S"); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].action).toEqual(Button.CANCEL); + let iconA = getIconWithSettingName(config, SettingInterfaceGamepad.Button_Action); + expect(iconA).toEqual('T_X_B_Color_Alt.png'); + + swapCurrentKeys(config, SettingInterfaceGamepad.Button_Action, 0); // cancel + let iconB = getIconWithSettingName(config, SettingInterfaceGamepad.Button_Action); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].key).toEqual("RC_S"); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].action).toEqual(Button.ACTION); + expect(iconB).toEqual('T_X_A_Color_Alt.png'); }); }); \ No newline at end of file From 5dd9e017ea0749f5531791e76bc6875a38673ad5 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Tue, 14 May 2024 02:54:21 +0200 Subject: [PATCH 66/81] fix icon --- src/configs/gamepad-utils.ts | 11 ++--- src/test/gamepad_remaping.test.ts | 73 ++++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 9 deletions(-) diff --git a/src/configs/gamepad-utils.ts b/src/configs/gamepad-utils.ts index 473336ef2..b6445cbf0 100644 --- a/src/configs/gamepad-utils.ts +++ b/src/configs/gamepad-utils.ts @@ -72,19 +72,14 @@ export function swapCurrentKeys(config: InterfaceConfig, settingName, pressedBut if (prevKey.key === nextKey.key) { // special case when back to back and not enough info to get back to previous button const toRestore = getKeyAndSettingNameFromCurrentKeysWithAction(config, prevKey.from.action); - const iconToRestore = config.icons[toRestore.key]; config.custom[prevKey.key] = prevKey.from.action; - config.icons[prevKey.key] = iconToRestore; + config.icons[prevKey.key] = prevKey.from.icon; config.custom[toRestore.key] = prevKey.action; config.icons[toRestore.key] = prevKey.icon; - config.currentKeys[settingName].from = prevKey; - config.currentKeys[toRestore.settingName].from = { - key: toRestore.key, - action: prevKey.from.action, - icon: iconToRestore, - }; + delete config.currentKeys[settingName].from; + delete config.currentKeys[toRestore.settingName].from; } else { config.custom[previousBind.key] = newBind.action; config.custom[newBind.key] = previousBind.action; diff --git a/src/test/gamepad_remaping.test.ts b/src/test/gamepad_remaping.test.ts index 8bd683519..7ac8b0a05 100644 --- a/src/test/gamepad_remaping.test.ts +++ b/src/test/gamepad_remaping.test.ts @@ -6,7 +6,7 @@ import { getKeyAndActionFromCurrentKeysWithSettingName, getKeyForSettingName, getKeyFromMapping, - getKeyWithAction, + getKeyWithAction, regenerateCustom, reloadCurrentKeys, swapCurrentKeys, } from "#app/configs/gamepad-utils"; @@ -183,4 +183,75 @@ describe('Test Keyboard', () => { expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].action).toEqual(Button.ACTION); expect(iconB).toEqual('T_X_A_Color_Alt.png'); }); + + it('Check 4 swap back to back', () => { + expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].key).toEqual("RC_S"); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].action).toEqual(Button.ACTION); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].icon).toEqual('T_X_A_Color_Alt.png'); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].key).toEqual("RC_E"); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].action).toEqual(Button.CANCEL); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].icon).toEqual('T_X_B_Color_Alt.png'); + swapCurrentKeys(config, SettingInterfaceGamepad.Button_Action, 1); // cancel + expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].key).toEqual("RC_S"); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].action).toEqual(Button.CANCEL); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].icon).toEqual('T_X_B_Color_Alt.png'); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].key).toEqual("RC_E"); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].action).toEqual(Button.ACTION); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].icon).toEqual('T_X_A_Color_Alt.png'); + + swapCurrentKeys(config, SettingInterfaceGamepad.Button_Action, 0); // cancel + expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].key).toEqual("RC_S"); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].action).toEqual(Button.ACTION); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].icon).toEqual('T_X_A_Color_Alt.png'); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].key).toEqual("RC_E"); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].action).toEqual(Button.CANCEL); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].icon).toEqual('T_X_B_Color_Alt.png'); + + swapCurrentKeys(config, SettingInterfaceGamepad.Button_Action, 1); // cancel + expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].key).toEqual("RC_S"); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].action).toEqual(Button.CANCEL); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].icon).toEqual('T_X_B_Color_Alt.png'); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].key).toEqual("RC_E"); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].action).toEqual(Button.ACTION); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].icon).toEqual('T_X_A_Color_Alt.png'); + + swapCurrentKeys(config, SettingInterfaceGamepad.Button_Action, 0); // cancel + expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].key).toEqual("RC_S"); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].action).toEqual(Button.ACTION); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].icon).toEqual('T_X_A_Color_Alt.png'); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].key).toEqual("RC_E"); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].action).toEqual(Button.CANCEL); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].icon).toEqual('T_X_B_Color_Alt.png'); + }); + + it('Custom scenario: B icon duplicate', () => { + config.currentKeys[SettingInterfaceGamepad.Button_Action] = { + "key": "RC_S", + "action": 6, + "icon": "T_X_B_Color_Alt.png", + "from": { + "key": "RC_S", + "action": 5, + "icon": "T_X_A_Color_Alt.png" + } + }; + config.currentKeys[SettingInterfaceGamepad.Button_Cancel] = { + "key": "RC_E", + "action": 5, + "icon": "T_X_A_Color_Alt.png", + "from": { + "key": "RC_E", + "action": 6, + "icon": "T_X_B_Color_Alt.png" + } + }; + regenerateCustom(config); + swapCurrentKeys(config, SettingInterfaceGamepad.Button_Action, 0); // cancel + expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].key).toEqual("RC_S"); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].action).toEqual(Button.ACTION); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].icon).toEqual('T_X_A_Color_Alt.png'); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].key).toEqual("RC_E"); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].action).toEqual(Button.CANCEL); + expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].icon).toEqual('T_X_B_Color_Alt.png'); + }) }); \ No newline at end of file From 82e093490754ef84a431eaa43875e55cccc2d514 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Tue, 14 May 2024 03:00:34 +0200 Subject: [PATCH 67/81] added a test to fix unbinded key --- src/test/keyboard_remaping.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/test/keyboard_remaping.test.ts b/src/test/keyboard_remaping.test.ts index 7c2705ca7..4564f4243 100644 --- a/src/test/keyboard_remaping.test.ts +++ b/src/test/keyboard_remaping.test.ts @@ -265,4 +265,20 @@ describe('Test Keyboard', () => { expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].action).toEqual(Button.RIGHT); expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].icon).toEqual("T_D_Key_Dark.png"); }) + + + it('Swap alt with a key not binded yet', () => { + const settingNameA = SettingInterfaceKeyboard.Alt_Button_Up; + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); + + swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Up, Phaser.Input.Keyboard.KeyCodes.B); + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_B_Key_Dark.png"); + + }) }); \ No newline at end of file From a2eab6259bd70d699ea5cc0e2ad3b16600606928 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Tue, 14 May 2024 04:05:32 +0200 Subject: [PATCH 68/81] handled delete bind and assign back --- src/configs/gamepad-utils.ts | 58 ++++++++++++------- src/inputs-controller.ts | 1 + src/test/keyboard_remaping.test.ts | 40 ++++++++++++- .../settings/settings-keyboard-ui-handler.ts | 17 +++++- src/utils.ts | 6 ++ 5 files changed, 98 insertions(+), 24 deletions(-) diff --git a/src/configs/gamepad-utils.ts b/src/configs/gamepad-utils.ts index b6445cbf0..3773afb03 100644 --- a/src/configs/gamepad-utils.ts +++ b/src/configs/gamepad-utils.ts @@ -64,30 +64,42 @@ export function getKeyAndActionFromCurrentKeysWithPressedButton(config, pressedB return getKeyAndActionFromCurrentKeysWithSettingName(config, settingName); } +export function assignNewKey(config: InterfaceConfig, settingName, pressedButton, previousBind): void { + const key = getKeyFromMapping(config, pressedButton); + const icon = config.ogIcons[key]; + config.icons[previousBind.key] = icon; + config.currentKeys[settingName].icon = icon; + config.custom[key] = previousBind.action; +} + export function swapCurrentKeys(config: InterfaceConfig, settingName, pressedButton): void { const previousBind = getKeyAndActionFromCurrentKeysWithSettingName(config, settingName); const prevKey = deepCopy(previousBind); const newBind = getKeyAndActionFromCurrentKeysWithPressedButton(config, pressedButton); - const nextKey = deepCopy(newBind); - if (prevKey.key === nextKey.key) { - // special case when back to back and not enough info to get back to previous button - const toRestore = getKeyAndSettingNameFromCurrentKeysWithAction(config, prevKey.from.action); - config.custom[prevKey.key] = prevKey.from.action; - config.icons[prevKey.key] = prevKey.from.icon; - - config.custom[toRestore.key] = prevKey.action; - config.icons[toRestore.key] = prevKey.icon; - - delete config.currentKeys[settingName].from; - delete config.currentKeys[toRestore.settingName].from; + if (!newBind) { + assignNewKey(config, settingName, pressedButton, previousBind); } else { - config.custom[previousBind.key] = newBind.action; - config.custom[newBind.key] = previousBind.action; - config.icons[previousBind.key] = newBind.icon; - config.icons[newBind.key] = previousBind.icon; - const nextSettingName = getKeyAndSettingNameFromCurrentKeysWithAction(config, newBind.action).settingName; - config.currentKeys[settingName].from = prevKey; - config.currentKeys[nextSettingName].from = nextKey; + const nextKey = deepCopy(newBind); + if (prevKey.key === nextKey.key) { + // special case when back to back and not enough info to get back to previous button + const toRestore = getKeyAndSettingNameFromCurrentKeysWithAction(config, prevKey.from.action); + config.custom[prevKey.key] = prevKey.from.action; + config.icons[prevKey.key] = prevKey.from.icon; + + config.custom[toRestore.key] = prevKey.action; + config.icons[toRestore.key] = prevKey.icon; + + delete config.currentKeys[settingName].from; + delete config.currentKeys[toRestore.settingName].from; + } else { + config.custom[previousBind.key] = newBind.action; + config.custom[newBind.key] = previousBind.action; + config.icons[previousBind.key] = newBind.icon; + config.icons[newBind.key] = previousBind.icon; + const nextSettingName = getKeyAndSettingNameFromCurrentKeysWithAction(config, newBind.action).settingName; + config.currentKeys[settingName].from = prevKey; + config.currentKeys[nextSettingName].from = nextKey; + } } reloadCurrentKeys(config); } @@ -102,7 +114,7 @@ export function reloadCurrentKeys(config): void { const icon = config.icons[key]; if (!currentKeys[settingName]) currentKeys[settingName] = {}; currentKeys[settingName].key = key; - currentKeys[settingName].action = action; + currentKeys[settingName].action = action === undefined ? currentKeys[settingName].action : action; currentKeys[settingName].icon = icon; } config.currentKeys = deepCopy(currentKeys); @@ -115,4 +127,10 @@ export function regenerateCustom(config): void { custom[key] = action; } config.custom = deepCopy(custom); +} + +export function deleteBind(config, settingName): void { + const { key } = getKeyAndActionFromCurrentKeysWithSettingName(config, settingName); + delete config.currentKeys[settingName].icon + config.custom[key] = undefined } \ No newline at end of file diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 30072dc00..319017d90 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -36,6 +36,7 @@ export interface InterfaceConfig { padID: string; padType: string; gamepadMapping: GamepadMapping; + ogIcons: IconsMapping; icons: IconsMapping; setting: SettingMapping; default: MappingLayout; diff --git a/src/test/keyboard_remaping.test.ts b/src/test/keyboard_remaping.test.ts index 4564f4243..94552caf2 100644 --- a/src/test/keyboard_remaping.test.ts +++ b/src/test/keyboard_remaping.test.ts @@ -1,6 +1,7 @@ import {beforeEach, expect, describe, it} from "vitest"; import cfg_keyboard_example, {SettingInterfaceKeyboard} from "#app/test/cfg_keyboard_example"; import { + deleteBind, getIconWithPressedButton, getIconWithSettingName, getKeyAndActionFromCurrentKeysWithSettingName, @@ -23,6 +24,7 @@ describe('Test Keyboard', () => { beforeEach(() => { config = deepCopy(cfg_keyboard_example); config.custom = {...config.default} + config.ogIcons = {...config.icons} reloadCurrentKeys(config); }); @@ -268,8 +270,6 @@ describe('Test Keyboard', () => { it('Swap alt with a key not binded yet', () => { - const settingNameA = SettingInterfaceKeyboard.Alt_Button_Up; - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); @@ -279,6 +279,40 @@ describe('Test Keyboard', () => { expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_B_Key_Dark.png"); - }) + + + it('Delete bind', () => { + const settingNameA = SettingInterfaceKeyboard.Alt_Button_Up; + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); + deleteBind(config, settingNameA) + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual(undefined); + }) + + + it('Delete bind then asign', () => { + const settingNameA = SettingInterfaceKeyboard.Alt_Button_Up; + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); + deleteBind(config, settingNameA) + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual(undefined); + + swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Up, Phaser.Input.Keyboard.KeyCodes.B); + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_B_Key_Dark.png"); + }) + }); \ No newline at end of file diff --git a/src/ui/settings/settings-keyboard-ui-handler.ts b/src/ui/settings/settings-keyboard-ui-handler.ts index fdbbb5c07..2564f3966 100644 --- a/src/ui/settings/settings-keyboard-ui-handler.ts +++ b/src/ui/settings/settings-keyboard-ui-handler.ts @@ -2,9 +2,10 @@ import BattleScene from "../../battle-scene"; import {Mode} from "../ui"; import cfg_keyboard_azerty from "#app/configs/cfg_keyboard_azerty"; import {SettingKeyboard, settingKeyboardDefaults, settingKeyboardOptions} from "#app/system/settings-keyboard"; -import {truncateString} from "#app/utils"; +import {reverseValueToKeySetting, truncateString} from "#app/utils"; import AbstractSettingsUiUiHandler from "#app/ui/settings/abstract-settings-ui-handler"; import {InterfaceConfig} from "#app/inputs-controller"; +import {deleteBind} from "#app/configs/gamepad-utils"; export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandler { @@ -18,6 +19,20 @@ export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandl this.commonSettingsCount = 1; this.textureOverride = 'keyboard'; this.localStoragePropertyName = 'settingsKeyboard'; + + const deleteEvent = scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.DELETE); + deleteEvent.on('up', this.onDeleteDown, this); + } + + onDeleteDown(): void { + const cursor = this.cursor + this.scrollCursor; // Calculate the absolute cursor position. + console.log('delete pressed', cursor, this.settingLabels[cursor].text); + const selection = this.settingLabels[cursor].text; + const key = reverseValueToKeySetting(selection); + const setting = SettingKeyboard[key]; + deleteBind(this.getActiveConfig(), setting); + need to handle the "no icon" + console.log('setting:', setting); } getActiveConfig(): InterfaceConfig { diff --git a/src/utils.ts b/src/utils.ts index 82fad57ea..e83815841 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -352,4 +352,10 @@ export function truncateString(str: String, maxLength: number = 10) { export function deepCopy(values: object): object { return JSON.parse(JSON.stringify(values)); +} + +export function reverseValueToKeySetting(input) { + const words = input.split(' '); + const capitalizedWords = words.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()); + return capitalizedWords.join('_'); } \ No newline at end of file From dfd73c36163e87403de26f8912d2224a7512e045 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Tue, 14 May 2024 17:18:31 +0200 Subject: [PATCH 69/81] fix some small bugs - still need to fix the latest test --- src/configs/gamepad-utils.ts | 59 ++++++-- src/test/keyboard_remaping.test.ts | 129 +++++++++++++++++- src/ui/settings/gamepad-binding-ui-handler.ts | 4 +- .../settings/keyboard-binding-ui-handler.ts | 4 +- .../settings/settings-keyboard-ui-handler.ts | 16 ++- 5 files changed, 191 insertions(+), 21 deletions(-) diff --git a/src/configs/gamepad-utils.ts b/src/configs/gamepad-utils.ts index 3773afb03..d48064ac3 100644 --- a/src/configs/gamepad-utils.ts +++ b/src/configs/gamepad-utils.ts @@ -69,14 +69,32 @@ export function assignNewKey(config: InterfaceConfig, settingName, pressedButton const icon = config.ogIcons[key]; config.icons[previousBind.key] = icon; config.currentKeys[settingName].icon = icon; - config.custom[key] = previousBind.action; + + config.custom[key] = previousBind.action !== -1 ? previousBind.action : previousBind.from.action; + config.custom[previousBind.key] = -1; + config.currentKeys[settingName].replacedBy = key; + + delete config.currentKeys[settingName].from } export function swapCurrentKeys(config: InterfaceConfig, settingName, pressedButton): void { const previousBind = getKeyAndActionFromCurrentKeysWithSettingName(config, settingName); const prevKey = deepCopy(previousBind); const newBind = getKeyAndActionFromCurrentKeysWithPressedButton(config, pressedButton); - if (!newBind) { + if (newBind && previousBind.action === -1) { + //special case when rebinding deleted key with already assigned key + const toRestore = deepCopy(newBind); + config.custom[newBind.key] = prevKey.from.action; + config.icons[prevKey.key] = newBind.icon; + config.icons[newBind.key] = prevKey.from.icon; + + delete prevKey.from; + + const nextSettingName = getKeyAndSettingNameFromCurrentKeysWithAction(config, newBind.action, newBind.isAlt).settingName; + config.currentKeys[nextSettingName].from = toRestore; + config.currentKeys[nextSettingName].isDeleted = true; + config.currentKeys[settingName].replacedBy = toRestore.key; + } else if (!newBind) { assignNewKey(config, settingName, pressedButton, previousBind); } else { const nextKey = deepCopy(newBind); @@ -96,7 +114,7 @@ export function swapCurrentKeys(config: InterfaceConfig, settingName, pressedBut config.custom[newBind.key] = previousBind.action; config.icons[previousBind.key] = newBind.icon; config.icons[newBind.key] = previousBind.icon; - const nextSettingName = getKeyAndSettingNameFromCurrentKeysWithAction(config, newBind.action).settingName; + const nextSettingName = getKeyAndSettingNameFromCurrentKeysWithAction(config, newBind.action, newBind.isAlt).settingName; config.currentKeys[settingName].from = prevKey; config.currentKeys[nextSettingName].from = nextKey; } @@ -112,10 +130,27 @@ export function reloadCurrentKeys(config): void { const settingName = config.setting[key]; const action = config.custom[key]; const icon = config.icons[key]; + if (currentKeys[settingName]?.latestReplacedBy) { + console.log(''); + } if (!currentKeys[settingName]) currentKeys[settingName] = {}; currentKeys[settingName].key = key; - currentKeys[settingName].action = action === undefined ? currentKeys[settingName].action : action; - currentKeys[settingName].icon = icon; + currentKeys[settingName].isAlt = settingName.includes("ALT_"); + const previousAction = config.custom[currentKeys[settingName].replacedBy] + if (action === -1 && previousAction !== undefined) { + currentKeys[settingName].action = previousAction; + currentKeys[settingName].icon = icon; + currentKeys[settingName].latestReplacedBy = config.currentKeys[settingName].replacedBy + delete currentKeys[settingName].replacedBy; + } else if (currentKeys[settingName].isDeleted) { + currentKeys[settingName].action = -1; + currentKeys[settingName].icon = undefined; + currentKeys[settingName].latestIsDeleted = config.currentKeys[settingName].isDeleted + delete currentKeys[settingName].isDeleted; + } else { + currentKeys[settingName].action = action; + currentKeys[settingName].icon = action === -1 ? undefined : icon; + } } config.currentKeys = deepCopy(currentKeys); } @@ -123,14 +158,22 @@ export function reloadCurrentKeys(config): void { export function regenerateCustom(config): void { const custom = deepCopy(config.custom); for (const settingName of Object.keys(config.currentKeys)) { - const {key, action} = config.currentKeys[settingName]; - custom[key] = action; + const {key, action, latestReplacedBy, latestIsDeleted} = config.currentKeys[settingName]; + if (latestReplacedBy) { + custom[key] = -1; + custom[latestReplacedBy] = action; + } else if (!latestIsDeleted) { + custom[key] = action; + } } config.custom = deepCopy(custom); } export function deleteBind(config, settingName): void { const { key } = getKeyAndActionFromCurrentKeysWithSettingName(config, settingName); + const prev = deepCopy(config.currentKeys[settingName]); delete config.currentKeys[settingName].icon - config.custom[key] = undefined + config.currentKeys[settingName].from = prev; + config.custom[key] = -1; + reloadCurrentKeys(config); } \ No newline at end of file diff --git a/src/test/keyboard_remaping.test.ts b/src/test/keyboard_remaping.test.ts index 94552caf2..d27d806e5 100644 --- a/src/test/keyboard_remaping.test.ts +++ b/src/test/keyboard_remaping.test.ts @@ -291,12 +291,12 @@ describe('Test Keyboard', () => { deleteBind(config, settingNameA) expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(-1); expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual(undefined); }) - it('Delete bind then asign', () => { + it('Delete bind then asign not existing button', () => { const settingNameA = SettingInterfaceKeyboard.Alt_Button_Up; expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); @@ -305,14 +305,137 @@ describe('Test Keyboard', () => { deleteBind(config, settingNameA) expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(-1); expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual(undefined); + expect(config.custom["KEY_Z"]).toEqual(-1); swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Up, Phaser.Input.Keyboard.KeyCodes.B); expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_B_Key_Dark.png"); + expect(config.custom["KEY_B"]).toEqual(Button.UP); + expect(config.custom["KEY_Z"]).toEqual(-1); + }) + + + it('swap bind, then Delete bind then assign bind', () => { + const settingNameA = SettingInterfaceKeyboard.Alt_Button_Up; + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); + expect(config.custom["KEY_Z"]).toEqual(Button.UP); + + swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Up, Phaser.Input.Keyboard.KeyCodes.B); + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_B_Key_Dark.png"); + expect(config.custom["KEY_B"]).toEqual(Button.UP); + expect(config.custom["KEY_Z"]).toEqual(-1); + + deleteBind(config, settingNameA); + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(-1); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual(undefined); + expect(config.custom["KEY_Z"]).toEqual(-1); + + swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Up, Phaser.Input.Keyboard.KeyCodes.B); + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_B_Key_Dark.png"); + expect(config.custom["KEY_B"]).toEqual(Button.UP); + }) + + + it('Delete bind then asign not already existing button', () => { + const settingNameA = SettingInterfaceKeyboard.Alt_Button_Up; + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); + expect(config.custom["KEY_Z"]).toEqual(Button.UP); + deleteBind(config, settingNameA) + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(-1); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual(undefined); + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Cycle_Ability].key).toEqual("KEY_L"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Cycle_Ability].action).toEqual(Button.CYCLE_ABILITY); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Cycle_Ability].icon).toEqual("T_L_Key_Dark.png"); + expect(config.custom["KEY_Z"]).toEqual(-1); + expect(config.custom["KEY_L"]).toEqual(Button.CYCLE_ABILITY); + + swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Up, Phaser.Input.Keyboard.KeyCodes.L); + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_L_Key_Dark.png"); + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Cycle_Ability].key).toEqual("KEY_L"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Cycle_Ability].action).toEqual(-1); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Cycle_Ability].icon).toEqual(undefined); + + expect(config.custom["KEY_Z"]).toEqual(-1); + expect(config.custom["KEY_L"]).toEqual(Button.UP); + }) + + + it('Custom scenario 2, regenerate customs when init key is not from setting', () => { + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); + expect(config.custom["KEY_Z"]).toEqual(Button.UP); + + swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Up, Phaser.Input.Keyboard.KeyCodes.T); + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_T_Key_Dark.png"); + + expect(config.custom["KEY_Z"]).toEqual(-1); + expect(config.custom["KEY_T"]).toEqual(Button.UP); + }) + + + it('change alt to unknown touch than another one alt with another unknown touch', () => { + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); + expect(config.custom["KEY_Z"]).toEqual(Button.UP); + + swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Up, Phaser.Input.Keyboard.KeyCodes.T); + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_T_Key_Dark.png"); + + expect(config.custom["KEY_Z"]).toEqual(-1); + expect(config.custom["KEY_T"]).toEqual(Button.UP); + + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].key).toEqual("KEY_S"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].action).toEqual(Button.DOWN); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].icon).toEqual("T_S_Key_Dark.png"); + + swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Down, Phaser.Input.Keyboard.KeyCodes.U); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].key).toEqual("KEY_S"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].action).toEqual(Button.DOWN); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].icon).toEqual("T_U_Key_Dark.png"); + + + expect(config.custom["KEY_S"]).toEqual(-1); + expect(config.custom["KEY_U"]).toEqual(Button.DOWN); + expect(config.custom["KEY_Z"]).toEqual(-1); + expect(config.custom["KEY_T"]).toEqual(Button.UP); + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_T_Key_Dark.png"); }) }); \ No newline at end of file diff --git a/src/ui/settings/gamepad-binding-ui-handler.ts b/src/ui/settings/gamepad-binding-ui-handler.ts index e9345e825..96ec0595f 100644 --- a/src/ui/settings/gamepad-binding-ui-handler.ts +++ b/src/ui/settings/gamepad-binding-ui-handler.ts @@ -2,8 +2,7 @@ import BattleScene from "../../battle-scene"; import AbstractBindingUiHandler from "../settings/abrast-binding-ui-handler"; import {Mode} from "../ui"; import { - getKeyAndActionFromCurrentKeysWithPressedButton, - getKeyAndActionFromCurrentKeysWithSettingName, getKeyFromMapping, + getKeyAndActionFromCurrentKeysWithSettingName, getKeyFromMapping, regenerateCustom, } from "#app/configs/gamepad-utils"; @@ -33,6 +32,7 @@ export default class GamepadBindingUiHandler extends AbstractBindingUiHandler { const activeConfig = this.scene.inputController.getActiveConfig(); this.scene.inputController.swapBinding(activeConfig, this.target, this.buttonPressed) this.scene.gameData.saveCustomMapping(this.scene.inputController?.chosenGamepad, activeConfig.currentKeys); + regenerateCustom(activeConfig); return true; } } \ No newline at end of file diff --git a/src/ui/settings/keyboard-binding-ui-handler.ts b/src/ui/settings/keyboard-binding-ui-handler.ts index 242d0dfef..e2d6599b0 100644 --- a/src/ui/settings/keyboard-binding-ui-handler.ts +++ b/src/ui/settings/keyboard-binding-ui-handler.ts @@ -2,8 +2,7 @@ import BattleScene from "../../battle-scene"; import AbstractBindingUiHandler from "../settings/abrast-binding-ui-handler"; import {Mode} from "../ui"; import { - getKeyAndActionFromCurrentKeysWithPressedButton, - getKeyAndActionFromCurrentKeysWithSettingName, getKeyFromMapping, + getKeyAndActionFromCurrentKeysWithSettingName, getKeyFromMapping, regenerateCustom, } from "#app/configs/gamepad-utils"; @@ -32,6 +31,7 @@ export default class KeyboardBindingUiHandler extends AbstractBindingUiHandler { const activeConfig = this.scene.inputController.getActiveKeyboardConfig(); this.scene.inputController.swapBinding(activeConfig, this.target, this.buttonPressed) this.scene.gameData.saveCustomKeyboardMapping(this.scene.inputController?.chosenKeyboard, activeConfig.currentKeys); + regenerateCustom(activeConfig); return true; } diff --git a/src/ui/settings/settings-keyboard-ui-handler.ts b/src/ui/settings/settings-keyboard-ui-handler.ts index 2564f3966..4c3962f21 100644 --- a/src/ui/settings/settings-keyboard-ui-handler.ts +++ b/src/ui/settings/settings-keyboard-ui-handler.ts @@ -26,13 +26,13 @@ export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandl onDeleteDown(): void { const cursor = this.cursor + this.scrollCursor; // Calculate the absolute cursor position. - console.log('delete pressed', cursor, this.settingLabels[cursor].text); const selection = this.settingLabels[cursor].text; const key = reverseValueToKeySetting(selection); const setting = SettingKeyboard[key]; + const activeConfig = this.getActiveConfig(); deleteBind(this.getActiveConfig(), setting); - need to handle the "no icon" - console.log('setting:', setting); + this.saveCustomKeyboardMappingToLocalStorage(activeConfig); + this.updateBindings(); } getActiveConfig(): InterfaceConfig { @@ -76,8 +76,12 @@ export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandl } - saveSettingToLocalStorage(setting, cursor): void { - if (this.settingDevice[setting] !== this.settingDevice.Default_Layout) - this.scene.gameData.saveKeyboardSetting(setting, cursor) + saveCustomKeyboardMappingToLocalStorage(config): void { + this.scene.gameData.saveCustomKeyboardMapping(this.scene.inputController?.chosenKeyboard, config.currentKeys); + } + + saveSettingToLocalStorage(settingName, cursor): void { + if (this.settingDevice[settingName] !== this.settingDevice.Default_Layout) + this.scene.gameData.saveKeyboardSetting(settingName, cursor) } } \ No newline at end of file From 3251846aea17fa6e69f1f1431adaaba0898ad3cf Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Tue, 14 May 2024 17:34:43 +0200 Subject: [PATCH 70/81] every tests passed (for now) --- src/configs/gamepad-utils.ts | 10 ++++------ src/test/keyboard_remaping.test.ts | 5 +++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/configs/gamepad-utils.ts b/src/configs/gamepad-utils.ts index d48064ac3..7835e8d88 100644 --- a/src/configs/gamepad-utils.ts +++ b/src/configs/gamepad-utils.ts @@ -130,18 +130,14 @@ export function reloadCurrentKeys(config): void { const settingName = config.setting[key]; const action = config.custom[key]; const icon = config.icons[key]; - if (currentKeys[settingName]?.latestReplacedBy) { - console.log(''); - } if (!currentKeys[settingName]) currentKeys[settingName] = {}; currentKeys[settingName].key = key; currentKeys[settingName].isAlt = settingName.includes("ALT_"); const previousAction = config.custom[currentKeys[settingName].replacedBy] - if (action === -1 && previousAction !== undefined) { + if (action === -1 && previousAction !== undefined && !currentKeys[settingName].isDeleted) { currentKeys[settingName].action = previousAction; currentKeys[settingName].icon = icon; currentKeys[settingName].latestReplacedBy = config.currentKeys[settingName].replacedBy - delete currentKeys[settingName].replacedBy; } else if (currentKeys[settingName].isDeleted) { currentKeys[settingName].action = -1; currentKeys[settingName].icon = undefined; @@ -173,7 +169,9 @@ export function deleteBind(config, settingName): void { const { key } = getKeyAndActionFromCurrentKeysWithSettingName(config, settingName); const prev = deepCopy(config.currentKeys[settingName]); delete config.currentKeys[settingName].icon + const actualKey = prev.replacedBy || key; config.currentKeys[settingName].from = prev; - config.custom[key] = -1; + config.custom[actualKey] = -1; + config.currentKeys[settingName].isDeleted = true; reloadCurrentKeys(config); } \ No newline at end of file diff --git a/src/test/keyboard_remaping.test.ts b/src/test/keyboard_remaping.test.ts index d27d806e5..8eae9dd42 100644 --- a/src/test/keyboard_remaping.test.ts +++ b/src/test/keyboard_remaping.test.ts @@ -302,6 +302,7 @@ describe('Test Keyboard', () => { expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); + expect(config.custom["KEY_Z"]).toEqual(Button.UP); deleteBind(config, settingNameA) expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); @@ -326,6 +327,7 @@ describe('Test Keyboard', () => { expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); expect(config.custom["KEY_Z"]).toEqual(Button.UP); + expect(config.custom["KEY_B"]).toEqual(-1); swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Up, Phaser.Input.Keyboard.KeyCodes.B); @@ -341,6 +343,7 @@ describe('Test Keyboard', () => { expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(-1); expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual(undefined); expect(config.custom["KEY_Z"]).toEqual(-1); + expect(config.custom["KEY_B"]).toEqual(-1); swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Up, Phaser.Input.Keyboard.KeyCodes.B); @@ -348,6 +351,7 @@ describe('Test Keyboard', () => { expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_B_Key_Dark.png"); expect(config.custom["KEY_B"]).toEqual(Button.UP); + expect(config.custom["KEY_Z"]).toEqual(-1); }) @@ -390,6 +394,7 @@ describe('Test Keyboard', () => { expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); expect(config.custom["KEY_Z"]).toEqual(Button.UP); + expect(config.custom["KEY_T"]).toEqual(-1); swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Up, Phaser.Input.Keyboard.KeyCodes.T); From c1e9e0c691a3f468bcf8c77103e1c3d2f1fed0a4 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Tue, 14 May 2024 18:20:01 +0200 Subject: [PATCH 71/81] fix icons issue + identified a new bug => new test scenario --- src/inputs-controller.ts | 14 +- src/system/game-data.ts | 14 +- src/test/keyboard_remaping.test.ts | 149 +++++++++++++++++- src/ui/settings/gamepad-binding-ui-handler.ts | 2 +- .../settings/keyboard-binding-ui-handler.ts | 2 +- .../settings/settings-keyboard-ui-handler.ts | 2 +- 6 files changed, 170 insertions(+), 13 deletions(-) diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 319017d90..40b51addb 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -340,13 +340,14 @@ export class InputsController { const config = deepCopy(this.getConfig(gamepadID)); config.custom = this.configs[gamepad]?.custom || {...config.default}; config.ogIcons = {...config.icons}; + config.icons = this.configs[gamepad]?.icons || {...config.icons}; config.currentKeys = this.configs[gamepad]?.currentKeys; if (!this.configs[gamepad]?.currentKeys) reloadCurrentKeys(config); else regenerateCustom(config); this.configs[gamepad] = config; - this.scene.gameData?.saveCustomMapping(this.chosenGamepad, this.configs[gamepad]?.currentKeys); + this.scene.gameData?.saveCustomMapping(this.chosenGamepad, this.configs[gamepad]?.currentKeys, this.configs[gamepad]?.icons); } if (this.chosenGamepad === thisGamepad.id) this.initChosenGamepad(this.chosenGamepad) } @@ -355,14 +356,15 @@ export class InputsController { for (const layout of ['default']) { const config = deepCopy(this.getConfigKeyboard(layout)); config.custom = this.keyboardConfigs[layout]?.custom || {...config.default}; - config.currentKeys = this.keyboardConfigs[layout]?.currentKeys; config.ogIcons = {...config.icons}; + config.icons = this.keyboardConfigs[layout]?.icons || {...config.icons}; + config.currentKeys = this.keyboardConfigs[layout]?.currentKeys; if (!this.keyboardConfigs[layout]?.currentKeys) reloadCurrentKeys(config); else regenerateCustom(config); this.keyboardConfigs[layout] = config; - this.scene.gameData?.saveCustomKeyboardMapping(this.chosenKeyboard, this.keyboardConfigs[layout]?.currentKeys); + this.scene.gameData?.saveCustomKeyboardMapping(this.chosenKeyboard, this.keyboardConfigs[layout]?.currentKeys, this.keyboardConfigs[layout]?.icons); } this.initChosenLayoutKeyboard(this.chosenKeyboard) } @@ -765,11 +767,13 @@ export class InputsController { */ injectConfig(gamepadName: String, customMappings): void { if (!this.configs[gamepadName]) this.configs[gamepadName] = {}; - this.configs[gamepadName].currentKeys = customMappings; + this.configs[gamepadName].currentKeys = customMappings.currentKeys; + this.configs[gamepadName].icons = customMappings.icons; } injectKeyboardConfig(layout: string, customMappings): void { if (!this.keyboardConfigs[layout]) this.keyboardConfigs[layout] = {}; - this.keyboardConfigs[layout].currentKeys = customMappings; + this.keyboardConfigs[layout].currentKeys = customMappings.currentKeys; + this.keyboardConfigs[layout].icons = customMappings.icons; } swapBinding(config, settingName, pressedButton): void { diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 31da8c084..4bf0496fe 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -498,20 +498,26 @@ export class GameData { return true; } - public saveCustomMapping(gamepadName: string, currentKeys): boolean { + public saveCustomMapping(gamepadName: string, currentKeys, icons): boolean { let customMappings: object = {}; if (localStorage.hasOwnProperty('customMapping')) customMappings = JSON.parse(localStorage.getItem('customMapping')); - customMappings[gamepadName] = currentKeys; + customMappings[gamepadName] = { + currentKeys, + icons + }; localStorage.setItem('customMapping', JSON.stringify(customMappings)); return true; } - public saveCustomKeyboardMapping(keyboardLayout: string, currentKeys): boolean { + public saveCustomKeyboardMapping(keyboardLayout: string, currentKeys, icons): boolean { let customKeyboardMappings: object = {}; if (localStorage.hasOwnProperty('customKeyboardMappings')) customKeyboardMappings = JSON.parse(localStorage.getItem('customKeyboardMappings')); - customKeyboardMappings[keyboardLayout] = currentKeys; + customKeyboardMappings[keyboardLayout] = { + currentKeys, + icons + }; localStorage.setItem('customKeyboardMappings', JSON.stringify(customKeyboardMappings)); return true; } diff --git a/src/test/keyboard_remaping.test.ts b/src/test/keyboard_remaping.test.ts index 8eae9dd42..ccdbd6a33 100644 --- a/src/test/keyboard_remaping.test.ts +++ b/src/test/keyboard_remaping.test.ts @@ -7,7 +7,7 @@ import { getKeyAndActionFromCurrentKeysWithSettingName, getKeyForSettingName, getKeyFromMapping, - getKeyWithAction, + getKeyWithAction, regenerateCustom, reloadCurrentKeys, swapCurrentKeys, } from "#app/configs/gamepad-utils"; @@ -443,4 +443,151 @@ describe('Test Keyboard', () => { expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_T_Key_Dark.png"); }) + + it('reload scenario with 1 bind already reassigned', () => { + config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up] = { + "key": "KEY_Z", + "isAlt": true, + "action": 3, + "icon": "T_D_Key_Dark.png", + "from": { + "key": "KEY_Z", + "isAlt": true, + "action": 0, + "icon": "T_Z_Key_Dark.png" + } + }; + config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right] = { + "key": "KEY_D", + "isAlt": true, + "action": 0, + "icon": "T_Z_Key_Dark.png", + "from": { + "key": "KEY_D", + "isAlt": true, + "action": 3, + "icon": "T_D_Key_Dark.png" + } + } + config.icons["KEY_D"] = "T_Z_Key_Dark.png"; + config.icons["KEY_Z"] = "T_D_Key_Dark.png"; + regenerateCustom(config); + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.RIGHT); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_D_Key_Dark.png"); + expect(config.custom["KEY_Z"]).toEqual(Button.RIGHT); + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].key).toEqual("KEY_D"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].icon).toEqual("T_Z_Key_Dark.png"); + expect(config.custom["KEY_D"]).toEqual(Button.UP); + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].key).toEqual("KEY_ARROW_UP"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].icon).toEqual("T_Up_Key_Dark.png"); + expect(config.custom["KEY_ARROW_UP"]).toEqual(Button.UP); + + swapCurrentKeys(config, SettingInterfaceKeyboard.Button_Up, Phaser.Input.Keyboard.KeyCodes.RIGHT); + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.RIGHT); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_D_Key_Dark.png"); + expect(config.custom["KEY_Z"]).toEqual(Button.RIGHT); + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].key).toEqual("KEY_D"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].icon).toEqual("T_Z_Key_Dark.png"); + expect(config.custom["KEY_D"]).toEqual(Button.UP); + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].key).toEqual("KEY_ARROW_UP"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].action).toEqual(Button.RIGHT); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].icon).toEqual("T_Right_Key_Dark.png"); + expect(config.custom["KEY_ARROW_UP"]).toEqual(Button.RIGHT); + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].key).toEqual("KEY_ARROW_RIGHT"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].icon).toEqual("T_Up_Key_Dark.png"); + expect(config.custom["KEY_ARROW_RIGHT"]).toEqual(Button.UP); + }); + + + it('Swap multiple touch alt and main', () => { + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].key).toEqual("KEY_ARROW_UP"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].icon).toEqual("T_Up_Key_Dark.png"); + expect(config.custom["KEY_ARROW_UP"]).toEqual(Button.UP); + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); + expect(config.custom["KEY_Z"]).toEqual(Button.UP); + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].key).toEqual("KEY_ARROW_RIGHT"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].action).toEqual(Button.RIGHT); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].icon).toEqual("T_Right_Key_Dark.png"); + expect(config.custom["KEY_ARROW_RIGHT"]).toEqual(Button.RIGHT); + + swapCurrentKeys(config, SettingInterfaceKeyboard.Button_Up, Phaser.Input.Keyboard.KeyCodes.RIGHT); + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].key).toEqual("KEY_ARROW_UP"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].action).toEqual(Button.RIGHT); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].icon).toEqual("T_Right_Key_Dark.png"); + expect(config.custom["KEY_ARROW_UP"]).toEqual(Button.RIGHT); + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); + expect(config.custom["KEY_Z"]).toEqual(Button.UP); + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].key).toEqual("KEY_ARROW_RIGHT"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].icon).toEqual("T_Up_Key_Dark.png"); + expect(config.custom["KEY_ARROW_RIGHT"]).toEqual(Button.UP); + + swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Up, Phaser.Input.Keyboard.KeyCodes.D); + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].key).toEqual("KEY_ARROW_UP"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].action).toEqual(Button.RIGHT); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].icon).toEqual("T_Right_Key_Dark.png"); + expect(config.custom["KEY_ARROW_UP"]).toEqual(Button.RIGHT); + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.RIGHT); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_D_Key_Dark.png"); + expect(config.custom["KEY_Z"]).toEqual(Button.RIGHT); + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].key).toEqual("KEY_ARROW_RIGHT"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].icon).toEqual("T_Up_Key_Dark.png"); + expect(config.custom["KEY_ARROW_RIGHT"]).toEqual(Button.UP); + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].key).toEqual("KEY_D"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].icon).toEqual("T_Z_Key_Dark.png"); + expect(config.custom["KEY_D"]).toEqual(Button.UP); + + swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Up, Phaser.Input.Keyboard.KeyCodes.Z); + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].key).toEqual("KEY_ARROW_UP"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].action).toEqual(Button.RIGHT); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].icon).toEqual("T_Right_Key_Dark.png"); + expect(config.custom["KEY_ARROW_UP"]).toEqual(Button.RIGHT); + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); + expect(config.custom["KEY_Z"]).toEqual(Button.UP); + + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].key).toEqual("KEY_ARROW_RIGHT"); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].icon).toEqual("T_Up_Key_Dark.png"); + expect(config.custom["KEY_ARROW_RIGHT"]).toEqual(Button.UP); + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].key).toEqual("KEY_D"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].action).toEqual(Button.RIGHT); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].icon).toEqual("T_D_Key_Dark.png"); + expect(config.custom["KEY_D"]).toEqual(Button.RIGHT); + }) + }); \ No newline at end of file diff --git a/src/ui/settings/gamepad-binding-ui-handler.ts b/src/ui/settings/gamepad-binding-ui-handler.ts index 96ec0595f..ca0b696dc 100644 --- a/src/ui/settings/gamepad-binding-ui-handler.ts +++ b/src/ui/settings/gamepad-binding-ui-handler.ts @@ -31,7 +31,7 @@ export default class GamepadBindingUiHandler extends AbstractBindingUiHandler { swapAction() { const activeConfig = this.scene.inputController.getActiveConfig(); this.scene.inputController.swapBinding(activeConfig, this.target, this.buttonPressed) - this.scene.gameData.saveCustomMapping(this.scene.inputController?.chosenGamepad, activeConfig.currentKeys); + this.scene.gameData.saveCustomMapping(this.scene.inputController?.chosenGamepad, activeConfig.currentKeys, activeConfig.icons); regenerateCustom(activeConfig); return true; } diff --git a/src/ui/settings/keyboard-binding-ui-handler.ts b/src/ui/settings/keyboard-binding-ui-handler.ts index e2d6599b0..d2e0c792c 100644 --- a/src/ui/settings/keyboard-binding-ui-handler.ts +++ b/src/ui/settings/keyboard-binding-ui-handler.ts @@ -30,7 +30,7 @@ export default class KeyboardBindingUiHandler extends AbstractBindingUiHandler { swapAction() { const activeConfig = this.scene.inputController.getActiveKeyboardConfig(); this.scene.inputController.swapBinding(activeConfig, this.target, this.buttonPressed) - this.scene.gameData.saveCustomKeyboardMapping(this.scene.inputController?.chosenKeyboard, activeConfig.currentKeys); + this.scene.gameData.saveCustomKeyboardMapping(this.scene.inputController?.chosenKeyboard, activeConfig.currentKeys, activeConfig.icons); regenerateCustom(activeConfig); return true; } diff --git a/src/ui/settings/settings-keyboard-ui-handler.ts b/src/ui/settings/settings-keyboard-ui-handler.ts index 4c3962f21..ad2b04cf4 100644 --- a/src/ui/settings/settings-keyboard-ui-handler.ts +++ b/src/ui/settings/settings-keyboard-ui-handler.ts @@ -77,7 +77,7 @@ export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandl } saveCustomKeyboardMappingToLocalStorage(config): void { - this.scene.gameData.saveCustomKeyboardMapping(this.scene.inputController?.chosenKeyboard, config.currentKeys); + this.scene.gameData.saveCustomKeyboardMapping(this.scene.inputController?.chosenKeyboard, config.currentKeys, config.icons); } saveSettingToLocalStorage(settingName, cursor): void { From 9c4f04fe46362fc2bb03441b6999639511aca34c Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Tue, 14 May 2024 18:23:35 +0200 Subject: [PATCH 72/81] easy fix, all tests passed --- src/configs/gamepad-utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/configs/gamepad-utils.ts b/src/configs/gamepad-utils.ts index 7835e8d88..514e906c9 100644 --- a/src/configs/gamepad-utils.ts +++ b/src/configs/gamepad-utils.ts @@ -100,7 +100,7 @@ export function swapCurrentKeys(config: InterfaceConfig, settingName, pressedBut const nextKey = deepCopy(newBind); if (prevKey.key === nextKey.key) { // special case when back to back and not enough info to get back to previous button - const toRestore = getKeyAndSettingNameFromCurrentKeysWithAction(config, prevKey.from.action); + const toRestore = getKeyAndSettingNameFromCurrentKeysWithAction(config, prevKey.from.action, settingName.includes("ALT_")); config.custom[prevKey.key] = prevKey.from.action; config.icons[prevKey.key] = prevKey.from.icon; From c222f0346d1ac2fe2ba457b713c763c0013276c9 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Tue, 14 May 2024 18:54:48 +0200 Subject: [PATCH 73/81] fix bind after multiple delete --- src/configs/gamepad-utils.ts | 8 +++++-- src/test/keyboard_remaping.test.ts | 38 ++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/configs/gamepad-utils.ts b/src/configs/gamepad-utils.ts index 514e906c9..70e0c0d28 100644 --- a/src/configs/gamepad-utils.ts +++ b/src/configs/gamepad-utils.ts @@ -84,15 +84,17 @@ export function swapCurrentKeys(config: InterfaceConfig, settingName, pressedBut if (newBind && previousBind.action === -1) { //special case when rebinding deleted key with already assigned key const toRestore = deepCopy(newBind); + const iconFromThePressedButton = config.ogIcons[prevKey.key]; config.custom[newBind.key] = prevKey.from.action; - config.icons[prevKey.key] = newBind.icon; + config.icons[prevKey.key] = newBind.icon || iconFromThePressedButton; config.icons[newBind.key] = prevKey.from.icon; delete prevKey.from; const nextSettingName = getKeyAndSettingNameFromCurrentKeysWithAction(config, newBind.action, newBind.isAlt).settingName; - config.currentKeys[nextSettingName].from = toRestore; + config.currentKeys[nextSettingName].from = toRestore.action === -1 ? config.currentKeys[nextSettingName].from : toRestore; config.currentKeys[nextSettingName].isDeleted = true; + config.currentKeys[settingName].isDeleted = false; config.currentKeys[settingName].replacedBy = toRestore.key; } else if (!newBind) { assignNewKey(config, settingName, pressedButton, previousBind); @@ -160,6 +162,8 @@ export function regenerateCustom(config): void { custom[latestReplacedBy] = action; } else if (!latestIsDeleted) { custom[key] = action; + } else if (latestIsDeleted) { + custom[key] = -1; } } config.custom = deepCopy(custom); diff --git a/src/test/keyboard_remaping.test.ts b/src/test/keyboard_remaping.test.ts index ccdbd6a33..284adb2ca 100644 --- a/src/test/keyboard_remaping.test.ts +++ b/src/test/keyboard_remaping.test.ts @@ -590,4 +590,42 @@ describe('Test Keyboard', () => { expect(config.custom["KEY_D"]).toEqual(Button.RIGHT); }) + it('by method: Delete 2 binds, than rebind one of them', () => { + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); + expect(config.custom["KEY_Z"]).toEqual(Button.UP); + deleteBind(config, SettingInterfaceKeyboard.Alt_Button_Up); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(-1); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual(undefined); + expect(config.custom["KEY_Z"]).toEqual(-1); + + + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].key).toEqual("KEY_S"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].action).toEqual(Button.DOWN); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].icon).toEqual("T_S_Key_Dark.png"); + expect(config.custom["KEY_S"]).toEqual(Button.DOWN); + deleteBind(config, SettingInterfaceKeyboard.Alt_Button_Down); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].key).toEqual("KEY_S"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].action).toEqual(-1); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].icon).toEqual(undefined); + expect(config.custom["KEY_S"]).toEqual(-1); + + + swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Up, Phaser.Input.Keyboard.KeyCodes.Z); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); + expect(config.custom["KEY_Z"]).toEqual(Button.UP); + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].key).toEqual("KEY_S"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].action).toEqual(-1); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].icon).toEqual(undefined); + expect(config.custom["KEY_S"]).toEqual(-1); + + }); + }); \ No newline at end of file From c7d26ac9bc60055f704c77c427f8e37a88270f91 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Tue, 14 May 2024 19:39:01 +0200 Subject: [PATCH 74/81] fix an assign bug with tests --- src/configs/gamepad-utils.ts | 9 +- src/test/keyboard_remaping.test.ts | 179 +++++++++++++++++++++++++---- 2 files changed, 165 insertions(+), 23 deletions(-) diff --git a/src/configs/gamepad-utils.ts b/src/configs/gamepad-utils.ts index 70e0c0d28..9d383ea22 100644 --- a/src/configs/gamepad-utils.ts +++ b/src/configs/gamepad-utils.ts @@ -70,9 +70,10 @@ export function assignNewKey(config: InterfaceConfig, settingName, pressedButton config.icons[previousBind.key] = icon; config.currentKeys[settingName].icon = icon; - config.custom[key] = previousBind.action !== -1 ? previousBind.action : previousBind.from.action; config.custom[previousBind.key] = -1; + config.custom[key] = previousBind.action !== -1 ? previousBind.action : previousBind.from.action; config.currentKeys[settingName].replacedBy = key; + config.currentKeys[settingName].latestIsDeleted = false; delete config.currentKeys[settingName].from } @@ -81,7 +82,9 @@ export function swapCurrentKeys(config: InterfaceConfig, settingName, pressedBut const previousBind = getKeyAndActionFromCurrentKeysWithSettingName(config, settingName); const prevKey = deepCopy(previousBind); const newBind = getKeyAndActionFromCurrentKeysWithPressedButton(config, pressedButton); - if (newBind && previousBind.action === -1) { + if (newBind?.action === -1 && previousBind.action === -1) { + assignNewKey(config, settingName, pressedButton, previousBind); + } else if (newBind && previousBind.action === -1) { //special case when rebinding deleted key with already assigned key const toRestore = deepCopy(newBind); const iconFromThePressedButton = config.ogIcons[prevKey.key]; @@ -100,7 +103,7 @@ export function swapCurrentKeys(config: InterfaceConfig, settingName, pressedBut assignNewKey(config, settingName, pressedButton, previousBind); } else { const nextKey = deepCopy(newBind); - if (prevKey.key === nextKey.key) { + if (prevKey.key === nextKey.key && prevKey.from) { // special case when back to back and not enough info to get back to previous button const toRestore = getKeyAndSettingNameFromCurrentKeysWithAction(config, prevKey.from.action, settingName.includes("ALT_")); config.custom[prevKey.key] = prevKey.from.action; diff --git a/src/test/keyboard_remaping.test.ts b/src/test/keyboard_remaping.test.ts index 284adb2ca..a48afb932 100644 --- a/src/test/keyboard_remaping.test.ts +++ b/src/test/keyboard_remaping.test.ts @@ -50,12 +50,12 @@ describe('Test Keyboard', () => { }); it('Check key for currenly Assigned to setting name', () => { const settingName = SettingInterfaceKeyboard.Button_Left; - const { key } = getKeyAndActionFromCurrentKeysWithSettingName(config, settingName); + const {key} = getKeyAndActionFromCurrentKeysWithSettingName(config, settingName); expect(key).toEqual('KEY_ARROW_LEFT'); }); it('Check key for currenly Assigned to setting name alt', () => { const settingName = SettingInterfaceKeyboard.Alt_Button_Left; - const { key } = getKeyAndActionFromCurrentKeysWithSettingName(config, settingName); + const {key} = getKeyAndActionFromCurrentKeysWithSettingName(config, settingName); expect(key).toEqual('KEY_Q'); }); it('Check icon for currenly Assigned to key code', () => { @@ -446,28 +446,28 @@ describe('Test Keyboard', () => { it('reload scenario with 1 bind already reassigned', () => { config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up] = { - "key": "KEY_Z", - "isAlt": true, - "action": 3, - "icon": "T_D_Key_Dark.png", - "from": { "key": "KEY_Z", "isAlt": true, - "action": 0, - "icon": "T_Z_Key_Dark.png" - } + "action": 3, + "icon": "T_D_Key_Dark.png", + "from": { + "key": "KEY_Z", + "isAlt": true, + "action": 0, + "icon": "T_Z_Key_Dark.png" + } }; config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right] = { - "key": "KEY_D", - "isAlt": true, - "action": 0, - "icon": "T_Z_Key_Dark.png", - "from": { "key": "KEY_D", "isAlt": true, - "action": 3, - "icon": "T_D_Key_Dark.png" - } + "action": 0, + "icon": "T_Z_Key_Dark.png", + "from": { + "key": "KEY_D", + "isAlt": true, + "action": 3, + "icon": "T_D_Key_Dark.png" + } } config.icons["KEY_D"] = "T_Z_Key_Dark.png"; config.icons["KEY_Z"] = "T_D_Key_Dark.png"; @@ -603,7 +603,6 @@ describe('Test Keyboard', () => { expect(config.custom["KEY_Z"]).toEqual(-1); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].key).toEqual("KEY_S"); expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].action).toEqual(Button.DOWN); expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].icon).toEqual("T_S_Key_Dark.png"); @@ -626,6 +625,146 @@ describe('Test Keyboard', () => { expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].icon).toEqual(undefined); expect(config.custom["KEY_S"]).toEqual(-1); + + swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Down, Phaser.Input.Keyboard.KeyCodes.S); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); + expect(config.custom["KEY_Z"]).toEqual(Button.UP); + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].key).toEqual("KEY_S"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].action).toEqual(Button.DOWN); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].icon).toEqual("T_S_Key_Dark.png"); + expect(config.custom["KEY_S"]).toEqual(Button.DOWN); + + let keyDown = Phaser.Input.Keyboard.KeyCodes.S; + let key = getKeyFromMapping(config, keyDown); + let buttonDown = config.custom[key]; + expect(buttonDown).toEqual(Button.DOWN); + + keyDown = Phaser.Input.Keyboard.KeyCodes.Z; + key = getKeyFromMapping(config, keyDown); + buttonDown = config.custom[key]; + expect(buttonDown).toEqual(Button.UP); + }); -}); \ No newline at end of file + it("test keyboard listener", () => { + const keyDown = Phaser.Input.Keyboard.KeyCodes.S; + const key = getKeyFromMapping(config, keyDown); + const buttonDown = config.custom[key]; + expect(buttonDown).toEqual(Button.DOWN); + }); + + it("another test with 2 delete", () => { + config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up] = { + "key": "KEY_Z", + "isAlt": true, + "action": -1, + "from": { + "key": "KEY_Z", + "isAlt": true, + "action": 0, + "icon": "T_Z_Key_Dark.png" + }, + "latestIsDeleted": true + }; + regenerateCustom(config); + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(-1); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual(undefined); + expect(config.custom["KEY_Z"]).toEqual(-1); + + swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Down, Phaser.Input.Keyboard.KeyCodes.S); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].key).toEqual("KEY_S"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].action).toEqual(Button.DOWN); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].icon).toEqual("T_S_Key_Dark.png"); + expect(config.custom["KEY_S"]).toEqual(Button.DOWN); + }); + + it("another test with 2 delete", () => { + config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up] = { + "key": "KEY_Z", + "isAlt": true, + "action": -1, + "from": { + "key": "KEY_Z", + "isAlt": true, + "action": 0, + "icon": "T_Z_Key_Dark.png" + }, + "latestIsDeleted": true + }; + regenerateCustom(config); + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(-1); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual(undefined); + expect(config.custom["KEY_Z"]).toEqual(-1); + + swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Up, Phaser.Input.Keyboard.KeyCodes.Z); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); + expect(config.custom["KEY_Z"]).toEqual(Button.UP); + + let keyDown = Phaser.Input.Keyboard.KeyCodes.S; + let key = getKeyFromMapping(config, keyDown); + let buttonDown = config.custom[key]; + expect(buttonDown).toEqual(Button.DOWN); + + keyDown = Phaser.Input.Keyboard.KeyCodes.Z; + key = getKeyFromMapping(config, keyDown); + buttonDown = config.custom[key]; + expect(buttonDown).toEqual(Button.UP); + }); + + it("another test with 2 delete part 2", () => { + config.currentKeys = { + "ALT_BUTTON_UP": { + "key": "KEY_Z", + "isAlt": true, + "action": -1, + "from": { + "key": "KEY_Z", + "isAlt": true, + "action": 0, + "icon": "T_Z_Key_Dark.png" + }, + "latestIsDeleted": true + }, + "ALT_BUTTON_DOWN": { + "key": "KEY_S", + "isAlt": true, + "action": -1, + "from": { + "key": "KEY_S", + "isAlt": true, + "action": 1, + "icon": "T_S_Key_Dark.png" + }, + "latestIsDeleted": true + }, + } + regenerateCustom(config); + + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(-1); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual(undefined); + expect(config.custom["KEY_Z"]).toEqual(-1); + + swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Up, Phaser.Input.Keyboard.KeyCodes.Z); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); + expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); + expect(config.custom["KEY_Z"]).toEqual(Button.UP); + + const keyDown = Phaser.Input.Keyboard.KeyCodes.Z; + const key = getKeyFromMapping(config, keyDown); + const buttonDown = config.custom[key]; + expect(buttonDown).toEqual(Button.UP); +}); + +}) +; \ No newline at end of file From fcdbf2b5c0867312a365194d0c03dded13b00ebd Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Tue, 14 May 2024 19:41:11 +0200 Subject: [PATCH 75/81] fix stuck screen on unknwo button --- src/ui/settings/gamepad-binding-ui-handler.ts | 4 ++-- src/ui/settings/keyboard-binding-ui-handler.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/settings/gamepad-binding-ui-handler.ts b/src/ui/settings/gamepad-binding-ui-handler.ts index ca0b696dc..1ecd28fcd 100644 --- a/src/ui/settings/gamepad-binding-ui-handler.ts +++ b/src/ui/settings/gamepad-binding-ui-handler.ts @@ -18,12 +18,12 @@ export default class GamepadBindingUiHandler extends AbstractBindingUiHandler { const blacklist = [12, 13, 14, 15]; // d-pad buttons are blacklisted. // Check conditions before processing the button press. if (!this.listening || pad.id !== this.scene.inputController?.chosenGamepad || blacklist.includes(button.index) || this.buttonPressed !== null) return; - this.buttonPressed = button.index; const activeConfig = this.scene.inputController.getActiveConfig(); const type = activeConfig.padType - const key = getKeyFromMapping(activeConfig, this.buttonPressed); + const key = getKeyFromMapping(activeConfig, button.index); const buttonIcon = activeConfig.ogIcons[key]; if (!buttonIcon) return; + this.buttonPressed = button.index; const assignedButtonIcon = getKeyAndActionFromCurrentKeysWithSettingName(activeConfig, this.target)?.icon; this.onInputDown(buttonIcon, assignedButtonIcon, type); } diff --git a/src/ui/settings/keyboard-binding-ui-handler.ts b/src/ui/settings/keyboard-binding-ui-handler.ts index d2e0c792c..fd23fc5fe 100644 --- a/src/ui/settings/keyboard-binding-ui-handler.ts +++ b/src/ui/settings/keyboard-binding-ui-handler.ts @@ -18,11 +18,11 @@ export default class KeyboardBindingUiHandler extends AbstractBindingUiHandler { const key = event.keyCode; // // Check conditions before processing the button press. if (!this.listening || this.buttonPressed !== null) return; - this.buttonPressed = key; const activeConfig = this.scene.inputController.getActiveKeyboardConfig(); const _key = getKeyFromMapping(activeConfig, key); const buttonIcon = activeConfig.ogIcons[_key]; if (!buttonIcon) return; + this.buttonPressed = key; const assignedButtonIcon = getKeyAndActionFromCurrentKeysWithSettingName(activeConfig, this.target)?.icon; this.onInputDown(buttonIcon, assignedButtonIcon, 'keyboard'); } From db7a972ff1dd1c06f95661b74a9da6f739f157f1 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Tue, 14 May 2024 23:43:35 +0200 Subject: [PATCH 76/81] added back touch support --- src/inputs-controller.ts | 1 + src/touch-controls.js | 67 ++++++++++++---------------------------- 2 files changed, 20 insertions(+), 48 deletions(-) diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 40b51addb..ab85ad12b 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -164,6 +164,7 @@ export class InputsController { this.scene.input.keyboard.on('keydown', this.keyboardKeyDown, this); this.scene.input.keyboard.on('keyup', this.keyboardKeyUp, this); } + initTouchControls(this.events); // Keyboard // this.setupKeyboardControls(); } diff --git a/src/touch-controls.js b/src/touch-controls.js index d3e8e37ab..836c91eaa 100644 --- a/src/touch-controls.js +++ b/src/touch-controls.js @@ -1,11 +1,13 @@ +import {Button} from "./enums/buttons"; + export const keys = new Map(); export const keysDown = new Map(); let lastTouchedId; -export function initTouchControls(buttonMap) { +export function initTouchControls(events) { for (const button of document.querySelectorAll('[data-key]')) { // @ts-ignore - bindKey(button, button.dataset.key, buttonMap); + bindKey(button, button.dataset.key, events); } } @@ -27,34 +29,27 @@ export function isMobile() { * * @param {string} eventType Type of the keyboard event * @param {string} button Button to simulate - * @param {object} buttonMap Map of buttons to key objects */ -function simulateKeyboardEvent(eventType, button, buttonMap) { - const key = buttonMap[button]; +function simulateKeyboardEvent(eventType, key, events) { + if (!Button.hasOwnProperty(key)) return; + const button = Button[key]; switch (eventType) { case 'keydown': - key.onDown({}); + events.emit('input_down', { + controller_type: 'touch', + button: button, + }); break; case 'keyup': - key.onUp({}); + events.emit('input_up', { + controller_type: 'touch', + button: button, + }); break; } } -/** - * Simulate a keyboard input from 'keydown' to 'keyup' - * - * @param {string} key Key to simulate - * @param {object} buttonMap Map of buttons to key objects - */ -function simulateKeyboardInput(key, buttonMap) { - simulateKeyboardEvent('keydown', key, buttonMap); - window.setTimeout(() => { - simulateKeyboardEvent('keyup', key, buttonMap); - }, 100); -} - /** * Bind a node by a specific key to simulate on touch * @@ -62,12 +57,12 @@ function simulateKeyboardInput(key, buttonMap) { * @param {string} key Key to simulate * @param {object} buttonMap Map of buttons to key objects */ -function bindKey(node, key, buttonMap) { +function bindKey(node, key, events) { keys.set(node.id, key); node.addEventListener('touchstart', event => { event.preventDefault(); - simulateKeyboardEvent('keydown', key, buttonMap); + simulateKeyboardEvent('keydown', key, events); keysDown.set(event.target.id, node.id); node.classList.add('active'); }); @@ -78,7 +73,7 @@ function bindKey(node, key, buttonMap) { const pressedKey = keysDown.get(event.target.id); if (pressedKey && keys.has(pressedKey)) { const key = keys.get(pressedKey); - simulateKeyboardEvent('keyup', key, buttonMap); + simulateKeyboardEvent('keyup', key, events); } keysDown.delete(event.target.id); @@ -88,28 +83,4 @@ function bindKey(node, key, buttonMap) { document.getElementById(lastTouchedId).classList.remove('active'); } }); - - // Inspired by https://github.com/pulsejet/mkxp-web/blob/262a2254b684567311c9f0e135ee29f6e8c3613e/extra/js/dpad.js - node.addEventListener('touchmove', event => { - const { target, clientX, clientY } = event.changedTouches[0]; - const origTargetId = keysDown.get(target.id); - const nextTargetId = document.elementFromPoint(clientX, clientY).id; - if (origTargetId === nextTargetId) - return; - - if (origTargetId) { - const key = keys.get(origTargetId); - simulateKeyboardEvent('keyup', key, buttonMap); - keysDown.delete(target.id); - document.getElementById(origTargetId).classList.remove('active'); - } - - if (keys.has(nextTargetId)) { - const key = keys.get(nextTargetId); - simulateKeyboardEvent('keydown', key, buttonMap); - keysDown.set(target.id, nextTargetId); - lastTouchedId = nextTargetId; - document.getElementById(nextTargetId).classList.add('active'); - } - }); -} \ No newline at end of file +} From b75bea7402e57871e9f0008dcc4c3ba209212591 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Wed, 15 May 2024 00:26:04 +0200 Subject: [PATCH 77/81] added more touch control on setting --- index.css | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/index.css b/index.css index dd47387ad..31d552a0e 100644 --- a/index.css +++ b/index.css @@ -146,7 +146,9 @@ body { margin-left: 10%; } -#touchControls:not([data-ui-mode='STARTER_SELECT']) #apad .apadRectBtnContainer > .apadSqBtn, #touchControls:not([data-ui-mode='STARTER_SELECT']) #apad .apadSqBtnContainer { +#touchControls:not([data-ui-mode='STARTER_SELECT']):not([data-ui-mode='SETTINGS']):not([data-ui-mode='SETTINGS_GAMEPAD']):not([data-ui-mode='SETTINGS_KEYBOARD']) #apad .apadRectBtnContainer > .apadSqBtn, +#touchControls:not([data-ui-mode='STARTER_SELECT']):not([data-ui-mode='SETTINGS']):not([data-ui-mode='SETTINGS_GAMEPAD']):not([data-ui-mode='SETTINGS_KEYBOARD']) #apad .apadSqBtnContainer +{ display: none; } From ac9a011da44c3381c5ef209d432845edbeebf3de Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Wed, 15 May 2024 00:32:38 +0200 Subject: [PATCH 78/81] added a message instead of nothing in the keyboard menu if playing on mobile --- src/inputs-controller.ts | 11 ++++--- .../settings/settings-keyboard-ui-handler.ts | 31 +++++++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index ab85ad12b..4c6dcd928 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -389,10 +389,14 @@ export class InputsController { } } - keyboardKeyDown(event): void { - const keyDown = event.keyCode; + checkIfKeyboardIsInit(): void { if (!this.keyboardConfigs[this.chosenKeyboard]?.padID) this.setupKeyboard(); + } + + keyboardKeyDown(event): void { + const keyDown = event.keyCode; + this.checkIfKeyboardIsInit(); if (this.keys.includes(keyDown)) return; this.keys.push(keyDown); const key = getKeyFromMapping(this.keyboardConfigs[this.chosenKeyboard], keyDown); @@ -410,8 +414,7 @@ export class InputsController { keyboardKeyUp(event): void { const keyDown = event.keyCode; this.keys = this.keys.filter(k => k !== keyDown); - if (!this.keyboardConfigs[this.chosenKeyboard]?.padID) - this.setupKeyboard(); + this.checkIfKeyboardIsInit() const key = getKeyFromMapping(this.keyboardConfigs[this.chosenKeyboard], keyDown); const buttonUp = this.keyboardConfigs[this.chosenKeyboard].custom[key]; if (buttonUp !== undefined) { diff --git a/src/ui/settings/settings-keyboard-ui-handler.ts b/src/ui/settings/settings-keyboard-ui-handler.ts index ad2b04cf4..fd5a86876 100644 --- a/src/ui/settings/settings-keyboard-ui-handler.ts +++ b/src/ui/settings/settings-keyboard-ui-handler.ts @@ -6,6 +6,7 @@ import {reverseValueToKeySetting, truncateString} from "#app/utils"; import AbstractSettingsUiUiHandler from "#app/ui/settings/abstract-settings-ui-handler"; import {InterfaceConfig} from "#app/inputs-controller"; import {deleteBind} from "#app/configs/gamepad-utils"; +import {addTextObject, TextStyle} from "#app/ui/text"; export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandler { @@ -24,6 +25,22 @@ export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandl deleteEvent.on('up', this.onDeleteDown, this); } + setup() { + super.setup(); + // If no gamepads are detected, set up a default UI prompt in the settings container. + this.layout['noKeyboard'] = new Map(); + const optionsContainer = this.scene.add.container(0, 0); + optionsContainer.setVisible(false); // Initially hide the container as no gamepads are connected. + const label = addTextObject(this.scene, 8, 28, 'Please press a key on your keyboard', TextStyle.SETTINGS_LABEL); + label.setOrigin(0, 0); + optionsContainer.add(label); + this.settingsContainer.add(optionsContainer); + + // Map the 'noKeyboard' layout options for easy access. + this.layout['noKeyboard'].optionsContainer = optionsContainer; + this.layout['noKeyboard'].label = label; + } + onDeleteDown(): void { const cursor = this.cursor + this.scrollCursor; // Calculate the absolute cursor position. const selection = this.settingLabels[cursor].text; @@ -45,6 +62,20 @@ export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandl return settings; } + 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['noKeyboard']; + // 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; + } + + return super.setLayout(activeConfig); + } + navigateMenuLeft(): boolean { this.scene.ui.setMode(Mode.SETTINGS_GAMEPAD) return true; From cba904961350a195e6f23efd87968d4c89102f4b Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Wed, 15 May 2024 02:44:00 +0200 Subject: [PATCH 79/81] removed the option to change layout in the keyboard menu since there is only one for now --- src/system/settings-keyboard.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/system/settings-keyboard.ts b/src/system/settings-keyboard.ts index b308153fb..5de921420 100644 --- a/src/system/settings-keyboard.ts +++ b/src/system/settings-keyboard.ts @@ -43,7 +43,7 @@ export enum SettingKeyboard { } export const settingKeyboardOptions: SettingOptions = { - [SettingKeyboard.Default_Layout]: ['Default', 'Change'], + [SettingKeyboard.Default_Layout]: ['Default'], [SettingKeyboard.Button_Up]: [`KEY ${Button.UP.toString()}`, 'Change'], [SettingKeyboard.Alt_Button_Up]: [`KEY ${Button.UP.toString()}`, 'Change'], [SettingKeyboard.Button_Down]: [`KEY ${Button.DOWN.toString()}`, 'Change'], From 85a0debb6c3af8842f1d142ff4c217fac1794dc9 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Wed, 15 May 2024 03:04:43 +0200 Subject: [PATCH 80/81] hide deleted keys --- src/ui/settings/abstract-settings-ui-handler.ts | 8 ++++++-- src/ui/settings/settings-keyboard-ui-handler.ts | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/ui/settings/abstract-settings-ui-handler.ts b/src/ui/settings/abstract-settings-ui-handler.ts index 81ddacb18..4f8187b24 100644 --- a/src/ui/settings/abstract-settings-ui-handler.ts +++ b/src/ui/settings/abstract-settings-ui-handler.ts @@ -242,9 +242,13 @@ export default abstract class AbstractSettingsUiUiHandler extends UiHandler { // 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 {key, icon} = getKeyAndActionFromCurrentKeysWithSettingName(activeConfig, elm); - this.inputsIcons[key].setFrame(icon); // Set the icon frame to the inputs icon object. + if (icon) { + this.inputsIcons[key].setFrame(icon); + this.inputsIcons[key].alpha = 1; + } else { + this.inputsIcons[key].alpha = 0; + } } // Set the cursor and scroll cursor to their initial positions. diff --git a/src/ui/settings/settings-keyboard-ui-handler.ts b/src/ui/settings/settings-keyboard-ui-handler.ts index fd5a86876..91995aeb3 100644 --- a/src/ui/settings/settings-keyboard-ui-handler.ts +++ b/src/ui/settings/settings-keyboard-ui-handler.ts @@ -98,6 +98,7 @@ export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandl if (setting === this.settingDevice.Default_Layout) { // Iterate over all layouts excluding the 'noGamepads' special case. for (const _key of Object.keys(this.layout)) { + if (_key === 'noKeyboard') continue; // Skip updating the no gamepad 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)); From 7ca1a276a315f83437b271b7f282ff23642541c1 Mon Sep 17 00:00:00 2001 From: Greenlamp2 <44787002+Greenlamp2@users.noreply.github.com> Date: Thu, 16 May 2024 01:15:11 +0200 Subject: [PATCH 81/81] refactor to simplify the overcomplicated config file (#1) * refactor done * starting gamepad tests * renamed tests --- src/configs/cfg_keyboard_azerty.ts | 113 +-- src/configs/configHandler.ts | 136 ++++ src/configs/gamepad-utils.ts | 184 ----- src/configs/pad_dualshock.ts | 46 +- src/configs/pad_generic.ts | 46 +- src/configs/pad_unlicensedSNES.ts | 44 +- src/configs/pad_xbox360.ts | 46 +- src/enums/devices.ts | 4 + src/inputs-controller.ts | 244 ++---- src/system/game-data.ts | 51 +- src/system/settings-gamepad.ts | 12 + src/test/cfg_gamepad_example.ts | 97 --- ...ard_example.ts => cfg_keyboard.example.ts} | 145 ++-- src/test/gamepad_remaping.test.ts | 257 ------ src/test/helpers/inGameManip.ts | 44 + src/test/helpers/menuManip.ts | 96 +++ src/test/keyboard_remaping.test.ts | 770 ------------------ src/test/rebinding_setting.test.ts | 311 +++++++ .../settings/abstract-settings-ui-handler.ts | 24 +- src/ui/settings/gamepad-binding-ui-handler.ts | 27 +- .../settings/keyboard-binding-ui-handler.ts | 25 +- .../settings/settings-gamepad-ui-handler.ts | 6 +- .../settings/settings-keyboard-ui-handler.ts | 9 +- 23 files changed, 952 insertions(+), 1785 deletions(-) create mode 100644 src/configs/configHandler.ts delete mode 100644 src/configs/gamepad-utils.ts create mode 100644 src/enums/devices.ts delete mode 100644 src/test/cfg_gamepad_example.ts rename src/test/{cfg_keyboard_example.ts => cfg_keyboard.example.ts} (70%) delete mode 100644 src/test/gamepad_remaping.test.ts create mode 100644 src/test/helpers/inGameManip.ts create mode 100644 src/test/helpers/menuManip.ts delete mode 100644 src/test/keyboard_remaping.test.ts create mode 100644 src/test/rebinding_setting.test.ts diff --git a/src/configs/cfg_keyboard_azerty.ts b/src/configs/cfg_keyboard_azerty.ts index e0a1fac29..3dc71103c 100644 --- a/src/configs/cfg_keyboard_azerty.ts +++ b/src/configs/cfg_keyboard_azerty.ts @@ -165,7 +165,43 @@ const cfg_keyboard_azerty = { KEY_BACKSPACE: "T_Backspace_Alt_Key_Dark.png", KEY_ALT: "T_Alt_Key_Dark.png" }, - setting: { + settings: { + [SettingKeyboard.Button_Up]: Button.UP, + [SettingKeyboard.Button_Down]: Button.DOWN, + [SettingKeyboard.Button_Left]: Button.LEFT, + [SettingKeyboard.Button_Right]: Button.RIGHT, + [SettingKeyboard.Button_Submit]: Button.SUBMIT, + [SettingKeyboard.Button_Action]: Button.ACTION, + [SettingKeyboard.Button_Cancel]: Button.CANCEL, + [SettingKeyboard.Button_Menu]: Button.MENU, + [SettingKeyboard.Button_Stats]: Button.STATS, + [SettingKeyboard.Button_Cycle_Shiny]: Button.CYCLE_SHINY, + [SettingKeyboard.Button_Cycle_Form]: Button.CYCLE_FORM, + [SettingKeyboard.Button_Cycle_Gender]: Button.CYCLE_GENDER, + [SettingKeyboard.Button_Cycle_Ability]: Button.CYCLE_ABILITY, + [SettingKeyboard.Button_Cycle_Nature]: Button.CYCLE_NATURE, + [SettingKeyboard.Button_Cycle_Variant]: Button.CYCLE_VARIANT, + [SettingKeyboard.Button_Speed_Up]: Button.SPEED_UP, + [SettingKeyboard.Button_Slow_Down]: Button.SLOW_DOWN, + [SettingKeyboard.Alt_Button_Up]: Button.UP, + [SettingKeyboard.Alt_Button_Down]: Button.DOWN, + [SettingKeyboard.Alt_Button_Left]: Button.LEFT, + [SettingKeyboard.Alt_Button_Right]: Button.RIGHT, + [SettingKeyboard.Alt_Button_Submit]: Button.SUBMIT, + [SettingKeyboard.Alt_Button_Action]: Button.ACTION, + [SettingKeyboard.Alt_Button_Cancel]: Button.CANCEL, + [SettingKeyboard.Alt_Button_Menu]: Button.MENU, + [SettingKeyboard.Alt_Button_Stats]: Button.STATS, + [SettingKeyboard.Alt_Button_Cycle_Shiny]: Button.CYCLE_SHINY, + [SettingKeyboard.Alt_Button_Cycle_Form]: Button.CYCLE_FORM, + [SettingKeyboard.Alt_Button_Cycle_Gender]: Button.CYCLE_GENDER, + [SettingKeyboard.Alt_Button_Cycle_Ability]: Button.CYCLE_ABILITY, + [SettingKeyboard.Alt_Button_Cycle_Nature]: Button.CYCLE_NATURE, + [SettingKeyboard.Alt_Button_Cycle_Variant]: Button.CYCLE_VARIANT, + [SettingKeyboard.Alt_Button_Speed_Up]: Button.SPEED_UP, + [SettingKeyboard.Alt_Button_Slow_Down]: Button.SLOW_DOWN, + }, + default: { KEY_ARROW_UP: SettingKeyboard.Button_Up, KEY_ARROW_DOWN: SettingKeyboard.Button_Down, KEY_ARROW_LEFT: SettingKeyboard.Button_Left, @@ -183,62 +219,25 @@ const cfg_keyboard_azerty = { KEY_V: SettingKeyboard.Button_Cycle_Variant, KEY_PLUS: SettingKeyboard.Button_Speed_Up, KEY_MINUS: SettingKeyboard.Button_Slow_Down, - - KEY_Z: SettingKeyboard.Alt_Button_Up, - KEY_S: SettingKeyboard.Alt_Button_Down, - KEY_Q: SettingKeyboard.Alt_Button_Left, - KEY_D: SettingKeyboard.Alt_Button_Right, - KEY_CTRL: SettingKeyboard.Alt_Button_Submit, - KEY_W: SettingKeyboard.Alt_Button_Action, - KEY_X: SettingKeyboard.Alt_Button_Cancel, - KEY_TAB: SettingKeyboard.Alt_Button_Menu, - KEY_SHIFT: SettingKeyboard.Alt_Button_Stats, - KEY_P: SettingKeyboard.Alt_Button_Cycle_Shiny, - KEY_M: SettingKeyboard.Alt_Button_Cycle_Form, - KEY_O: SettingKeyboard.Alt_Button_Cycle_Gender, - KEY_L: SettingKeyboard.Alt_Button_Cycle_Ability, - KEY_I: SettingKeyboard.Alt_Button_Cycle_Nature, - KEY_K: SettingKeyboard.Alt_Button_Cycle_Variant, - KEY_PAGE_UP: SettingKeyboard.Alt_Button_Speed_Up, - KEY_PAGE_DOWN: SettingKeyboard.Alt_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, KEY_A: -1, KEY_B: -1, - KEY_D: Button.RIGHT, + KEY_D: SettingKeyboard.Alt_Button_Right, KEY_H: -1, - KEY_I: Button.CYCLE_NATURE, + KEY_I: SettingKeyboard.Alt_Button_Cycle_Nature, KEY_J: -1, - KEY_K: Button.CYCLE_VARIANT, - KEY_L: Button.CYCLE_ABILITY, - KEY_M: Button.CYCLE_FORM, - KEY_O: Button.CYCLE_GENDER, - KEY_P: Button.CYCLE_SHINY, - KEY_Q: Button.LEFT, - KEY_S: Button.DOWN, + KEY_K: SettingKeyboard.Alt_Button_Cycle_Variant, + KEY_L: SettingKeyboard.Alt_Button_Cycle_Ability, + KEY_M: SettingKeyboard.Alt_Button_Cycle_Form, + KEY_O: SettingKeyboard.Alt_Button_Cycle_Gender, + KEY_P: SettingKeyboard.Alt_Button_Cycle_Shiny, + KEY_Q: SettingKeyboard.Alt_Button_Left, + KEY_S: SettingKeyboard.Alt_Button_Down, KEY_T: -1, KEY_U: -1, - KEY_W: Button.ACTION, - KEY_X: Button.CANCEL, + KEY_W: SettingKeyboard.Alt_Button_Action, + KEY_X: SettingKeyboard.Alt_Button_Cancel, KEY_Y: -1, - KEY_Z: Button.UP, + KEY_Z: SettingKeyboard.Alt_Button_Up, KEY_0: -1, KEY_1: -1, KEY_2: -1, @@ -249,7 +248,7 @@ const cfg_keyboard_azerty = { KEY_7: -1, KEY_8: -1, KEY_9: -1, - KEY_CTRL: Button.SUBMIT, + KEY_CTRL: SettingKeyboard.Alt_Button_Submit, KEY_DEL: -1, KEY_END: -1, KEY_F1: -1, @@ -266,17 +265,19 @@ const cfg_keyboard_azerty = { KEY_F12: -1, KEY_HOME: -1, KEY_INSERT: -1, - KEY_PAGE_DOWN: Button.SLOW_DOWN, - KEY_PAGE_UP: Button.SPEED_UP, + KEY_PAGE_DOWN: SettingKeyboard.Alt_Button_Slow_Down, + KEY_PAGE_UP: SettingKeyboard.Alt_Button_Speed_Up, KEY_QUOTATION: -1, - KEY_SHIFT: Button.STATS, - KEY_TAB: Button.MENU, + KEY_SHIFT: SettingKeyboard.Alt_Button_Stats, + KEY_TAB: SettingKeyboard.Alt_Button_Menu, KEY_TILDE: -1, KEY_LEFT_BRACKET: -1, KEY_RIGHT_BRACKET: -1, KEY_SEMICOLON: -1, KEY_ALT: -1 - } + }, + main: [], + alt: [], }; export default cfg_keyboard_azerty; diff --git a/src/configs/configHandler.ts b/src/configs/configHandler.ts new file mode 100644 index 000000000..3b31338fa --- /dev/null +++ b/src/configs/configHandler.ts @@ -0,0 +1,136 @@ +export function getKeyWithKeycode(config, keycode) { + return Object.keys(config.gamepadMapping).find(key => config.gamepadMapping[key] === keycode); +} + +export function getSettingNameWithKeycode(config, keycode) { + const key = getKeyWithKeycode(config, keycode); + return config.custom[key]; +} + +export function getIconWithKeycode(config, keycode) { + const key = getKeyWithKeycode(config, keycode); + return config.icons[key]; +} + +export function getButtonWithKeycode(config, keycode) { + const settingName = getSettingNameWithKeycode(config, keycode); + return config.settings[settingName]; +} + +export function getKeycodeWithKey(config, key) { + return config.gamepadMapping[key] +} + +export function getKeyWithSettingName(config, settingName) { + return Object.keys(config.custom).find(key => config.custom[key] === settingName); +} + +export function getSettingNameWithKey(config, key) { + return config.custom[key] +} + +export function getIconWithKey(config, key) { + return config.icons[key] +} + +export function getIconSpecialCase(config, keycode, settingName) { + const potentialKey = isAlreadyBinded(config, keycode, settingName); + if (potentialKey) return getIconWithKey(config, potentialKey); + return null; +} + +export function getButtonWithSettingName(config, settingName) { + return config.settings[settingName]; +} + +export function getButtonWithKey(config, key) { + const settingName = config.custom[key]; + return getButtonWithSettingName(config, settingName); +} + +export function getIconWithSettingName(config, settingName) { + const key = getKeyWithSettingName(config, settingName); + return getIconWithKey(config, key); +} + +export function getKeycodeWithSettingName(config, settingName) { + const key = getKeyWithSettingName(config, settingName); + return getKeycodeWithKey(config, key); +} + +export function getSettingNameWithButton(config, button, alt= false) { + return Object.keys(config.settings).find(k => { + const a = !alt && !k.includes("ALT_"); + const b = alt && k.includes("ALT_"); + const c = config.settings[k] === button; + return (a || b) && c; + }); +} + +export function getKeyWithButton(config, button, alt= false) { + const settingName = getSettingNameWithButton(config, button, alt); + return getKeyWithSettingName(config, settingName); +} + +export function getKeycodeWithButton(config, button, alt= false) { + const key = getKeyWithButton(config, button, alt); + return getKeycodeWithKey(config, key); +} + +export function getIconWithButton(config, button, alt= false) { + const key = getKeyWithButton(config, button, alt); + return getIconWithKey(config, key); +} + +export function isAlreadyBinded(config, keycode, settingNameTarget) { + const key = getKeyWithKeycode(config, keycode); + const isMain = config.main.includes(key); + + const isTargetMain = !settingNameTarget.includes("ALT_"); + const potentialExistingButton = getButtonWithSettingName(config, settingNameTarget); + const potentialExistingKey = getKeyWithButton(config, potentialExistingButton, !isMain); + + if (potentialExistingKey && isMain !== isTargetMain) return potentialExistingKey; + return null; +} + +export function swap(config, settingNameTarget, keycode) { + // 2 alt can't do the same thing + // 2 main can't do the same thing + // can't swap an alt if another alt is already doing the same + // can't swap a main if another main is already doing the same + const isDeleted = !getKeyWithSettingName(config, settingNameTarget); + if (isDeleted) { + const new_key = getKeyWithKeycode(config, keycode); + config.custom[new_key] = settingNameTarget; + return; + } + const potentialExistingKey = isAlreadyBinded(config, keycode, settingNameTarget); + + const prev_key = potentialExistingKey || getKeyWithSettingName(config, settingNameTarget); + const prev_settingName = getSettingNameWithKey(config, prev_key); + + const new_key = getKeyWithKeycode(config, keycode); + const new_settingName = getSettingNameWithKey(config, new_key); + + config.custom[prev_key] = new_settingName; + config.custom[new_key] = prev_settingName; + regenerateIdentifiers(config); +} + +export function deleteBind(config, settingName) { + const key = getKeyWithSettingName(config, settingName); + config.custom[key] = -1; + regenerateIdentifiers(config); +} + +export function regenerateIdentifiers(config) { + config.main = Object.keys(config.custom).filter(key => { + const value = config.custom[key] + return value !== -1 && !value.includes("ALT_"); + }); + config.alt = Object.keys(config.custom).filter(key => { + const value = config.custom[key] + return value !== -1 && value.includes("ALT_"); + }); +} \ No newline at end of file diff --git a/src/configs/gamepad-utils.ts b/src/configs/gamepad-utils.ts deleted file mode 100644 index 9d383ea22..000000000 --- a/src/configs/gamepad-utils.ts +++ /dev/null @@ -1,184 +0,0 @@ -import {InterfaceConfig} from "../inputs-controller"; -import {Button} from "#app/enums/buttons"; -import {deepCopy} from "#app/utils"; - -// Given a button index from an input event, return its naming from the mapping config -export function getKeyFromMapping(config: InterfaceConfig, index: number): String | null { - for (const key of Object.keys(config.gamepadMapping)) { - if (config.gamepadMapping[key] === index) return key; - } - return null; -} - -// Given a setting name, return the key assigned to it from the config file -export function getKeyForSettingName(config: InterfaceConfig, settingName: string): String | null { - for (const key of Object.keys(config.setting)) { - if (config.setting[key] === settingName) return key; - } - return null; -} - -// Given a setting name, return the key assigned to it from the config file -export function getIconForSettingName(config: InterfaceConfig, settingName: string): String | null { - const key = getKeyForSettingName(config, settingName); - return config.icons[key]; -} - -// Given a Button, return the custom key assigned to it from the config file -export function getKeyWithAction(config: InterfaceConfig, action: Button, alt: boolean = false): String | null { - // need to find a way to differentiate main/alt button - const { key } = getKeyAndSettingNameFromCurrentKeysWithAction(config, action, alt); - return key; -} - -// Given a button index from an input event, return its icon from the config file -export function getIconWithPressedButton(config: InterfaceConfig, pressedButton: number): String { - const key = getKeyFromMapping(config, pressedButton); - return config.icons[key]; -} - -// Given a setting name, return the icon currently assigned to this setting name -export function getIconWithSettingName(config: InterfaceConfig, settingName: string): string { - const { icon } = getKeyAndActionFromCurrentKeysWithSettingName(config, settingName) - return icon; -} - -function getKeyAndSettingNameFromCurrentKeysWithAction(config, _action, alt: boolean = false) { - for (const _settingName of Object.keys(config.currentKeys)) { - if (alt && !_settingName.includes("ALT_")) continue; - if (config.currentKeys[_settingName].action === _action) return { - settingName: _settingName, - key: config.currentKeys[_settingName].key, - }; - } - return null; -} - -export function getKeyAndActionFromCurrentKeysWithSettingName(config, settingName) { - return config.currentKeys[settingName]; -} - -export function getKeyAndActionFromCurrentKeysWithPressedButton(config, pressedButton) { - const key = getKeyFromMapping(config, pressedButton); - const settingName = Object.keys(config.currentKeys).find(_s => config.currentKeys[_s].key === key); - return getKeyAndActionFromCurrentKeysWithSettingName(config, settingName); -} - -export function assignNewKey(config: InterfaceConfig, settingName, pressedButton, previousBind): void { - const key = getKeyFromMapping(config, pressedButton); - const icon = config.ogIcons[key]; - config.icons[previousBind.key] = icon; - config.currentKeys[settingName].icon = icon; - - config.custom[previousBind.key] = -1; - config.custom[key] = previousBind.action !== -1 ? previousBind.action : previousBind.from.action; - config.currentKeys[settingName].replacedBy = key; - config.currentKeys[settingName].latestIsDeleted = false; - - delete config.currentKeys[settingName].from -} - -export function swapCurrentKeys(config: InterfaceConfig, settingName, pressedButton): void { - const previousBind = getKeyAndActionFromCurrentKeysWithSettingName(config, settingName); - const prevKey = deepCopy(previousBind); - const newBind = getKeyAndActionFromCurrentKeysWithPressedButton(config, pressedButton); - if (newBind?.action === -1 && previousBind.action === -1) { - assignNewKey(config, settingName, pressedButton, previousBind); - } else if (newBind && previousBind.action === -1) { - //special case when rebinding deleted key with already assigned key - const toRestore = deepCopy(newBind); - const iconFromThePressedButton = config.ogIcons[prevKey.key]; - config.custom[newBind.key] = prevKey.from.action; - config.icons[prevKey.key] = newBind.icon || iconFromThePressedButton; - config.icons[newBind.key] = prevKey.from.icon; - - delete prevKey.from; - - const nextSettingName = getKeyAndSettingNameFromCurrentKeysWithAction(config, newBind.action, newBind.isAlt).settingName; - config.currentKeys[nextSettingName].from = toRestore.action === -1 ? config.currentKeys[nextSettingName].from : toRestore; - config.currentKeys[nextSettingName].isDeleted = true; - config.currentKeys[settingName].isDeleted = false; - config.currentKeys[settingName].replacedBy = toRestore.key; - } else if (!newBind) { - assignNewKey(config, settingName, pressedButton, previousBind); - } else { - const nextKey = deepCopy(newBind); - if (prevKey.key === nextKey.key && prevKey.from) { - // special case when back to back and not enough info to get back to previous button - const toRestore = getKeyAndSettingNameFromCurrentKeysWithAction(config, prevKey.from.action, settingName.includes("ALT_")); - config.custom[prevKey.key] = prevKey.from.action; - config.icons[prevKey.key] = prevKey.from.icon; - - config.custom[toRestore.key] = prevKey.action; - config.icons[toRestore.key] = prevKey.icon; - - delete config.currentKeys[settingName].from; - delete config.currentKeys[toRestore.settingName].from; - } else { - config.custom[previousBind.key] = newBind.action; - config.custom[newBind.key] = previousBind.action; - config.icons[previousBind.key] = newBind.icon; - config.icons[newBind.key] = previousBind.icon; - const nextSettingName = getKeyAndSettingNameFromCurrentKeysWithAction(config, newBind.action, newBind.isAlt).settingName; - config.currentKeys[settingName].from = prevKey; - config.currentKeys[nextSettingName].from = nextKey; - } - } - reloadCurrentKeys(config); -} - - -export function reloadCurrentKeys(config): void { - // need to rework this to include keys that were not there at the begining - const currentKeys = config.currentKeys ? deepCopy(config.currentKeys) : {}; - for (const key of Object.keys(config.setting)) { - const settingName = config.setting[key]; - const action = config.custom[key]; - const icon = config.icons[key]; - if (!currentKeys[settingName]) currentKeys[settingName] = {}; - currentKeys[settingName].key = key; - currentKeys[settingName].isAlt = settingName.includes("ALT_"); - const previousAction = config.custom[currentKeys[settingName].replacedBy] - if (action === -1 && previousAction !== undefined && !currentKeys[settingName].isDeleted) { - currentKeys[settingName].action = previousAction; - currentKeys[settingName].icon = icon; - currentKeys[settingName].latestReplacedBy = config.currentKeys[settingName].replacedBy - } else if (currentKeys[settingName].isDeleted) { - currentKeys[settingName].action = -1; - currentKeys[settingName].icon = undefined; - currentKeys[settingName].latestIsDeleted = config.currentKeys[settingName].isDeleted - delete currentKeys[settingName].isDeleted; - } else { - currentKeys[settingName].action = action; - currentKeys[settingName].icon = action === -1 ? undefined : icon; - } - } - config.currentKeys = deepCopy(currentKeys); -} - -export function regenerateCustom(config): void { - const custom = deepCopy(config.custom); - for (const settingName of Object.keys(config.currentKeys)) { - const {key, action, latestReplacedBy, latestIsDeleted} = config.currentKeys[settingName]; - if (latestReplacedBy) { - custom[key] = -1; - custom[latestReplacedBy] = action; - } else if (!latestIsDeleted) { - custom[key] = action; - } else if (latestIsDeleted) { - custom[key] = -1; - } - } - config.custom = deepCopy(custom); -} - -export function deleteBind(config, settingName): void { - const { key } = getKeyAndActionFromCurrentKeysWithSettingName(config, settingName); - const prev = deepCopy(config.currentKeys[settingName]); - delete config.currentKeys[settingName].icon - const actualKey = prev.replacedBy || key; - config.currentKeys[settingName].from = prev; - config.custom[actualKey] = -1; - config.currentKeys[settingName].isDeleted = true; - reloadCurrentKeys(config); -} \ No newline at end of file diff --git a/src/configs/pad_dualshock.ts b/src/configs/pad_dualshock.ts index 353027426..845e7d610 100644 --- a/src/configs/pad_dualshock.ts +++ b/src/configs/pad_dualshock.ts @@ -45,7 +45,30 @@ const pad_dualshock = { LC_E: "T_P4_Dpad_Right_Default.png", TOUCH: "T_P4_Touch_Pad_Default.png" }, - setting: { + settings: { + [SettingGamepad.Button_Up]: Button.UP, + [SettingGamepad.Button_Down]: Button.DOWN, + [SettingGamepad.Button_Left]: Button.LEFT, + [SettingGamepad.Button_Right]: Button.RIGHT, + [SettingGamepad.Button_Action]: Button.ACTION, + [SettingGamepad.Button_Cancel]: Button.CANCEL, + [SettingGamepad.Button_Cycle_Nature]: Button.CYCLE_NATURE, + [SettingGamepad.Button_Cycle_Variant]: Button.CYCLE_VARIANT, + [SettingGamepad.Button_Menu]: Button.MENU, + [SettingGamepad.Button_Stats]: Button.STATS, + [SettingGamepad.Button_Cycle_Form]: Button.CYCLE_FORM, + [SettingGamepad.Button_Cycle_Shiny]: Button.CYCLE_SHINY, + [SettingGamepad.Button_Cycle_Gender]: Button.CYCLE_GENDER, + [SettingGamepad.Button_Cycle_Ability]: Button.CYCLE_ABILITY, + [SettingGamepad.Button_Speed_Up]: Button.SPEED_UP, + [SettingGamepad.Button_Slow_Down]: Button.SLOW_DOWN, + [SettingGamepad.Button_Submit]: Button.SUBMIT + }, + default: { + LC_N: SettingGamepad.Button_Up, + LC_S: SettingGamepad.Button_Down, + LC_W: SettingGamepad.Button_Left, + LC_E: SettingGamepad.Button_Right, RC_S: SettingGamepad.Button_Action, RC_E: SettingGamepad.Button_Cancel, RC_W: SettingGamepad.Button_Cycle_Nature, @@ -60,25 +83,8 @@ const pad_dualshock = { RS: SettingGamepad.Button_Slow_Down, TOUCH: SettingGamepad.Button_Submit, }, - default: { - RC_S: Button.ACTION, - RC_E: Button.CANCEL, - RC_W: Button.CYCLE_NATURE, - RC_N: Button.CYCLE_VARIANT, - START: Button.MENU, - SELECT: Button.STATS, - LB: Button.CYCLE_FORM, - RB: Button.CYCLE_SHINY, - LT: Button.CYCLE_GENDER, - RT: Button.CYCLE_ABILITY, - LS: Button.SPEED_UP, - RS: Button.SLOW_DOWN, - LC_N: Button.UP, - LC_S: Button.DOWN, - LC_W: Button.LEFT, - LC_E: Button.RIGHT, - TOUCH: Button.SUBMIT, - } + main: [], + alt: [], }; export default pad_dualshock; diff --git a/src/configs/pad_generic.ts b/src/configs/pad_generic.ts index 1e38c6aca..ce2edd421 100644 --- a/src/configs/pad_generic.ts +++ b/src/configs/pad_generic.ts @@ -43,7 +43,29 @@ const pad_generic = { LC_W: "T_X_Dpad_Left_Alt.png", LC_E: "T_X_Dpad_Right_Alt.png", }, - setting: { + settings: { + [SettingGamepad.Button_Up]: Button.UP, + [SettingGamepad.Button_Down]: Button.DOWN, + [SettingGamepad.Button_Left]: Button.LEFT, + [SettingGamepad.Button_Right]: Button.RIGHT, + [SettingGamepad.Button_Action]: Button.ACTION, + [SettingGamepad.Button_Cancel]: Button.CANCEL, + [SettingGamepad.Button_Cycle_Nature]: Button.CYCLE_NATURE, + [SettingGamepad.Button_Cycle_Variant]: Button.CYCLE_VARIANT, + [SettingGamepad.Button_Menu]: Button.MENU, + [SettingGamepad.Button_Stats]: Button.STATS, + [SettingGamepad.Button_Cycle_Form]: Button.CYCLE_FORM, + [SettingGamepad.Button_Cycle_Shiny]: Button.CYCLE_SHINY, + [SettingGamepad.Button_Cycle_Gender]: Button.CYCLE_GENDER, + [SettingGamepad.Button_Cycle_Ability]: Button.CYCLE_ABILITY, + [SettingGamepad.Button_Speed_Up]: Button.SPEED_UP, + [SettingGamepad.Button_Slow_Down]: Button.SLOW_DOWN + }, + default: { + LC_N: SettingGamepad.Button_Up, + LC_S: SettingGamepad.Button_Down, + LC_W: SettingGamepad.Button_Left, + LC_E: SettingGamepad.Button_Right, RC_S: SettingGamepad.Button_Action, RC_E: SettingGamepad.Button_Cancel, RC_W: SettingGamepad.Button_Cycle_Nature, @@ -55,26 +77,10 @@ const pad_generic = { LT: SettingGamepad.Button_Cycle_Gender, RT: SettingGamepad.Button_Cycle_Ability, LS: SettingGamepad.Button_Speed_Up, - RS: SettingGamepad.Button_Slow_Down, + RS: SettingGamepad.Button_Slow_Down }, - default: { - RC_S: Button.ACTION, //5 - RC_E: Button.CANCEL, // 6 - RC_W: Button.CYCLE_NATURE, - RC_N: Button.CYCLE_VARIANT, - START: Button.MENU, - SELECT: Button.STATS, - LB: Button.CYCLE_FORM, //10 - RB: Button.CYCLE_SHINY, //9 - LT: Button.CYCLE_GENDER, //11 - RT: Button.CYCLE_ABILITY, - LS: Button.SPEED_UP, - RS: Button.SLOW_DOWN, - LC_N: Button.UP, - LC_S: Button.DOWN, - LC_W: Button.LEFT, - LC_E: Button.RIGHT, - } + main: [], + alt: [], }; export default pad_generic; diff --git a/src/configs/pad_unlicensedSNES.ts b/src/configs/pad_unlicensedSNES.ts index 30ed331c3..c60ccbe74 100644 --- a/src/configs/pad_unlicensedSNES.ts +++ b/src/configs/pad_unlicensedSNES.ts @@ -35,7 +35,29 @@ const pad_unlicensedSNES = { LC_W: "T_X_Dpad_Left_Alt.png", LC_E: "T_X_Dpad_Right_Alt.png", }, - setting: { + settings: { + [SettingGamepad.Button_Up]: Button.UP, + [SettingGamepad.Button_Down]: Button.DOWN, + [SettingGamepad.Button_Left]: Button.LEFT, + [SettingGamepad.Button_Right]: Button.RIGHT, + [SettingGamepad.Button_Action]: Button.ACTION, + [SettingGamepad.Button_Cancel]: Button.CANCEL, + [SettingGamepad.Button_Cycle_Nature]: Button.CYCLE_NATURE, + [SettingGamepad.Button_Cycle_Variant]: Button.CYCLE_VARIANT, + [SettingGamepad.Button_Menu]: Button.MENU, + [SettingGamepad.Button_Stats]: Button.STATS, + [SettingGamepad.Button_Cycle_Form]: Button.CYCLE_FORM, + [SettingGamepad.Button_Cycle_Shiny]: Button.CYCLE_SHINY, + [SettingGamepad.Button_Cycle_Gender]: Button.CYCLE_GENDER, + [SettingGamepad.Button_Cycle_Ability]: Button.CYCLE_ABILITY, + [SettingGamepad.Button_Speed_Up]: Button.SPEED_UP, + [SettingGamepad.Button_Slow_Down]: Button.SLOW_DOWN + }, + default: { + LC_N: SettingGamepad.Button_Up, + LC_S: SettingGamepad.Button_Down, + LC_W: SettingGamepad.Button_Left, + LC_E: SettingGamepad.Button_Right, RC_S: SettingGamepad.Button_Action, RC_E: SettingGamepad.Button_Cancel, RC_W: SettingGamepad.Button_Cycle_Nature, @@ -44,21 +66,13 @@ const pad_unlicensedSNES = { SELECT: SettingGamepad.Button_Stats, LB: SettingGamepad.Button_Cycle_Form, RB: SettingGamepad.Button_Cycle_Shiny, + LT: -1, + RT: -1, + LS: -1, + RS: -1 }, - default: { - RC_S: Button.ACTION, - RC_E: Button.CANCEL, - RC_W: Button.CYCLE_NATURE, - RC_N: Button.CYCLE_VARIANT, - START: Button.MENU, - SELECT: Button.STATS, - LB: Button.CYCLE_FORM, - RB: Button.CYCLE_SHINY, - LC_N: Button.UP, - LC_S: Button.DOWN, - LC_W: Button.LEFT, - LC_E: Button.RIGHT, - } + main: [], + alt: [], }; export default pad_unlicensedSNES; diff --git a/src/configs/pad_xbox360.ts b/src/configs/pad_xbox360.ts index 08bbd71a5..3f820e59d 100644 --- a/src/configs/pad_xbox360.ts +++ b/src/configs/pad_xbox360.ts @@ -43,7 +43,29 @@ const pad_xbox360 = { LC_W: "T_X_Dpad_Left_Alt.png", LC_E: "T_X_Dpad_Right_Alt.png", }, - setting: { + settings: { + [SettingGamepad.Button_Up]: Button.UP, + [SettingGamepad.Button_Down]: Button.DOWN, + [SettingGamepad.Button_Left]: Button.LEFT, + [SettingGamepad.Button_Right]: Button.RIGHT, + [SettingGamepad.Button_Action]: Button.ACTION, + [SettingGamepad.Button_Cancel]: Button.CANCEL, + [SettingGamepad.Button_Cycle_Nature]: Button.CYCLE_NATURE, + [SettingGamepad.Button_Cycle_Variant]: Button.CYCLE_VARIANT, + [SettingGamepad.Button_Menu]: Button.MENU, + [SettingGamepad.Button_Stats]: Button.STATS, + [SettingGamepad.Button_Cycle_Form]: Button.CYCLE_FORM, + [SettingGamepad.Button_Cycle_Shiny]: Button.CYCLE_SHINY, + [SettingGamepad.Button_Cycle_Gender]: Button.CYCLE_GENDER, + [SettingGamepad.Button_Cycle_Ability]: Button.CYCLE_ABILITY, + [SettingGamepad.Button_Speed_Up]: Button.SPEED_UP, + [SettingGamepad.Button_Slow_Down]: Button.SLOW_DOWN + }, + default: { + LC_N: SettingGamepad.Button_Up, + LC_S: SettingGamepad.Button_Down, + LC_W: SettingGamepad.Button_Left, + LC_E: SettingGamepad.Button_Right, RC_S: SettingGamepad.Button_Action, RC_E: SettingGamepad.Button_Cancel, RC_W: SettingGamepad.Button_Cycle_Nature, @@ -55,26 +77,10 @@ const pad_xbox360 = { LT: SettingGamepad.Button_Cycle_Gender, RT: SettingGamepad.Button_Cycle_Ability, LS: SettingGamepad.Button_Speed_Up, - RS: SettingGamepad.Button_Slow_Down, + RS: SettingGamepad.Button_Slow_Down }, - default: { - RC_S: Button.ACTION, //5 - RC_E: Button.CANCEL, // 6 - RC_W: Button.CYCLE_NATURE, - RC_N: Button.CYCLE_VARIANT, //14 - START: Button.MENU, //7 - SELECT: Button.STATS, //8 - LB: Button.CYCLE_FORM, - RB: Button.CYCLE_SHINY, - LT: Button.CYCLE_GENDER, - RT: Button.CYCLE_ABILITY, - LS: Button.SPEED_UP, - RS: Button.SLOW_DOWN, - LC_N: Button.UP, - LC_S: Button.DOWN, - LC_W: Button.LEFT, - LC_E: Button.RIGHT, - } + main: [], + alt: [], }; export default pad_xbox360; diff --git a/src/enums/devices.ts b/src/enums/devices.ts new file mode 100644 index 000000000..b085dfbad --- /dev/null +++ b/src/enums/devices.ts @@ -0,0 +1,4 @@ +export enum Device { + GAMEPAD, + KEYBOARD, +} diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 4c6dcd928..389ccabc6 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -1,4 +1,5 @@ import * as Utils from "./utils"; +import {deepCopy} from "./utils"; import {initTouchControls} from './touch-controls'; import pad_generic from "./configs/pad_generic"; import pad_unlicensedSNES from "./configs/pad_unlicensedSNES"; @@ -9,12 +10,8 @@ import {Mode} from "./ui/ui"; import SettingsGamepadUiHandler from "./ui/settings/settings-gamepad-ui-handler"; import SettingsKeyboardUiHandler from "./ui/settings/settings-keyboard-ui-handler"; import cfg_keyboard_azerty from "./configs/cfg_keyboard_azerty"; -import { - getKeyAndActionFromCurrentKeysWithPressedButton, - getKeyFromMapping, regenerateCustom, - reloadCurrentKeys, swapCurrentKeys -} from "#app/configs/gamepad-utils"; -import {deepCopy} from "./utils"; +import {Device} from "#app/enums/devices"; +import {getButtonWithKeycode, regenerateIdentifiers, swap} from "#app/configs/configHandler"; export interface GamepadMapping { [key: string]: number; @@ -78,12 +75,10 @@ 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 selectedDevice; - public chosenGamepad: String; - public chosenKeyboard: string = "default"; private disconnectedGamepads: Array = new Array(); private pauseUpdate: boolean = false; @@ -102,10 +97,15 @@ export class InputsController { * Specific buttons like MENU and STATS are set not to repeat their actions. * It concludes by calling the `init` method to complete the setup. */ + constructor(scene: Phaser.Scene) { this.scene = scene; this.time = this.scene.time; this.buttonKeys = []; + this.selectedDevice = { + [Device.GAMEPAD]: null, + [Device.KEYBOARD]: 'default' + } for (const b of Utils.getEnumValues(Button)) { this.interactions[b] = { @@ -131,8 +131,8 @@ export class InputsController { init(): void { this.events = this.scene.game.events; - if (localStorage.hasOwnProperty('chosenGamepad')) { - this.chosenGamepad = localStorage.getItem('chosenGamepad'); + if (localStorage.hasOwnProperty('selectedDevice')) { + this.selectedDevice = JSON.parse(localStorage.getItem('selectedDevice')); } this.scene.game.events.on(Phaser.Core.Events.BLUR, () => { this.loseFocus() @@ -234,8 +234,8 @@ export class InputsController { // Prevents repeating button interactions when gamepad support is disabled. if ( (!this.gamepadSupport && this.interactions[b].source === 'gamepad') || - (this.interactions[b].source === 'gamepad' && this.interactions[b].sourceName && this.interactions[b].sourceName !== this.chosenGamepad) || - (this.interactions[b].source === 'keyboard' && this.interactions[b].sourceName && this.interactions[b].sourceName !== this.chosenKeyboard) || + (this.interactions[b].source === 'gamepad' && this.interactions[b].sourceName && this.interactions[b].sourceName !== this.selectedDevice[Device.GAMEPAD]) || + (this.interactions[b].source === 'keyboard' && this.interactions[b].sourceName && this.interactions[b].sourceName !== this.selectedDevice[Device.KEYBOARD]) || this.pauseUpdate ) { // Deletes the last interaction for a button if gamepad is disabled. @@ -266,23 +266,17 @@ export class InputsController { * @param gamepadName Optional parameter to specify the name of the gamepad to initialize as chosen. */ initChosenGamepad(gamepadName?: String): void { - let name = gamepadName; if (gamepadName) - this.chosenGamepad = gamepadName; - else - name = this.chosenGamepad; - localStorage.setItem('chosenGamepad', name); + this.selectedDevice[Device.GAMEPAD] = gamepadName.toLowerCase(); + localStorage.setItem('selectedDevice', JSON.stringify(this.selectedDevice)); const handler = this.scene.ui?.handlers[Mode.SETTINGS_GAMEPAD] as SettingsGamepadUiHandler; handler && handler.updateChosenGamepadDisplay() } initChosenLayoutKeyboard(layoutKeyboard?: String): void { - let name = layoutKeyboard; if (layoutKeyboard) - this.chosenKeyboard = layoutKeyboard; - else - name = this.chosenKeyboard; - localStorage.setItem('chosenKeyboardLayout', name); + this.selectedDevice[Device.KEYBOARD] = layoutKeyboard.toLowerCase(); + localStorage.setItem('selectedDevice', JSON.stringify(this.selectedDevice)); const handler = this.scene.ui?.handlers[Mode.SETTINGS_KEYBOARD] as SettingsKeyboardUiHandler; handler && handler.updateChosenKeyboardDisplay() } @@ -297,22 +291,6 @@ export class InputsController { */ onDisconnect(thisGamepad: Phaser.Input.Gamepad.Gamepad): void { this.disconnectedGamepads.push(thisGamepad.id); - /** commented for now this code because i don't know anymore if it's good to do that - * for example i'm playing with a wireless gamepad that shutdown after 5 min - * i don't want my game to use my second controller when i turn back on my main gamepad - * we look for gamepads still connected by substracting the 2 arrays - */ - // const gamepadsLeft = this.gamepads.filter(g => !this.disconnectedGamepads.includes(g.id)).map(g => g); - // we check if the chosen gamepad is still connected - // const chosenIsConnected = gamepadsLeft.some(g => g.id === this.chosenGamepad); - // if the chosen gamepad is disconnected, and we got others gamepad connected - // if (!chosenIsConnected && gamepadsLeft?.length) { - // We remove the previously chosen gamepad - // this.clearChosenGamepad(); - // and we set the first of the gamepad still connected as the chosen one. - // this.setChosenGamepad(gamepadsLeft[0].id); - // return; - // } } /** @@ -338,36 +316,24 @@ export class InputsController { const allGamepads = this.getGamepadsName(); for (const gamepad of allGamepads) { const gamepadID = gamepad.toLowerCase(); + if (!this.selectedDevice[Device.GAMEPAD]) + this.setChosenGamepad(gamepadID); const config = deepCopy(this.getConfig(gamepadID)); - config.custom = this.configs[gamepad]?.custom || {...config.default}; - config.ogIcons = {...config.icons}; - config.icons = this.configs[gamepad]?.icons || {...config.icons}; - config.currentKeys = this.configs[gamepad]?.currentKeys; - if (!this.configs[gamepad]?.currentKeys) - reloadCurrentKeys(config); - else - regenerateCustom(config); - this.configs[gamepad] = config; - this.scene.gameData?.saveCustomMapping(this.chosenGamepad, this.configs[gamepad]?.currentKeys, this.configs[gamepad]?.icons); + config.custom = this.configs[gamepadID]?.custom || {...config.default}; + this.configs[gamepadID] = config; + this.scene.gameData?.saveMappingConfigs(gamepadID, this.configs[gamepadID]); } - if (this.chosenGamepad === thisGamepad.id) this.initChosenGamepad(this.chosenGamepad) + this.initChosenGamepad(this.selectedDevice[Device.GAMEPAD]) } setupKeyboard(): void { for (const layout of ['default']) { const config = deepCopy(this.getConfigKeyboard(layout)); - config.custom = this.keyboardConfigs[layout]?.custom || {...config.default}; - config.ogIcons = {...config.icons}; - config.icons = this.keyboardConfigs[layout]?.icons || {...config.icons}; - config.currentKeys = this.keyboardConfigs[layout]?.currentKeys; - if (!this.keyboardConfigs[layout]?.currentKeys) - reloadCurrentKeys(config); - else - regenerateCustom(config); - this.keyboardConfigs[layout] = config; - this.scene.gameData?.saveCustomKeyboardMapping(this.chosenKeyboard, this.keyboardConfigs[layout]?.currentKeys, this.keyboardConfigs[layout]?.icons); + config.custom = this.configs[layout]?.custom || {...config.default}; + this.configs[layout] = config; + this.scene.gameData?.saveMappingConfigs(this.selectedDevice[Device.KEYBOARD], this.configs[layout]); } - this.initChosenLayoutKeyboard(this.chosenKeyboard) + this.initChosenLayoutKeyboard(this.selectedDevice[Device.KEYBOARD]) } /** @@ -390,7 +356,7 @@ export class InputsController { } checkIfKeyboardIsInit(): void { - if (!this.keyboardConfigs[this.chosenKeyboard]?.padID) + if (!this.getActiveConfig(Device.KEYBOARD)?.padID) this.setupKeyboard(); } @@ -399,15 +365,14 @@ export class InputsController { this.checkIfKeyboardIsInit(); if (this.keys.includes(keyDown)) return; this.keys.push(keyDown); - const key = getKeyFromMapping(this.keyboardConfigs[this.chosenKeyboard], keyDown); - const buttonDown = this.keyboardConfigs[this.chosenKeyboard].custom[key]; + const buttonDown = getButtonWithKeycode(this.getActiveConfig(Device.KEYBOARD), keyDown); this.lastSource = 'keyboard'; if (buttonDown !== undefined) { this.events.emit('input_down', { controller_type: 'keyboard', button: buttonDown, }); - this.setLastProcessedMovementTime(buttonDown, 'keyboard', this.chosenKeyboard); + this.setLastProcessedMovementTime(buttonDown, 'keyboard', this.selectedDevice[Device.KEYBOARD]); } } @@ -415,8 +380,7 @@ export class InputsController { const keyDown = event.keyCode; this.keys = this.keys.filter(k => k !== keyDown); this.checkIfKeyboardIsInit() - const key = getKeyFromMapping(this.keyboardConfigs[this.chosenKeyboard], keyDown); - const buttonUp = this.keyboardConfigs[this.chosenKeyboard].custom[key]; + const buttonUp = getButtonWithKeycode(this.getActiveConfig(Device.KEYBOARD), keyDown); if (buttonUp !== undefined) { this.events.emit('input_up', { controller_type: 'keyboard', @@ -436,14 +400,13 @@ export class InputsController { * @param value The intensity or value of the button press, if applicable. */ gamepadButtonDown(pad: Phaser.Input.Gamepad.Gamepad, button: Phaser.Input.Gamepad.Button, value: number): void { - if (!this.keyboardConfigs[this.chosenKeyboard]?.padID) + if (!this.configs[this.selectedDevice[Device.KEYBOARD]]?.padID) this.setupKeyboard(); if (!pad) return; - if (!this.chosenGamepad) + if (!this.selectedDevice[Device.GAMEPAD]) this.setChosenGamepad(pad.id); - if (!this.gamepadSupport || pad.id.toLowerCase() !== this.chosenGamepad.toLowerCase()) return; - const key = getKeyFromMapping(this.configs[pad.id], button.index); - const buttonDown = this.configs[pad.id].custom[key]; + if (!this.gamepadSupport || pad.id.toLowerCase() !== this.selectedDevice[Device.GAMEPAD].toLowerCase()) return; + const buttonDown = getButtonWithKeycode(this.getActiveConfig(Device.GAMEPAD), button.index); this.lastSource = 'gamepad'; if (buttonDown !== undefined) { this.events.emit('input_down', { @@ -465,9 +428,8 @@ export class InputsController { */ gamepadButtonUp(pad: Phaser.Input.Gamepad.Gamepad, button: Phaser.Input.Gamepad.Button, value: number): void { if (!pad) return; - if (!this.gamepadSupport || pad.id !== this.chosenGamepad) return; - const key = getKeyFromMapping(this.configs[pad.id], button.index); - const buttonUp = this.configs[pad.id]?.custom[key]; + if (!this.gamepadSupport || pad.id.toLowerCase() !== this.selectedDevice[Device.GAMEPAD]) return; + const buttonUp = getButtonWithKeycode(this.getActiveConfig(Device.GAMEPAD), button.index); if (buttonUp !== undefined) { this.events.emit('input_up', { controller_type: 'gamepad', @@ -477,100 +439,6 @@ export class InputsController { } } - /** - * Configures keyboard controls for the game, mapping physical keys to game actions. - * - * @remarks - * This method sets up keyboard bindings for game controls using Phaser's `KeyCodes`. Each game action, represented - * by a button in the `Button` enum, is associated with one or more physical keys. For example, movement actions - * (up, down, left, right) are mapped to both arrow keys and WASD keys. Actions such as submit, cancel, and other - * game-specific functions are mapped to appropriate keys like Enter, Space, etc. - * - * The method does the following: - * - Defines a `keyConfig` object that associates each `Button` enum value with an array of `KeyCodes`. - * - Iterates over all values of the `Button` enum to set up these key bindings within the Phaser game scene. - * - For each button, it adds the respective keys to the game's input system and stores them in `this.buttonKeys`. - * - Additional configurations for mobile or alternative input schemes are stored in `mobileKeyConfig`. - * - * Post-setup, it initializes touch controls (if applicable) and starts listening for keyboard inputs using - * `listenInputKeyboard`, ensuring that all configured keys are actively monitored for player interactions. - */ - setupKeyboardControls(): void { - const keyCodes = Phaser.Input.Keyboard.KeyCodes; - const keyConfig = { - [Button.UP]: [keyCodes.UP, keyCodes.W], - [Button.DOWN]: [keyCodes.DOWN, keyCodes.S], - [Button.LEFT]: [keyCodes.LEFT, keyCodes.A], - [Button.RIGHT]: [keyCodes.RIGHT, keyCodes.D], - [Button.SUBMIT]: [keyCodes.ENTER], - [Button.ACTION]: [keyCodes.SPACE, keyCodes.Z], - [Button.CANCEL]: [keyCodes.BACKSPACE, keyCodes.X], - [Button.MENU]: [keyCodes.ESC, keyCodes.M], - [Button.STATS]: [keyCodes.SHIFT, keyCodes.C], - [Button.CYCLE_SHINY]: [keyCodes.R], - [Button.CYCLE_FORM]: [keyCodes.F], - [Button.CYCLE_GENDER]: [keyCodes.G], - [Button.CYCLE_ABILITY]: [keyCodes.E], - [Button.CYCLE_NATURE]: [keyCodes.N], - [Button.CYCLE_VARIANT]: [keyCodes.V], - [Button.SPEED_UP]: [keyCodes.PLUS], - [Button.SLOW_DOWN]: [keyCodes.MINUS] - }; - const mobileKeyConfig = {}; - for (const b of Utils.getEnumValues(Button)) { - const keys: Phaser.Input.Keyboard.Key[] = []; - if (keyConfig.hasOwnProperty(b)) { - for (let k of keyConfig[b]) - keys.push(this.scene.input.keyboard.addKey(k, false)); - mobileKeyConfig[Button[b]] = keys[0]; - } - this.buttonKeys[b] = keys; - } - - initTouchControls(mobileKeyConfig); - this.listenInputKeyboard(); - } - - /** - * Sets up event listeners for keyboard inputs on all registered keys. - * - * @remarks - * This method iterates over an array of keyboard button rows (`this.buttonKeys`), adding 'down' and 'up' - * event listeners for each key. These listeners handle key press and release actions respectively. - * - * - **Key Down Event**: When a key is pressed down, the method emits an 'input_down' event with the button - * and the source ('keyboard'). It also records the time and state of the key press by calling - * `setLastProcessedMovementTime`. - * - * - **Key Up Event**: When a key is released, the method emits an 'input_up' event similarly, specifying the button - * and source. It then clears the recorded press time and state by calling - * `delLastProcessedMovementTime`. - * - * This setup ensures that each key on the keyboard is monitored for press and release events, - * and that these events are properly communicated within the system. - */ - listenInputKeyboard(): void { - this.buttonKeys.forEach((row, index) => { - for (const key of row) { - key.on('down', () => { - this.lastSource = 'keyboard'; - this.events.emit('input_down', { - controller_type: 'keyboard', - button: index, - }); - this.setLastProcessedMovementTime(index, 'keyboard'); - }); - key.on('up', () => { - this.events.emit('input_up', { - controller_type: 'keyboard', - button: index, - }); - this.delLastProcessedMovementTime(index); - }); - } - }); - } - /** * Retrieves the configuration object for a gamepad based on its identifier. The method identifies specific gamepad models * based on substrings in the identifier and returns predefined configurations for recognized models. @@ -633,7 +501,7 @@ export class InputsController { this.interactions[button].pressTime = this.time.now; this.interactions[button].isPressed = true; this.interactions[button].source = source; - this.interactions[button].sourceName = sourceName; + this.interactions[button].sourceName = sourceName.toLowerCase(); } /** @@ -741,24 +609,13 @@ export class InputsController { } /** - * 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. + * Retrieves the active configuration for the currently chosen device. + * It checks if a specific device ID is stored in configurations and returns it. * * @returns InterfaceConfig The configuration object for the active gamepad, or null if not set. */ - getActiveConfig(): InterfaceConfig | null { - if (this.configs[this.chosenGamepad]?.padID) return this.configs[this.chosenGamepad] - 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 InterfaceConfig The configuration object for the active gamepad, or null if not set. - */ - getActiveKeyboardConfig(): InterfaceConfig | null { - if (this.keyboardConfigs[this.chosenKeyboard]?.padID) return this.keyboardConfigs[this.chosenKeyboard] + getActiveConfig(device: Device) { + if (this.configs[this.selectedDevice[device]]?.padID) return this.configs[this.selectedDevice[device]] return null; } @@ -769,20 +626,17 @@ export class InputsController { * @param gamepadName The identifier of the gamepad to configure. * @param customMappings The custom mapping configuration to apply to the gamepad. */ - injectConfig(gamepadName: String, customMappings): void { - if (!this.configs[gamepadName]) this.configs[gamepadName] = {}; - this.configs[gamepadName].currentKeys = customMappings.currentKeys; - this.configs[gamepadName].icons = customMappings.icons; - } - injectKeyboardConfig(layout: string, customMappings): void { - if (!this.keyboardConfigs[layout]) this.keyboardConfigs[layout] = {}; - this.keyboardConfigs[layout].currentKeys = customMappings.currentKeys; - this.keyboardConfigs[layout].icons = customMappings.icons; + injectConfig(selectedDevice: string, mappingConfigs): void { + if (!this.configs[selectedDevice]) this.configs[selectedDevice] = {}; + this.configs[selectedDevice].custom = mappingConfigs.custom; + this.configs[selectedDevice].main = mappingConfigs.main; + this.configs[selectedDevice].alt = mappingConfigs.alt; + regenerateIdentifiers(this.configs[selectedDevice]); } swapBinding(config, settingName, pressedButton): void { this.pauseUpdate = true; - swapCurrentKeys(config, settingName, pressedButton) + swap(config, settingName, pressedButton); setTimeout(() => this.pauseUpdate = false, 500); } } \ No newline at end of file diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 4bf0496fe..cfbe05c9f 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -233,8 +233,7 @@ export class GameData { this.scene = scene; this.loadSettings(); this.loadGamepadSettings(); - this.loadCustomMapping(); - this.loadCustomKeyboardMapping(); + this.loadMappingConfigs(); this.trainerId = Utils.randInt(65536); this.secretId = Utils.randInt(65536); this.starterData = {}; @@ -498,45 +497,23 @@ export class GameData { return true; } - public saveCustomMapping(gamepadName: string, currentKeys, icons): boolean { - let customMappings: object = {}; - if (localStorage.hasOwnProperty('customMapping')) - customMappings = JSON.parse(localStorage.getItem('customMapping')); - customMappings[gamepadName] = { - currentKeys, - icons - }; - localStorage.setItem('customMapping', JSON.stringify(customMappings)); + public saveMappingConfigs(gamepadName: string, config): boolean { + const key = gamepadName.toLowerCase(); + let mappingConfigs: object = {}; + if (localStorage.hasOwnProperty('mappingConfigs')) + mappingConfigs = JSON.parse(localStorage.getItem('mappingConfigs')); + if (!mappingConfigs[key]) mappingConfigs[key] = {}; + mappingConfigs[key].custom = config.custom; + localStorage.setItem('mappingConfigs', JSON.stringify(mappingConfigs)); return true; } - public saveCustomKeyboardMapping(keyboardLayout: string, currentKeys, icons): boolean { - let customKeyboardMappings: object = {}; - if (localStorage.hasOwnProperty('customKeyboardMappings')) - customKeyboardMappings = JSON.parse(localStorage.getItem('customKeyboardMappings')); - customKeyboardMappings[keyboardLayout] = { - currentKeys, - icons - }; - localStorage.setItem('customKeyboardMappings', JSON.stringify(customKeyboardMappings)); - return true; - } - - public loadCustomKeyboardMapping(): boolean { - if (!localStorage.hasOwnProperty('customKeyboardMappings')) + public loadMappingConfigs(): boolean { + if (!localStorage.hasOwnProperty('mappingConfigs')) return false; - const customKeyboardMappings = JSON.parse(localStorage.getItem('customKeyboardMappings')); - for (const key of Object.keys(customKeyboardMappings)) - this.scene.inputController.injectKeyboardConfig(key, customKeyboardMappings[key]); - - } - - public loadCustomMapping(): boolean { - if (!localStorage.hasOwnProperty('customMapping')) - return false; - const customMappings = JSON.parse(localStorage.getItem('customMapping')); - for (const key of Object.keys(customMappings)) - this.scene.inputController.injectConfig(key, customMappings[key]); + const mappingConfigs = JSON.parse(localStorage.getItem('mappingConfigs')); + for (const key of Object.keys(mappingConfigs)) + this.scene.inputController.injectConfig(key, mappingConfigs[key]); } diff --git a/src/system/settings-gamepad.ts b/src/system/settings-gamepad.ts index 488d8ecfa..f123985ff 100644 --- a/src/system/settings-gamepad.ts +++ b/src/system/settings-gamepad.ts @@ -8,6 +8,10 @@ import {Button} from "../enums/buttons"; export enum SettingGamepad { Default_Controller = "DEFAULT_CONTROLLER", Gamepad_Support = "GAMEPAD_SUPPORT", + 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", @@ -26,6 +30,10 @@ export enum SettingGamepad { export const settingGamepadOptions: SettingOptions = { [SettingGamepad.Default_Controller]: ['Default', 'Change'], [SettingGamepad.Gamepad_Support]: ['Auto', 'Disabled'], + [SettingGamepad.Button_Up]: [`KEY ${Button.UP.toString()}`, 'Change'], + [SettingGamepad.Button_Down]: [`KEY ${Button.DOWN.toString()}`, 'Change'], + [SettingGamepad.Button_Left]: [`KEY ${Button.LEFT.toString()}`, 'Change'], + [SettingGamepad.Button_Right]: [`KEY ${Button.RIGHT.toString()}`, 'Change'], [SettingGamepad.Button_Action]: [`KEY ${Button.ACTION.toString()}`, 'Change'], [SettingGamepad.Button_Cancel]: [`KEY ${Button.CANCEL.toString()}`, 'Change'], [SettingGamepad.Button_Menu]: [`KEY ${Button.MENU.toString()}`, 'Change'], @@ -44,6 +52,10 @@ export const settingGamepadOptions: SettingOptions = { export const settingGamepadDefaults: SettingDefaults = { [SettingGamepad.Default_Controller]: 0, [SettingGamepad.Gamepad_Support]: 0, + [SettingGamepad.Button_Up]: 0, + [SettingGamepad.Button_Down]: 0, + [SettingGamepad.Button_Left]: 0, + [SettingGamepad.Button_Right]: 0, [SettingGamepad.Button_Action]: 0, [SettingGamepad.Button_Cancel]: 0, [SettingGamepad.Button_Menu]: 0, diff --git a/src/test/cfg_gamepad_example.ts b/src/test/cfg_gamepad_example.ts deleted file mode 100644 index 739111288..000000000 --- a/src/test/cfg_gamepad_example.ts +++ /dev/null @@ -1,97 +0,0 @@ -import {Button} from "#app/enums/buttons"; - -export enum SettingInterfaceGamepad { - Default_Controller = "DEFAULT_CONTROLLER", - Gamepad_Support = "GAMEPAD_SUPPORT", - Button_Action = "BUTTON_ACTION", - Button_Cancel = "BUTTON_CANCEL", - Button_Menu = "BUTTON_MENU", - Button_Stats = "BUTTON_STATS", - Button_Cycle_Form = "BUTTON_CYCLE_FORM", - Button_Cycle_Shiny = "BUTTON_CYCLE_SHINY", - Button_Cycle_Gender = "BUTTON_CYCLE_GENDER", - Button_Cycle_Ability = "BUTTON_CYCLE_ABILITY", - Button_Cycle_Nature = "BUTTON_CYCLE_NATURE", - Button_Cycle_Variant = "BUTTON_CYCLE_VARIANT", - Button_Speed_Up = "BUTTON_SPEED_UP", - Button_Slow_Down = "BUTTON_SLOW_DOWN", - Button_Submit = "BUTTON_SUBMIT", -} - -/** - * Generic pad mapping - */ -const pad_xbox360 = { - padID: 'Xbox 360 controller (XInput STANDARD GAMEPAD)', - padType: 'xbox', - gamepadMapping: { - RC_S: 0, - RC_E: 1, - RC_W: 2, - RC_N: 3, - START: 9, - SELECT: 8, - LB: 4, - RB: 5, - LT: 6, - RT: 7, - LS: 10, - RS: 11, - LC_N: 12, - LC_S: 13, - LC_W: 14, - LC_E: 15 - }, - icons: { - RC_S: "T_X_A_Color_Alt.png", - RC_E: "T_X_B_Color_Alt.png", - RC_W: "T_X_X_Color_Alt.png", - RC_N: "T_X_Y_Color_Alt.png", - START: "T_X_X_Alt.png", - SELECT: "T_X_Share_Alt.png", - LB: "T_X_LB_Alt.png", - RB: "T_X_RB_Alt.png", - LT: "T_X_LT_Alt.png", - RT: "T_X_RT_Alt.png", - LS: "T_X_Left_Stick_Click_Alt_Alt.png", - RS: "T_X_Right_Stick_Click_Alt_Alt.png", - LC_N: "T_X_Dpad_Up_Alt.png", - LC_S: "T_X_Dpad_Down_Alt.png", - LC_W: "T_X_Dpad_Left_Alt.png", - LC_E: "T_X_Dpad_Right_Alt.png", - }, - setting: { - RC_S: SettingInterfaceGamepad.Button_Action, - RC_E: SettingInterfaceGamepad.Button_Cancel, - RC_W: SettingInterfaceGamepad.Button_Cycle_Nature, - RC_N: SettingInterfaceGamepad.Button_Cycle_Variant, - START: SettingInterfaceGamepad.Button_Menu, - SELECT: SettingInterfaceGamepad.Button_Stats, - LB: SettingInterfaceGamepad.Button_Cycle_Form, - RB: SettingInterfaceGamepad.Button_Cycle_Shiny, - LT: SettingInterfaceGamepad.Button_Cycle_Gender, - RT: SettingInterfaceGamepad.Button_Cycle_Ability, - LS: SettingInterfaceGamepad.Button_Speed_Up, - RS: SettingInterfaceGamepad.Button_Slow_Down, - }, - default: { - RC_S: Button.ACTION, //5 - RC_E: Button.CANCEL, // 6 - RC_W: Button.CYCLE_NATURE, - RC_N: Button.CYCLE_VARIANT, //14 - START: Button.MENU, //7 - SELECT: Button.STATS, //8 - LB: Button.CYCLE_FORM, - RB: Button.CYCLE_SHINY, - LT: Button.CYCLE_GENDER, - RT: Button.CYCLE_ABILITY, - LS: Button.SPEED_UP, - RS: Button.SLOW_DOWN, - LC_N: Button.UP, - LC_S: Button.DOWN, - LC_W: Button.LEFT, - LC_E: Button.RIGHT, - } -}; - -export default pad_xbox360; diff --git a/src/test/cfg_keyboard_example.ts b/src/test/cfg_keyboard.example.ts similarity index 70% rename from src/test/cfg_keyboard_example.ts rename to src/test/cfg_keyboard.example.ts index c97594013..cc18e83f6 100644 --- a/src/test/cfg_keyboard_example.ts +++ b/src/test/cfg_keyboard.example.ts @@ -1,6 +1,6 @@ import {Button} from "#app/enums/buttons"; -export enum SettingInterfaceKeyboard { +export enum SettingInterface { Default_Layout = "DEFAULT_LAYOUT", Button_Up = "BUTTON_UP", Alt_Button_Up = "ALT_BUTTON_UP", @@ -202,80 +202,79 @@ const cfg_keyboard_azerty = { KEY_BACKSPACE: "T_Backspace_Alt_Key_Dark.png", KEY_ALT: "T_Alt_Key_Dark.png" }, - setting: { - KEY_ARROW_UP: SettingInterfaceKeyboard.Button_Up, - KEY_ARROW_DOWN: SettingInterfaceKeyboard.Button_Down, - KEY_ARROW_LEFT: SettingInterfaceKeyboard.Button_Left, - KEY_ARROW_RIGHT: SettingInterfaceKeyboard.Button_Right, - KEY_ENTER: SettingInterfaceKeyboard.Button_Submit, - KEY_SPACE: SettingInterfaceKeyboard.Button_Action, - KEY_BACKSPACE: SettingInterfaceKeyboard.Button_Cancel, - KEY_ESC: SettingInterfaceKeyboard.Button_Menu, - KEY_C: SettingInterfaceKeyboard.Button_Stats, - KEY_R: SettingInterfaceKeyboard.Button_Cycle_Shiny, - KEY_F: SettingInterfaceKeyboard.Button_Cycle_Form, - KEY_G: SettingInterfaceKeyboard.Button_Cycle_Gender, - KEY_E: SettingInterfaceKeyboard.Button_Cycle_Ability, - KEY_N: SettingInterfaceKeyboard.Button_Cycle_Nature, - KEY_V: SettingInterfaceKeyboard.Button_Cycle_Variant, - KEY_PLUS: SettingInterfaceKeyboard.Button_Speed_Up, - KEY_MINUS: SettingInterfaceKeyboard.Button_Slow_Down, - - KEY_Z: SettingInterfaceKeyboard.Alt_Button_Up, - KEY_S: SettingInterfaceKeyboard.Alt_Button_Down, - KEY_Q: SettingInterfaceKeyboard.Alt_Button_Left, - KEY_D: SettingInterfaceKeyboard.Alt_Button_Right, - KEY_CTRL: SettingInterfaceKeyboard.Alt_Button_Submit, - KEY_W: SettingInterfaceKeyboard.Alt_Button_Action, - KEY_X: SettingInterfaceKeyboard.Alt_Button_Cancel, - KEY_TAB: SettingInterfaceKeyboard.Alt_Button_Menu, - KEY_SHIFT: SettingInterfaceKeyboard.Alt_Button_Stats, - KEY_P: SettingInterfaceKeyboard.Alt_Button_Cycle_Shiny, - KEY_M: SettingInterfaceKeyboard.Alt_Button_Cycle_Form, - KEY_O: SettingInterfaceKeyboard.Alt_Button_Cycle_Gender, - KEY_L: SettingInterfaceKeyboard.Alt_Button_Cycle_Ability, - KEY_I: SettingInterfaceKeyboard.Alt_Button_Cycle_Nature, - KEY_K: SettingInterfaceKeyboard.Alt_Button_Cycle_Variant, - KEY_PAGE_UP: SettingInterfaceKeyboard.Alt_Button_Speed_Up, - KEY_PAGE_DOWN: SettingInterfaceKeyboard.Alt_Button_Slow_Down, + settings: { + [SettingInterface.Button_Up]: Button.UP, + [SettingInterface.Button_Down]: Button.DOWN, + [SettingInterface.Button_Left]: Button.LEFT, + [SettingInterface.Button_Right]: Button.RIGHT, + [SettingInterface.Button_Submit]: Button.SUBMIT, + [SettingInterface.Button_Action]: Button.ACTION, + [SettingInterface.Button_Cancel]: Button.CANCEL, + [SettingInterface.Button_Menu]: Button.MENU, + [SettingInterface.Button_Stats]: Button.STATS, + [SettingInterface.Button_Cycle_Shiny]: Button.CYCLE_SHINY, + [SettingInterface.Button_Cycle_Form]: Button.CYCLE_FORM, + [SettingInterface.Button_Cycle_Gender]: Button.CYCLE_GENDER, + [SettingInterface.Button_Cycle_Ability]: Button.CYCLE_ABILITY, + [SettingInterface.Button_Cycle_Nature]: Button.CYCLE_NATURE, + [SettingInterface.Button_Cycle_Variant]: Button.CYCLE_VARIANT, + [SettingInterface.Button_Speed_Up]: Button.SPEED_UP, + [SettingInterface.Button_Slow_Down]: Button.SLOW_DOWN, + [SettingInterface.Alt_Button_Up]: Button.UP, + [SettingInterface.Alt_Button_Down]: Button.DOWN, + [SettingInterface.Alt_Button_Left]: Button.LEFT, + [SettingInterface.Alt_Button_Right]: Button.RIGHT, + [SettingInterface.Alt_Button_Submit]: Button.SUBMIT, + [SettingInterface.Alt_Button_Action]: Button.ACTION, + [SettingInterface.Alt_Button_Cancel]: Button.CANCEL, + [SettingInterface.Alt_Button_Menu]: Button.MENU, + [SettingInterface.Alt_Button_Stats]: Button.STATS, + [SettingInterface.Alt_Button_Cycle_Shiny]: Button.CYCLE_SHINY, + [SettingInterface.Alt_Button_Cycle_Form]: Button.CYCLE_FORM, + [SettingInterface.Alt_Button_Cycle_Gender]: Button.CYCLE_GENDER, + [SettingInterface.Alt_Button_Cycle_Ability]: Button.CYCLE_ABILITY, + [SettingInterface.Alt_Button_Cycle_Nature]: Button.CYCLE_NATURE, + [SettingInterface.Alt_Button_Cycle_Variant]: Button.CYCLE_VARIANT, + [SettingInterface.Alt_Button_Speed_Up]: Button.SPEED_UP, + [SettingInterface.Alt_Button_Slow_Down]: 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, + KEY_ARROW_UP: SettingInterface.Button_Up, + KEY_ARROW_DOWN: SettingInterface.Button_Down, + KEY_ARROW_LEFT: SettingInterface.Button_Left, + KEY_ARROW_RIGHT: SettingInterface.Button_Right, + KEY_ENTER: SettingInterface.Button_Submit, + KEY_SPACE: SettingInterface.Button_Action, + KEY_BACKSPACE: SettingInterface.Button_Cancel, + KEY_ESC: SettingInterface.Button_Menu, + KEY_C: SettingInterface.Button_Stats, + KEY_R: SettingInterface.Button_Cycle_Shiny, + KEY_F: SettingInterface.Button_Cycle_Form, + KEY_G: SettingInterface.Button_Cycle_Gender, + KEY_E: SettingInterface.Button_Cycle_Ability, + KEY_N: SettingInterface.Button_Cycle_Nature, + KEY_V: SettingInterface.Button_Cycle_Variant, + KEY_PLUS: SettingInterface.Button_Speed_Up, + KEY_MINUS: SettingInterface.Button_Slow_Down, KEY_A: -1, KEY_B: -1, - KEY_D: Button.RIGHT, + KEY_D: SettingInterface.Alt_Button_Right, KEY_H: -1, - KEY_I: Button.CYCLE_NATURE, + KEY_I: SettingInterface.Alt_Button_Cycle_Nature, KEY_J: -1, - KEY_K: Button.CYCLE_VARIANT, - KEY_L: Button.CYCLE_ABILITY, - KEY_M: Button.CYCLE_FORM, - KEY_O: Button.CYCLE_GENDER, - KEY_P: Button.CYCLE_SHINY, - KEY_Q: Button.LEFT, - KEY_S: Button.DOWN, + KEY_K: SettingInterface.Alt_Button_Cycle_Variant, + KEY_L: SettingInterface.Alt_Button_Cycle_Ability, + KEY_M: SettingInterface.Alt_Button_Cycle_Form, + KEY_O: SettingInterface.Alt_Button_Cycle_Gender, + KEY_P: SettingInterface.Alt_Button_Cycle_Shiny, + KEY_Q: SettingInterface.Alt_Button_Left, + KEY_S: SettingInterface.Alt_Button_Down, KEY_T: -1, KEY_U: -1, - KEY_W: Button.ACTION, - KEY_X: Button.CANCEL, + KEY_W: SettingInterface.Alt_Button_Action, + KEY_X: SettingInterface.Alt_Button_Cancel, KEY_Y: -1, - KEY_Z: Button.UP, + KEY_Z: SettingInterface.Alt_Button_Up, KEY_0: -1, KEY_1: -1, KEY_2: -1, @@ -286,7 +285,7 @@ const cfg_keyboard_azerty = { KEY_7: -1, KEY_8: -1, KEY_9: -1, - KEY_CTRL: Button.SUBMIT, + KEY_CTRL: SettingInterface.Alt_Button_Submit, KEY_DEL: -1, KEY_END: -1, KEY_F1: -1, @@ -303,17 +302,19 @@ const cfg_keyboard_azerty = { KEY_F12: -1, KEY_HOME: -1, KEY_INSERT: -1, - KEY_PAGE_DOWN: Button.SLOW_DOWN, - KEY_PAGE_UP: Button.SPEED_UP, + KEY_PAGE_DOWN: SettingInterface.Alt_Button_Slow_Down, + KEY_PAGE_UP: SettingInterface.Alt_Button_Speed_Up, KEY_QUOTATION: -1, - KEY_SHIFT: Button.STATS, - KEY_TAB: Button.MENU, + KEY_SHIFT: SettingInterface.Alt_Button_Stats, + KEY_TAB: SettingInterface.Alt_Button_Menu, KEY_TILDE: -1, KEY_LEFT_BRACKET: -1, KEY_RIGHT_BRACKET: -1, KEY_SEMICOLON: -1, KEY_ALT: -1 - } + }, + main: [], + alt: [], }; export default cfg_keyboard_azerty; diff --git a/src/test/gamepad_remaping.test.ts b/src/test/gamepad_remaping.test.ts deleted file mode 100644 index 7ac8b0a05..000000000 --- a/src/test/gamepad_remaping.test.ts +++ /dev/null @@ -1,257 +0,0 @@ -import {beforeEach, expect, describe, it} from "vitest"; -import cfg_gamepad_example, {SettingInterfaceGamepad} from "./cfg_gamepad_example"; -import { - getIconWithPressedButton, - getIconWithSettingName, - getKeyAndActionFromCurrentKeysWithSettingName, - getKeyForSettingName, - getKeyFromMapping, - getKeyWithAction, regenerateCustom, - reloadCurrentKeys, - swapCurrentKeys, -} from "#app/configs/gamepad-utils"; -import {Button} from "#app/enums/buttons"; - - -function deepCopy(config) { - return JSON.parse(JSON.stringify(config)); -} - -describe('Test Keyboard', () => { - let config; - beforeEach(() => { - const temp = {...cfg_gamepad_example} - config = deepCopy(temp); - config.custom = {...config.default} - config.ogIcons = {...config.icons} - reloadCurrentKeys(config); - }); - - it('Check if config is loaded', () => { - expect(config).not.toBeNull(); - }); - it('Check key for setting name', () => { - const settingName = SettingInterfaceGamepad.Button_Action; - const key = getKeyForSettingName(config, settingName); - expect(config.custom[key]).toEqual(Button.ACTION); - }); - it('Check key for Keyboard KeyCode', () => { - const key = getKeyFromMapping(config, 0); - expect(config.custom[key]).toEqual(Button.ACTION); - }); - it('Check key for currenly Assigned to action not alt', () => { - const key = getKeyWithAction(config, Button.ACTION, false); - expect(key).toEqual('RC_S'); - }); - it('Check key for currenly Assigned to setting name', () => { - const settingName = SettingInterfaceGamepad.Button_Action; - const { key } = getKeyAndActionFromCurrentKeysWithSettingName(config, settingName); - expect(key).toEqual('RC_S'); - }); - it('Check icon for currenly Assigned to key code', () => { - const icon = getIconWithPressedButton(config, 0); - expect(icon).toEqual('T_X_A_Color_Alt.png'); - }); - it('Check icon for currenly Assigned to setting name', () => { - const settingName = SettingInterfaceGamepad.Button_Action; - const icon = getIconWithSettingName(config, settingName); - expect(icon).toEqual('T_X_A_Color_Alt.png'); - }); - - - it('Check if current keys return the same', () => { - const settingNameA = SettingInterfaceGamepad.Button_Action; - const keyA = getKeyForSettingName(config, settingNameA); - const action = config.custom[keyA]; - expect(keyA).toEqual("RC_S"); - expect(action).toEqual(Button.ACTION); - - expect(config.currentKeys[settingNameA].key).toEqual(keyA); - expect(config.currentKeys[settingNameA].action).toEqual(action); - }); - - it('Check if new swap is working', () => { - const settingNameA = SettingInterfaceGamepad.Button_Action; - swapCurrentKeys(config, settingNameA, 1); - expect(config.currentKeys[settingNameA].key).toEqual("RC_S"); - expect(config.currentKeys[settingNameA].action).toEqual(Button.CANCEL); - }); - - it('Check if new double swap is working', () => { - const settingNameA = SettingInterfaceGamepad.Button_Action; - - swapCurrentKeys(config, settingNameA, 1); - expect(config.currentKeys[settingNameA].key).toEqual("RC_S"); - expect(config.currentKeys[settingNameA].action).toEqual(Button.CANCEL); - - swapCurrentKeys(config, settingNameA, 2); - expect(config.currentKeys[settingNameA].key).toEqual("RC_S"); - expect(config.currentKeys[settingNameA].action).toEqual(Button.CYCLE_NATURE); - }); - - it('Check if new triple swap is working', () => { - const settingNameA = SettingInterfaceGamepad.Button_Action; - const settingNameB = SettingInterfaceGamepad.Button_Cancel; - const settingNameC = SettingInterfaceGamepad.Button_Cycle_Nature; - - expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].key).toEqual("RC_S"); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].action).toEqual(Button.ACTION); - - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].key).toEqual("RC_E"); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].action).toEqual(Button.CANCEL); - - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cycle_Nature].key).toEqual("RC_W"); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cycle_Nature].action).toEqual(Button.CYCLE_NATURE); - - let iconA = getIconWithSettingName(config, settingNameA); - let iconB = getIconWithSettingName(config, settingNameB); - let iconC = getIconWithSettingName(config, settingNameC); - expect(iconA).toEqual('T_X_A_Color_Alt.png'); - expect(iconB).toEqual('T_X_B_Color_Alt.png'); - expect(iconC).toEqual('T_X_X_Color_Alt.png'); - - swapCurrentKeys(config, SettingInterfaceGamepad.Button_Action, 1); // cancel - - expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].key).toEqual("RC_S"); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].action).toEqual(Button.CANCEL); - - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].key).toEqual("RC_E"); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].action).toEqual(Button.ACTION); - - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cycle_Nature].key).toEqual("RC_W"); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cycle_Nature].action).toEqual(Button.CYCLE_NATURE); - - iconA = getIconWithSettingName(config, settingNameA); - iconB = getIconWithSettingName(config, settingNameB); - iconC = getIconWithSettingName(config, settingNameC); - expect(iconA).toEqual('T_X_B_Color_Alt.png'); - expect(iconB).toEqual('T_X_A_Color_Alt.png'); - expect(iconC).toEqual('T_X_X_Color_Alt.png'); - - swapCurrentKeys(config, SettingInterfaceGamepad.Button_Cancel, 2); // cycle nature - - expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].key).toEqual("RC_S"); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].action).toEqual(Button.CANCEL); // 6 - - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].key).toEqual("RC_E"); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].action).toEqual(Button.CYCLE_NATURE); // 13 - - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cycle_Nature].key).toEqual("RC_W"); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cycle_Nature].action).toEqual(Button.ACTION); // 5 - - iconA = getIconWithSettingName(config, settingNameA); - iconB = getIconWithSettingName(config, settingNameB); - iconC = getIconWithSettingName(config, settingNameC); - expect(iconA).toEqual('T_X_B_Color_Alt.png'); - expect(iconB).toEqual('T_X_X_Color_Alt.png'); - expect(iconC).toEqual('T_X_A_Color_Alt.png'); - - swapCurrentKeys(config, SettingInterfaceGamepad.Button_Cycle_Nature, 1); // cancel - - expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].key).toEqual("RC_S"); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].action).toEqual(Button.CANCEL); - - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].key).toEqual("RC_E"); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].action).toEqual(Button.ACTION); - - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cycle_Nature].key).toEqual("RC_W"); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cycle_Nature].action).toEqual(Button.CYCLE_NATURE); - - iconA = getIconWithSettingName(config, settingNameA); - iconB = getIconWithSettingName(config, settingNameB); - iconC = getIconWithSettingName(config, settingNameC); - expect(iconA).toEqual('T_X_B_Color_Alt.png'); - expect(iconB).toEqual('T_X_A_Color_Alt.png'); - expect(iconC).toEqual('T_X_X_Color_Alt.png'); - - expect(config.ogIcons["RC_S"]).toEqual("T_X_A_Color_Alt.png") - expect(config.ogIcons["RC_E"]).toEqual("T_X_B_Color_Alt.png") - expect(config.ogIcons["RC_N"]).toEqual("T_X_Y_Color_Alt.png") - expect(config.ogIcons["RC_W"]).toEqual("T_X_X_Color_Alt.png") - }); - - it('Check 2 swap back to back', () => { - swapCurrentKeys(config, SettingInterfaceGamepad.Button_Action, 1); // cancel - expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].key).toEqual("RC_S"); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].action).toEqual(Button.CANCEL); - let iconA = getIconWithSettingName(config, SettingInterfaceGamepad.Button_Action); - expect(iconA).toEqual('T_X_B_Color_Alt.png'); - - swapCurrentKeys(config, SettingInterfaceGamepad.Button_Action, 0); // cancel - let iconB = getIconWithSettingName(config, SettingInterfaceGamepad.Button_Action); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].key).toEqual("RC_S"); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].action).toEqual(Button.ACTION); - expect(iconB).toEqual('T_X_A_Color_Alt.png'); - }); - - it('Check 4 swap back to back', () => { - expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].key).toEqual("RC_S"); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].action).toEqual(Button.ACTION); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].icon).toEqual('T_X_A_Color_Alt.png'); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].key).toEqual("RC_E"); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].action).toEqual(Button.CANCEL); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].icon).toEqual('T_X_B_Color_Alt.png'); - swapCurrentKeys(config, SettingInterfaceGamepad.Button_Action, 1); // cancel - expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].key).toEqual("RC_S"); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].action).toEqual(Button.CANCEL); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].icon).toEqual('T_X_B_Color_Alt.png'); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].key).toEqual("RC_E"); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].action).toEqual(Button.ACTION); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].icon).toEqual('T_X_A_Color_Alt.png'); - - swapCurrentKeys(config, SettingInterfaceGamepad.Button_Action, 0); // cancel - expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].key).toEqual("RC_S"); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].action).toEqual(Button.ACTION); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].icon).toEqual('T_X_A_Color_Alt.png'); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].key).toEqual("RC_E"); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].action).toEqual(Button.CANCEL); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].icon).toEqual('T_X_B_Color_Alt.png'); - - swapCurrentKeys(config, SettingInterfaceGamepad.Button_Action, 1); // cancel - expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].key).toEqual("RC_S"); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].action).toEqual(Button.CANCEL); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].icon).toEqual('T_X_B_Color_Alt.png'); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].key).toEqual("RC_E"); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].action).toEqual(Button.ACTION); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].icon).toEqual('T_X_A_Color_Alt.png'); - - swapCurrentKeys(config, SettingInterfaceGamepad.Button_Action, 0); // cancel - expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].key).toEqual("RC_S"); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].action).toEqual(Button.ACTION); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].icon).toEqual('T_X_A_Color_Alt.png'); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].key).toEqual("RC_E"); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].action).toEqual(Button.CANCEL); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].icon).toEqual('T_X_B_Color_Alt.png'); - }); - - it('Custom scenario: B icon duplicate', () => { - config.currentKeys[SettingInterfaceGamepad.Button_Action] = { - "key": "RC_S", - "action": 6, - "icon": "T_X_B_Color_Alt.png", - "from": { - "key": "RC_S", - "action": 5, - "icon": "T_X_A_Color_Alt.png" - } - }; - config.currentKeys[SettingInterfaceGamepad.Button_Cancel] = { - "key": "RC_E", - "action": 5, - "icon": "T_X_A_Color_Alt.png", - "from": { - "key": "RC_E", - "action": 6, - "icon": "T_X_B_Color_Alt.png" - } - }; - regenerateCustom(config); - swapCurrentKeys(config, SettingInterfaceGamepad.Button_Action, 0); // cancel - expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].key).toEqual("RC_S"); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].action).toEqual(Button.ACTION); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Action].icon).toEqual('T_X_A_Color_Alt.png'); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].key).toEqual("RC_E"); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].action).toEqual(Button.CANCEL); - expect(config.currentKeys[SettingInterfaceGamepad.Button_Cancel].icon).toEqual('T_X_B_Color_Alt.png'); - }) -}); \ No newline at end of file diff --git a/src/test/helpers/inGameManip.ts b/src/test/helpers/inGameManip.ts new file mode 100644 index 000000000..7b7b0fedb --- /dev/null +++ b/src/test/helpers/inGameManip.ts @@ -0,0 +1,44 @@ +import {getSettingNameWithKeycode} from "#app/configs/configHandler"; +import {expect} from "vitest"; +import {SettingInterface} from "#app/test/cfg_keyboard.example"; + +export class InGameManip { + private config; + private keycode; + private settingName; + private icon; + constructor(config) { + this.config = config; + this.keycode = null; + this.settingName = null; + this.icon = null; + } + + whenWePressOnKeyboard(keycode) { + this.keycode = Phaser.Input.Keyboard.KeyCodes[keycode.toUpperCase()]; + return this; + } + + nothingShouldHappen() { + const settingName = getSettingNameWithKeycode(this.config, this.keycode); + expect(settingName).toEqual(-1); + return this; + } + + normalizeSettingNameString(input) { + // Convert the input string to lower case + const lowerCasedInput = input.toLowerCase(); + + // Replace underscores with spaces, capitalize the first letter of each word, and join them back with underscores + const words = lowerCasedInput.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)); + const result = words.join('_'); + + return result; + } + + weShouldTriggerTheButton(settingName) { + this.settingName = SettingInterface[this.normalizeSettingNameString(settingName)]; + expect(getSettingNameWithKeycode(this.config, this.keycode)).toEqual(this.settingName); + return this; + } +} diff --git a/src/test/helpers/menuManip.ts b/src/test/helpers/menuManip.ts new file mode 100644 index 000000000..f26e0667d --- /dev/null +++ b/src/test/helpers/menuManip.ts @@ -0,0 +1,96 @@ +import {SettingInterface} from "#app/test/cfg_keyboard.example"; +import {expect} from "vitest"; +import {Button} from "#app/enums/buttons"; +import { + deleteBind, + getIconWithKey, + getIconWithKeycode, + getIconWithSettingName, + getKeyWithKeycode, getKeyWithSettingName, isAlreadyBinded, swap +} from "#app/configs/configHandler"; + +export class MenuManip { + private config; + private settingName; + private keycode; + private icon; + private iconDisplayed; + private specialCaseIcon; + + constructor(config) { + this.config = config; + this.settingName = null; + this.icon = null; + this.iconDisplayed = null; + this.specialCaseIcon = null; + } + + convertNameToButtonString(input) { + // Check if the input starts with "Alt_Button" + if (input.startsWith("Alt_Button")) { + // Return the last part in uppercase + return input.split('_').pop().toUpperCase(); + } + + // Split the input string by underscore + const parts = input.split('_'); + + // Skip the first part and join the rest with an underscore + const result = parts.slice(1).map(part => part.toUpperCase()).join('_'); + + return result; + } + + whenCursorIsOnSetting(settingName) { + this.settingName = SettingInterface[settingName]; + const buttonName = this.convertNameToButtonString(settingName); + expect(this.config.settings[this.settingName]).toEqual(Button[buttonName]); + return this; + } + + iconDisplayedIs(icon) { + this.iconDisplayed = this.config.icons[icon]; + expect(getIconWithSettingName(this.config, this.settingName)).toEqual(this.iconDisplayed); + return this; + } + + thereShouldBeNoIconAnymore() { + const icon = getIconWithSettingName(this.config, this.settingName); + expect(icon === undefined).toEqual(true); + return this; + } + + thereShouldBeNoIcon() { + return this.thereShouldBeNoIconAnymore(); + } + + weWantThisBindInstead(keycode) { + this.keycode = Phaser.Input.Keyboard.KeyCodes[keycode]; + const icon = getIconWithKeycode(this.config, this.keycode); + const key = getKeyWithKeycode(this.config, this.keycode); + const _keys = key.toLowerCase().split("_"); + const iconIdentifier = _keys[_keys.length-1]; + expect(icon.toLowerCase().includes(iconIdentifier)).toEqual(true); + return this; + } + + OopsSpecialCaseIcon(icon) { + this.specialCaseIcon = this.config.icons[icon]; + const potentialExistingKey = isAlreadyBinded(this.config, this.keycode, this.settingName); + const prev_key = potentialExistingKey || getKeyWithSettingName(this.config, this.settingName); + expect(getIconWithKey(this.config, prev_key)).toEqual(this.specialCaseIcon); + return this; + } + + whenWeDelete(settingName?: string) { + this.settingName = SettingInterface[settingName] || this.settingName; + const key = getKeyWithSettingName(this.config, this.settingName); + deleteBind(this.config, this.settingName); + expect(this.config.custom[key]).toEqual(-1); + return this; + } + + confirm() { + swap(this.config, this.settingName, this.keycode); + } +} diff --git a/src/test/keyboard_remaping.test.ts b/src/test/keyboard_remaping.test.ts deleted file mode 100644 index a48afb932..000000000 --- a/src/test/keyboard_remaping.test.ts +++ /dev/null @@ -1,770 +0,0 @@ -import {beforeEach, expect, describe, it} from "vitest"; -import cfg_keyboard_example, {SettingInterfaceKeyboard} from "#app/test/cfg_keyboard_example"; -import { - deleteBind, - getIconWithPressedButton, - getIconWithSettingName, - getKeyAndActionFromCurrentKeysWithSettingName, - getKeyForSettingName, - getKeyFromMapping, - getKeyWithAction, regenerateCustom, - reloadCurrentKeys, - swapCurrentKeys, -} from "#app/configs/gamepad-utils"; -import {Button} from "#app/enums/buttons"; - - -function deepCopy(config) { - return JSON.parse(JSON.stringify(config)); -} - - -describe('Test Keyboard', () => { - let config; - beforeEach(() => { - config = deepCopy(cfg_keyboard_example); - config.custom = {...config.default} - config.ogIcons = {...config.icons} - reloadCurrentKeys(config); - }); - - it('Check if config is loaded', () => { - expect(config).not.toBeNull(); - }); - it('Check key for setting name', () => { - const settingName = SettingInterfaceKeyboard.Button_Left; - const key = getKeyForSettingName(config, settingName); - expect(config.custom[key]).toEqual(Button.LEFT); - }); - it('Check key for Keyboard KeyCode', () => { - const key = getKeyFromMapping(config, Phaser.Input.Keyboard.KeyCodes.LEFT); - expect(config.custom[key]).toEqual(Button.LEFT); - }); - it('Check key for currenly Assigned to action not alt', () => { - const key = getKeyWithAction(config, Button.LEFT, false); - expect(key).toEqual('KEY_ARROW_LEFT'); - }); - it('Check key for currenly Assigned to action alt', () => { - const key = getKeyWithAction(config, Button.LEFT, true); - expect(key).toEqual('KEY_Q'); - }); - it('Check key for currenly Assigned to setting name', () => { - const settingName = SettingInterfaceKeyboard.Button_Left; - const {key} = getKeyAndActionFromCurrentKeysWithSettingName(config, settingName); - expect(key).toEqual('KEY_ARROW_LEFT'); - }); - it('Check key for currenly Assigned to setting name alt', () => { - const settingName = SettingInterfaceKeyboard.Alt_Button_Left; - const {key} = getKeyAndActionFromCurrentKeysWithSettingName(config, settingName); - expect(key).toEqual('KEY_Q'); - }); - it('Check icon for currenly Assigned to key code', () => { - const icon = getIconWithPressedButton(config, Phaser.Input.Keyboard.KeyCodes.LEFT); - expect(icon).toEqual('T_Left_Key_Dark.png'); - }); - it('Check icon for currenly Assigned to key code alt', () => { - const icon = getIconWithPressedButton(config, Phaser.Input.Keyboard.KeyCodes.Q); - expect(icon).toEqual('T_Q_Key_Dark.png'); - }); - it('Check icon for currenly Assigned to setting name', () => { - const settingName = SettingInterfaceKeyboard.Button_Left; - const icon = getIconWithSettingName(config, settingName); - expect(icon).toEqual('T_Left_Key_Dark.png'); - }); - it('Check icon for currenly Assigned to setting name alt', () => { - const settingName = SettingInterfaceKeyboard.Alt_Button_Left; - const icon = getIconWithSettingName(config, settingName); - expect(icon).toEqual('T_Q_Key_Dark.png'); - }); - - - it('Check if current keys return the same', () => { - const settingNameA = SettingInterfaceKeyboard.Button_Left; - const keyA = getKeyForSettingName(config, settingNameA); - const action = config.custom[keyA]; - expect(keyA).toEqual("KEY_ARROW_LEFT"); - expect(action).toEqual(Button.LEFT); - - expect(config.currentKeys[settingNameA].key).toEqual(keyA); - expect(config.currentKeys[settingNameA].action).toEqual(action); - }); - - it('Check if new swap is working', () => { - const settingNameA = SettingInterfaceKeyboard.Button_Left; - swapCurrentKeys(config, settingNameA, Phaser.Input.Keyboard.KeyCodes.RIGHT); - expect(config.currentKeys[settingNameA].key).toEqual("KEY_ARROW_LEFT"); - expect(config.currentKeys[settingNameA].action).toEqual(Button.RIGHT); - }); - - it('Check if new double swap is working', () => { - const settingNameA = SettingInterfaceKeyboard.Button_Left; - - swapCurrentKeys(config, settingNameA, Phaser.Input.Keyboard.KeyCodes.RIGHT); - expect(config.currentKeys[settingNameA].key).toEqual("KEY_ARROW_LEFT"); - expect(config.currentKeys[settingNameA].action).toEqual(Button.RIGHT); - - swapCurrentKeys(config, settingNameA, Phaser.Input.Keyboard.KeyCodes.UP); - expect(config.currentKeys[settingNameA].key).toEqual("KEY_ARROW_LEFT"); - expect(config.currentKeys[settingNameA].action).toEqual(Button.UP); - }); - - it('Check if new triple swap is working', () => { - const settingNameA = SettingInterfaceKeyboard.Button_Left; - const settingNameB = SettingInterfaceKeyboard.Button_Action; - const settingNameC = SettingInterfaceKeyboard.Button_Right; - const settingNameD = SettingInterfaceKeyboard.Button_Up; - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].key).toEqual("KEY_ARROW_LEFT"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].action).toEqual(Button.LEFT); - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Action].key).toEqual("KEY_SPACE"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Action].action).toEqual(Button.ACTION); - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].key).toEqual("KEY_ARROW_RIGHT"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].action).toEqual(Button.RIGHT); - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].key).toEqual("KEY_ARROW_UP"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].action).toEqual(Button.UP); - - let iconA = getIconWithSettingName(config, settingNameA); - let iconB = getIconWithSettingName(config, settingNameB); - let iconC = getIconWithSettingName(config, settingNameC); - let iconD = getIconWithSettingName(config, settingNameD); - expect(iconA).toEqual('T_Left_Key_Dark.png'); - expect(iconB).toEqual('T_Space_Key_Dark.png'); - expect(iconC).toEqual('T_Right_Key_Dark.png'); - expect(iconD).toEqual('T_Up_Key_Dark.png'); - - swapCurrentKeys(config, SettingInterfaceKeyboard.Button_Left, Phaser.Input.Keyboard.KeyCodes.RIGHT); // left->RIGHT, right->LEFT - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].key).toEqual("KEY_ARROW_LEFT"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].action).toEqual(Button.RIGHT); - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Action].key).toEqual("KEY_SPACE"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Action].action).toEqual(Button.ACTION); - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].key).toEqual("KEY_ARROW_RIGHT"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].action).toEqual(Button.LEFT); - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].key).toEqual("KEY_ARROW_UP"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].action).toEqual(Button.UP); - - iconA = getIconWithSettingName(config, settingNameA); - iconB = getIconWithSettingName(config, settingNameB); - iconC = getIconWithSettingName(config, settingNameC); - iconD = getIconWithSettingName(config, settingNameD); - expect(iconA).toEqual('T_Right_Key_Dark.png'); - expect(iconB).toEqual('T_Space_Key_Dark.png'); - expect(iconC).toEqual('T_Left_Key_Dark.png'); - expect(iconD).toEqual('T_Up_Key_Dark.png'); - - swapCurrentKeys(config, SettingInterfaceKeyboard.Button_Action, Phaser.Input.Keyboard.KeyCodes.UP); // action->UP - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].key).toEqual("KEY_ARROW_LEFT"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].action).toEqual(Button.RIGHT); - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Action].key).toEqual("KEY_SPACE"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Action].action).toEqual(Button.UP) - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].key).toEqual("KEY_ARROW_RIGHT"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].action).toEqual(Button.LEFT); - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].key).toEqual("KEY_ARROW_UP"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].action).toEqual(Button.ACTION); - - iconA = getIconWithSettingName(config, settingNameA); - iconB = getIconWithSettingName(config, settingNameB); - iconC = getIconWithSettingName(config, settingNameC); - iconD = getIconWithSettingName(config, settingNameD); - expect(iconA).toEqual('T_Right_Key_Dark.png'); - expect(iconB).toEqual('T_Up_Key_Dark.png'); - expect(iconC).toEqual('T_Left_Key_Dark.png'); - expect(iconD).toEqual('T_Space_Key_Dark.png'); - - swapCurrentKeys(config, SettingInterfaceKeyboard.Button_Right, Phaser.Input.Keyboard.KeyCodes.UP); // right->UP, action->LEFT - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].key).toEqual("KEY_ARROW_LEFT"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].action).toEqual(Button.RIGHT); - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Action].key).toEqual("KEY_SPACE"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Action].action).toEqual(Button.UP) - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].key).toEqual("KEY_ARROW_RIGHT"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].action).toEqual(Button.ACTION); - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].key).toEqual("KEY_ARROW_UP"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].action).toEqual(Button.LEFT); - - iconA = getIconWithSettingName(config, settingNameA); - iconB = getIconWithSettingName(config, settingNameB); - iconC = getIconWithSettingName(config, settingNameC); - iconD = getIconWithSettingName(config, settingNameD); - expect(iconA).toEqual('T_Right_Key_Dark.png'); - expect(iconB).toEqual('T_Up_Key_Dark.png'); - expect(iconC).toEqual('T_Space_Key_Dark.png'); - expect(iconD).toEqual('T_Left_Key_Dark.png'); - }); - - - it('Swap alt with another main', () => { - const settingNameA = SettingInterfaceKeyboard.Button_Left; - const settingNameB = SettingInterfaceKeyboard.Alt_Button_Right; - const settingNameC = SettingInterfaceKeyboard.Button_Up; - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].key).toEqual("KEY_ARROW_LEFT"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].action).toEqual(Button.LEFT); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].icon).toEqual("T_Left_Key_Dark.png"); - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].key).toEqual("KEY_D"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].action).toEqual(Button.RIGHT); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].icon).toEqual("T_D_Key_Dark.png"); - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].key).toEqual("KEY_ARROW_UP"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].icon).toEqual("T_Up_Key_Dark.png"); - - swapCurrentKeys(config, SettingInterfaceKeyboard.Button_Left, Phaser.Input.Keyboard.KeyCodes.D); - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].key).toEqual("KEY_ARROW_LEFT"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].action).toEqual(Button.RIGHT); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].icon).toEqual("T_D_Key_Dark.png"); - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Left].key).toEqual("KEY_Q"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Left].action).toEqual(Button.LEFT); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Left].icon).toEqual("T_Q_Key_Dark.png"); - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].key).toEqual("KEY_ARROW_RIGHT"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].action).toEqual(Button.RIGHT); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].icon).toEqual("T_Right_Key_Dark.png"); - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].key).toEqual("KEY_D"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].action).toEqual(Button.LEFT); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].icon).toEqual("T_Left_Key_Dark.png"); - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].key).toEqual("KEY_ARROW_UP"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].icon).toEqual("T_Up_Key_Dark.png"); - - swapCurrentKeys(config, SettingInterfaceKeyboard.Button_Up, Phaser.Input.Keyboard.KeyCodes.LEFT); - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].key).toEqual("KEY_ARROW_LEFT"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Left].icon).toEqual("T_Up_Key_Dark.png"); - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Left].key).toEqual("KEY_Q"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Left].action).toEqual(Button.LEFT); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Left].icon).toEqual("T_Q_Key_Dark.png"); - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].key).toEqual("KEY_ARROW_RIGHT"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].action).toEqual(Button.RIGHT); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].icon).toEqual("T_Right_Key_Dark.png"); - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].key).toEqual("KEY_D"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].action).toEqual(Button.LEFT); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].icon).toEqual("T_Left_Key_Dark.png"); - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].key).toEqual("KEY_ARROW_UP"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].action).toEqual(Button.RIGHT); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].icon).toEqual("T_D_Key_Dark.png"); - }) - - - it('Swap alt with a key not binded yet', () => { - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); - - swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Up, Phaser.Input.Keyboard.KeyCodes.B); - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_B_Key_Dark.png"); - }) - - - it('Delete bind', () => { - const settingNameA = SettingInterfaceKeyboard.Alt_Button_Up; - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); - deleteBind(config, settingNameA) - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(-1); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual(undefined); - }) - - - it('Delete bind then asign not existing button', () => { - const settingNameA = SettingInterfaceKeyboard.Alt_Button_Up; - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); - expect(config.custom["KEY_Z"]).toEqual(Button.UP); - deleteBind(config, settingNameA) - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(-1); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual(undefined); - expect(config.custom["KEY_Z"]).toEqual(-1); - - swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Up, Phaser.Input.Keyboard.KeyCodes.B); - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_B_Key_Dark.png"); - expect(config.custom["KEY_B"]).toEqual(Button.UP); - expect(config.custom["KEY_Z"]).toEqual(-1); - }) - - - it('swap bind, then Delete bind then assign bind', () => { - const settingNameA = SettingInterfaceKeyboard.Alt_Button_Up; - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); - expect(config.custom["KEY_Z"]).toEqual(Button.UP); - expect(config.custom["KEY_B"]).toEqual(-1); - - swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Up, Phaser.Input.Keyboard.KeyCodes.B); - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_B_Key_Dark.png"); - expect(config.custom["KEY_B"]).toEqual(Button.UP); - expect(config.custom["KEY_Z"]).toEqual(-1); - - deleteBind(config, settingNameA); - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(-1); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual(undefined); - expect(config.custom["KEY_Z"]).toEqual(-1); - expect(config.custom["KEY_B"]).toEqual(-1); - - swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Up, Phaser.Input.Keyboard.KeyCodes.B); - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_B_Key_Dark.png"); - expect(config.custom["KEY_B"]).toEqual(Button.UP); - expect(config.custom["KEY_Z"]).toEqual(-1); - }) - - - it('Delete bind then asign not already existing button', () => { - const settingNameA = SettingInterfaceKeyboard.Alt_Button_Up; - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); - expect(config.custom["KEY_Z"]).toEqual(Button.UP); - deleteBind(config, settingNameA) - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(-1); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual(undefined); - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Cycle_Ability].key).toEqual("KEY_L"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Cycle_Ability].action).toEqual(Button.CYCLE_ABILITY); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Cycle_Ability].icon).toEqual("T_L_Key_Dark.png"); - expect(config.custom["KEY_Z"]).toEqual(-1); - expect(config.custom["KEY_L"]).toEqual(Button.CYCLE_ABILITY); - - swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Up, Phaser.Input.Keyboard.KeyCodes.L); - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_L_Key_Dark.png"); - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Cycle_Ability].key).toEqual("KEY_L"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Cycle_Ability].action).toEqual(-1); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Cycle_Ability].icon).toEqual(undefined); - - expect(config.custom["KEY_Z"]).toEqual(-1); - expect(config.custom["KEY_L"]).toEqual(Button.UP); - }) - - - it('Custom scenario 2, regenerate customs when init key is not from setting', () => { - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); - expect(config.custom["KEY_Z"]).toEqual(Button.UP); - expect(config.custom["KEY_T"]).toEqual(-1); - - swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Up, Phaser.Input.Keyboard.KeyCodes.T); - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_T_Key_Dark.png"); - - expect(config.custom["KEY_Z"]).toEqual(-1); - expect(config.custom["KEY_T"]).toEqual(Button.UP); - }) - - - it('change alt to unknown touch than another one alt with another unknown touch', () => { - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); - expect(config.custom["KEY_Z"]).toEqual(Button.UP); - - swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Up, Phaser.Input.Keyboard.KeyCodes.T); - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_T_Key_Dark.png"); - - expect(config.custom["KEY_Z"]).toEqual(-1); - expect(config.custom["KEY_T"]).toEqual(Button.UP); - - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].key).toEqual("KEY_S"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].action).toEqual(Button.DOWN); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].icon).toEqual("T_S_Key_Dark.png"); - - swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Down, Phaser.Input.Keyboard.KeyCodes.U); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].key).toEqual("KEY_S"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].action).toEqual(Button.DOWN); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].icon).toEqual("T_U_Key_Dark.png"); - - - expect(config.custom["KEY_S"]).toEqual(-1); - expect(config.custom["KEY_U"]).toEqual(Button.DOWN); - expect(config.custom["KEY_Z"]).toEqual(-1); - expect(config.custom["KEY_T"]).toEqual(Button.UP); - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_T_Key_Dark.png"); - }) - - - it('reload scenario with 1 bind already reassigned', () => { - config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up] = { - "key": "KEY_Z", - "isAlt": true, - "action": 3, - "icon": "T_D_Key_Dark.png", - "from": { - "key": "KEY_Z", - "isAlt": true, - "action": 0, - "icon": "T_Z_Key_Dark.png" - } - }; - config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right] = { - "key": "KEY_D", - "isAlt": true, - "action": 0, - "icon": "T_Z_Key_Dark.png", - "from": { - "key": "KEY_D", - "isAlt": true, - "action": 3, - "icon": "T_D_Key_Dark.png" - } - } - config.icons["KEY_D"] = "T_Z_Key_Dark.png"; - config.icons["KEY_Z"] = "T_D_Key_Dark.png"; - regenerateCustom(config); - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.RIGHT); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_D_Key_Dark.png"); - expect(config.custom["KEY_Z"]).toEqual(Button.RIGHT); - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].key).toEqual("KEY_D"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].icon).toEqual("T_Z_Key_Dark.png"); - expect(config.custom["KEY_D"]).toEqual(Button.UP); - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].key).toEqual("KEY_ARROW_UP"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].icon).toEqual("T_Up_Key_Dark.png"); - expect(config.custom["KEY_ARROW_UP"]).toEqual(Button.UP); - - swapCurrentKeys(config, SettingInterfaceKeyboard.Button_Up, Phaser.Input.Keyboard.KeyCodes.RIGHT); - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.RIGHT); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_D_Key_Dark.png"); - expect(config.custom["KEY_Z"]).toEqual(Button.RIGHT); - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].key).toEqual("KEY_D"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].icon).toEqual("T_Z_Key_Dark.png"); - expect(config.custom["KEY_D"]).toEqual(Button.UP); - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].key).toEqual("KEY_ARROW_UP"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].action).toEqual(Button.RIGHT); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].icon).toEqual("T_Right_Key_Dark.png"); - expect(config.custom["KEY_ARROW_UP"]).toEqual(Button.RIGHT); - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].key).toEqual("KEY_ARROW_RIGHT"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].icon).toEqual("T_Up_Key_Dark.png"); - expect(config.custom["KEY_ARROW_RIGHT"]).toEqual(Button.UP); - }); - - - it('Swap multiple touch alt and main', () => { - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].key).toEqual("KEY_ARROW_UP"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].icon).toEqual("T_Up_Key_Dark.png"); - expect(config.custom["KEY_ARROW_UP"]).toEqual(Button.UP); - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); - expect(config.custom["KEY_Z"]).toEqual(Button.UP); - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].key).toEqual("KEY_ARROW_RIGHT"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].action).toEqual(Button.RIGHT); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].icon).toEqual("T_Right_Key_Dark.png"); - expect(config.custom["KEY_ARROW_RIGHT"]).toEqual(Button.RIGHT); - - swapCurrentKeys(config, SettingInterfaceKeyboard.Button_Up, Phaser.Input.Keyboard.KeyCodes.RIGHT); - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].key).toEqual("KEY_ARROW_UP"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].action).toEqual(Button.RIGHT); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].icon).toEqual("T_Right_Key_Dark.png"); - expect(config.custom["KEY_ARROW_UP"]).toEqual(Button.RIGHT); - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); - expect(config.custom["KEY_Z"]).toEqual(Button.UP); - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].key).toEqual("KEY_ARROW_RIGHT"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].icon).toEqual("T_Up_Key_Dark.png"); - expect(config.custom["KEY_ARROW_RIGHT"]).toEqual(Button.UP); - - swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Up, Phaser.Input.Keyboard.KeyCodes.D); - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].key).toEqual("KEY_ARROW_UP"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].action).toEqual(Button.RIGHT); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].icon).toEqual("T_Right_Key_Dark.png"); - expect(config.custom["KEY_ARROW_UP"]).toEqual(Button.RIGHT); - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.RIGHT); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_D_Key_Dark.png"); - expect(config.custom["KEY_Z"]).toEqual(Button.RIGHT); - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].key).toEqual("KEY_ARROW_RIGHT"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].icon).toEqual("T_Up_Key_Dark.png"); - expect(config.custom["KEY_ARROW_RIGHT"]).toEqual(Button.UP); - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].key).toEqual("KEY_D"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].icon).toEqual("T_Z_Key_Dark.png"); - expect(config.custom["KEY_D"]).toEqual(Button.UP); - - swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Up, Phaser.Input.Keyboard.KeyCodes.Z); - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].key).toEqual("KEY_ARROW_UP"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].action).toEqual(Button.RIGHT); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Up].icon).toEqual("T_Right_Key_Dark.png"); - expect(config.custom["KEY_ARROW_UP"]).toEqual(Button.RIGHT); - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); - expect(config.custom["KEY_Z"]).toEqual(Button.UP); - - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].key).toEqual("KEY_ARROW_RIGHT"); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Button_Right].icon).toEqual("T_Up_Key_Dark.png"); - expect(config.custom["KEY_ARROW_RIGHT"]).toEqual(Button.UP); - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].key).toEqual("KEY_D"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].action).toEqual(Button.RIGHT); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Right].icon).toEqual("T_D_Key_Dark.png"); - expect(config.custom["KEY_D"]).toEqual(Button.RIGHT); - }) - - it('by method: Delete 2 binds, than rebind one of them', () => { - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); - expect(config.custom["KEY_Z"]).toEqual(Button.UP); - deleteBind(config, SettingInterfaceKeyboard.Alt_Button_Up); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(-1); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual(undefined); - expect(config.custom["KEY_Z"]).toEqual(-1); - - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].key).toEqual("KEY_S"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].action).toEqual(Button.DOWN); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].icon).toEqual("T_S_Key_Dark.png"); - expect(config.custom["KEY_S"]).toEqual(Button.DOWN); - deleteBind(config, SettingInterfaceKeyboard.Alt_Button_Down); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].key).toEqual("KEY_S"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].action).toEqual(-1); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].icon).toEqual(undefined); - expect(config.custom["KEY_S"]).toEqual(-1); - - - swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Up, Phaser.Input.Keyboard.KeyCodes.Z); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); - expect(config.custom["KEY_Z"]).toEqual(Button.UP); - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].key).toEqual("KEY_S"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].action).toEqual(-1); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].icon).toEqual(undefined); - expect(config.custom["KEY_S"]).toEqual(-1); - - - swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Down, Phaser.Input.Keyboard.KeyCodes.S); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); - expect(config.custom["KEY_Z"]).toEqual(Button.UP); - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].key).toEqual("KEY_S"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].action).toEqual(Button.DOWN); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].icon).toEqual("T_S_Key_Dark.png"); - expect(config.custom["KEY_S"]).toEqual(Button.DOWN); - - let keyDown = Phaser.Input.Keyboard.KeyCodes.S; - let key = getKeyFromMapping(config, keyDown); - let buttonDown = config.custom[key]; - expect(buttonDown).toEqual(Button.DOWN); - - keyDown = Phaser.Input.Keyboard.KeyCodes.Z; - key = getKeyFromMapping(config, keyDown); - buttonDown = config.custom[key]; - expect(buttonDown).toEqual(Button.UP); - - }); - - it("test keyboard listener", () => { - const keyDown = Phaser.Input.Keyboard.KeyCodes.S; - const key = getKeyFromMapping(config, keyDown); - const buttonDown = config.custom[key]; - expect(buttonDown).toEqual(Button.DOWN); - }); - - it("another test with 2 delete", () => { - config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up] = { - "key": "KEY_Z", - "isAlt": true, - "action": -1, - "from": { - "key": "KEY_Z", - "isAlt": true, - "action": 0, - "icon": "T_Z_Key_Dark.png" - }, - "latestIsDeleted": true - }; - regenerateCustom(config); - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(-1); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual(undefined); - expect(config.custom["KEY_Z"]).toEqual(-1); - - swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Down, Phaser.Input.Keyboard.KeyCodes.S); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].key).toEqual("KEY_S"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].action).toEqual(Button.DOWN); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Down].icon).toEqual("T_S_Key_Dark.png"); - expect(config.custom["KEY_S"]).toEqual(Button.DOWN); - }); - - it("another test with 2 delete", () => { - config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up] = { - "key": "KEY_Z", - "isAlt": true, - "action": -1, - "from": { - "key": "KEY_Z", - "isAlt": true, - "action": 0, - "icon": "T_Z_Key_Dark.png" - }, - "latestIsDeleted": true - }; - regenerateCustom(config); - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(-1); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual(undefined); - expect(config.custom["KEY_Z"]).toEqual(-1); - - swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Up, Phaser.Input.Keyboard.KeyCodes.Z); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); - expect(config.custom["KEY_Z"]).toEqual(Button.UP); - - let keyDown = Phaser.Input.Keyboard.KeyCodes.S; - let key = getKeyFromMapping(config, keyDown); - let buttonDown = config.custom[key]; - expect(buttonDown).toEqual(Button.DOWN); - - keyDown = Phaser.Input.Keyboard.KeyCodes.Z; - key = getKeyFromMapping(config, keyDown); - buttonDown = config.custom[key]; - expect(buttonDown).toEqual(Button.UP); - }); - - it("another test with 2 delete part 2", () => { - config.currentKeys = { - "ALT_BUTTON_UP": { - "key": "KEY_Z", - "isAlt": true, - "action": -1, - "from": { - "key": "KEY_Z", - "isAlt": true, - "action": 0, - "icon": "T_Z_Key_Dark.png" - }, - "latestIsDeleted": true - }, - "ALT_BUTTON_DOWN": { - "key": "KEY_S", - "isAlt": true, - "action": -1, - "from": { - "key": "KEY_S", - "isAlt": true, - "action": 1, - "icon": "T_S_Key_Dark.png" - }, - "latestIsDeleted": true - }, - } - regenerateCustom(config); - - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(-1); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual(undefined); - expect(config.custom["KEY_Z"]).toEqual(-1); - - swapCurrentKeys(config, SettingInterfaceKeyboard.Alt_Button_Up, Phaser.Input.Keyboard.KeyCodes.Z); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].key).toEqual("KEY_Z"); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].action).toEqual(Button.UP); - expect(config.currentKeys[SettingInterfaceKeyboard.Alt_Button_Up].icon).toEqual("T_Z_Key_Dark.png"); - expect(config.custom["KEY_Z"]).toEqual(Button.UP); - - const keyDown = Phaser.Input.Keyboard.KeyCodes.Z; - const key = getKeyFromMapping(config, keyDown); - const buttonDown = config.custom[key]; - expect(buttonDown).toEqual(Button.UP); -}); - -}) -; \ No newline at end of file diff --git a/src/test/rebinding_setting.test.ts b/src/test/rebinding_setting.test.ts new file mode 100644 index 000000000..b73e2c7d0 --- /dev/null +++ b/src/test/rebinding_setting.test.ts @@ -0,0 +1,311 @@ +import {afterEach, beforeEach, describe, expect, it} from "vitest"; +import cfg_keyboard_azerty from "#app/test/cfg_keyboard.example"; +import {SettingInterface} from "#app/test/cfg_keyboard.example"; +import {Button} from "#app/enums/buttons"; +import {deepCopy} from "#app/utils"; +import { + getButtonWithSettingName, + getIconWithSettingName, + getKeyWithKeycode, + getKeyWithSettingName, + getSettingNameWithKeycode, + regenerateIdentifiers, + swap +} from "#app/configs/configHandler"; +import {MenuManip} from "#app/test/helpers/menuManip"; +import {InGameManip} from "#app/test/helpers/inGameManip"; + + +describe('Test Rebinding', () => { + let config; + let inGame; + let inTheSettingMenu; + beforeEach(() => { + config = deepCopy(cfg_keyboard_azerty); + config.custom = {...config.default} + regenerateIdentifiers(config); + inGame = new InGameManip(config); + inTheSettingMenu = new MenuManip(config); + }); + + it('Check if config is loaded', () => { + expect(config).not.toBeNull(); + }); + it('Check button for setting name', () => { + const settingName = SettingInterface.Button_Left; + const button = config.settings[settingName]; + expect(button).toEqual(Button.LEFT); + }); + it('Check key for Keyboard KeyCode', () => { + const key = getKeyWithKeycode(config, Phaser.Input.Keyboard.KeyCodes.LEFT); + const settingName = config.custom[key]; + const button = config.settings[settingName]; + expect(button).toEqual(Button.LEFT); + }); + it('Check key for currenly Assigned to action not alt', () => { + const key = getKeyWithKeycode(config, Phaser.Input.Keyboard.KeyCodes.Q); + const settingName = config.custom[key]; + const button = config.settings[settingName]; + expect(button).toEqual(Button.LEFT); + }); + + it('Check key for currenly Assigned to setting name', () => { + const settingName = SettingInterface.Button_Left; + const key = getKeyWithSettingName(config, settingName); + expect(key).toEqual('KEY_ARROW_LEFT'); + }); + it('Check key for currenly Assigned to setting name alt', () => { + const settingName = SettingInterface.Alt_Button_Left; + const key = getKeyWithSettingName(config, settingName); + expect(key).toEqual('KEY_Q'); + }); + it('Check key from key code', () => { + const keycode = Phaser.Input.Keyboard.KeyCodes.LEFT; + const key = getKeyWithKeycode(config, keycode); + expect(key).toEqual('KEY_ARROW_LEFT'); + }); + it('Check icon for currenly Assigned to key code', () => { + const keycode = Phaser.Input.Keyboard.KeyCodes.LEFT; + const key = getKeyWithKeycode(config, keycode); + const icon = config.icons[key]; + expect(icon).toEqual('T_Left_Key_Dark.png'); + }); + it('Check icon for currenly Assigned to key code', () => { + const keycode = Phaser.Input.Keyboard.KeyCodes.Q; + const key = getKeyWithKeycode(config, keycode); + const icon = config.icons[key]; + expect(icon).toEqual('T_Q_Key_Dark.png'); + }); + it('Check icon for currenly Assigned to setting name', () => { + const settingName = SettingInterface.Button_Left; + const key = getKeyWithSettingName(config, settingName); + const icon = config.icons[key]; + expect(icon).toEqual('T_Left_Key_Dark.png'); + }); + it('Check icon for currenly Assigned to setting name alt', () => { + const settingName = SettingInterface.Alt_Button_Left; + const key = getKeyWithSettingName(config, settingName); + const icon = config.icons[key]; + expect(icon).toEqual('T_Q_Key_Dark.png'); + }); + it('Check if is working', () => { + const settingNameA = SettingInterface.Button_Left; + const settingNameB = SettingInterface.Button_Right; + swap(config, SettingInterface.Button_Left, Phaser.Input.Keyboard.KeyCodes.RIGHT); + expect(getButtonWithSettingName(config, settingNameA)).toEqual(Button.LEFT); + expect(getSettingNameWithKeycode(config, Phaser.Input.Keyboard.KeyCodes.RIGHT)).toEqual(SettingInterface.Button_Left) + expect(getButtonWithSettingName(config, settingNameB)).toEqual(Button.RIGHT); + expect(getSettingNameWithKeycode(config, Phaser.Input.Keyboard.KeyCodes.LEFT)).toEqual(SettingInterface.Button_Right) + expect(getIconWithSettingName(config, settingNameA)).toEqual(config.icons.KEY_ARROW_RIGHT); + expect(getIconWithSettingName(config, settingNameB)).toEqual(config.icons.KEY_ARROW_LEFT); + }); + + it('Check if double swap is working', () => { + inGame.whenWePressOnKeyboard("LEFT").weShouldTriggerTheButton("Button_Left"); + inGame.whenWePressOnKeyboard("RIGHT").weShouldTriggerTheButton("Button_Right"); + inGame.whenWePressOnKeyboard("UP").weShouldTriggerTheButton("Button_Up"); + + inTheSettingMenu.whenCursorIsOnSetting("Button_Left").iconDisplayedIs("KEY_ARROW_LEFT").weWantThisBindInstead("RIGHT").confirm(); + + inGame.whenWePressOnKeyboard("LEFT").weShouldTriggerTheButton("Button_Right"); + inGame.whenWePressOnKeyboard("RIGHT").weShouldTriggerTheButton("Button_Left"); + inGame.whenWePressOnKeyboard("UP").weShouldTriggerTheButton("Button_Up"); + + inTheSettingMenu.whenCursorIsOnSetting("Button_Left").iconDisplayedIs("KEY_ARROW_RIGHT").weWantThisBindInstead("UP").confirm(); + + inGame.whenWePressOnKeyboard("LEFT").weShouldTriggerTheButton("Button_Right"); + inGame.whenWePressOnKeyboard("RIGHT").weShouldTriggerTheButton("Button_Up"); + inGame.whenWePressOnKeyboard("UP").weShouldTriggerTheButton("Button_Left"); + + inTheSettingMenu.whenCursorIsOnSetting("Button_Left").iconDisplayedIs("KEY_ARROW_UP").weWantThisBindInstead("RIGHT").confirm(); + + inGame.whenWePressOnKeyboard("LEFT").weShouldTriggerTheButton("Button_Right"); + inGame.whenWePressOnKeyboard("RIGHT").weShouldTriggerTheButton("Button_Left"); + inGame.whenWePressOnKeyboard("UP").weShouldTriggerTheButton("Button_Up"); + }); + + it('Check if triple swap is working', () => { + inTheSettingMenu.whenCursorIsOnSetting("Button_Left").iconDisplayedIs("KEY_ARROW_LEFT").weWantThisBindInstead("RIGHT").confirm(); + + inGame.whenWePressOnKeyboard("LEFT").weShouldTriggerTheButton("Button_Right"); + inGame.whenWePressOnKeyboard("RIGHT").weShouldTriggerTheButton("Button_Left"); + inGame.whenWePressOnKeyboard("UP").weShouldTriggerTheButton("Button_Up"); + + inTheSettingMenu.whenCursorIsOnSetting("Button_Right").iconDisplayedIs("KEY_ARROW_LEFT").weWantThisBindInstead("UP").confirm(); + inGame.whenWePressOnKeyboard("LEFT").weShouldTriggerTheButton("Button_Up"); + inGame.whenWePressOnKeyboard("RIGHT").weShouldTriggerTheButton("Button_Left"); + inGame.whenWePressOnKeyboard("UP").weShouldTriggerTheButton("Button_Right"); + + inTheSettingMenu.whenCursorIsOnSetting("Button_Left").iconDisplayedIs("KEY_ARROW_RIGHT").weWantThisBindInstead("LEFT").confirm(); + inGame.whenWePressOnKeyboard("LEFT").weShouldTriggerTheButton("Button_Left"); + inGame.whenWePressOnKeyboard("RIGHT").weShouldTriggerTheButton("Button_Up"); + inGame.whenWePressOnKeyboard("UP").weShouldTriggerTheButton("Button_Right"); + }); + + it('Swap alt with another main', () => { + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Alt_Button_Right"); + inGame.whenWePressOnKeyboard("LEFT").weShouldTriggerTheButton("Button_Left"); + inGame.whenWePressOnKeyboard("Q").weShouldTriggerTheButton("Alt_Button_Left"); + inTheSettingMenu.whenCursorIsOnSetting("Button_Left").iconDisplayedIs("KEY_ARROW_LEFT").weWantThisBindInstead("D").OopsSpecialCaseIcon("KEY_Q").confirm(); + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Alt_Button_Left"); + inGame.whenWePressOnKeyboard("LEFT").weShouldTriggerTheButton("Button_Left"); + inGame.whenWePressOnKeyboard("Q").weShouldTriggerTheButton("Alt_Button_Right"); + }); + + it('multiple Swap alt with another main', () => { + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Alt_Button_Right"); + inGame.whenWePressOnKeyboard("LEFT").weShouldTriggerTheButton("Button_Left"); + inGame.whenWePressOnKeyboard("LEFT").weShouldTriggerTheButton("Button_Left"); + inGame.whenWePressOnKeyboard("UP").weShouldTriggerTheButton("Button_Up"); + inTheSettingMenu.whenCursorIsOnSetting("Button_Left").iconDisplayedIs("KEY_ARROW_LEFT").weWantThisBindInstead("D").OopsSpecialCaseIcon("KEY_Q").confirm(); + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Alt_Button_Left"); + inGame.whenWePressOnKeyboard("LEFT").weShouldTriggerTheButton("Button_Left"); + inGame.whenWePressOnKeyboard("Q").weShouldTriggerTheButton("Alt_Button_Right"); + inGame.whenWePressOnKeyboard("UP").weShouldTriggerTheButton("Button_Up"); + inTheSettingMenu.whenCursorIsOnSetting("Button_Up").iconDisplayedIs("KEY_ARROW_UP").weWantThisBindInstead("LEFT").confirm(); + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Alt_Button_Left"); + inGame.whenWePressOnKeyboard("LEFT").weShouldTriggerTheButton("Button_Up"); + inGame.whenWePressOnKeyboard("Q").weShouldTriggerTheButton("Alt_Button_Right"); + inGame.whenWePressOnKeyboard("UP").weShouldTriggerTheButton("Button_Left"); + }); + + it('Swap alt with a key not binded yet', () => { + inGame.whenWePressOnKeyboard("Z").weShouldTriggerTheButton("Alt_Button_Up"); + inGame.whenWePressOnKeyboard("B").nothingShouldHappen(); + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Up").iconDisplayedIs("KEY_Z").weWantThisBindInstead("B").confirm(); + inGame.whenWePressOnKeyboard("Z").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("B").weShouldTriggerTheButton("Alt_Button_Up"); + + }); + + it('Delete bind', () => { + inGame.whenWePressOnKeyboard("LEFT").weShouldTriggerTheButton("Button_Left"); + inTheSettingMenu.whenWeDelete("Button_Left").thereShouldBeNoIconAnymore(); + inGame.whenWePressOnKeyboard("LEFT").nothingShouldHappen(); + }); + + it('Delete bind then assign a not yet binded button', () => { + inGame.whenWePressOnKeyboard("LEFT").weShouldTriggerTheButton("Button_Left"); + inGame.whenWePressOnKeyboard("Q").weShouldTriggerTheButton("Alt_Button_Left"); + inGame.whenWePressOnKeyboard("B").nothingShouldHappen(); + inTheSettingMenu.whenWeDelete("Button_Left").thereShouldBeNoIconAnymore(); + inGame.whenWePressOnKeyboard("LEFT").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("Q").weShouldTriggerTheButton("Alt_Button_Left"); + inGame.whenWePressOnKeyboard("B").nothingShouldHappen(); + + inTheSettingMenu.whenCursorIsOnSetting("Button_Left").thereShouldBeNoIcon().weWantThisBindInstead("B").confirm(); + inGame.whenWePressOnKeyboard("B").weShouldTriggerTheButton("Button_Left"); + inGame.whenWePressOnKeyboard("LEFT").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("Q").weShouldTriggerTheButton("Alt_Button_Left"); + }) + + it('Delete bind then assign a not yet binded button', () => { + inGame.whenWePressOnKeyboard("LEFT").weShouldTriggerTheButton("Button_Left"); + inGame.whenWePressOnKeyboard("Q").weShouldTriggerTheButton("Alt_Button_Left"); + inGame.whenWePressOnKeyboard("B").nothingShouldHappen(); + inTheSettingMenu.whenWeDelete("Button_Left").thereShouldBeNoIconAnymore(); + inGame.whenWePressOnKeyboard("LEFT").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("RIGHT").weShouldTriggerTheButton("Button_Right"); + inTheSettingMenu.whenCursorIsOnSetting("Button_Left").thereShouldBeNoIcon().weWantThisBindInstead("RIGHT").confirm(); + inGame.whenWePressOnKeyboard("LEFT").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("RIGHT").weShouldTriggerTheButton("Button_Left"); + }); + + + it('swap 2 bind, than delete 1 bind than assign another bind', () => { + inGame.whenWePressOnKeyboard("LEFT").weShouldTriggerTheButton("Button_Left"); + inGame.whenWePressOnKeyboard("RIGHT").weShouldTriggerTheButton("Button_Right"); + inTheSettingMenu.whenCursorIsOnSetting("Button_Left").iconDisplayedIs("KEY_ARROW_LEFT").weWantThisBindInstead("RIGHT").confirm(); + inGame.whenWePressOnKeyboard("LEFT").weShouldTriggerTheButton("Button_Right"); + inGame.whenWePressOnKeyboard("RIGHT").weShouldTriggerTheButton("Button_Left"); + inTheSettingMenu.whenCursorIsOnSetting("Button_Left").whenWeDelete().thereShouldBeNoIconAnymore(); + inGame.whenWePressOnKeyboard("LEFT").weShouldTriggerTheButton("Button_Right"); + inGame.whenWePressOnKeyboard("RIGHT").nothingShouldHappen(); + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Up").iconDisplayedIs("KEY_Z").weWantThisBindInstead("B").confirm(); + inGame.whenWePressOnKeyboard("LEFT").weShouldTriggerTheButton("Button_Right"); + inGame.whenWePressOnKeyboard("RIGHT").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("B").weShouldTriggerTheButton("Alt_Button_Up"); + }); + + + it('Delete bind then assign not already existing button', () => { + inGame.whenWePressOnKeyboard("LEFT").weShouldTriggerTheButton("Button_Left"); + inGame.whenWePressOnKeyboard("Q").weShouldTriggerTheButton("Alt_Button_Left"); + inGame.whenWePressOnKeyboard("B").nothingShouldHappen(); + inTheSettingMenu.whenWeDelete("Button_Left").thereShouldBeNoIconAnymore(); + inGame.whenWePressOnKeyboard("LEFT").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("RIGHT").weShouldTriggerTheButton("Button_Right"); + inTheSettingMenu.whenCursorIsOnSetting("Button_Left").thereShouldBeNoIcon().weWantThisBindInstead("L").confirm(); + inGame.whenWePressOnKeyboard("L").weShouldTriggerTheButton("Button_Left"); + }); + + it('change alt bind to not already existing button, than another one alt bind with another not already existing button', () => { + inGame.whenWePressOnKeyboard("Z").weShouldTriggerTheButton("Alt_Button_Up"); + inGame.whenWePressOnKeyboard("S").weShouldTriggerTheButton("Alt_Button_Down"); + inGame.whenWePressOnKeyboard("T").nothingShouldHappen(); + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Up").iconDisplayedIs("KEY_Z").weWantThisBindInstead("T").confirm(); + inGame.whenWePressOnKeyboard("T").weShouldTriggerTheButton("Alt_Button_Up"); + inGame.whenWePressOnKeyboard("Z").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("U").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("S").weShouldTriggerTheButton("Alt_Button_Down"); + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Down").iconDisplayedIs("KEY_S").weWantThisBindInstead("U").confirm(); + inGame.whenWePressOnKeyboard("T").weShouldTriggerTheButton("Alt_Button_Up"); + inGame.whenWePressOnKeyboard("Z").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("U").weShouldTriggerTheButton("Alt_Button_Down"); + inGame.whenWePressOnKeyboard("S").nothingShouldHappen(); + }); + + it('Swap multiple touch alt and main', () => { + inGame.whenWePressOnKeyboard("UP").weShouldTriggerTheButton("Button_Up"); + inGame.whenWePressOnKeyboard("RIGHT").weShouldTriggerTheButton("Button_Right"); + inGame.whenWePressOnKeyboard("Z").weShouldTriggerTheButton("Alt_Button_Up"); + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Alt_Button_Right"); + inTheSettingMenu.whenCursorIsOnSetting("Button_Up").iconDisplayedIs("KEY_ARROW_UP").weWantThisBindInstead("RIGHT").confirm(); + inGame.whenWePressOnKeyboard("UP").weShouldTriggerTheButton("Button_Right"); + inGame.whenWePressOnKeyboard("RIGHT").weShouldTriggerTheButton("Button_Up"); + inGame.whenWePressOnKeyboard("Z").weShouldTriggerTheButton("Alt_Button_Up"); + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Alt_Button_Right"); + + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Up").iconDisplayedIs("KEY_Z").weWantThisBindInstead("D").confirm(); + inGame.whenWePressOnKeyboard("UP").weShouldTriggerTheButton("Button_Right"); + inGame.whenWePressOnKeyboard("RIGHT").weShouldTriggerTheButton("Button_Up"); + inGame.whenWePressOnKeyboard("Z").weShouldTriggerTheButton("Alt_Button_Right"); + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Alt_Button_Up"); + + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Up").iconDisplayedIs("KEY_D").weWantThisBindInstead("Z").confirm(); + inGame.whenWePressOnKeyboard("UP").weShouldTriggerTheButton("Button_Right"); + inGame.whenWePressOnKeyboard("RIGHT").weShouldTriggerTheButton("Button_Up"); + inGame.whenWePressOnKeyboard("Z").weShouldTriggerTheButton("Alt_Button_Up"); + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Alt_Button_Right"); + }) + + + it('Delete 2 bind then reassign one of them', () => { + inGame.whenWePressOnKeyboard("LEFT").weShouldTriggerTheButton("Button_Left"); + inGame.whenWePressOnKeyboard("Q").weShouldTriggerTheButton("Alt_Button_Left"); + inGame.whenWePressOnKeyboard("B").nothingShouldHappen(); + + inTheSettingMenu.whenWeDelete("Button_Left").thereShouldBeNoIconAnymore(); + inGame.whenWePressOnKeyboard("LEFT").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("Q").weShouldTriggerTheButton("Alt_Button_Left"); + inGame.whenWePressOnKeyboard("B").nothingShouldHappen(); + + inTheSettingMenu.whenWeDelete("Alt_Button_Left").thereShouldBeNoIconAnymore(); + inGame.whenWePressOnKeyboard("LEFT").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("Q").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("B").nothingShouldHappen(); + + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Left").thereShouldBeNoIcon().weWantThisBindInstead("Q").confirm(); + inGame.whenWePressOnKeyboard("LEFT").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("Q").weShouldTriggerTheButton("Alt_Button_Left"); + inGame.whenWePressOnKeyboard("B").nothingShouldHappen(); + }); + + it("test keyboard listener", () => { + const keyDown = Phaser.Input.Keyboard.KeyCodes.S; + const key = getKeyWithKeycode(config, keyDown); + const settingName = config.custom[key]; + const buttonDown = config.settings[settingName]; + expect(buttonDown).toEqual(Button.DOWN); + }); +}); diff --git a/src/ui/settings/abstract-settings-ui-handler.ts b/src/ui/settings/abstract-settings-ui-handler.ts index 4f8187b24..c55272ff9 100644 --- a/src/ui/settings/abstract-settings-ui-handler.ts +++ b/src/ui/settings/abstract-settings-ui-handler.ts @@ -5,10 +5,7 @@ import {InterfaceConfig} from "../../inputs-controller"; import {addWindow} from "../ui-theme"; import {addTextObject, TextStyle} from "../text"; import {Button} from "../../enums/buttons"; -import { - getKeyAndActionFromCurrentKeysWithSettingName, - getKeyForSettingName -} from "#app/configs/gamepad-utils"; +import {getIconWithSettingName, getKeyWithSettingName} from "#app/configs/configHandler"; export interface InputsIcons { [key: string]: Phaser.GameObjects.Sprite; @@ -107,7 +104,7 @@ export default abstract class AbstractSettingsUiUiHandler extends UiHandler { optionsContainer.setVisible(false); // Gather all gamepad binding settings from the configuration. - const bindingSettings = Object.keys(config.setting).map(k => config.setting[k]); + const bindingSettings = Object.keys(config.settings); // Array to hold labels for different settings such as 'Default Controller', 'Gamepad Support', etc. const settingLabels: Phaser.GameObjects.Text[] = []; @@ -121,7 +118,7 @@ export default abstract class AbstractSettingsUiUiHandler extends UiHandler { // Fetch common setting keys such as 'Default 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.setting).map(k => config.setting[k])]; + 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])); @@ -142,6 +139,9 @@ export default abstract class AbstractSettingsUiUiHandler extends UiHandler { const valueLabels: Phaser.GameObjects.Text[] = [] // Process each option for the current setting. + const aaa = this.settingDeviceOptions; + const bbb = this.settingDevice; + const ccc = this.settingDevice[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])) { @@ -154,11 +154,10 @@ export default abstract class AbstractSettingsUiUiHandler extends UiHandler { continue; } // For null options, add an icon for the key. - const key = getKeyForSettingName(config as InterfaceConfig, this.settingDevice[setting]); const icon = this.scene.add.sprite(0, 0, this.textureOverride ? this.textureOverride : config.padType); icon.setScale(0.1); icon.setOrigin(0, -0.1); - inputsIcons[key] = icon; + inputsIcons[this.settingDevice[setting]] = icon; optionsContainer.add(icon); valueLabels.push(icon); continue; @@ -242,12 +241,13 @@ export default abstract class AbstractSettingsUiUiHandler extends UiHandler { // For each element in the binding settings, update the icon according to the current assignment. for (const elm of this.bindingSettings) { - const {key, icon} = getKeyAndActionFromCurrentKeysWithSettingName(activeConfig, elm); + const icon = getIconWithSettingName(activeConfig, elm); if (icon) { - this.inputsIcons[key].setFrame(icon); - this.inputsIcons[key].alpha = 1; + this.inputsIcons[elm].setFrame(icon); + this.inputsIcons[elm].alpha = 1; } else { - this.inputsIcons[key].alpha = 0; + if (!this.inputsIcons[elm]) debugger; + this.inputsIcons[elm].alpha = 0; } } diff --git a/src/ui/settings/gamepad-binding-ui-handler.ts b/src/ui/settings/gamepad-binding-ui-handler.ts index 1ecd28fcd..d0461ec46 100644 --- a/src/ui/settings/gamepad-binding-ui-handler.ts +++ b/src/ui/settings/gamepad-binding-ui-handler.ts @@ -1,9 +1,8 @@ import BattleScene from "../../battle-scene"; import AbstractBindingUiHandler from "../settings/abrast-binding-ui-handler"; import {Mode} from "../ui"; -import { - getKeyAndActionFromCurrentKeysWithSettingName, getKeyFromMapping, regenerateCustom, -} from "#app/configs/gamepad-utils"; +import {Device} from "#app/enums/devices"; +import {getIconSpecialCase, getIconWithSettingName, getKeyWithKeycode} from "#app/configs/configHandler"; export default class GamepadBindingUiHandler extends AbstractBindingUiHandler { @@ -14,25 +13,29 @@ export default class GamepadBindingUiHandler extends AbstractBindingUiHandler { scene.input.gamepad.on('down', this.gamepadButtonDown, this); } + getSelectedDevice() { + return this.scene.inputController?.selectedDevice[Device.GAMEPAD]; + } + gamepadButtonDown(pad: Phaser.Input.Gamepad.Gamepad, button: Phaser.Input.Gamepad.Button, value: number): void { const blacklist = [12, 13, 14, 15]; // d-pad buttons are blacklisted. // Check conditions before processing the button press. - if (!this.listening || pad.id !== this.scene.inputController?.chosenGamepad || blacklist.includes(button.index) || this.buttonPressed !== null) return; - const activeConfig = this.scene.inputController.getActiveConfig(); + if (!this.listening || pad.id.toLowerCase() !== this.getSelectedDevice() || blacklist.includes(button.index) || this.buttonPressed !== null) return; + const activeConfig = this.scene.inputController.getActiveConfig(Device.GAMEPAD); const type = activeConfig.padType - const key = getKeyFromMapping(activeConfig, button.index); - const buttonIcon = activeConfig.ogIcons[key]; + const key = getKeyWithKeycode(activeConfig, button.index); + const buttonIcon = activeConfig.icons[key]; if (!buttonIcon) return; this.buttonPressed = button.index; - const assignedButtonIcon = getKeyAndActionFromCurrentKeysWithSettingName(activeConfig, this.target)?.icon; - this.onInputDown(buttonIcon, assignedButtonIcon, type); + const specialCaseIcon = getIconSpecialCase(activeConfig, button.index, this.target); + const assignedButtonIcon = getIconWithSettingName(activeConfig, this.target); + this.onInputDown(buttonIcon, specialCaseIcon || assignedButtonIcon, type); } swapAction() { - const activeConfig = this.scene.inputController.getActiveConfig(); + const activeConfig = this.scene.inputController.getActiveConfig(Device.GAMEPAD); this.scene.inputController.swapBinding(activeConfig, this.target, this.buttonPressed) - this.scene.gameData.saveCustomMapping(this.scene.inputController?.chosenGamepad, activeConfig.currentKeys, activeConfig.icons); - regenerateCustom(activeConfig); + this.scene.gameData.saveMappingConfigs(this.getSelectedDevice(), activeConfig); return true; } } \ No newline at end of file diff --git a/src/ui/settings/keyboard-binding-ui-handler.ts b/src/ui/settings/keyboard-binding-ui-handler.ts index fd23fc5fe..218d6e075 100644 --- a/src/ui/settings/keyboard-binding-ui-handler.ts +++ b/src/ui/settings/keyboard-binding-ui-handler.ts @@ -1,9 +1,8 @@ import BattleScene from "../../battle-scene"; import AbstractBindingUiHandler from "../settings/abrast-binding-ui-handler"; import {Mode} from "../ui"; -import { - getKeyAndActionFromCurrentKeysWithSettingName, getKeyFromMapping, regenerateCustom, -} from "#app/configs/gamepad-utils"; +import {getIconSpecialCase, getIconWithSettingName, getKeyWithKeycode} from "#app/configs/configHandler"; +import {Device} from "#app/enums/devices"; export default class KeyboardBindingUiHandler extends AbstractBindingUiHandler { @@ -14,24 +13,28 @@ export default class KeyboardBindingUiHandler extends AbstractBindingUiHandler { scene.input.keyboard.on('keydown', this.onKeyDown, this); } + getSelectedDevice() { + return this.scene.inputController?.selectedDevice[Device.KEYBOARD]; + } + onKeyDown(event): void { const key = event.keyCode; // // Check conditions before processing the button press. if (!this.listening || this.buttonPressed !== null) return; - const activeConfig = this.scene.inputController.getActiveKeyboardConfig(); - const _key = getKeyFromMapping(activeConfig, key); - const buttonIcon = activeConfig.ogIcons[_key]; + const activeConfig = this.scene.inputController.getActiveConfig(Device.KEYBOARD); + const _key = getKeyWithKeycode(activeConfig, key); + const buttonIcon = activeConfig.icons[_key]; if (!buttonIcon) return; this.buttonPressed = key; - const assignedButtonIcon = getKeyAndActionFromCurrentKeysWithSettingName(activeConfig, this.target)?.icon; - this.onInputDown(buttonIcon, assignedButtonIcon, 'keyboard'); + const specialCaseIcon = getIconSpecialCase(activeConfig, key, this.target); + const assignedButtonIcon = getIconWithSettingName(activeConfig, this.target); + this.onInputDown(buttonIcon, specialCaseIcon || assignedButtonIcon, 'keyboard'); } swapAction() { - const activeConfig = this.scene.inputController.getActiveKeyboardConfig(); + const activeConfig = this.scene.inputController.getActiveConfig(Device.KEYBOARD); this.scene.inputController.swapBinding(activeConfig, this.target, this.buttonPressed) - this.scene.gameData.saveCustomKeyboardMapping(this.scene.inputController?.chosenKeyboard, activeConfig.currentKeys, activeConfig.icons); - regenerateCustom(activeConfig); + this.scene.gameData.saveMappingConfigs(this.getSelectedDevice(), activeConfig); return true; } diff --git a/src/ui/settings/settings-gamepad-ui-handler.ts b/src/ui/settings/settings-gamepad-ui-handler.ts index 450a328d8..936180c98 100644 --- a/src/ui/settings/settings-gamepad-ui-handler.ts +++ b/src/ui/settings/settings-gamepad-ui-handler.ts @@ -2,12 +2,12 @@ import BattleScene from "../../battle-scene"; import {addTextObject, TextStyle} from "../text"; import {Mode} from "../ui"; import {SettingGamepad, settingGamepadDefaults, settingGamepadOptions} from "../../system/settings-gamepad"; -import {truncateString} from "../../utils"; import pad_xbox360 from "#app/configs/pad_xbox360"; import pad_dualshock from "#app/configs/pad_dualshock"; import pad_unlicensedSNES from "#app/configs/pad_unlicensedSNES"; import {InterfaceConfig} from "#app/inputs-controller"; import AbstractSettingsUiUiHandler from "#app/ui/settings/abstract-settings-ui-handler"; +import {Device} from "#app/enums/devices"; export default class SettingsGamepadUiHandler extends AbstractSettingsUiUiHandler { @@ -39,7 +39,7 @@ export default class SettingsGamepadUiHandler extends AbstractSettingsUiUiHandle } getActiveConfig(): InterfaceConfig { - return this.scene.inputController.getActiveConfig(); + return this.scene.inputController.getActiveConfig(Device.GAMEPAD); } getLocalStorageSetting(): object { @@ -89,7 +89,7 @@ export default class SettingsGamepadUiHandler extends AbstractSettingsUiUiHandle // 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.chosenGamepad, 30)); + this.layout[_key].optionValueLabels[index][0].setText(this.scene.inputController.selectedDevice[Device.GAMEPAD], 30); } } } diff --git a/src/ui/settings/settings-keyboard-ui-handler.ts b/src/ui/settings/settings-keyboard-ui-handler.ts index 91995aeb3..38b5b63fe 100644 --- a/src/ui/settings/settings-keyboard-ui-handler.ts +++ b/src/ui/settings/settings-keyboard-ui-handler.ts @@ -5,8 +5,9 @@ import {SettingKeyboard, settingKeyboardDefaults, settingKeyboardOptions} from " import {reverseValueToKeySetting, truncateString} from "#app/utils"; import AbstractSettingsUiUiHandler from "#app/ui/settings/abstract-settings-ui-handler"; import {InterfaceConfig} from "#app/inputs-controller"; -import {deleteBind} from "#app/configs/gamepad-utils"; import {addTextObject, TextStyle} from "#app/ui/text"; +import {deleteBind} from "#app/configs/configHandler"; +import {Device} from "#app/enums/devices"; export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandler { @@ -53,7 +54,7 @@ export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandl } getActiveConfig(): InterfaceConfig { - return this.scene.inputController.getActiveKeyboardConfig(); + return this.scene.inputController.getActiveConfig(Device.KEYBOARD); } getLocalStorageSetting(): object { @@ -101,7 +102,7 @@ export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandl if (_key === 'noKeyboard') continue; // Skip updating the no gamepad 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)); + this.layout[_key].optionValueLabels[index][0].setText(truncateString(this.scene.inputController.selectedDevice[Device.KEYBOARD], 30)); } } } @@ -109,7 +110,7 @@ export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandl } saveCustomKeyboardMappingToLocalStorage(config): void { - this.scene.gameData.saveCustomKeyboardMapping(this.scene.inputController?.chosenKeyboard, config.currentKeys, config.icons); + this.scene.gameData.saveMappingConfigs(this.scene.inputController?.selectedDevice[Device.KEYBOARD], config); } saveSettingToLocalStorage(settingName, cursor): void {