diff --git a/index.css b/index.css index df305781646..1a695fa0c4f 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 > .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 .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; } @@ -175,4 +177,70 @@ body { input:-internal-autofill-selected { -webkit-background-clip: text; background-clip: text; +} +#banner { + display: none; + position: absolute; + top: 33.2%; + left: 0; + text-align: center; + z-index: 1000; /* Ensures the banner is on top of other elements */ + & > img { + opacity: 50%; + } +} + +/* Firefox old*/ +@-moz-keyframes blink { + 0% { + opacity:1; + } + 50% { + opacity:0; + } + 100% { + opacity:1; + } +} + +@-webkit-keyframes blink { + 0% { + opacity:1; + } + 50% { + opacity:0; + } + 100% { + opacity:1; + } +} +/* IE */ +@-ms-keyframes blink { + 0% { + opacity:1; + } + 50% { + opacity:0; + } + 100% { + opacity:1; + } +} +/* Opera and prob css3 final iteration */ +@keyframes blink { + 0% { + opacity:1; + } + 50% { + opacity:0; + } + 100% { + opacity:1; + } +} +.blink-image { + -moz-animation: blink normal 4s infinite ease-in-out; /* Firefox */ + -webkit-animation: blink normal 4s infinite ease-in-out; /* Webkit */ + -ms-animation: blink normal 4s infinite ease-in-out; /* IE */ + animation: blink normal 4s infinite ease-in-out; /* Opera and prob css3 final iteration */ } \ No newline at end of file diff --git a/public/images/inputs/dualshock.json b/public/images/inputs/dualshock.json new file mode 100644 index 00000000000..9f782061289 --- /dev/null +++ b/public/images/inputs/dualshock.json @@ -0,0 +1,148 @@ +{"frames": [ + +{ + "filename": "CIRCLE.png", + "frame": {"x":0,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "CROSS.png", + "frame": {"x":12,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "DOWN.png", + "frame": {"x":24,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "L1.png", + "frame": {"x":36,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "L2.png", + "frame": {"x":48,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "L3.png", + "frame": {"x":60,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "LEFT.png", + "frame": {"x":72,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "R1.png", + "frame": {"x":84,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "R2.png", + "frame": {"x":96,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "R3.png", + "frame": {"x":108,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "RIGHT.png", + "frame": {"x":120,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "SELECT.png", + "frame": {"x":132,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "SQUARE.png", + "frame": {"x":144,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "START.png", + "frame": {"x":156,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "TOUCH.png", + "frame": {"x":168,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "TRIANGLE.png", + "frame": {"x":180,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "UP.png", + "frame": {"x":192,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}], +"meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "dualshock.png", + "format": "RGBA8888", + "size": {"w":204,"h":12}, + "scale": "1", + "smartupdate": "$TexturePacker:SmartUpdate:47df68ade4299adf7d334f25cb833ece:039b9ac469e3616fb9635a6a19cca50e:adc25708364be3d9e70074e95305c745$" +} +} diff --git a/public/images/inputs/dualshock.png b/public/images/inputs/dualshock.png new file mode 100644 index 00000000000..264f03a298e Binary files /dev/null and b/public/images/inputs/dualshock.png differ diff --git a/public/images/inputs/keyboard.json b/public/images/inputs/keyboard.json new file mode 100644 index 00000000000..b1902df10d6 --- /dev/null +++ b/public/images/inputs/keyboard.json @@ -0,0 +1,596 @@ +{"frames": [ + +{ + "filename": "0.png", + "frame": {"x":0,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "1.png", + "frame": {"x":12,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "2.png", + "frame": {"x":24,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "3.png", + "frame": {"x":36,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "4.png", + "frame": {"x":48,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "5.png", + "frame": {"x":60,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "6.png", + "frame": {"x":72,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "7.png", + "frame": {"x":84,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "8.png", + "frame": {"x":96,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "9.png", + "frame": {"x":108,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "A.png", + "frame": {"x":120,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "ALT.png", + "frame": {"x":132,"y":0,"w":16,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":16,"h":12}, + "sourceSize": {"w":16,"h":12} +}, +{ + "filename": "B.png", + "frame": {"x":148,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "BACK.png", + "frame": {"x":160,"y":0,"w":24,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":24,"h":12}, + "sourceSize": {"w":24,"h":12} +}, +{ + "filename": "C.png", + "frame": {"x":184,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "CTRL.png", + "frame": {"x":196,"y":0,"w":22,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":22,"h":12}, + "sourceSize": {"w":22,"h":12} +}, +{ + "filename": "D.png", + "frame": {"x":218,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "DEL.png", + "frame": {"x":230,"y":0,"w":17,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":17,"h":12}, + "sourceSize": {"w":17,"h":12} +}, +{ + "filename": "E.png", + "frame": {"x":247,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "END.png", + "frame": {"x":259,"y":0,"w":18,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":18,"h":12}, + "sourceSize": {"w":18,"h":12} +}, +{ + "filename": "ENTER.png", + "frame": {"x":277,"y":0,"w":27,"h":11}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":27,"h":11}, + "sourceSize": {"w":27,"h":11} +}, +{ + "filename": "ESC.png", + "frame": {"x":304,"y":0,"w":17,"h":11}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":17,"h":11}, + "sourceSize": {"w":17,"h":11} +}, +{ + "filename": "F.png", + "frame": {"x":321,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "F1.png", + "frame": {"x":333,"y":0,"w":13,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":12}, + "sourceSize": {"w":13,"h":12} +}, +{ + "filename": "F2.png", + "frame": {"x":346,"y":0,"w":13,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":12}, + "sourceSize": {"w":13,"h":12} +}, +{ + "filename": "F3.png", + "frame": {"x":359,"y":0,"w":13,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":12}, + "sourceSize": {"w":13,"h":12} +}, +{ + "filename": "F4.png", + "frame": {"x":372,"y":0,"w":13,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":12}, + "sourceSize": {"w":13,"h":12} +}, +{ + "filename": "F5.png", + "frame": {"x":385,"y":0,"w":13,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":12}, + "sourceSize": {"w":13,"h":12} +}, +{ + "filename": "F6.png", + "frame": {"x":398,"y":0,"w":13,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":12}, + "sourceSize": {"w":13,"h":12} +}, +{ + "filename": "F7.png", + "frame": {"x":411,"y":0,"w":13,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":12}, + "sourceSize": {"w":13,"h":12} +}, +{ + "filename": "F8.png", + "frame": {"x":424,"y":0,"w":13,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":12}, + "sourceSize": {"w":13,"h":12} +}, +{ + "filename": "F9.png", + "frame": {"x":437,"y":0,"w":13,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":13,"h":12}, + "sourceSize": {"w":13,"h":12} +}, +{ + "filename": "F10.png", + "frame": {"x":450,"y":0,"w":16,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":16,"h":12}, + "sourceSize": {"w":16,"h":12} +}, +{ + "filename": "F11.png", + "frame": {"x":466,"y":0,"w":15,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":15,"h":12}, + "sourceSize": {"w":15,"h":12} +}, +{ + "filename": "F12.png", + "frame": {"x":481,"y":0,"w":16,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":16,"h":12}, + "sourceSize": {"w":16,"h":12} +}, +{ + "filename": "G.png", + "frame": {"x":497,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "H.png", + "frame": {"x":509,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "HOME.png", + "frame": {"x":521,"y":0,"w":23,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":23,"h":12}, + "sourceSize": {"w":23,"h":12} +}, +{ + "filename": "I.png", + "frame": {"x":544,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "INS.png", + "frame": {"x":556,"y":0,"w":16,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":16,"h":12}, + "sourceSize": {"w":16,"h":12} +}, +{ + "filename": "J.png", + "frame": {"x":572,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "K.png", + "frame": {"x":584,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "KEY_ARROW_DOWN.png", + "frame": {"x":596,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "KEY_ARROW_LEFT.png", + "frame": {"x":608,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "KEY_ARROW_RIGHT.png", + "frame": {"x":620,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "KEY_ARROW_UP.png", + "frame": {"x":632,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "L.png", + "frame": {"x":644,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "LEFT_BRACKET.png", + "frame": {"x":656,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "M.png", + "frame": {"x":668,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "MINUS.png", + "frame": {"x":680,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "N.png", + "frame": {"x":692,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "O.png", + "frame": {"x":704,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "P.png", + "frame": {"x":716,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "PAGE_DOWN.png", + "frame": {"x":728,"y":0,"w":20,"h":11}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":20,"h":11}, + "sourceSize": {"w":20,"h":11} +}, +{ + "filename": "PAGE_UP.png", + "frame": {"x":748,"y":0,"w":20,"h":11}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":20,"h":11}, + "sourceSize": {"w":20,"h":11} +}, +{ + "filename": "PLUS.png", + "frame": {"x":768,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "Q.png", + "frame": {"x":780,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "QUOTE.png", + "frame": {"x":792,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "R.png", + "frame": {"x":804,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "RIGHT_BRACKET.png", + "frame": {"x":816,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "S.png", + "frame": {"x":828,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "SEMICOLON.png", + "frame": {"x":840,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "SHIFT.png", + "frame": {"x":852,"y":0,"w":23,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":23,"h":12}, + "sourceSize": {"w":23,"h":12} +}, +{ + "filename": "SPACE.png", + "frame": {"x":875,"y":0,"w":25,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":25,"h":12}, + "sourceSize": {"w":25,"h":12} +}, +{ + "filename": "T.png", + "frame": {"x":900,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "TAB.png", + "frame": {"x":912,"y":0,"w":19,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":19,"h":12}, + "sourceSize": {"w":19,"h":12} +}, +{ + "filename": "TILDE.png", + "frame": {"x":931,"y":0,"w":15,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":15,"h":12}, + "sourceSize": {"w":15,"h":12} +}, +{ + "filename": "U.png", + "frame": {"x":946,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "V.png", + "frame": {"x":958,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "W.png", + "frame": {"x":970,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "X.png", + "frame": {"x":982,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "Y.png", + "frame": {"x":994,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "Z.png", + "frame": {"x":1006,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}], +"meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "keyboard.png", + "format": "RGBA8888", + "size": {"w":1018,"h":12}, + "scale": "1", + "smartupdate": "$TexturePacker:SmartUpdate:085d4353a5c4d18c90f82f8926710d72:45908b22b446cf7f4904d4e0b658b16a:bad03abb89ad027d879c383c13fd51bc$" +} +} diff --git a/public/images/inputs/keyboard.png b/public/images/inputs/keyboard.png new file mode 100644 index 00000000000..67b26af12de Binary files /dev/null and b/public/images/inputs/keyboard.png differ diff --git a/public/images/inputs/xbox.json b/public/images/inputs/xbox.json new file mode 100644 index 00000000000..32c00003687 --- /dev/null +++ b/public/images/inputs/xbox.json @@ -0,0 +1,140 @@ +{"frames": [ + +{ + "filename": "Bumper_L.png", + "frame": {"x":0,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "Bumper_R.png", + "frame": {"x":12,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "DOWN.png", + "frame": {"x":24,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "LEFT.png", + "frame": {"x":36,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "LS.png", + "frame": {"x":48,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "RIGHT.png", + "frame": {"x":60,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "RS.png", + "frame": {"x":72,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "SELECT.png", + "frame": {"x":84,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "START.png", + "frame": {"x":96,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "Trigger_L.png", + "frame": {"x":108,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "Trigger_R.png", + "frame": {"x":120,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "UP.png", + "frame": {"x":132,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "XB_Letter_A_OL.png", + "frame": {"x":144,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "XB_Letter_B_OL.png", + "frame": {"x":156,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "XB_Letter_X_OL.png", + "frame": {"x":168,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}, +{ + "filename": "XB_Letter_Y_OL.png", + "frame": {"x":180,"y":0,"w":12,"h":12}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, + "sourceSize": {"w":12,"h":12} +}], +"meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "xbox.png", + "format": "RGBA8888", + "size": {"w":192,"h":12}, + "scale": "1", + "smartupdate": "$TexturePacker:SmartUpdate:dda9e220b2ea223723253388c465ea25:8ab4a5ecdc22848a8718a1285590a78c:7ad6008cd8fa3f9f4bfb17e0cfcbbb64$" +} +} diff --git a/public/images/inputs/xbox.png b/public/images/inputs/xbox.png new file mode 100644 index 00000000000..037fd8515ae Binary files /dev/null and b/public/images/inputs/xbox.png differ diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 82bb3d92b0c..a8e8d238ab6 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1,5 +1,5 @@ import Phaser from "phaser"; -import UI from "./ui/ui"; +import UI from "./ui/ui"; import { NextEncounterPhase, NewBiomeEncounterPhase, SelectBiomePhase, MessagePhase, TurnInitPhase, ReturnPhase, LevelCapPhase, ShowTrainerPhase, LoginPhase, MovePhase, TitlePhase, SwitchPhase } from "./phases"; import Pokemon, { PlayerPokemon, EnemyPokemon } from "./field/pokemon"; import PokemonSpecies, { PokemonSpeciesFilter, allSpecies, getPokemonSpecies } from "./data/pokemon-species"; @@ -131,8 +131,6 @@ export default class BattleScene extends SceneBase { public fusionPaletteSwaps: boolean = true; public enableTouchControls: boolean = false; public enableVibration: boolean = false; - public gamepadSupport: boolean = false; - public abSwapped: boolean = false; public disableMenu: boolean = false; diff --git a/src/configs/inputs/cfg_keyboard_qwerty.ts b/src/configs/inputs/cfg_keyboard_qwerty.ts new file mode 100644 index 00000000000..869b763d6c1 --- /dev/null +++ b/src/configs/inputs/cfg_keyboard_qwerty.ts @@ -0,0 +1,293 @@ +import {Button} from "#app/enums/buttons"; +import {SettingKeyboard} from "#app/system/settings-keyboard"; + +const cfg_keyboard_qwerty = { + padID: "default", + padType: "keyboard", + deviceMapping: { + 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: "A.png", + KEY_B: "B.png", + KEY_C: "C.png", + KEY_D: "D.png", + KEY_E: "E.png", + KEY_F: "F.png", + KEY_G: "G.png", + KEY_H: "H.png", + KEY_I: "I.png", + KEY_J: "J.png", + KEY_K: "K.png", + KEY_L: "L.png", + KEY_M: "M.png", + KEY_N: "N.png", + KEY_O: "O.png", + KEY_P: "P.png", + KEY_Q: "Q.png", + KEY_R: "R.png", + KEY_S: "S.png", + KEY_T: "T.png", + KEY_U: "U.png", + KEY_V: "V.png", + KEY_W: "W.png", + KEY_X: "X.png", + KEY_Y: "Y.png", + KEY_Z: "Z.png", + + KEY_0: "0.png", + KEY_1: "1.png", + KEY_2: "2.png", + KEY_3: "3.png", + KEY_4: "4.png", + KEY_5: "5.png", + KEY_6: "6.png", + KEY_7: "7.png", + KEY_8: "8.png", + KEY_9: "9.png", + + KEY_F1: "F1.png", + KEY_F2: "F2.png", + KEY_F3: "F3.png", + KEY_F4: "F4.png", + KEY_F5: "F5.png", + KEY_F6: "F6.png", + KEY_F7: "F7.png", + KEY_F8: "F8.png", + KEY_F9: "F9.png", + KEY_F10: "F10.png", + KEY_F11: "F11.png", + KEY_F12: "F12.png", + + + KEY_PAGE_DOWN: "PAGE_DOWN.png", + KEY_PAGE_UP: "PAGE_UP.png", + + KEY_CTRL: "CTRL.png", + KEY_DEL: "DEL.png", + KEY_END: "END.png", + KEY_ENTER: "ENTER.png", + KEY_ESC: "ESC.png", + KEY_HOME: "HOME.png", + KEY_INSERT: "INS.png", + + KEY_PLUS: "PLUS.png", + KEY_MINUS: "MINUS.png", + KEY_QUOTATION: "QUOTE.png", + KEY_SHIFT: "SHIFT.png", + + KEY_SPACE: "SPACE.png", + KEY_TAB: "TAB.png", + KEY_TILDE: "TILDE.png", + + KEY_ARROW_UP: "KEY_ARROW_UP.png", + KEY_ARROW_DOWN: "KEY_ARROW_DOWN.png", + KEY_ARROW_LEFT: "KEY_ARROW_LEFT.png", + KEY_ARROW_RIGHT: "KEY_ARROW_RIGHT.png", + + KEY_LEFT_BRACKET: "LEFT_BRACKET.png", + KEY_RIGHT_BRACKET: "RIGHT_BRACKET.png", + + KEY_SEMICOLON: "SEMICOLON.png", + + KEY_BACKSPACE: "BACK.png", + KEY_ALT: "ALT.png" + }, + 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.V, + [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.V, + [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, + 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: -1, + KEY_MINUS: -1, + KEY_A: SettingKeyboard.Alt_Button_Left, + KEY_B: -1, + KEY_D: SettingKeyboard.Alt_Button_Right, + KEY_H: -1, + KEY_I: -1, + KEY_J: -1, + KEY_K: -1, + KEY_L: -1, + KEY_M: SettingKeyboard.Alt_Button_Menu, + KEY_O: -1, + KEY_P: -1, + KEY_Q: -1, + KEY_S: SettingKeyboard.Alt_Button_Down, + KEY_T: SettingKeyboard.Alt_Button_Cycle_Form, + KEY_U: -1, + KEY_W: SettingKeyboard.Alt_Button_Up, + KEY_X: SettingKeyboard.Alt_Button_Cancel, + KEY_Y: SettingKeyboard.Alt_Button_Cycle_Shiny, + KEY_Z: SettingKeyboard.Alt_Button_Action, + 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: SettingKeyboard.Button_Slow_Down, + KEY_PAGE_UP: SettingKeyboard.Button_Speed_Up, + KEY_QUOTATION: -1, + KEY_SHIFT: SettingKeyboard.Alt_Button_Stats, + KEY_TAB: -1, + KEY_TILDE: -1, + KEY_LEFT_BRACKET: -1, + KEY_RIGHT_BRACKET: -1, + KEY_SEMICOLON: -1, + KEY_ALT: -1 + }, + blacklist: [ + "KEY_ENTER", + "KEY_ESC", + "KEY_SPACE", + "KEY_BACKSPACE", + "KEY_ARROW_UP", + "KEY_ARROW_DOWN", + "KEY_ARROW_LEFT", + "KEY_ARROW_RIGHT", + "KEY_DEL", + "KEY_HOME", + ] +}; + +export default cfg_keyboard_qwerty; diff --git a/src/configs/inputs/configHandler.ts b/src/configs/inputs/configHandler.ts new file mode 100644 index 00000000000..0770ebffce1 --- /dev/null +++ b/src/configs/inputs/configHandler.ts @@ -0,0 +1,208 @@ +import {Device} from "#app/enums/devices"; + +/** + * Retrieves the key associated with the specified keycode from the mapping. + * + * @param config - The configuration object containing the mapping. + * @param keycode - The keycode to search for. + * @returns The key associated with the specified keycode. + */ +export function getKeyWithKeycode(config, keycode) { + return Object.keys(config.deviceMapping).find(key => config.deviceMapping[key] === keycode); +} + +/** + * Retrieves the setting name associated with the specified keycode. + * + * @param config - The configuration object containing custom settings. + * @param keycode - The keycode to search for. + * @returns The setting name associated with the specified keycode. + */ +export function getSettingNameWithKeycode(config, keycode) { + const key = getKeyWithKeycode(config, keycode); + return config.custom[key]; +} + +/** + * Retrieves the icon associated with the specified keycode. + * + * @param config - The configuration object containing icons. + * @param keycode - The keycode to search for. + * @returns The icon associated with the specified keycode. + */ +export function getIconWithKeycode(config, keycode) { + const key = getKeyWithKeycode(config, keycode); + return config.icons[key]; +} + +/** + * Retrieves the button associated with the specified keycode. + * + * @param config - The configuration object containing settings. + * @param keycode - The keycode to search for. + * @returns The button associated with the specified keycode. + */ +export function getButtonWithKeycode(config, keycode) { + const settingName = getSettingNameWithKeycode(config, keycode); + return config.settings[settingName]; +} + +/** + * Retrieves the key associated with the specified setting name. + * + * @param config - The configuration object containing custom settings. + * @param settingName - The setting name to search for. + * @returns The key associated with the specified setting name. + */ +export function getKeyWithSettingName(config, settingName) { + return Object.keys(config.custom).find(key => config.custom[key] === settingName); +} + +/** + * Retrieves the setting name associated with the specified key. + * + * @param config - The configuration object containing custom settings. + * @param key - The key to search for. + * @returns The setting name associated with the specified key. + */ +export function getSettingNameWithKey(config, key) { + return config.custom[key]; +} + +/** + * Retrieves the icon associated with the specified key. + * + * @param config - The configuration object containing icons. + * @param key - The key to search for. + * @returns The icon associated with the specified key. + */ +export function getIconWithKey(config, key) { + return config.icons[key]; +} + +/** + * Retrieves the icon associated with the specified setting name. + * + * @param config - The configuration object containing icons. + * @param settingName - The setting name to search for. + * @returns The icon associated with the specified setting name. + */ +export function getIconWithSettingName(config, settingName) { + const key = getKeyWithSettingName(config, settingName); + return getIconWithKey(config, key); +} + +export function getIconForLatestInput(configs, source, devices, settingName) { + let config; + if (source === "gamepad") { + config = configs[devices[Device.GAMEPAD]]; + } else { + config = configs[devices[Device.KEYBOARD]]; + } + const icon = getIconWithSettingName(config, settingName); + if (!icon) { + const isAlt = settingName.includes("ALT_"); + let altSettingName; + if (isAlt) { + altSettingName = settingName.split("ALT_").splice(1)[0]; + } else { + altSettingName = `ALT_${settingName}`; + } + return getIconWithSettingName(config, altSettingName); + } + return icon; +} + +export function assign(config, settingNameTarget, keycode): boolean { + // first, we need to check if this keycode is already used on another settingName + if (!canIAssignThisKey(config, getKeyWithKeycode(config, keycode)) || !canIOverrideThisSetting(config, settingNameTarget)) { + return false; + } + const previousSettingName = getSettingNameWithKeycode(config, keycode); + // if it was already bound, we delete the bind + if (previousSettingName) { + const previousKey = getKeyWithSettingName(config, previousSettingName); + config.custom[previousKey] = -1; + } + // then, we need to delete the current key for this settingName + const currentKey = getKeyWithSettingName(config, settingNameTarget); + config.custom[currentKey] = -1; + + // then, the new key is assigned to the new settingName + const newKey = getKeyWithKeycode(config, keycode); + config.custom[newKey] = settingNameTarget; + return true; +} + +export function swap(config, settingNameTarget, keycode) { + // only for gamepad + if (config.padType === "keyboard") { + return false; + } + const prev_key = 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; + return true; +} + +/** + * Deletes the binding of the specified setting name. + * + * @param config - The configuration object containing custom settings. + * @param settingName - The setting name to delete. + */ +export function deleteBind(config, settingName) { + const key = getKeyWithSettingName(config, settingName); + if (config.blacklist.includes(key)) { + return false; + } + config.custom[key] = -1; + return true; +} + +export function canIAssignThisKey(config, key) { + const settingName = getSettingNameWithKey(config, key); + if (config.blacklist?.includes(key)) { + return false; + } + if (settingName === -1) { + return true; + } + // if (isTheLatestBind(config, settingName)) { + // return false; + // } + return true; +} + +export function canIOverrideThisSetting(config, settingName) { + const key = getKeyWithSettingName(config, settingName); + // || isTheLatestBind(config, settingName) no longer needed since action and cancel are protected + if (config.blacklist?.includes(key)) { + return false; + } + return true; +} + +export function canIDeleteThisKey(config, key) { + return canIAssignThisKey(config, key); +} + +// export function isTheLatestBind(config, settingName) { +// if (config.padType !== "keyboard") { +// return false; +// } +// const isAlt = settingName.includes("ALT_"); +// let altSettingName; +// if (isAlt) { +// altSettingName = settingName.split("ALT_").splice(1)[0]; +// } else { +// altSettingName = `ALT_${settingName}`; +// } +// const secondButton = getKeyWithSettingName(config, altSettingName); +// return secondButton === undefined; +// } diff --git a/src/configs/inputs/pad_dualshock.ts b/src/configs/inputs/pad_dualshock.ts new file mode 100644 index 00000000000..b0aaedf0ab8 --- /dev/null +++ b/src/configs/inputs/pad_dualshock.ts @@ -0,0 +1,88 @@ +import {SettingGamepad} from "../../system/settings-gamepad"; +import {Button} from "../../enums/buttons"; + +/** + * Dualshock mapping + */ +const pad_dualshock = { + padID: "Dualshock", + padType: "dualshock", + deviceMapping: { + RC_S: 0, + RC_E: 1, + RC_W: 2, + RC_N: 3, + START: 9, // Options + SELECT: 8, // Share + LB: 4, + RB: 5, + LT: 6, + RT: 7, + LS: 10, + RS: 11, + LC_N: 12, + LC_S: 13, + LC_W: 14, + LC_E: 15, + TOUCH: 17 + }, + icons: { + RC_S: "CROSS.png", + RC_E: "CIRCLE.png", + RC_W: "SQUARE.png", + RC_N: "TRIANGLE.png", + START: "START.png", + SELECT: "SELECT.png", + LB: "L1.png", + RB: "R1.png", + LT: "L2.png", + RT: "R2.png", + LS: "L3.png", + RS: "R3.png", + LC_N: "UP.png", + LC_S: "DOWN.png", + LC_W: "LEFT.png", + LC_E: "RIGHT.png", + TOUCH: "TOUCH.png" + }, + 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.V, + [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, + 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, + }, +}; + +export default pad_dualshock; diff --git a/src/configs/inputs/pad_generic.ts b/src/configs/inputs/pad_generic.ts new file mode 100644 index 00000000000..f209f6858bd --- /dev/null +++ b/src/configs/inputs/pad_generic.ts @@ -0,0 +1,90 @@ +import {SettingGamepad} from "../../system/settings-gamepad"; +import {Button} from "../../enums/buttons"; + +/** + * Generic pad mapping + */ +const pad_generic = { + padID: "Generic", + padType: "xbox", + deviceMapping: { + 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: "XB_Letter_A_OL.png", + RC_E: "XB_Letter_B_OL.png", + RC_W: "XB_Letter_X_OL.png", + RC_N: "XB_Letter_Y_OL.png", + START: "START.png", + SELECT: "SELECT.png", + LB: "Bumper_L.png", + RB: "Bumper_R.png", + LT: "Trigger_L.png", + RT: "Trigger_R.png", + LS: "LS.png", + RS: "RS.png", + LC_N: "UP.png", + LC_S: "DOWN.png", + LC_W: "LEFT.png", + LC_E: "RIGHT.png", + }, + 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.V, + [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, + 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 + }, + blacklist: [ + "LC_N", + "LC_S", + "LC_W", + "LC_E", + ] +}; + +export default pad_generic; diff --git a/src/configs/inputs/pad_procon.ts b/src/configs/inputs/pad_procon.ts new file mode 100644 index 00000000000..ccaaf5fc635 --- /dev/null +++ b/src/configs/inputs/pad_procon.ts @@ -0,0 +1,85 @@ +import {SettingGamepad} from "#app/system/settings-gamepad"; +import {Button} from "#app/enums/buttons"; + +/** + * Nintendo Pro Controller mapping + */ +const pad_procon = { + padID: "Pro Controller", + padType: "xbox", + deviceMapping: { + RC_S: 1, + RC_E: 0, + RC_W: 3, + RC_N: 2, + 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, + MENU: 16, // Home + }, + icons: { + RC_S: "XB_Letter_B_OL.png", + RC_E: "XB_Letter_A_OL.png", + RC_W: "XB_Letter_Y_OL.png", + RC_N: "XB_Letter_X_OL.png", + START: "START.png", + SELECT: "SELECT.png", + LB: "Bumper_L.png", + RB: "Bumper_R.png", + LT: "Trigger_L.png", + RT: "Trigger_R.png", + LS: "LS.png", + RS: "RS.png", + LC_N: "UP.png", + LC_S: "DOWN.png", + LC_W: "LEFT.png", + LC_E: "RIGHT.png", + }, + 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.V, + [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, + 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 + }, +}; + +export default pad_procon; diff --git a/src/configs/inputs/pad_unlicensedSNES.ts b/src/configs/inputs/pad_unlicensedSNES.ts new file mode 100644 index 00000000000..803d30442b5 --- /dev/null +++ b/src/configs/inputs/pad_unlicensedSNES.ts @@ -0,0 +1,76 @@ +import {SettingGamepad} from "../../system/settings-gamepad"; +import {Button} from "../../enums/buttons"; + +/** + * 081f-e401 - UnlicensedSNES + */ +const pad_unlicensedSNES = { + padID: "081f-e401", + padType: "xbox", + deviceMapping : { + RC_S: 2, + RC_E: 1, + RC_W: 3, + RC_N: 0, + START: 9, + SELECT: 8, + LB: 4, + RB: 5, + LC_N: 12, + LC_S: 13, + LC_W: 14, + LC_E: 15 + }, + icons: { + RC_S: "XB_Letter_A_OL.png", + RC_E: "XB_Letter_B_OL.png", + RC_W: "XB_Letter_X_OL.png", + RC_N: "XB_Letter_Y_OL.png", + START: "START.png", + SELECT: "SELECT.png", + LB: "Bumper_L.png", + RB: "Bumper_R.png", + LC_N: "UP.png", + LC_S: "DOWN.png", + LC_W: "LEFT.png", + LC_E: "RIGHT.png", + }, + 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.V, + [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, + 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: -1, + RT: -1, + LS: -1, + RS: -1 + }, +}; + +export default pad_unlicensedSNES; diff --git a/src/configs/inputs/pad_xbox360.ts b/src/configs/inputs/pad_xbox360.ts new file mode 100644 index 00000000000..213ed7f89fb --- /dev/null +++ b/src/configs/inputs/pad_xbox360.ts @@ -0,0 +1,84 @@ +import {SettingGamepad} from "../../system/settings-gamepad"; +import {Button} from "#app/enums/buttons"; + +/** + * Generic pad mapping + */ +const pad_xbox360 = { + padID: "Xbox 360 controller (XInput STANDARD GAMEPAD)", + padType: "xbox", + deviceMapping: { + 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: "XB_Letter_A_OL.png", + RC_E: "XB_Letter_B_OL.png", + RC_W: "XB_Letter_X_OL.png", + RC_N: "XB_Letter_Y_OL.png", + START: "START.png", + SELECT: "SELECT.png", + LB: "Bumper_L.png", + RB: "Bumper_R.png", + LT: "Trigger_L.png", + RT: "Trigger_R.png", + LS: "LS.png", + RS: "RS.png", + LC_N: "UP.png", + LC_S: "DOWN.png", + LC_W: "LEFT.png", + LC_E: "RIGHT.png", + }, + 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.V, + [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, + 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 + }, +}; + +export default pad_xbox360; diff --git a/src/configs/pad_dualshock.ts b/src/configs/pad_dualshock.ts deleted file mode 100644 index 4700e8e6c00..00000000000 --- a/src/configs/pad_dualshock.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Dualshock mapping - */ -const pad_dualshock = { - padID: "Dualshock", - padType: "Sony", - gamepadMapping: { - RC_S: 0, - RC_E: 1, - RC_W: 2, - RC_N: 3, - START: 9, // Options - SELECT: 8, // Share - LB: 4, - RB: 5, - LT: 6, - RT: 7, - LS: 10, - RS: 11, - LC_N: 12, - LC_S: 13, - LC_W: 14, - LC_E: 15, - MENU: 16, - TOUCH: 17 - }, -}; - -export default pad_dualshock; diff --git a/src/configs/pad_generic.ts b/src/configs/pad_generic.ts deleted file mode 100644 index 22e8c6e0579..00000000000 --- a/src/configs/pad_generic.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Generic pad mapping - */ -const pad_generic = { - padID: "Generic", - padType: "generic", - 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 - }, -}; - -export default pad_generic; diff --git a/src/configs/pad_procon.ts b/src/configs/pad_procon.ts deleted file mode 100644 index c9efe6fb262..00000000000 --- a/src/configs/pad_procon.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Nintendo Pro Controller mapping - */ -const pad_procon = { - padID: "Pro Controller", - padType: "Nintendo", - gamepadMapping: { - RC_S: 1, - RC_E: 0, - RC_W: 3, - RC_N: 2, - 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, - MENU: 16, // Home - }, -}; - -export default pad_procon; diff --git a/src/configs/pad_unlicensedSNES.ts b/src/configs/pad_unlicensedSNES.ts deleted file mode 100644 index 808e30cb6b4..00000000000 --- a/src/configs/pad_unlicensedSNES.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * 081f-e401 - UnlicensedSNES - */ -const pad_unlicensedSNES = { - padID: "081f-e401", - padType: "snes", - gamepadMapping : { - RC_S: 2, - RC_E: 1, - RC_W: 3, - RC_N: 0, - START: 9, - SELECT: 8, - LB: 4, - RB: 5, - LC_N: 12, - LC_S: 13, - LC_W: 14, - LC_E: 15 - } -}; - -export default pad_unlicensedSNES; diff --git a/src/configs/pad_xbox360.ts b/src/configs/pad_xbox360.ts deleted file mode 100644 index 50ec4f71c67..00000000000 --- a/src/configs/pad_xbox360.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * 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, - MENU: 16 - }, -}; - -export default pad_xbox360; diff --git a/src/enums/devices.ts b/src/enums/devices.ts new file mode 100644 index 00000000000..b085dfbada3 --- /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 cb7b828f57c..2791f3b5b85 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -1,30 +1,70 @@ import Phaser from "phaser"; import * as Utils from "./utils"; -import {ButtonKey, initTouchControls} from "./touch-controls"; -import pad_generic from "./configs/pad_generic"; -import pad_unlicensedSNES from "./configs/pad_unlicensedSNES"; -import pad_xbox360 from "./configs/pad_xbox360"; -import pad_dualshock from "./configs/pad_dualshock"; -import pad_procon from "./configs/pad_procon"; +import {deepCopy} from "./utils"; +import {initTouchControls} from "./touch-controls"; +import pad_generic from "./configs/inputs/pad_generic"; +import pad_unlicensedSNES from "./configs/inputs/pad_unlicensedSNES"; +import pad_xbox360 from "./configs/inputs/pad_xbox360"; +import pad_dualshock from "./configs/inputs/pad_dualshock"; +import pad_procon from "./configs/inputs/pad_procon"; import {Button} from "./enums/buttons"; +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_qwerty from "./configs/inputs/cfg_keyboard_qwerty"; +import {Device} from "#app/enums/devices"; +import { + assign, + getButtonWithKeycode, + getIconForLatestInput, swap, +} from "#app/configs/inputs/configHandler"; import BattleScene from "./battle-scene"; +import {SettingGamepad} from "#app/system/settings-gamepad"; +import {SettingKeyboard} from "#app/system/settings-keyboard"; -export interface GamepadMapping { +export interface DeviceMapping { [key: string]: number; } -export interface GamepadConfig { - padID: string; - padType: string; - gamepadMapping: GamepadMapping; +export interface IconsMapping { + [key: string]: string; } -export interface ActionGamepadMapping { +export interface SettingMapping { [key: string]: Button; } +export interface MappingLayout { + [key: string]: SettingGamepad | SettingKeyboard | number; +} + +export interface InterfaceConfig { + padID: string; + padType: string; + deviceMapping: DeviceMapping; + icons: IconsMapping; + settings: SettingMapping; + default: MappingLayout; + custom?: MappingLayout; +} + const repeatInputDelayMillis = 250; +// Phaser.Input.Gamepad.GamepadPlugin#refreshPads +declare module "phaser" { + namespace Input { + namespace Gamepad { + interface GamepadPlugin { + /** + * Refreshes the list of connected Gamepads. + * This is called automatically when a gamepad is connected or disconnected, and during the update loop. + */ + refreshPads(): void; + } + } + } +} + /** * Manages and handles all input controls for the game, including keyboard and gamepad interactions. * @@ -49,17 +89,24 @@ const repeatInputDelayMillis = 250; */ export class InputsController { private buttonKeys: Phaser.Input.Keyboard.Key[][]; - private gamepads: Phaser.Input.Gamepad.Gamepad[] = new Array(); + private gamepads: Array = new Array(); private scene: BattleScene; + public events: Phaser.Events.EventEmitter; private buttonLock: Button; - private buttonLock2: Button; private interactions: Map> = new Map(); private time: Phaser.Time.Clock; - private player: GamepadMapping; + private configs: Map = new Map(); - private gamepadSupport: boolean = true; - public events: Phaser.Events.EventEmitter; + public gamepadSupport: boolean = true; + public selectedDevice; + + private disconnectedGamepads: Array = new Array(); + + 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. @@ -72,10 +119,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: BattleScene) { 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] = { @@ -99,18 +151,28 @@ 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; + 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) { + if (!thisGamepad) { + return; + } this.refreshGamepads(); this.setupGamepad(thisGamepad); + this.onReconnect(thisGamepad); + }, this); + + this.scene.input.gamepad.on("disconnected", function (thisGamepad) { + this.onDisconnect(thisGamepad); // when a gamepad is disconnected }, this); // Check to see if the gamepad has already been setup by the browser + this.scene.input.gamepad.refreshPads(); if (this.scene.input.gamepad.total) { this.refreshGamepads(); for (const thisGamepad of this.gamepads) { @@ -120,10 +182,10 @@ 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(); + initTouchControls(this.events); } /** @@ -149,35 +211,58 @@ export class InputsController { this.gamepadSupport = true; } else { this.gamepadSupport = false; - // if we disable the gamepad, we want to release every key pressed this.deactivatePressedKey(); } } + /** + * 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); + } + + /** + * Sets the currently chosen keyboard layout and initializes related settings. + * + * @param layoutKeyboard - The identifier of the keyboard layout to set as chosen. + */ + setChosenKeyboardLayout(layoutKeyboard: String): void { + this.deactivatePressedKey(); + this.initChosenLayoutKeyboard(layoutKeyboard); + } + /** * 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()) { 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") { + if ( + (!this.gamepadSupport && this.interactions[b].source === "gamepad") || + (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. - this.delLastProcessedMovementTime(b); + this.delLastProcessedMovementTime(b as Button); return; } // Emits an event for the button press. @@ -185,25 +270,104 @@ export class InputsController { controller_type: this.interactions[b].source, button: b, }); - this.setLastProcessedMovementTime(b, this.interactions[b].source); + this.setLastProcessedMovementTime(b as Button, this.interactions[b].source, this.interactions[b].sourceName); } } } /** - * Configures a gamepad for use based on its device ID. + * 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 (gamepadName) { + this.selectedDevice[Device.GAMEPAD] = gamepadName.toLowerCase(); + } + const handler = this.scene.ui?.handlers[Mode.SETTINGS_GAMEPAD] as SettingsGamepadUiHandler; + handler && handler.updateChosenGamepadDisplay(); + } + + /** + * Initializes the chosen keyboard layout by setting its identifier in the local storage and updating the UI to reflect the chosen layout. + * If a layout name is provided, it uses that as the chosen layout; otherwise, it defaults to the currently chosen layout. + * @param layoutKeyboard Optional parameter to specify the name of the keyboard layout to initialize as chosen. + */ + initChosenLayoutKeyboard(layoutKeyboard?: String): void { + if (layoutKeyboard) { + this.selectedDevice[Device.KEYBOARD] = layoutKeyboard.toLowerCase(); + } + 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 + * 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 to set up. + * @param thisGamepad The gamepad that has been disconnected. + */ + onDisconnect(thisGamepad: Phaser.Input.Gamepad.Gamepad): void { + this.disconnectedGamepads.push(thisGamepad.id); + } + + /** + * 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. * - * @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 has been reconnected. + */ + onReconnect(thisGamepad: Phaser.Input.Gamepad.Gamepad): void { + this.disconnectedGamepads = this.disconnectedGamepads.filter(g => g !== thisGamepad.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 that is being set up. */ setupGamepad(thisGamepad: Phaser.Input.Gamepad.Gamepad): void { - const gamepadID = thisGamepad.id.toLowerCase(); - const mappedPad = this.mapGamepad(gamepadID); - this.player = mappedPad.gamepadMapping; + 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)) as InterfaceConfig; + config.custom = this.configs[gamepadID]?.custom || {...config.default}; + this.configs[gamepadID] = config; + this.scene.gameData?.saveMappingConfigs(gamepadID, this.configs[gamepadID]); + } + this.lastSource = "gamepad"; + const handler = this.scene.ui?.handlers[Mode.SETTINGS_GAMEPAD] as SettingsGamepadUiHandler; + handler && handler.updateChosenGamepadDisplay(); + } + + /** + * Initializes or updates configurations for connected keyboards. + */ + setupKeyboard(): void { + for (const layout of ["default"]) { + const config = deepCopy(this.getConfigKeyboard(layout)) as InterfaceConfig; + 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.selectedDevice[Device.KEYBOARD]); } /** @@ -226,89 +390,110 @@ 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. + * Ensures the keyboard is initialized by checking if there is an active configuration for the keyboard. + * If not, it sets up the keyboard with default configurations. */ - getActionGamepadMapping(): ActionGamepadMapping { - const gamepadMapping = {}; - if (!this?.player) { - return gamepadMapping; + ensureKeyboardIsInit(): void { + if (!this.getActiveConfig(Device.KEYBOARD)?.padID) { + this.setupKeyboard(); } - gamepadMapping[this.player.LC_N] = Button.UP; - gamepadMapping[this.player.LC_S] = Button.DOWN; - gamepadMapping[this.player.LC_W] = Button.LEFT; - gamepadMapping[this.player.LC_E] = Button.RIGHT; - gamepadMapping[this.player.TOUCH] = Button.SUBMIT; - gamepadMapping[this.player.RC_S] = this.scene.abSwapped ? Button.CANCEL : Button.ACTION; - gamepadMapping[this.player.RC_E] = this.scene.abSwapped ? Button.ACTION : Button.CANCEL; - gamepadMapping[this.player.SELECT] = Button.STATS; - gamepadMapping[this.player.START] = Button.MENU; - gamepadMapping[this.player.RB] = Button.CYCLE_SHINY; - gamepadMapping[this.player.LB] = Button.CYCLE_FORM; - gamepadMapping[this.player.LT] = Button.CYCLE_GENDER; - gamepadMapping[this.player.RT] = Button.CYCLE_ABILITY; - gamepadMapping[this.player.RC_W] = Button.CYCLE_NATURE; - gamepadMapping[this.player.RC_N] = Button.V; - gamepadMapping[this.player.LS] = Button.SPEED_UP; - gamepadMapping[this.player.RS] = Button.SLOW_DOWN; - - return gamepadMapping; } /** - * Handles the 'down' event for gamepad buttons, emitting appropriate events and updating the interaction state. + * Handles the keydown event for the keyboard. * - * @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 event The keyboard event. */ - gamepadButtonDown(pad: Phaser.Input.Gamepad.Gamepad, button: Phaser.Input.Gamepad.Button, value: number): void { - if (!this.gamepadSupport) { + keyboardKeyDown(event): void { + this.lastSource = "keyboard"; + const keyDown = event.keyCode; + this.ensureKeyboardIsInit(); + if (this.keys.includes(keyDown)) { return; } - const actionMapping = this.getActionGamepadMapping(); - const buttonDown = actionMapping.hasOwnProperty(button.index) && actionMapping[button.index]; + this.keys.push(keyDown); + const buttonDown = getButtonWithKeycode(this.getActiveConfig(Device.KEYBOARD), keyDown); + if (buttonDown !== undefined) { + this.events.emit("input_down", { + controller_type: "keyboard", + button: buttonDown, + }); + this.setLastProcessedMovementTime(buttonDown, "keyboard", this.selectedDevice[Device.KEYBOARD]); + } + } + + /** + * Handles the keyup event for the keyboard. + * + * @param event The keyboard event. + */ + keyboardKeyUp(event): void { + this.lastSource = "keyboard"; + const keyDown = event.keyCode; + this.keys = this.keys.filter(k => k !== keyDown); + this.ensureKeyboardIsInit(); + const buttonUp = getButtonWithKeycode(this.getActiveConfig(Device.KEYBOARD), keyDown); + if (buttonUp !== undefined) { + this.events.emit("input_up", { + controller_type: "keyboard", + button: buttonUp, + }); + this.delLastProcessedMovementTime(buttonUp); + } + } + + /** + * 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 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 (!this.configs[this.selectedDevice[Device.KEYBOARD]]?.padID) { + this.setupKeyboard(); + } + if (!pad) { + return; + } + this.lastSource = "gamepad"; + if (!this.selectedDevice[Device.GAMEPAD] || (this.scene.ui.getMode() !== Mode.GAMEPAD_BINDING && this.selectedDevice[Device.GAMEPAD] !== pad.id.toLowerCase())) { + this.setChosenGamepad(pad.id); + } + if (!this.gamepadSupport || pad.id.toLowerCase() !== this.selectedDevice[Device.GAMEPAD].toLowerCase()) { + return; + } + const activeConfig = this.getActiveConfig(Device.GAMEPAD); + const buttonDown = activeConfig && getButtonWithKeycode(activeConfig, button.index); if (buttonDown !== undefined) { this.events.emit("input_down", { controller_type: "gamepad", button: buttonDown, }); - this.setLastProcessedMovementTime(buttonDown, "gamepad"); + this.setLastProcessedMovementTime(buttonDown, "gamepad", pad.id); } } /** - * 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 (!this.gamepadSupport) { + if (!pad) { return; } - const actionMapping = this.getActionGamepadMapping(); - const buttonUp = actionMapping.hasOwnProperty(button.index) && actionMapping[button.index]; + this.lastSource = "gamepad"; + 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", @@ -319,114 +504,14 @@ export class InputsController { } /** - * Configures keyboard controls for the game, mapping physical keys to game actions. + * 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. * - * @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. + * @param id The identifier string of the gamepad. + * @returns InterfaceConfig The configuration object corresponding to the identified gamepad type. */ - 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.V]: [keyCodes.V], - [Button.SPEED_UP]: [keyCodes.PLUS], - [Button.SLOW_DOWN]: [keyCodes.MINUS] - }; - const mobileKeyConfig = new Map(); - for (const b of Utils.getEnumValues(Button)) { - const keys: Phaser.Input.Keyboard.Key[] = []; - if (keyConfig.hasOwnProperty(b)) { - for (const 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.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); - }); - } - }); - } - - /** - * 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 the ID includes both '057e' and '2009', it is identified as a Pro controller gamepad. - * If no specific identifiers are recognized, a generic gamepad configuration is returned. - */ - mapGamepad(id: string): GamepadConfig { + getConfig(id: string): InterfaceConfig { id = id.toLowerCase(); if (id.includes("081f") && id.includes("e401")) { @@ -442,6 +527,20 @@ export class InputsController { return pad_generic; } + /** + * Retrieves the configuration object for a keyboard layout based on its identifier. + * + * @param id The identifier string of the keyboard layout. + * @returns InterfaceConfig The configuration object corresponding to the identified keyboard layout. + */ + getConfigKeyboard(id: string): InterfaceConfig { + if (id === "default") { + return cfg_keyboard_qwerty; + } + + return cfg_keyboard_qwerty; + } + /** * repeatInputDurationJustPassed returns true if @param button has been held down long * enough to fire a repeated input. A button must claim the buttonLock before @@ -471,7 +570,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"): void { + setLastProcessedMovementTime(button: Button, source: String = "keyboard", sourceName?: String): void { if (!this.interactions.hasOwnProperty(button)) { return; } @@ -479,6 +578,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.toLowerCase(); } /** @@ -503,6 +603,7 @@ export class InputsController { this.interactions[button].pressTime = null; this.interactions[button].isPressed = false; this.interactions[button].source = null; + this.interactions[button].sourceName = null; } /** @@ -512,7 +613,7 @@ export class InputsController { * 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 + * - Releases button lock for predefined buttons, 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: @@ -524,55 +625,44 @@ 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)) { if (this.interactions.hasOwnProperty(b)) { this.interactions[b].pressTime = null; this.interactions[b].isPressed = false; this.interactions[b].source = null; + this.interactions[b].sourceName = null; } } + setTimeout(() => this.pauseUpdate = false, 500); } /** * 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`. + * @returns `true` if the button is locked, 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); + return this.buttonLock === button; } /** - * Sets a lock on a given button if it is not already locked. + * Sets a lock on a given button. * * @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. + * It checks if the button is already locked. */ 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 { - this.buttonLock = button; - } + this.buttonLock = button; } /** @@ -581,15 +671,93 @@ export class InputsController { * @param button - The button whose lock is to be released. * * @remarks - * This method checks both lock variables (`buttonLock` and `buttonLock2`). + * This method checks lock variable. * 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; + } + } + + /** + * 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(device: Device) { + if (this.configs[this.selectedDevice[device]]?.padID) { + return this.configs[this.selectedDevice[device]]; + } + return null; + } + + getIconForLatestInputRecorded(settingName) { + if (this.lastSource === "keyboard") { + this.ensureKeyboardIsInit(); + } + return getIconForLatestInput(this.configs, this.lastSource, this.selectedDevice, settingName); + } + + getLastSourceDevice(): Device { + if (this.lastSource === "gamepad") { + return Device.GAMEPAD; + } else { + return Device.KEYBOARD; + } + } + + getLastSourceConfig() { + const sourceDevice = this.getLastSourceDevice(); + if (sourceDevice === Device.KEYBOARD) { + this.ensureKeyboardIsInit(); + } + return this.getActiveConfig(sourceDevice); + } + + getLastSourceType() { + const config = this.getLastSourceConfig(); + return config?.padType; + } + + /** + * Injects a custom mapping configuration into the configuration for a specific gamepad. + * If the device does not have an existing configuration, it initializes one first. + * + * @param selectedDevice The identifier of the device to configure. + * @param mappingConfigs The mapping configuration to apply to the device. + */ + injectConfig(selectedDevice: string, mappingConfigs): void { + if (!this.configs[selectedDevice]) { + this.configs[selectedDevice] = {}; + } + this.configs[selectedDevice].custom = mappingConfigs.custom; + } + + resetConfigs(): void { + this.configs = new Map(); + if (this.getGamepadsName()?.length) { + this.setupGamepad(this.selectedDevice[Device.GAMEPAD]); + } + this.setupKeyboard(); + } + + /** + * Swaps a binding in the configuration. + * + * @param config The configuration object. + * @param settingName The name of the setting to swap. + * @param pressedButton The button that was pressed. + */ + assignBinding(config, settingName, pressedButton): boolean { + this.pauseUpdate = true; + setTimeout(() => this.pauseUpdate = false, 500); + if (config.padType === "keyboard") { + return assign(config, settingName, pressedButton); + } else { + return swap(config, settingName, pressedButton); } } } diff --git a/src/loading-scene.ts b/src/loading-scene.ts index 51adaf6c746..fe63b39f805 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -251,6 +251,10 @@ export class LoadingScene extends SceneBase { } } + this.loadAtlas("dualshock", "inputs"); + this.loadAtlas("xbox", "inputs"); + this.loadAtlas("keyboard", "inputs"); + this.loadSe("select"); this.loadSe("menu_open"); this.loadSe("hit"); diff --git a/src/system/game-data.ts b/src/system/game-data.ts index b1cbf77f4c6..dc73634e70b 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -30,6 +30,8 @@ import { allMoves } from "../data/move"; import { TrainerVariant } from "../field/trainer"; import { OutdatedPhase, ReloadSessionPhase } from "#app/phases"; import { Variant, variantData } from "#app/data/variant"; +import {setSettingGamepad, SettingGamepad, settingGamepadDefaults} from "./settings-gamepad"; +import {setSettingKeyboard, SettingKeyboard, settingKeyboardDefaults} from "#app/system/settings-keyboard"; import { TerrainChangedEvent, WeatherChangedEvent } from "#app/field/arena-events.js"; const saveKey = "x0i2O7WRiANTqPmZ"; // Temporary; secure encryption is not yet necessary @@ -243,6 +245,8 @@ export class GameData { constructor(scene: BattleScene) { this.scene = scene; this.loadSettings(); + this.loadGamepadSettings(); + this.loadMappingConfigs(); this.trainerId = Utils.randInt(65536); this.secretId = Utils.randInt(65536); this.starterData = {}; @@ -565,6 +569,125 @@ export class GameData { return true; } + /** + * Saves the mapping configurations for a specified device. + * + * @param deviceName - The name of the device for which the configurations are being saved. + * @param config - The configuration object containing custom mapping details. + * @returns `true` if the configurations are successfully saved. + */ + public saveMappingConfigs(deviceName: string, config): boolean { + const key = deviceName.toLowerCase(); // Convert the gamepad name to lowercase to use as a key + let mappingConfigs: object = {}; // Initialize an empty object to hold the mapping configurations + if (localStorage.hasOwnProperty("mappingConfigs")) {// Check if 'mappingConfigs' exists in localStorage + mappingConfigs = JSON.parse(localStorage.getItem("mappingConfigs")); + } // Parse the existing 'mappingConfigs' from localStorage + if (!mappingConfigs[key]) { + mappingConfigs[key] = {}; + } // If there is no configuration for the given key, create an empty object for it + mappingConfigs[key].custom = config.custom; // Assign the custom configuration to the mapping configuration for the given key + localStorage.setItem("mappingConfigs", JSON.stringify(mappingConfigs)); // Save the updated mapping configurations back to localStorage + return true; // Return true to indicate the operation was successful + } + + /** + * Loads the mapping configurations from localStorage and injects them into the input controller. + * + * @returns `true` if the configurations are successfully loaded and injected; `false` if no configurations are found in localStorage. + * + * @remarks + * This method checks if the 'mappingConfigs' entry exists in localStorage. If it does not exist, the method returns `false`. + * If 'mappingConfigs' exists, it parses the configurations and injects each configuration into the input controller + * for the corresponding gamepad or device key. The method then returns `true` to indicate success. + */ + public loadMappingConfigs(): boolean { + if (!localStorage.hasOwnProperty("mappingConfigs")) {// Check if 'mappingConfigs' exists in localStorage + return false; + } // If 'mappingConfigs' does not exist, return false + + const mappingConfigs = JSON.parse(localStorage.getItem("mappingConfigs")); // Parse the existing 'mappingConfigs' from localStorage + + for (const key of Object.keys(mappingConfigs)) {// Iterate over the keys of the mapping configurations + this.scene.inputController.injectConfig(key, mappingConfigs[key]); + } // Inject each configuration into the input controller for the corresponding key + + return true; // Return true to indicate the operation was successful + } + + public resetMappingToFactory(): boolean { + if (!localStorage.hasOwnProperty("mappingConfigs")) {// Check if 'mappingConfigs' exists in localStorage + return false; + } // If 'mappingConfigs' does not exist, return false + localStorage.removeItem("mappingConfigs"); + this.scene.inputController.resetConfigs(); + } + + /** + * Saves a gamepad setting to localStorage. + * + * @param setting - The gamepad setting to save. + * @param valueIndex - The index of the value to set for the gamepad setting. + * @returns `true` if the setting is successfully saved. + * + * @remarks + * This method initializes an empty object for gamepad settings if none exist in localStorage. + * It then updates the setting in the current scene and iterates over the default gamepad settings + * to update the specified setting with the new value. Finally, it saves the updated settings back + * to localStorage and returns `true` to indicate success. + */ + public saveGamepadSetting(setting: SettingGamepad, valueIndex: integer): boolean { + let settingsGamepad: object = {}; // Initialize an empty object to hold the gamepad settings + + if (localStorage.hasOwnProperty("settingsGamepad")) { // Check if 'settingsGamepad' exists in localStorage + settingsGamepad = JSON.parse(localStorage.getItem("settingsGamepad")); // Parse the existing 'settingsGamepad' from localStorage + } + + setSettingGamepad(this.scene, setting as SettingGamepad, valueIndex); // Set the gamepad setting in the current scene + + Object.keys(settingGamepadDefaults).forEach(s => { // Iterate over the default gamepad settings + if (s === setting) {// If the current setting matches, update its value + settingsGamepad[s] = valueIndex; + } + }); + + localStorage.setItem("settingsGamepad", JSON.stringify(settingsGamepad)); // Save the updated gamepad settings back to localStorage + + return true; // Return true to indicate the operation was successful + } + + /** + * Saves a keyboard setting to localStorage. + * + * @param setting - The keyboard setting to save. + * @param valueIndex - The index of the value to set for the keyboard setting. + * @returns `true` if the setting is successfully saved. + * + * @remarks + * This method initializes an empty object for keyboard settings if none exist in localStorage. + * It then updates the setting in the current scene and iterates over the default keyboard settings + * to update the specified setting with the new value. Finally, it saves the updated settings back + * to localStorage and returns `true` to indicate success. + */ + public saveKeyboardSetting(setting: SettingKeyboard, valueIndex: integer): boolean { + let settingsKeyboard: object = {}; // Initialize an empty object to hold the keyboard settings + + if (localStorage.hasOwnProperty("settingsKeyboard")) { // Check if 'settingsKeyboard' exists in localStorage + settingsKeyboard = JSON.parse(localStorage.getItem("settingsKeyboard")); // Parse the existing 'settingsKeyboard' from localStorage + } + + setSettingKeyboard(this.scene, setting as SettingKeyboard, valueIndex); // Set the keyboard setting in the current scene + + Object.keys(settingKeyboardDefaults).forEach(s => { // Iterate over the default keyboard settings + if (s === setting) {// If the current setting matches, update its value + settingsKeyboard[s] = valueIndex; + } + }); + + localStorage.setItem("settingsKeyboard", JSON.stringify(settingsKeyboard)); // Save the updated keyboard settings back to localStorage + + return true; // Return true to indicate the operation was successful + } + private loadSettings(): boolean { Object.values(Setting).map(setting => setting as Setting).forEach(setting => setSetting(this.scene, setting, settingDefaults[setting])); @@ -579,6 +702,19 @@ export class GameData { } } + 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; + } + const settingsGamepad = JSON.parse(localStorage.getItem("settingsGamepad")); + + for (const setting of Object.keys(settingsGamepad)) { + setSettingGamepad(this.scene, setting as SettingGamepad, settingsGamepad[setting]); + } + } + public saveTutorialFlag(tutorial: Tutorial, flag: boolean): boolean { let tutorials: object = {}; if (localStorage.hasOwnProperty("tutorials")) { diff --git a/src/system/settings-gamepad.ts b/src/system/settings-gamepad.ts new file mode 100644 index 00000000000..22cc07efce1 --- /dev/null +++ b/src/system/settings-gamepad.ts @@ -0,0 +1,147 @@ +import BattleScene from "../battle-scene"; +import {SettingDefaults, SettingOptions} from "./settings"; +import SettingsGamepadUiHandler from "../ui/settings/settings-gamepad-ui-handler"; +import {Mode} from "../ui/ui"; +import {truncateString} from "../utils"; +import {Button} from "../enums/buttons"; +import {SettingKeyboard} from "#app/system/settings-keyboard"; + +export enum SettingGamepad { + Controller = "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", + 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", +} + +export const settingGamepadOptions: SettingOptions = { + [SettingGamepad.Controller]: ["Default", "Change"], + [SettingGamepad.Gamepad_Support]: ["Auto", "Disabled"], + [SettingGamepad.Button_Up]: [`KEY ${Button.UP.toString()}`, "Press action to assign"], + [SettingGamepad.Button_Down]: [`KEY ${Button.DOWN.toString()}`, "Press action to assign"], + [SettingGamepad.Button_Left]: [`KEY ${Button.LEFT.toString()}`, "Press action to assign"], + [SettingGamepad.Button_Right]: [`KEY ${Button.RIGHT.toString()}`, "Press action to assign"], + [SettingGamepad.Button_Action]: [`KEY ${Button.ACTION.toString()}`, "Press action to assign"], + [SettingGamepad.Button_Cancel]: [`KEY ${Button.CANCEL.toString()}`, "Press action to assign"], + [SettingGamepad.Button_Menu]: [`KEY ${Button.MENU.toString()}`, "Press action to assign"], + [SettingGamepad.Button_Stats]: [`KEY ${Button.STATS.toString()}`, "Press action to assign"], + [SettingGamepad.Button_Cycle_Form]: [`KEY ${Button.CYCLE_FORM.toString()}`, "Press action to assign"], + [SettingGamepad.Button_Cycle_Shiny]: [`KEY ${Button.CYCLE_SHINY.toString()}`, "Press action to assign"], + [SettingGamepad.Button_Cycle_Gender]: [`KEY ${Button.CYCLE_GENDER.toString()}`, "Press action to assign"], + [SettingGamepad.Button_Cycle_Ability]: [`KEY ${Button.CYCLE_ABILITY.toString()}`, "Press action to assign"], + [SettingGamepad.Button_Cycle_Nature]: [`KEY ${Button.CYCLE_NATURE.toString()}`, "Press action to assign"], + [SettingGamepad.Button_Cycle_Variant]: [`KEY ${Button.V.toString()}`, "Press action to assign"], + [SettingGamepad.Button_Speed_Up]: [`KEY ${Button.SPEED_UP.toString()}`, "Press action to assign"], + [SettingGamepad.Button_Slow_Down]: [`KEY ${Button.SLOW_DOWN.toString()}`, "Press action to assign"], + [SettingGamepad.Button_Submit]: [`KEY ${Button.SUBMIT.toString()}`, "Press action to assign"], +}; + +export const settingGamepadDefaults: SettingDefaults = { + [SettingGamepad.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, + [SettingGamepad.Button_Stats]: 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, + [SettingGamepad.Button_Cycle_Variant]: 0, + [SettingGamepad.Button_Speed_Up]: 0, + [SettingGamepad.Button_Slow_Down]: 0, + [SettingGamepad.Button_Submit]: 0, +}; + +export const settingGamepadBlackList = [ + SettingKeyboard.Button_Up, + SettingKeyboard.Button_Down, + SettingKeyboard.Button_Left, + SettingKeyboard.Button_Right, +]; + +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.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_Submit: + if (value) { + if (scene.ui) { + const cancelHandler = (success: boolean = false) : boolean => { + scene.ui.revertMode(); + (scene.ui.getHandler() as SettingsGamepadUiHandler).updateBindings(); + return success; + }; + scene.ui.setOverlayMode(Mode.GAMEPAD_BINDING, { + target: setting, + cancelHandler: cancelHandler, + }); + } + } + break; + case SettingGamepad.Controller: + if (value) { + 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.Controller), 0, true); + (scene.ui.getHandler() as SettingsGamepadUiHandler).updateBindings(); + return false; + }; + const changeGamepadHandler = (gamepad: string) => { + scene.inputController.setChosenGamepad(gamepad); + cancelHandler(); + return true; + }; + scene.ui.setOverlayMode(Mode.OPTION_SELECT, { + options: [...gp.map((g: string) => ({ + label: truncateString(g, 30), // Truncate the gamepad name for display + handler: () => changeGamepadHandler(g) + })), { + label: "Cancel", + handler: cancelHandler, + }] + }); + return false; + } + } + break; + } + + return true; +} diff --git a/src/system/settings-keyboard.ts b/src/system/settings-keyboard.ts new file mode 100644 index 00000000000..4ffe6ad3e70 --- /dev/null +++ b/src/system/settings-keyboard.ts @@ -0,0 +1,208 @@ +import {SettingDefaults, SettingOptions} from "#app/system/settings"; +import {Button} from "#app/enums/buttons"; +import BattleScene from "#app/battle-scene"; +import {Mode} from "#app/ui/ui"; +import SettingsKeyboardUiHandler from "#app/ui/settings/settings-keyboard-ui-handler"; + +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'], + [SettingKeyboard.Button_Up]: [`KEY ${Button.UP.toString()}`, "Press action to assign"], + [SettingKeyboard.Button_Down]: [`KEY ${Button.DOWN.toString()}`, "Press action to assign"], + [SettingKeyboard.Alt_Button_Up]: [`KEY ${Button.UP.toString()}`, "Press action to assign"], + [SettingKeyboard.Button_Left]: [`KEY ${Button.LEFT.toString()}`, "Press action to assign"], + [SettingKeyboard.Button_Right]: [`KEY ${Button.RIGHT.toString()}`, "Press action to assign"], + [SettingKeyboard.Button_Action]: [`KEY ${Button.ACTION.toString()}`, "Press action to assign"], + [SettingKeyboard.Button_Menu]: [`KEY ${Button.MENU.toString()}`, "Press action to assign"], + [SettingKeyboard.Button_Submit]: [`KEY ${Button.SUBMIT.toString()}`, "Press action to assign"], + + [SettingKeyboard.Alt_Button_Down]: [`KEY ${Button.DOWN.toString()}`, "Press action to assign"], + [SettingKeyboard.Alt_Button_Left]: [`KEY ${Button.LEFT.toString()}`, "Press action to assign"], + [SettingKeyboard.Alt_Button_Right]: [`KEY ${Button.RIGHT.toString()}`, "Press action to assign"], + [SettingKeyboard.Alt_Button_Action]: [`KEY ${Button.ACTION.toString()}`, "Press action to assign"], + [SettingKeyboard.Button_Cancel]: [`KEY ${Button.CANCEL.toString()}`, "Press action to assign"], + [SettingKeyboard.Alt_Button_Cancel]: [`KEY ${Button.CANCEL.toString()}`, "Press action to assign"], + [SettingKeyboard.Alt_Button_Menu]: [`KEY ${Button.MENU.toString()}`, "Press action to assign"], + [SettingKeyboard.Button_Stats]: [`KEY ${Button.STATS.toString()}`, "Press action to assign"], + [SettingKeyboard.Alt_Button_Stats]: [`KEY ${Button.STATS.toString()}`, "Press action to assign"], + [SettingKeyboard.Button_Cycle_Form]: [`KEY ${Button.CYCLE_FORM.toString()}`, "Press action to assign"], + [SettingKeyboard.Alt_Button_Cycle_Form]: [`KEY ${Button.CYCLE_FORM.toString()}`, "Press action to assign"], + [SettingKeyboard.Button_Cycle_Shiny]: [`KEY ${Button.CYCLE_SHINY.toString()}`, "Press action to assign"], + [SettingKeyboard.Alt_Button_Cycle_Shiny]: [`KEY ${Button.CYCLE_SHINY.toString()}`, "Press action to assign"], + [SettingKeyboard.Button_Cycle_Gender]: [`KEY ${Button.CYCLE_GENDER.toString()}`, "Press action to assign"], + [SettingKeyboard.Alt_Button_Cycle_Gender]: [`KEY ${Button.CYCLE_GENDER.toString()}`, "Press action to assign"], + [SettingKeyboard.Button_Cycle_Ability]: [`KEY ${Button.CYCLE_ABILITY.toString()}`, "Press action to assign"], + [SettingKeyboard.Alt_Button_Cycle_Ability]: [`KEY ${Button.CYCLE_ABILITY.toString()}`, "Press action to assign"], + [SettingKeyboard.Button_Cycle_Nature]: [`KEY ${Button.CYCLE_NATURE.toString()}`, "Press action to assign"], + [SettingKeyboard.Alt_Button_Cycle_Nature]: [`KEY ${Button.CYCLE_NATURE.toString()}`, "Press action to assign"], + [SettingKeyboard.Button_Cycle_Variant]: [`KEY ${Button.V.toString()}`, "Press action to assign"], + [SettingKeyboard.Alt_Button_Cycle_Variant]: [`KEY ${Button.V.toString()}`, "Press action to assign"], + [SettingKeyboard.Button_Speed_Up]: [`KEY ${Button.SPEED_UP.toString()}`, "Press action to assign"], + [SettingKeyboard.Alt_Button_Speed_Up]: [`KEY ${Button.SPEED_UP.toString()}`, "Press action to assign"], + [SettingKeyboard.Button_Slow_Down]: [`KEY ${Button.SLOW_DOWN.toString()}`, "Press action to assign"], + [SettingKeyboard.Alt_Button_Slow_Down]: [`KEY ${Button.SLOW_DOWN.toString()}`, "Press action to assign"], + [SettingKeyboard.Alt_Button_Submit]: [`KEY ${Button.SUBMIT.toString()}`, "Press action to assign"], +}; + +export const 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_Menu]: 0, + [SettingKeyboard.Button_Submit]: 0, + + [SettingKeyboard.Alt_Button_Up]: 0, + [SettingKeyboard.Alt_Button_Down]: 0, + [SettingKeyboard.Alt_Button_Left]: 0, + [SettingKeyboard.Alt_Button_Right]: 0, + [SettingKeyboard.Alt_Button_Action]: 0, + [SettingKeyboard.Button_Cancel]: 0, + [SettingKeyboard.Alt_Button_Cancel]: 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.Alt_Button_Submit]: 0, +}; + +export const settingKeyboardBlackList = [ + SettingKeyboard.Button_Submit, + SettingKeyboard.Button_Menu, + SettingKeyboard.Button_Action, + SettingKeyboard.Button_Cancel, + SettingKeyboard.Button_Up, + SettingKeyboard.Button_Down, + SettingKeyboard.Button_Left, + SettingKeyboard.Button_Right, +]; + + +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: + case SettingKeyboard.Button_Stats: + case SettingKeyboard.Button_Cycle_Shiny: + case SettingKeyboard.Button_Cycle_Form: + case SettingKeyboard.Button_Cycle_Gender: + case SettingKeyboard.Button_Cycle_Ability: + case SettingKeyboard.Button_Cycle_Nature: + 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: + case SettingKeyboard.Alt_Button_Submit: + if (value) { + if (scene.ui) { + const cancelHandler = (success: boolean = false) : boolean => { + scene.ui.revertMode(); + (scene.ui.getHandler() as SettingsKeyboardUiHandler).updateBindings(); + return success; + }; + scene.ui.setOverlayMode(Mode.KEYBOARD_BINDING, { + target: setting, + cancelHandler: cancelHandler, + }); + } + } + 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: 'Default', + // handler: changeKeyboardHandler, + // }] + // }); + // return false; + // } + } + return true; + +} diff --git a/src/system/settings.ts b/src/system/settings.ts index 3cfc6ba9be4..f2a3548da4a 100644 --- a/src/system/settings.ts +++ b/src/system/settings.ts @@ -1,4 +1,3 @@ -import SettingsUiHandler from "#app/ui/settings-ui-handler"; import { Mode } from "#app/ui/ui"; import i18next from "i18next"; import BattleScene from "../battle-scene"; @@ -7,6 +6,7 @@ import { updateWindowType } from "../ui/ui-theme"; import { PlayerGender } from "./game-data"; import { CandyUpgradeNotificationChangedEvent } from "#app/battle-scene-events.js"; import { MoneyFormat } from "../enums/money-format"; +import SettingsUiHandler from "#app/ui/settings/settings-ui-handler"; export enum Setting { Game_Speed = "GAME_SPEED", @@ -31,8 +31,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" } @@ -68,8 +66,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"] }; @@ -97,8 +93,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 }; @@ -194,14 +188,6 @@ 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: - 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/test/helpers/inGameManip.ts b/src/test/helpers/inGameManip.ts new file mode 100644 index 00000000000..c81602dff6d --- /dev/null +++ b/src/test/helpers/inGameManip.ts @@ -0,0 +1,79 @@ +import { + getIconForLatestInput, + getSettingNameWithKeycode +} from "#app/configs/inputs/configHandler"; +import {expect} from "vitest"; +import {SettingKeyboard} from "#app/system/settings-keyboard"; + +export class InGameManip { + private config; + private keycode; + private settingName; + private icon; + private configs; + private latestSource; + private selectedDevice; + + constructor(configs, config, selectedDevice) { + this.config = config; + this.configs = configs; + this.selectedDevice = selectedDevice; + this.keycode = null; + this.settingName = null; + this.icon = null; + this.latestSource = 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; + } + + forTheWantedBind(settingName) { + if (!settingName.includes("Button_")) { + settingName = "Button_" + settingName; + } + this.settingName = SettingKeyboard[settingName]; + return this; + } + + weShouldSeeTheIcon(icon) { + if (!icon.includes("KEY_")) { + icon = "KEY_" + icon; + } + this.icon = this.config.icons[icon]; + expect(getIconForLatestInput(this.configs, this.latestSource, this.selectedDevice, this.settingName)).toEqual(this.icon); + return this; + } + + forTheSource(source) { + this.latestSource = source; + 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) { + if (!settingName.includes("Button_")) { + settingName = "Button_" + settingName; + } + this.settingName = SettingKeyboard[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 00000000000..2377ab70c64 --- /dev/null +++ b/src/test/helpers/menuManip.ts @@ -0,0 +1,131 @@ +import {expect} from "vitest"; +import { + deleteBind, + getIconWithKeycode, + getIconWithSettingName, + getKeyWithKeycode, + getKeyWithSettingName, + assign, + getSettingNameWithKeycode, canIAssignThisKey, canIDeleteThisKey, canIOverrideThisSetting +} from "#app/configs/inputs/configHandler"; +import {SettingKeyboard} from "#app/system/settings-keyboard"; + +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) { + if (!settingName.includes("Button_")) { + settingName = "Button_" + settingName; + } + this.settingName = SettingKeyboard[settingName]; + return this; + } + + iconDisplayedIs(icon) { + if (!(icon.toUpperCase().includes("KEY_"))) { + icon = "KEY_" + icon.toUpperCase(); + } + 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(); + } + + nothingShouldHappen() { + const settingName = getSettingNameWithKeycode(this.config, this.keycode); + expect(settingName).toEqual(-1); + return this; + } + + 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; + } + + whenWeDelete(settingName?: string) { + this.settingName = SettingKeyboard[settingName] || this.settingName; + // const key = getKeyWithSettingName(this.config, this.settingName); + deleteBind(this.config, this.settingName); + // expect(this.config.custom[key]).toEqual(-1); + return this; + } + + whenWeTryToDelete(settingName?: string) { + this.settingName = SettingKeyboard[settingName] || this.settingName; + deleteBind(this.config, this.settingName); + return this; + } + + confirmAssignment() { + assign(this.config, this.settingName, this.keycode); + } + + butLetsForceIt() { + this.confirm(); + } + + + confirm() { + assign(this.config, this.settingName, this.keycode); + } + + weCantAssignThisKey() { + const key = getKeyWithKeycode(this.config, this.keycode); + expect(canIAssignThisKey(this.config, key)).toEqual(false); + return this; + } + + weCantOverrideThisBind() { + expect(canIOverrideThisSetting(this.config, this.settingName)).toEqual(false); + return this; + } + + weCantDelete() { + const key = getKeyWithSettingName(this.config, this.settingName); + expect(canIDeleteThisKey(this.config, key)).toEqual(false); + return this; + } +} diff --git a/src/test/rebinding_setting.test.ts b/src/test/rebinding_setting.test.ts new file mode 100644 index 00000000000..92376006263 --- /dev/null +++ b/src/test/rebinding_setting.test.ts @@ -0,0 +1,417 @@ +import {beforeEach, describe, expect, it} from "vitest"; +import {Button} from "#app/enums/buttons"; +import {deepCopy} from "#app/utils"; +import { + getKeyWithKeycode, + getKeyWithSettingName, +} from "#app/configs/inputs/configHandler"; +import {MenuManip} from "#app/test/helpers/menuManip"; +import {InGameManip} from "#app/test/helpers/inGameManip"; +import {Device} from "#app/enums/devices"; +import {InterfaceConfig} from "#app/inputs-controller"; +import cfg_keyboard_qwerty from "#app/configs/inputs/cfg_keyboard_qwerty"; +import {SettingKeyboard} from "#app/system/settings-keyboard"; + + +describe("Test Rebinding", () => { + let config; + let inGame; + let inTheSettingMenu; + const configs: Map = new Map(); + const selectedDevice = { + [Device.GAMEPAD]: null, + [Device.KEYBOARD]: "default", + }; + + beforeEach(() => { + config = deepCopy(cfg_keyboard_qwerty); + config.custom = {...config.default}; + configs["default"] = config; + inGame = new InGameManip(configs, config, selectedDevice); + inTheSettingMenu = new MenuManip(config); + }); + + it("Check if config is loaded", () => { + expect(config).not.toBeNull(); + }); + it("Check button for setting name", () => { + const settingName = SettingKeyboard.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.A); + 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 = SettingKeyboard.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 = SettingKeyboard.Alt_Button_Left; + const key = getKeyWithSettingName(config, settingName); + expect(key).toEqual("KEY_A"); + }); + 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("KEY_ARROW_LEFT.png"); + }); + it("Check icon for currenly Assigned to key code", () => { + const keycode = Phaser.Input.Keyboard.KeyCodes.A; + const key = getKeyWithKeycode(config, keycode); + const icon = config.icons[key]; + expect(icon).toEqual("A.png"); + }); + it("Check icon for currenly Assigned to setting name", () => { + const settingName = SettingKeyboard.Button_Left; + const key = getKeyWithSettingName(config, settingName); + const icon = config.icons[key]; + expect(icon).toEqual("KEY_ARROW_LEFT.png"); + }); + it("Check icon for currenly Assigned to setting name alt", () => { + const settingName = SettingKeyboard.Alt_Button_Left; + const key = getKeyWithSettingName(config, settingName); + const icon = config.icons[key]; + expect(icon).toEqual("A.png"); + }); + + it("Check if is working", () => { + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Left").iconDisplayedIs("A"); + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Right").iconDisplayedIs("D"); + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Left").iconDisplayedIs("A").weWantThisBindInstead("D").confirm(); + inGame.whenWePressOnKeyboard("A").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Alt_Button_Left"); + }); + + it("Check prevent rebind indirectly the d-pad buttons", () => { + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Left").iconDisplayedIs("A"); + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Right").iconDisplayedIs("D"); + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Left").iconDisplayedIs("A").weWantThisBindInstead("LEFT").weCantAssignThisKey().butLetsForceIt(); + inGame.whenWePressOnKeyboard("LEFT").weShouldTriggerTheButton("Left"); + inGame.whenWePressOnKeyboard("A").weShouldTriggerTheButton("Alt_Button_Left"); + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Alt_Button_Right"); + }); + + it("Swap alt with a d-pad main", () => { + inGame.whenWePressOnKeyboard("UP").weShouldTriggerTheButton("Button_Up"); + inGame.whenWePressOnKeyboard("W").weShouldTriggerTheButton("Alt_Button_Up"); + inTheSettingMenu.whenCursorIsOnSetting("Button_Up").iconDisplayedIs("KEY_ARROW_UP").weWantThisBindInstead("W").weCantOverrideThisBind().butLetsForceIt(); + inGame.whenWePressOnKeyboard("UP").weShouldTriggerTheButton("Button_Up"); + inGame.whenWePressOnKeyboard("W").weShouldTriggerTheButton("Alt_Button_Up"); + }); + + it("Check if double assign d-pad is blocked", () => { + inGame.whenWePressOnKeyboard("RIGHT").weShouldTriggerTheButton("Button_Right"); + inGame.whenWePressOnKeyboard("UP").weShouldTriggerTheButton("Button_Up"); + + inTheSettingMenu.whenCursorIsOnSetting("Button_Left").iconDisplayedIs("KEY_ARROW_LEFT").weWantThisBindInstead("RIGHT").weCantOverrideThisBind().weCantAssignThisKey().butLetsForceIt(); + + 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("UP").weCantOverrideThisBind().weCantAssignThisKey().butLetsForceIt(); + + 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").weCantOverrideThisBind().weCantAssignThisKey().butLetsForceIt(); + + inGame.whenWePressOnKeyboard("LEFT").weShouldTriggerTheButton("Button_Left"); + inGame.whenWePressOnKeyboard("RIGHT").weShouldTriggerTheButton("Button_Right"); + inGame.whenWePressOnKeyboard("UP").weShouldTriggerTheButton("Button_Up"); + }); + + it("Check if double assign is working", () => { + inGame.whenWePressOnKeyboard("A").weShouldTriggerTheButton("Alt_Button_Left"); + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Alt_Button_Right"); + inGame.whenWePressOnKeyboard("W").weShouldTriggerTheButton("Alt_Button_Up"); + + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Left").iconDisplayedIs("KEY_A").weWantThisBindInstead("D").confirm(); + + inGame.whenWePressOnKeyboard("A").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Alt_Button_Left"); + inGame.whenWePressOnKeyboard("W").weShouldTriggerTheButton("Alt_Button_Up"); + + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Left").iconDisplayedIs("KEY_D").weWantThisBindInstead("W").confirm(); + + inGame.whenWePressOnKeyboard("A").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("D").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("W").weShouldTriggerTheButton("Alt_Button_Left"); + + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Left").iconDisplayedIs("KEY_W").weWantThisBindInstead("D").confirm(); + + inGame.whenWePressOnKeyboard("A").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Alt_Button_Left"); + inGame.whenWePressOnKeyboard("W").nothingShouldHappen(); + }); + + it("Check if triple swap d-pad is prevented", () => { + inTheSettingMenu.whenCursorIsOnSetting("Button_Left").iconDisplayedIs("KEY_ARROW_LEFT").weWantThisBindInstead("RIGHT").weCantOverrideThisBind().weCantAssignThisKey().butLetsForceIt(); + + inGame.whenWePressOnKeyboard("LEFT").weShouldTriggerTheButton("Button_Left"); + inGame.whenWePressOnKeyboard("RIGHT").weShouldTriggerTheButton("Button_Right"); + inGame.whenWePressOnKeyboard("UP").weShouldTriggerTheButton("Button_Up"); + + inTheSettingMenu.whenCursorIsOnSetting("Button_Right").iconDisplayedIs("KEY_ARROW_RIGHT").weWantThisBindInstead("UP").weCantOverrideThisBind().weCantAssignThisKey().butLetsForceIt(); + 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("LEFT").weCantOverrideThisBind().weCantAssignThisKey().butLetsForceIt(); + inGame.whenWePressOnKeyboard("LEFT").weShouldTriggerTheButton("Button_Left"); + inGame.whenWePressOnKeyboard("RIGHT").weShouldTriggerTheButton("Button_Right"); + inGame.whenWePressOnKeyboard("UP").weShouldTriggerTheButton("Button_Up"); + }); + + it("Check if triple swap is working", () => { + inGame.whenWePressOnKeyboard("A").weShouldTriggerTheButton("Alt_Button_Left"); + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Alt_Button_Right"); + inGame.whenWePressOnKeyboard("W").weShouldTriggerTheButton("Alt_Button_Up"); + + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Left").iconDisplayedIs("KEY_A").weWantThisBindInstead("D").confirm(); + + inGame.whenWePressOnKeyboard("A").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Alt_Button_Left"); + inGame.whenWePressOnKeyboard("W").weShouldTriggerTheButton("Alt_Button_Up"); + + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Right").thereShouldBeNoIcon().weWantThisBindInstead("W").confirm(); + + inGame.whenWePressOnKeyboard("A").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Alt_Button_Left"); + inGame.whenWePressOnKeyboard("W").weShouldTriggerTheButton("Alt_Button_Right"); + + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Left").iconDisplayedIs("KEY_D").weWantThisBindInstead("A").confirm(); + + inGame.whenWePressOnKeyboard("A").weShouldTriggerTheButton("Alt_Button_Left"); + inGame.whenWePressOnKeyboard("D").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("W").weShouldTriggerTheButton("Alt_Button_Right"); + }); + + it("Swap alt with a main", () => { + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Alt_Button_Right"); + inGame.whenWePressOnKeyboard("R").weShouldTriggerTheButton("Cycle_Shiny"); + inTheSettingMenu.whenCursorIsOnSetting("Cycle_Shiny").iconDisplayedIs("KEY_R").weWantThisBindInstead("D").confirm(); + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Cycle_Shiny"); + inGame.whenWePressOnKeyboard("R").nothingShouldHappen(); + }); + + it("multiple Swap alt with another main", () => { + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Alt_Button_Right"); + inGame.whenWePressOnKeyboard("R").weShouldTriggerTheButton("Button_Cycle_Shiny"); + inGame.whenWePressOnKeyboard("A").weShouldTriggerTheButton("Alt_Button_Left"); + inGame.whenWePressOnKeyboard("F").weShouldTriggerTheButton("Button_Cycle_Form"); + inTheSettingMenu.whenCursorIsOnSetting("Button_Cycle_Shiny").iconDisplayedIs("KEY_R").weWantThisBindInstead("D").confirm(); + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Button_Cycle_Shiny"); + inGame.whenWePressOnKeyboard("R").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("A").weShouldTriggerTheButton("Alt_Button_Left"); + inGame.whenWePressOnKeyboard("F").weShouldTriggerTheButton("Button_Cycle_Form"); + inTheSettingMenu.whenCursorIsOnSetting("Button_Cycle_Form").iconDisplayedIs("KEY_F").weWantThisBindInstead("R").confirm(); + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Button_Cycle_Shiny"); + inGame.whenWePressOnKeyboard("R").weShouldTriggerTheButton("Button_Cycle_Form"); + inGame.whenWePressOnKeyboard("A").weShouldTriggerTheButton("Alt_Button_Left"); + inGame.whenWePressOnKeyboard("F").nothingShouldHappen(); + }); + + it("Swap alt with a key not binded yet", () => { + inGame.whenWePressOnKeyboard("W").weShouldTriggerTheButton("Alt_Button_Up"); + inGame.whenWePressOnKeyboard("B").nothingShouldHappen(); + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Up").iconDisplayedIs("KEY_W").weWantThisBindInstead("B").confirm(); + inGame.whenWePressOnKeyboard("W").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("B").weShouldTriggerTheButton("Alt_Button_Up"); + }); + + it("Delete blacklisted bind", () => { + inGame.whenWePressOnKeyboard("LEFT").weShouldTriggerTheButton("Button_Left"); + inTheSettingMenu.whenWeDelete("Button_Left").weCantDelete().iconDisplayedIs("KEY_ARROW_LEFT"); + inGame.whenWePressOnKeyboard("LEFT").weShouldTriggerTheButton("Button_Left"); + }); + + it("Delete bind", () => { + inGame.whenWePressOnKeyboard("A").weShouldTriggerTheButton("Alt_Button_Left"); + inTheSettingMenu.whenWeDelete("Alt_Button_Left").thereShouldBeNoIconAnymore(); + inGame.whenWePressOnKeyboard("A").nothingShouldHappen(); + }); + + it("Delete bind then assign a not yet binded button", () => { + inTheSettingMenu.whenWeDelete("Alt_Button_Left").thereShouldBeNoIconAnymore(); + inGame.whenWePressOnKeyboard("A").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("B").nothingShouldHappen(); + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Left").thereShouldBeNoIcon().weWantThisBindInstead("B").confirm(); + inGame.whenWePressOnKeyboard("A").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("B").weShouldTriggerTheButton("Alt_Button_Left"); + }); + it("swap 2 bind, than delete 1 bind than assign another bind", () => { + inGame.whenWePressOnKeyboard("R").weShouldTriggerTheButton("Button_Cycle_Shiny"); + inGame.whenWePressOnKeyboard("F").weShouldTriggerTheButton("Button_Cycle_Form"); + inGame.whenWePressOnKeyboard("W").weShouldTriggerTheButton("Alt_Button_Up"); + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Alt_Button_Right"); + inTheSettingMenu.whenCursorIsOnSetting("Button_Cycle_Shiny").iconDisplayedIs("KEY_R").weWantThisBindInstead("D").confirm(); + inGame.whenWePressOnKeyboard("R").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("F").weShouldTriggerTheButton("Button_Cycle_Form"); + inGame.whenWePressOnKeyboard("W").weShouldTriggerTheButton("Alt_Button_Up"); + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Button_Cycle_Shiny"); + + inTheSettingMenu.whenCursorIsOnSetting("Button_Cycle_Form").iconDisplayedIs("KEY_F").weWantThisBindInstead("W").confirm(); + inGame.whenWePressOnKeyboard("R").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("F").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("W").weShouldTriggerTheButton("Button_Cycle_Form"); + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Button_Cycle_Shiny"); + inGame.whenWePressOnKeyboard("A").weShouldTriggerTheButton("Alt_Button_Left"); + + inTheSettingMenu.whenWeDelete("Alt_Button_Left").thereShouldBeNoIconAnymore(); + inGame.whenWePressOnKeyboard("R").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("F").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("W").weShouldTriggerTheButton("Button_Cycle_Form"); + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Button_Cycle_Shiny"); + inGame.whenWePressOnKeyboard("S").weShouldTriggerTheButton("Alt_Button_Down"); + inGame.whenWePressOnKeyboard("A").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("B").nothingShouldHappen(); + + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Down").iconDisplayedIs("KEY_S").weWantThisBindInstead("B").confirm(); + inGame.whenWePressOnKeyboard("R").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("F").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("W").weShouldTriggerTheButton("Button_Cycle_Form"); + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Button_Cycle_Shiny"); + inGame.whenWePressOnKeyboard("S").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("A").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("B").weShouldTriggerTheButton("Alt_Button_Down"); + }); + + + it("Delete bind then assign not already existing button", () => { + + inGame.whenWePressOnKeyboard("A").weShouldTriggerTheButton("Alt_Button_Left"); + inGame.whenWePressOnKeyboard("B").nothingShouldHappen(); + + inTheSettingMenu.whenWeDelete("Alt_Button_Left").thereShouldBeNoIconAnymore(); + inGame.whenWePressOnKeyboard("A").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("B").nothingShouldHappen(); + + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Left").thereShouldBeNoIcon().weWantThisBindInstead("B").confirm(); + inGame.whenWePressOnKeyboard("A").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("B").weShouldTriggerTheButton("Alt_Button_Left"); + }); + + + it("change alt bind to not already existing button, than another one alt bind with another not already existing button", () => { + inGame.whenWePressOnKeyboard("A").weShouldTriggerTheButton("Alt_Button_Left"); + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Alt_Button_Right"); + inGame.whenWePressOnKeyboard("B").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("U").nothingShouldHappen(); + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Left").iconDisplayedIs("KEY_A").weWantThisBindInstead("B").confirm(); + inGame.whenWePressOnKeyboard("A").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Alt_Button_Right"); + inGame.whenWePressOnKeyboard("B").weShouldTriggerTheButton("Alt_Button_Left"); + inGame.whenWePressOnKeyboard("U").nothingShouldHappen(); + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Right").iconDisplayedIs("KEY_D").weWantThisBindInstead("U").confirm(); + inGame.whenWePressOnKeyboard("A").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("D").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("B").weShouldTriggerTheButton("Alt_Button_Left"); + inGame.whenWePressOnKeyboard("U").weShouldTriggerTheButton("Alt_Button_Right"); + }); + + it("Swap multiple touch alt and main", () => { + inGame.whenWePressOnKeyboard("UP").weShouldTriggerTheButton("Button_Up"); + inGame.whenWePressOnKeyboard("RIGHT").weShouldTriggerTheButton("Button_Right"); + inGame.whenWePressOnKeyboard("W").weShouldTriggerTheButton("Alt_Button_Up"); + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Alt_Button_Right"); + inTheSettingMenu.whenCursorIsOnSetting("Button_Up").iconDisplayedIs("KEY_ARROW_UP").weWantThisBindInstead("RIGHT").weCantOverrideThisBind().weCantAssignThisKey().butLetsForceIt(); + inGame.whenWePressOnKeyboard("UP").weShouldTriggerTheButton("Button_Up"); + inGame.whenWePressOnKeyboard("RIGHT").weShouldTriggerTheButton("Button_Right"); + inGame.whenWePressOnKeyboard("W").weShouldTriggerTheButton("Alt_Button_Up"); + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Alt_Button_Right"); + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Up").iconDisplayedIs("KEY_W").weWantThisBindInstead("D").confirm(); + inGame.whenWePressOnKeyboard("UP").weShouldTriggerTheButton("Button_Up"); + inGame.whenWePressOnKeyboard("RIGHT").weShouldTriggerTheButton("Button_Right"); + inGame.whenWePressOnKeyboard("W").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Alt_Button_Up"); + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Up").iconDisplayedIs("KEY_D").weWantThisBindInstead("W").confirm(); + inGame.whenWePressOnKeyboard("UP").weShouldTriggerTheButton("Button_Up"); + inGame.whenWePressOnKeyboard("RIGHT").weShouldTriggerTheButton("Button_Right"); + inGame.whenWePressOnKeyboard("W").weShouldTriggerTheButton("Alt_Button_Up"); + inGame.whenWePressOnKeyboard("D").nothingShouldHappen(); + }); + + + it("Delete 2 bind then reassign one of them", () => { + + inGame.whenWePressOnKeyboard("A").weShouldTriggerTheButton("Alt_Button_Left"); + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Alt_Button_Right"); + + inTheSettingMenu.whenWeDelete("Alt_Button_Left").thereShouldBeNoIconAnymore(); + inGame.whenWePressOnKeyboard("A").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("D").weShouldTriggerTheButton("Alt_Button_Right"); + + inTheSettingMenu.whenWeDelete("Alt_Button_Right").thereShouldBeNoIconAnymore(); + inGame.whenWePressOnKeyboard("A").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("D").nothingShouldHappen(); + + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Left").thereShouldBeNoIcon().weWantThisBindInstead("A").confirm(); + inGame.whenWePressOnKeyboard("A").weShouldTriggerTheButton("Alt_Button_Left"); + inGame.whenWePressOnKeyboard("D").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); + }); + + it("retrieve the correct icon for a given source", () => { + inTheSettingMenu.whenCursorIsOnSetting("Cycle_Shiny").iconDisplayedIs("KEY_R"); + inTheSettingMenu.whenCursorIsOnSetting("Cycle_Form").iconDisplayedIs("KEY_F"); + inGame.forTheSource("keyboard").forTheWantedBind("Cycle_Shiny").weShouldSeeTheIcon("R"); + inGame.forTheSource("keyboard").forTheWantedBind("Cycle_Form").weShouldSeeTheIcon("F"); + }); + + it("check the key displayed on confirm", () => { + inGame.whenWePressOnKeyboard("ENTER").weShouldTriggerTheButton("Button_Submit"); + inGame.whenWePressOnKeyboard("UP").weShouldTriggerTheButton("Button_Up"); + inGame.whenWePressOnKeyboard("DOWN").weShouldTriggerTheButton("Button_Down"); + inGame.whenWePressOnKeyboard("LEFT").weShouldTriggerTheButton("Button_Left"); + inGame.whenWePressOnKeyboard("RIGHT").weShouldTriggerTheButton("Button_Right"); + inGame.whenWePressOnKeyboard("ESC").weShouldTriggerTheButton("Button_Menu"); + inGame.whenWePressOnKeyboard("HOME").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("DELETE").nothingShouldHappen(); + inTheSettingMenu.whenCursorIsOnSetting("Button_Submit").iconDisplayedIs("KEY_ENTER").whenWeDelete().iconDisplayedIs("KEY_ENTER"); + inTheSettingMenu.whenCursorIsOnSetting("Button_Up").iconDisplayedIs("KEY_ARROW_UP").whenWeDelete().iconDisplayedIs("KEY_ARROW_UP"); + inTheSettingMenu.whenCursorIsOnSetting("Button_Down").iconDisplayedIs("KEY_ARROW_DOWN").whenWeDelete().iconDisplayedIs("KEY_ARROW_DOWN"); + inTheSettingMenu.whenCursorIsOnSetting("Button_Left").iconDisplayedIs("KEY_ARROW_LEFT").whenWeDelete().iconDisplayedIs("KEY_ARROW_LEFT"); + inTheSettingMenu.whenCursorIsOnSetting("Button_Right").iconDisplayedIs("KEY_ARROW_RIGHT").whenWeDelete().iconDisplayedIs("KEY_ARROW_RIGHT"); + inTheSettingMenu.whenCursorIsOnSetting("Button_Menu").iconDisplayedIs("KEY_ESC").whenWeDelete().iconDisplayedIs("KEY_ESC"); + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Up").iconDisplayedIs("KEY_W").whenWeDelete().thereShouldBeNoIconAnymore(); + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Up").thereShouldBeNoIcon().weWantThisBindInstead("DELETE").weCantAssignThisKey().butLetsForceIt(); + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Up").thereShouldBeNoIcon().weWantThisBindInstead("HOME").weCantAssignThisKey().butLetsForceIt(); + inGame.whenWePressOnKeyboard("DELETE").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("HOME").nothingShouldHappen(); + inGame.whenWePressOnKeyboard("W").nothingShouldHappen(); + }); + + it("check to delete all the binds of an action", () => { + inGame.whenWePressOnKeyboard("V").weShouldTriggerTheButton("Button_Cycle_Variant"); + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Cycle_Variant").thereShouldBeNoIcon().weWantThisBindInstead("K").confirm(); + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Cycle_Variant").iconDisplayedIs("KEY_K").whenWeDelete().thereShouldBeNoIconAnymore(); + inTheSettingMenu.whenCursorIsOnSetting("Button_Cycle_Variant").iconDisplayedIs("KEY_V").whenWeDelete().thereShouldBeNoIconAnymore(); + }); +}); diff --git a/src/touch-controls.ts b/src/touch-controls.ts index d828e709f31..3b734e01467 100644 --- a/src/touch-controls.ts +++ b/src/touch-controls.ts @@ -1,153 +1,137 @@ -export interface ButtonKey { - onDown: (opt: object) => void; - onUp: (opt: object) => void; -} +import {Button} from "./enums/buttons"; +import EventEmitter = Phaser.Events.EventEmitter; -export type ButtonMap = Map; - -export const keys = new Map(); -export const keysDown = new Map(); +// Create a map to store key bindings +export const keys = new Map(); +// Create a map to store keys that are currently pressed +export const keysDown = new Map(); +// Variable to store the ID of the last touched element let lastTouchedId: string; /** - * Initializes all touch controls + * Initialize touch controls by binding keys to buttons. * - * @param buttonMap Map of buttons to key objects + * @param events - The event emitter for handling input events. */ -export function initTouchControls(buttonMap: ButtonMap) { +export function initTouchControls(events: EventEmitter): void { preventElementZoom(document.querySelector("#dpad")); preventElementZoom(document.querySelector("#apad")); - - for (const button of document.querySelectorAll("[data-key]")) { - bindKey(button, button.dataset.key, buttonMap); + // Select all elements with the 'data-key' attribute and bind keys to them + for (const button of document.querySelectorAll("[data-key]")) { + // @ts-ignore - Bind the key to the button using the dataset key + bindKey(button, button.dataset.key, events); } } /** - * Check if the device has a touchscreen + * Check if the device has a touchscreen. + * + * @returns `true` if the device has a touchscreen, otherwise `false`. */ -export function hasTouchscreen() { +export function hasTouchscreen(): boolean { return window.matchMedia("(hover: none), (pointer: coarse)").matches; } /** - * Check if it's a mobile device through the user-agent + * Check if the device is a mobile device. + * + * @returns `true` if the device is a mobile device, otherwise `false`. */ -export function isMobile() { - const userAgent = navigator.userAgent || navigator.vendor || window["opera"]; - return (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(userAgent)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(userAgent.substr(0, 4))); +export function isMobile(): boolean { + let ret = false; + (function (a) { + // Check the user agent string against a regex for mobile devices + if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4))) { + ret = true; + } + })(navigator.userAgent || navigator.vendor || window["opera"]); + return ret; } /** - * Simulate a keyboard event on the canvas + * Simulates a keyboard event on the canvas. * - * @param eventType Type of the keyboard event - * @param button Button to simulate - * @param buttonMap Map of buttons to key objects + * @param eventType - The type of the keyboard event ('keydown' or 'keyup'). + * @param key - The key to simulate. + * @param events - The event emitter for handling input events. + * + * @remarks + * This function checks if the key exists in the Button enum. If it does, it retrieves the corresponding button + * and emits the appropriate event ('input_down' or 'input_up') based on the event type. */ -function simulateKeyboardEvent(eventType: string, button: string, buttonMap: ButtonMap) { - const key = buttonMap[button]; +function simulateKeyboardEvent(eventType: string, key: string, events: EventEmitter) { + if (!Button.hasOwnProperty(key)) { + return; + } + const button = Button[key]; switch (eventType) { case "keydown": - key.onDown({}); + events.emit("input_down", { + controller_type: "keyboard", + button: button, + }); break; case "keyup": - key.onUp({}); + events.emit("input_up", { + controller_type: "keyboard", + button: button, + }); break; } } /** - * Simulate a keyboard input from 'keydown' to 'keyup' + * Binds a node to a specific key to simulate keyboard events on touch. * - * @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 + * @param node - The DOM element to bind the key to. + * @param key - The key to simulate. + * @param events - The event emitter for handling input events. * - * @param node The node to bind a key to - * @param key Key to simulate - * @param buttonMap Map of buttons to key objects + * @remarks + * This function binds touch events to a node to simulate 'keydown' and 'keyup' keyboard events. + * It adds the key to the keys map and tracks the keydown state. When a touch starts, it simulates + * a 'keydown' event and adds an 'active' class to the node. When the touch ends, it simulates a 'keyup' + * event, removes the keydown state, and removes the 'active' class from the node and the last touched element. */ -function bindKey(node: Element, key: string, buttonMap: ButtonMap) { +function bindKey(node: HTMLElement, key: string, events) { keys.set(node.id, key); - node.addEventListener("touchstart", (event: TouchEvent) => { + node.addEventListener("touchstart", event => { event.preventDefault(); - simulateKeyboardEvent("keydown", key, buttonMap); - if (!(event.target instanceof Element)) { - return; - } - keysDown.set(event.target.id, node.id); + simulateKeyboardEvent("keydown", key, events); + keysDown.set(event.target["id"], node.id); node.classList.add("active"); }); - node.addEventListener("touchend", (event: TouchEvent) => { + node.addEventListener("touchend", event => { event.preventDefault(); - if (!(event.target instanceof Element)) { - return; - } - const pressedKey = keysDown.get(event.target.id); + 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); + keysDown.delete(event.target["id"]); node.classList.remove("active"); if (lastTouchedId) { document.getElementById(lastTouchedId).classList.remove("active"); } }); - - // Inspired by https://github.com/pulsejet/mkxp-web/blob/262a2254b684567311c9f0e135ee29f6e8c3613e/extra/js/dpad.js - node.addEventListener("touchmove", (event : TouchEvent) => { - if (!(event.changedTouches[0].target instanceof Element)) { - return; - } - 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"); - } - }); } /** - * Prevent zoom on specified element - * * {@link https://stackoverflow.com/a/39778831/4622620|Source} * - * @param element The element to prevent zoom on + * Prevent zoom on specified element + * @param {HTMLElement} element */ -function preventElementZoom(element: HTMLElement) { +function preventElementZoom(element: HTMLElement): void { + if (!element) { + return; + } element.addEventListener("touchstart", (event: TouchEvent) => { if (!(event.currentTarget instanceof HTMLElement)) { diff --git a/src/ui-inputs.ts b/src/ui-inputs.ts index d443e4d85b7..a5fb631644a 100644 --- a/src/ui-inputs.ts +++ b/src/ui-inputs.ts @@ -4,8 +4,10 @@ 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 "./ui/settings/settings-gamepad-ui-handler"; +import SettingsKeyboardUiHandler from "#app/ui/settings/settings-keyboard-ui-handler"; import BattleScene from "./battle-scene"; type ActionKeys = Record void>; @@ -159,7 +161,9 @@ export class UiInputs { } buttonCycleOption(button: Button): void { - if (this.scene.ui?.getHandler() instanceof StarterSelectUiHandler) { + const whitelist = [StarterSelectUiHandler, SettingsUiHandler, SettingsGamepadUiHandler, SettingsKeyboardUiHandler]; + const uiHandler = this.scene.ui?.getHandler(); + if (whitelist.some(handler => uiHandler instanceof handler)) { this.scene.ui.processInput(button); } else if (button === Button.V) { this.buttonInfo(true); diff --git a/src/ui/settings/abstract-binding-ui-handler.ts b/src/ui/settings/abstract-binding-ui-handler.ts new file mode 100644 index 00000000000..fe0736ce101 --- /dev/null +++ b/src/ui/settings/abstract-binding-ui-handler.ts @@ -0,0 +1,259 @@ +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"; +import {NavigationManager} from "#app/ui/settings/navigationMenu"; + +/** + * Abstract class for handling UI elements related to button bindings. + */ +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 timerText: 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; + abstract swapAction(): boolean; + + protected timeLeftAutoClose: number = 5; + protected countdownTimer; + + // The specific setting being modified. + protected target; + + /** + * Constructor for the AbstractBindingUiHandler. + * + * @param scene - The BattleScene instance. + * @param mode - The UI mode. + */ + constructor(scene: BattleScene, mode?: Mode) { + super(scene, mode); + } + + /** + * Setup UI elements. + */ + 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.timerText = addTextObject(this.scene, 0, 0, "(5)", TextStyle.WINDOW); + this.timerText.setOrigin(0, 0); + this.timerText.setPositionRelative(this.unlockText, (this.unlockText.width/6) + 5, 0); + this.optionSelectContainer.add(this.timerText); + + 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.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.actionsContainer.add(this.cancelLabel); + } + + manageAutoCloseTimer() { + clearTimeout(this.countdownTimer); + this.countdownTimer = setTimeout(() => { + this.timeLeftAutoClose -= 1; + this.timerText.setText(`(${this.timeLeftAutoClose})`); + if (this.timeLeftAutoClose >= 0) { + this.manageAutoCloseTimer(); + } else { + this.cancelFn(); + } + }, 1000); + } + + /** + * Show the UI with the provided arguments. + * + * @param args - Arguments to be passed to the show method. + * @returns `true` if successful. + */ + show(args: any[]): boolean { + super.show(args); + this.buttonPressed = null; + this.timeLeftAutoClose = 5; + 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; + this.manageAutoCloseTimer(); + }, 100); + return true; + } + + /** + * Get the width of the window. + * + * @returns The window width. + */ + getWindowWidth(): number { + return 160; + } + + /** + * Get the height of the window. + * + * @returns The window height. + */ + getWindowHeight(): number { + return 64; + } + + /** + * Process the input for the given button. + * + * @param button - The button to process. + * @returns `true` if the input was processed successfully. + */ + processInput(button: Button): boolean { + 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(); + NavigationManager.getInstance().updateIcons(); + this.cancelFn(success); + } + break; + } + + // Plays a select sound effect if an action was successfully processed. + if (success) { + ui.playSelect(); + } else { + ui.playError(); + } + + return success; + } + + /** + * Set the cursor to the specified position. + * + * @param cursor - The cursor position to set. + * @returns `true` if the cursor was set successfully. + */ + setCursor(cursor: integer): boolean { + 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 the UI elements and state. + */ + clear() { + super.clear(); + clearTimeout(this.countdownTimer); + this.timerText.setText("(5)"); + this.timeLeftAutoClose = 5; + this.listening = false; + this.target = null; + this.cancelFn = null; + this.optionSelectContainer.setVisible(false); + this.actionsContainer.setVisible(false); + this.newButtonIcon.setVisible(false); + this.buttonPressed = null; + } + + /** + * Handle input down events. + * + * @param buttonIcon - The icon of the button that was pressed. + * @param assignedButtonIcon - The icon of the button that is assigned. + * @param type - The type of button press. + */ + onInputDown(buttonIcon: string, assignedButtonIcon: string, type: string): void { + clearTimeout(this.countdownTimer); + this.timerText.setText(""); + this.newButtonIcon.setTexture(type); + this.newButtonIcon.setFrame(buttonIcon); + if (assignedButtonIcon) { + this.targetButtonIcon.setTexture(type); + this.targetButtonIcon.setFrame(assignedButtonIcon); + this.targetButtonIcon.setVisible(true); + this.swapText.setVisible(true); + } + this.newButtonIcon.setVisible(true); + this.setCursor(0); + this.actionsContainer.setVisible(true); + } +} 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 00000000000..f37ce05b69f --- /dev/null +++ b/src/ui/settings/abstract-settings-ui-handler.ts @@ -0,0 +1,647 @@ +import UiHandler from "../ui-handler"; +import BattleScene from "../../battle-scene"; +import {Mode} from "../ui"; +import {InterfaceConfig} from "../../inputs-controller"; +import {addWindow} from "../ui-theme"; +import {addTextObject, TextStyle} from "../text"; +import {Button} from "../../enums/buttons"; +import {getIconWithSettingName} from "#app/configs/inputs/configHandler"; +import NavigationMenu, {NavigationManager} from "#app/ui/settings/navigationMenu"; + +export interface InputsIcons { + [key: string]: Phaser.GameObjects.Sprite; +} + +export interface LayoutConfig { + optionsContainer: Phaser.GameObjects.Container; + inputsIcons: InputsIcons; + settingLabels: Phaser.GameObjects.Text[]; + optionValueLabels: Phaser.GameObjects.Text[][]; + optionCursors: integer[]; + keys: string[]; + bindingSettings: Array; +} +/** + * Abstract class for handling UI elements related to settings. + */ +export default abstract class AbstractSettingsUiUiHandler extends UiHandler { + protected settingsContainer: Phaser.GameObjects.Container; + protected optionsContainer: Phaser.GameObjects.Container; + protected navigationContainer: NavigationMenu; + + protected scrollCursor: integer; + protected optionCursors: integer[]; + protected cursorObj: Phaser.GameObjects.NineSlice; + + protected optionsBg: Phaser.GameObjects.NineSlice; + protected actionsBg: Phaser.GameObjects.NineSlice; + + protected settingLabels: Phaser.GameObjects.Text[]; + protected optionValueLabels: Phaser.GameObjects.Text[][]; + + // layout will contain the 3 Gamepad tab for each config - dualshock, xbox, snes + protected layout: Map = new Map(); + // Will contain the input icons from the selected layout + protected inputsIcons: InputsIcons; + protected navigationIcons: InputsIcons; + // list all the setting keys used in the selected layout (because dualshock has more buttons than xbox) + protected keys: Array; + + // Store the specific settings related to key bindings for the current gamepad configuration. + protected bindingSettings: Array; + + protected settingDevice; + protected settingBlacklisted; + protected settingDeviceDefaults; + protected settingDeviceOptions; + protected configs; + protected commonSettingsCount; + protected textureOverride; + protected titleSelected; + protected localStoragePropertyName; + protected rowsToDisplay: number; + + abstract getLocalStorageSetting(): object; + abstract navigateMenuLeft(): boolean; + abstract navigateMenuRight(): boolean; + abstract saveSettingToLocalStorage(setting, cursor): void; + abstract getActiveConfig(): InterfaceConfig; + abstract setSetting(scene: BattleScene, setting, value: integer): boolean; + + /** + * Constructor for the AbstractSettingsUiUiHandler. + * + * @param scene - The BattleScene instance. + * @param mode - The UI mode. + */ + constructor(scene: BattleScene, mode?: Mode) { + super(scene, mode); + this.rowsToDisplay = 8; + } + + /** + * Setup UI elements. + */ + setup() { + const ui = this.getUi(); + this.navigationIcons = {}; + + this.settingsContainer = this.scene.add.container(1, -(this.scene.game.canvas.height / 6) + 1); + + this.settingsContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, this.scene.game.canvas.width / 6, this.scene.game.canvas.height / 6), Phaser.Geom.Rectangle.Contains); + + this.navigationContainer = new NavigationMenu(this.scene, 0, 0); + + this.optionsBg = addWindow(this.scene, 0, this.navigationContainer.height, (this.scene.game.canvas.width / 6) - 2, (this.scene.game.canvas.height / 6) - 16 - this.navigationContainer.height - 2); + this.optionsBg.setOrigin(0, 0); + + + this.actionsBg = addWindow(this.scene, 0, (this.scene.game.canvas.height / 6) - this.navigationContainer.height, (this.scene.game.canvas.width / 6) - 2, 22); + this.actionsBg.setOrigin(0, 0); + + const iconAction = this.scene.add.sprite(0, 0, "keyboard"); + iconAction.setOrigin(0, -0.1); + iconAction.setPositionRelative(this.actionsBg, this.navigationContainer.width - 32, 4); + this.navigationIcons["BUTTON_ACTION"] = iconAction; + + const actionText = addTextObject(this.scene, 0, 0, "Action", TextStyle.SETTINGS_LABEL); + actionText.setOrigin(0, 0.15); + actionText.setPositionRelative(iconAction, -actionText.width/6-2, 0); + + const iconCancel = this.scene.add.sprite(0, 0, "keyboard"); + iconCancel.setOrigin(0, -0.1); + iconCancel.setPositionRelative(this.actionsBg, this.navigationContainer.width - 100, 4); + this.navigationIcons["BUTTON_CANCEL"] = iconCancel; + + const cancelText = addTextObject(this.scene, 0, 0, "Cancel", TextStyle.SETTINGS_LABEL); + cancelText.setOrigin(0, 0.15); + cancelText.setPositionRelative(iconCancel, -cancelText.width/6-2, 0); + + const iconReset = this.scene.add.sprite(0, 0, "keyboard"); + iconReset.setOrigin(0, -0.1); + iconReset.setPositionRelative(this.actionsBg, this.navigationContainer.width - 180, 4); + this.navigationIcons["BUTTON_HOME"] = iconReset; + + const resetText = addTextObject(this.scene, 0, 0, "Reset all", TextStyle.SETTINGS_LABEL); + resetText.setOrigin(0, 0.15); + resetText.setPositionRelative(iconReset, -resetText.width/6-2, 0); + + this.settingsContainer.add(this.optionsBg); + this.settingsContainer.add(this.actionsBg); + this.settingsContainer.add(this.navigationContainer); + this.settingsContainer.add(iconAction); + this.settingsContainer.add(iconCancel); + this.settingsContainer.add(iconReset); + this.settingsContainer.add(actionText); + this.settingsContainer.add(cancelText); + this.settingsContainer.add(resetText); + + /// Initialize a new configuration "screen" for each type of gamepad. + for (const config of this.configs) { + // Create a map to store layout settings based on the pad type. + this.layout[config.padType] = new Map(); + // Create a container for gamepad options in the scene, initially hidden. + + const optionsContainer = this.scene.add.container(0, 0); + optionsContainer.setVisible(false); + + // Gather all binding settings from the configuration. + const bindingSettings = Object.keys(config.settings); + + // Array to hold labels for different settings such as 'Controller', 'Gamepad Support', etc. + const settingLabels: Phaser.GameObjects.Text[] = []; + + // Array to hold options for each setting, e.g., 'Auto', 'Disabled'. + const optionValueLabels: Phaser.GameObjects.GameObject[][] = []; + + // Object to store sprites for each button configuration. + const inputsIcons: InputsIcons = {}; + + // Fetch common setting keys such as 'Controller' and 'Gamepad Support' from gamepad settings. + const commonSettingKeys = Object.keys(this.settingDevice).slice(0, this.commonSettingsCount).map(key => this.settingDevice[key]); + // Combine common and specific bindings into a single array. + const specificBindingKeys = [...commonSettingKeys, ...Object.keys(config.settings)]; + // Fetch default values for these settings and prepare to highlight selected options. + const optionCursors = Object.values(Object.keys(this.settingDeviceDefaults).filter(s => specificBindingKeys.includes(s)).map(k => this.settingDeviceDefaults[k])); + // Filter out settings that are not relevant to the current gamepad configuration. + const settingFiltered = Object.keys(this.settingDevice).filter(_key => specificBindingKeys.includes(this.settingDevice[_key])); + // Loop through the filtered settings to manage display and options. + + settingFiltered.forEach((setting, s) => { + // Convert the setting key from format 'Key_Name' to 'Key name' for display. + const settingName = setting.replace(/\_/g, " "); + + // Create and add a text object for the setting name to the scene. + const isLock = this.settingBlacklisted.includes(this.settingDevice[setting]); + const labelStyle = isLock ? TextStyle.SETTINGS_LOCKED : TextStyle.SETTINGS_LABEL; + settingLabels[s] = addTextObject(this.scene, 8, 28 + s * 16, settingName, labelStyle); + settingLabels[s].setOrigin(0, 0); + optionsContainer.add(settingLabels[s]); + + // Initialize an array to store the option labels for this setting. + const valueLabels: Phaser.GameObjects.GameObject[] = []; + + // Process each option for the current setting. + for (const [o, option] of this.settingDeviceOptions[this.settingDevice[setting]].entries()) { + // Check if the current setting is for binding keys. + if (bindingSettings.includes(this.settingDevice[setting])) { + // Create a label for non-null options, typically indicating actionable options like 'change'. + if (o) { + const valueLabel = addTextObject(this.scene, 0, 0, isLock ? "" : option, TextStyle.WINDOW); + valueLabel.setOrigin(0, 0); + optionsContainer.add(valueLabel); + valueLabels.push(valueLabel); + continue; + } + // For null options, add an icon for the key. + const icon = this.scene.add.sprite(0, 0, this.textureOverride ? this.textureOverride : config.padType); + icon.setOrigin(0, -0.15); + inputsIcons[this.settingDevice[setting]] = icon; + optionsContainer.add(icon); + valueLabels.push(icon); + continue; + } + // For regular settings like 'Gamepad support', create a label and determine if it is selected. + const valueLabel = addTextObject(this.scene, 0, 0, option, this.settingDeviceDefaults[this.settingDevice[setting]] === o ? TextStyle.SETTINGS_SELECTED : TextStyle.WINDOW); + valueLabel.setOrigin(0, 0); + + optionsContainer.add(valueLabel); + + //if a setting has 2 options, valueLabels will be an array of 2 elements + valueLabels.push(valueLabel); + } + // Collect all option labels for this setting into the main array. + optionValueLabels.push(valueLabels); + + // Calculate the total width of all option labels within a specific setting + // This is achieved by summing the width of each option label + const totalWidth = optionValueLabels[s].map((o) => (o as Phaser.GameObjects.Text).width).reduce((total, width) => total += width, 0); + + // Define the minimum width for a label, ensuring it's at least 78 pixels wide or the width of the setting label plus some padding + const labelWidth = Math.max(130, settingLabels[s].displayWidth + 8); + + // Calculate the total available space for placing option labels next to their setting label + // We reserve space for the setting label and then distribute the remaining space evenly + const totalSpace = (300 - labelWidth) - totalWidth / 6; + // Calculate the spacing between options based on the available space divided by the number of gaps between labels + const optionSpacing = Math.floor(totalSpace / (optionValueLabels[s].length - 1)); + + // Initialize xOffset to zero, which will be used to position each option label horizontally + let xOffset = 0; + + // Start positioning each option label one by one + for (const value of optionValueLabels[s]) { + // Set the option label's position right next to the setting label, adjusted by xOffset + (value as Phaser.GameObjects.Text).setPositionRelative(settingLabels[s], labelWidth + xOffset, 0); + // Move the xOffset to the right for the next label, ensuring each label is spaced evenly + xOffset += (value as Phaser.GameObjects.Text).width / 6 + optionSpacing; + } + }); + + // Assigning the newly created components to the layout map under the specific gamepad type. + this.layout[config.padType].optionsContainer = optionsContainer; // Container for this pad's options. + this.layout[config.padType].inputsIcons = inputsIcons; // Icons for each input specific to this pad. + this.layout[config.padType].settingLabels = settingLabels; // Text labels for each setting available on this pad. + this.layout[config.padType].optionValueLabels = optionValueLabels; // Labels for values corresponding to each setting. + this.layout[config.padType].optionCursors = optionCursors; // Cursors to navigate through the options. + this.layout[config.padType].keys = specificBindingKeys; // Keys that identify each setting specifically bound to this pad. + this.layout[config.padType].bindingSettings = bindingSettings; // Settings that define how the keys are bound. + + // Add the options container to the overall settings container to be displayed in the UI. + this.settingsContainer.add(optionsContainer); + } + // Add the settings container to the UI. + ui.add(this.settingsContainer); + + // Initially hide the settings container until needed (e.g., when a gamepad is connected or a button is pressed). + this.settingsContainer.setVisible(false); + } + + /** + * Update the bindings for the current active device configuration. + */ + updateBindings(): void { + // Hide the options container for all layouts to reset the UI visibility. + Object.keys(this.layout).forEach((key) => this.layout[key].optionsContainer.setVisible(false)); + // Fetch the active gamepad configuration from the input controller. + const activeConfig = this.getActiveConfig(); + + // Set the UI layout for the active configuration. If unsuccessful, exit the function early. + if (!this.setLayout(activeConfig)) { + return; + } + + // Retrieve the gamepad settings from local storage or use an empty object if none exist. + const settings: object = this.getLocalStorageSetting(); + + // Update the cursor for each key based on the stored settings or default cursors. + this.keys.forEach((key, index) => { + this.setOptionCursor(index, settings.hasOwnProperty(key as string) ? settings[key as string] : this.optionCursors[index]); + }); + + // If the active configuration has no custom bindings set, exit the function early. + // by default, if custom does not exists, a default is assigned to it + // it only means the gamepad is not yet initalized + if (!activeConfig.custom) { + return; + } + + // For each element in the binding settings, update the icon according to the current assignment. + for (const elm of this.bindingSettings) { + const icon = getIconWithSettingName(activeConfig, elm); + if (icon) { + this.inputsIcons[elm as string].setFrame(icon); + this.inputsIcons[elm as string].alpha = 1; + } else { + this.inputsIcons[elm as string].alpha = 0; + } + } + + // Set the cursor and scroll cursor to their initial positions. + this.setCursor(this.cursor); + this.setScrollCursor(this.scrollCursor); + } + + updateNavigationDisplay() { + const specialIcons = { + "BUTTON_HOME": "HOME.png", + "BUTTON_DELETE": "DEL.png", + }; + for (const settingName of Object.keys(this.navigationIcons)) { + if (Object.keys(specialIcons).includes(settingName)) { + this.navigationIcons[settingName].setTexture("keyboard"); + this.navigationIcons[settingName].setFrame(specialIcons[settingName]); + this.navigationIcons[settingName].alpha = 1; + continue; + } + const icon = this.scene.inputController?.getIconForLatestInputRecorded(settingName); + if (icon) { + const type = this.scene.inputController?.getLastSourceType(); + this.navigationIcons[settingName].setTexture(type); + this.navigationIcons[settingName].setFrame(icon); + this.navigationIcons[settingName].alpha = 1; + } else { + this.navigationIcons[settingName].alpha = 0; + } + } + } + + /** + * Show the UI with the provided arguments. + * + * @param args - Arguments to be passed to the show method. + * @returns `true` if successful. + */ + show(args: any[]): boolean { + super.show(args); + + this.updateNavigationDisplay(); + NavigationManager.getInstance().updateIcons(); + // Update the bindings for the current active gamepad configuration. + this.updateBindings(); + + // Make the settings container visible to the user. + this.settingsContainer.setVisible(true); + // Reset the scroll cursor to the top of the settings container. + this.resetScroll(); + + // Move the settings container to the end of the UI stack to ensure it is displayed on top. + this.getUi().moveTo(this.settingsContainer, this.getUi().length - 1); + + // Hide any tooltips that might be visible before showing the settings container. + this.getUi().hideTooltip(); + + // Return true to indicate the UI was successfully shown. + return true; + } + + /** + * Set the UI layout for the active device configuration. + * + * @param activeConfig - The active device configuration. + * @returns `true` if the layout was successfully applied, otherwise `false`. + */ + setLayout(activeConfig: InterfaceConfig): boolean { + // Check if there is no active configuration (e.g., no gamepad connected). + if (!activeConfig) { + // Retrieve the layout for when no gamepads are connected. + const layout = this.layout["noGamepads"]; + // Make the options container visible to show message. + layout.optionsContainer.setVisible(true); + // Return false indicating the layout application was not successful due to lack of gamepad. + return false; + } + // Extract the type of the gamepad from the active configuration. + const configType = activeConfig.padType; + + // Retrieve the layout settings based on the type of the gamepad. + const layout = this.layout[configType]; + // Update the main controller with configuration details from the selected layout. + this.keys = layout.keys; + this.optionsContainer = layout.optionsContainer; + this.optionsContainer.setVisible(true); + this.settingLabels = layout.settingLabels; + this.optionValueLabels = layout.optionValueLabels; + this.optionCursors = layout.optionCursors; + this.inputsIcons = layout.inputsIcons; + this.bindingSettings = layout.bindingSettings; + + // Return true indicating the layout was successfully applied. + return true; + } + + /** + * Process the input for the given button. + * + * @param button - The button to process. + * @returns `true` if the input was processed successfully. + */ + processInput(button: Button): boolean { + const ui = this.getUi(); + // Defines the maximum number of rows that can be displayed on the screen. + let success = false; + this.updateNavigationDisplay(); + + // Handle the input based on the button pressed. + if (button === Button.CANCEL) { + // Handle cancel button press, reverting UI mode to previous state. + success = true; + NavigationManager.getInstance().reset(); + this.scene.ui.revertMode(); + } else { + const cursor = this.cursor + this.scrollCursor; // Calculate the absolute cursor position. + const setting = this.settingDevice[Object.keys(this.settingDevice)[cursor]]; + switch (button) { + case Button.ACTION: + if (!this.optionCursors || !this.optionValueLabels) { + return; + } + if (this.settingBlacklisted.includes(setting) || !setting.includes("BUTTON_")) { + success = false; + } else { + success = this.setSetting(this.scene, setting, 1); + } + break; + case Button.UP: // Move up in the menu. + if (!this.optionValueLabels) { + return false; + } + if (cursor) { // If not at the top, move the cursor up. + if (this.cursor) { + success = this.setCursor(this.cursor - 1); + } else {// If at the top of the visible items, scroll up. + success = this.setScrollCursor(this.scrollCursor - 1); + } + } else { + // When at the top of the menu and pressing UP, move to the bottommost item. + // First, set the cursor to the last visible element, preparing for the scroll to the end. + const successA = this.setCursor(this.rowsToDisplay - 1); + // Then, adjust the scroll to display the bottommost elements of the menu. + const successB = this.setScrollCursor(this.optionValueLabels.length - this.rowsToDisplay); + success = successA && successB; // success is just there to play the little validation sound effect + } + break; + case Button.DOWN: // Move down in the menu. + if (!this.optionValueLabels) { + return false; + } + if (cursor < this.optionValueLabels.length - 1) { + if (this.cursor < this.rowsToDisplay - 1) { + success = this.setCursor(this.cursor + 1); + } else if (this.scrollCursor < this.optionValueLabels.length - this.rowsToDisplay) { + success = this.setScrollCursor(this.scrollCursor + 1); + } + } else { + // When at the bottom of the menu and pressing DOWN, move to the topmost item. + // First, set the cursor to the first visible element, resetting the scroll to the top. + const successA = this.setCursor(0); + // Then, reset the scroll to start from the first element of the menu. + const successB = this.setScrollCursor(0); + success = successA && successB; // Indicates a successful cursor and scroll adjustment. + } + break; + case Button.LEFT: // Move selection left within the current option set. + if (!this.optionCursors || !this.optionValueLabels) { + return; + } + if (this.settingBlacklisted.includes(setting) || setting.includes("BUTTON_")) { + success = false; + } else if (this.optionCursors[cursor]) { + success = this.setOptionCursor(cursor, this.optionCursors[cursor] - 1, true); + } + break; + case Button.RIGHT: // Move selection right within the current option set. + if (!this.optionCursors || !this.optionValueLabels) { + return; + } + if (this.settingBlacklisted.includes(setting) || setting.includes("BUTTON_")) { + success = false; + } else if (this.optionCursors[cursor] < this.optionValueLabels[cursor].length - 1) { + success = this.setOptionCursor(cursor, this.optionCursors[cursor] + 1, true); + } + break; + case Button.CYCLE_FORM: + case Button.CYCLE_SHINY: + success = this.navigationContainer.navigate(button); + break; + } + } + + // If a change occurred, play the selection sound. + if (success) { + ui.playSelect(); + } + + return success; // Return whether the input resulted in a successful action. + } + + resetScroll() { + this.cursorObj?.destroy(); + this.cursorObj = null; + this.cursor = null; + this.setCursor(0); + this.setScrollCursor(0); + this.updateSettingsScroll(); + } + + /** + * Set the cursor to the specified position. + * + * @param cursor - The cursor position to set. + * @returns `true` if the cursor was set successfully. + */ + setCursor(cursor: integer): boolean { + const ret = super.setCursor(cursor); + // If the optionsContainer is not initialized, return the result from the parent class directly. + if (!this.optionsContainer) { + return ret; + } + + // Check if the cursor object exists, if not, create it. + if (!this.cursorObj) { + this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", null, (this.scene.game.canvas.width / 6) - 10, 16, 1, 1, 1, 1); + this.cursorObj.setOrigin(0, 0); // Set the origin to the top-left corner. + this.optionsContainer.add(this.cursorObj); // Add the cursor to the options container. + } + + // Update the position of the cursor object relative to the options background based on the current cursor and scroll positions. + this.cursorObj.setPositionRelative(this.optionsBg, 4, 4 + (this.cursor + this.scrollCursor) * 16); + + return ret; // Return the result from the parent class's setCursor method. + } + + /** + * Set the scroll cursor to the specified position. + * + * @param scrollCursor - The scroll cursor position to set. + * @returns `true` if the scroll cursor was set successfully. + */ + setScrollCursor(scrollCursor: integer): boolean { + // Check if the new scroll position is the same as the current one; if so, do not update. + if (scrollCursor === this.scrollCursor) { + return false; + } + + // Update the internal scroll cursor state + this.scrollCursor = scrollCursor; + + // Apply the new scroll position to the settings UI. + this.updateSettingsScroll(); + + // Reset the cursor to its current position to adjust its visibility after scrolling. + this.setCursor(this.cursor); + + return true; // Return true to indicate the scroll cursor was successfully updated. + } + + /** + * Set the option cursor to the specified position. + * + * @param settingIndex - The index of the setting. + * @param cursor - The cursor position to set. + * @param save - Whether to save the setting to local storage. + * @returns `true` if the option cursor was set successfully. + */ + setOptionCursor(settingIndex: integer, cursor: integer, save?: boolean): boolean { + // Retrieve the specific setting using the settingIndex from the settingDevice enumeration. + const setting = this.settingDevice[Object.keys(this.settingDevice)[settingIndex]]; + + // Get the current cursor position for this setting. + const lastCursor = this.optionCursors[settingIndex]; + + // Check if the setting is not part of the bindings (i.e., it's a regular setting). + if (!this.bindingSettings.includes(setting) && !setting.includes("BUTTON_")) { + // Get the label of the last selected option and revert its color to the default. + const lastValueLabel = this.optionValueLabels[settingIndex][lastCursor]; + lastValueLabel.setColor(this.getTextColor(TextStyle.WINDOW)); + lastValueLabel.setShadowColor(this.getTextColor(TextStyle.WINDOW, true)); + + // Update the cursor for the setting to the new position. + this.optionCursors[settingIndex] = cursor; + + // Change the color of the new selected option to indicate it's selected. + const newValueLabel = this.optionValueLabels[settingIndex][cursor]; + newValueLabel.setColor(this.getTextColor(TextStyle.SETTINGS_SELECTED)); + newValueLabel.setShadowColor(this.getTextColor(TextStyle.SETTINGS_SELECTED, true)); + } + + // If the save flag is set, save the setting to local storage + if (save) { + this.saveSettingToLocalStorage(setting, cursor); + } + + return true; // Return true to indicate the cursor was successfully updated. + } + + /** + * Update the scroll position of the settings UI. + */ + updateSettingsScroll(): void { + // Return immediately if the options container is not initialized. + if (!this.optionsContainer) { + return; + } + + // Set the vertical position of the options container based on the current scroll cursor, multiplying by the item height. + this.optionsContainer.setY(-16 * this.scrollCursor); + + // Iterate over all setting labels to update their visibility. + for (let s = 0; s < this.settingLabels.length; s++) { + // Determine if the current setting should be visible based on the scroll position. + const visible = s >= this.scrollCursor && s < this.scrollCursor + this.rowsToDisplay; + + // Set the visibility of the setting label and its corresponding options. + this.settingLabels[s].setVisible(visible); + for (const option of this.optionValueLabels[s]) { + option.setVisible(visible); + } + } + } + + /** + * Clear the UI elements and state. + */ + clear(): void { + super.clear(); + + // Hide the settings container to remove it from the view. + this.settingsContainer.setVisible(false); + + // Remove the cursor from the UI. + this.eraseCursor(); + } + + /** + * Erase the cursor from the UI. + */ + eraseCursor(): void { + // Check if a cursor object exists. + if (this.cursorObj) { + this.cursorObj.destroy(); + } // Destroy the cursor object to clean up resources. + + // Set the cursor object reference to null to fully dereference it. + this.cursorObj = null; + } + +} diff --git a/src/ui/settings/gamepad-binding-ui-handler.ts b/src/ui/settings/gamepad-binding-ui-handler.ts new file mode 100644 index 00000000000..1ab705d3278 --- /dev/null +++ b/src/ui/settings/gamepad-binding-ui-handler.ts @@ -0,0 +1,83 @@ +import BattleScene from "../../battle-scene"; +import AbstractBindingUiHandler from "./abstract-binding-ui-handler"; +import {Mode} from "../ui"; +import {Device} from "#app/enums/devices"; +import {getIconWithSettingName, getKeyWithKeycode} from "#app/configs/inputs/configHandler"; +import {addTextObject, TextStyle} from "#app/ui/text"; + + +export default class GamepadBindingUiHandler extends AbstractBindingUiHandler { + + constructor(scene: BattleScene, mode?: Mode) { + super(scene, mode); + this.scene.input.gamepad.on("down", this.gamepadButtonDown, this); + } + setup() { + super.setup(); + + // New button icon setup. + this.newButtonIcon = this.scene.add.sprite(0, 0, "xbox"); + 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.setPositionRelative(this.optionSelectBg, 78, 48); + this.targetButtonIcon.setOrigin(0.5); + this.targetButtonIcon.setVisible(false); + + 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); + this.actionsContainer.add(this.actionLabel); + + this.optionSelectContainer.add(this.newButtonIcon); + this.optionSelectContainer.add(this.swapText); + this.optionSelectContainer.add(this.targetButtonIcon); + } + + 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.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 = getKeyWithKeycode(activeConfig, button.index); + const buttonIcon = activeConfig.icons[key]; + if (!buttonIcon) { + return; + } + this.buttonPressed = button.index; + const assignedButtonIcon = getIconWithSettingName(activeConfig, this.target); + this.onInputDown(buttonIcon, assignedButtonIcon, type); + } + + swapAction(): boolean { + const activeConfig = this.scene.inputController.getActiveConfig(Device.GAMEPAD); + if (this.scene.inputController.assignBinding(activeConfig, this.target, this.buttonPressed)) { + this.scene.gameData.saveMappingConfigs(this.getSelectedDevice(), activeConfig); + return true; + } + return false; + } + + /** + * Clear the UI elements and state. + */ + clear() { + super.clear(); + this.targetButtonIcon.setVisible(false); + this.swapText.setVisible(false); + } +} diff --git a/src/ui/settings/keyboard-binding-ui-handler.ts b/src/ui/settings/keyboard-binding-ui-handler.ts new file mode 100644 index 00000000000..ca490857200 --- /dev/null +++ b/src/ui/settings/keyboard-binding-ui-handler.ts @@ -0,0 +1,73 @@ +import BattleScene from "../../battle-scene"; +import AbstractBindingUiHandler from "./abstract-binding-ui-handler"; +import {Mode} from "../ui"; +import { getKeyWithKeycode} from "#app/configs/inputs/configHandler"; +import {Device} from "#app/enums/devices"; +import {addTextObject, TextStyle} from "#app/ui/text"; + + +export default class KeyboardBindingUiHandler extends AbstractBindingUiHandler { + + 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() { + super.setup(); + + // New button icon setup. + this.newButtonIcon = this.scene.add.sprite(0, 0, "keyboard"); + this.newButtonIcon.setPositionRelative(this.optionSelectBg, 78, 32); + this.newButtonIcon.setOrigin(0.5); + this.newButtonIcon.setVisible(false); + + this.actionLabel = addTextObject(this.scene, 0, 0, "Assign button", TextStyle.SETTINGS_LABEL); + this.actionLabel.setOrigin(0, 0.5); + this.actionLabel.setPositionRelative(this.actionBg, this.actionBg.width - 80, this.actionBg.height / 2); + this.actionsContainer.add(this.actionLabel); + + this.optionSelectContainer.add(this.newButtonIcon); + } + + getSelectedDevice() { + return this.scene.inputController?.selectedDevice[Device.KEYBOARD]; + } + + onKeyDown(event): void { + const blacklist = [ + Phaser.Input.Keyboard.KeyCodes.UP, + Phaser.Input.Keyboard.KeyCodes.DOWN, + Phaser.Input.Keyboard.KeyCodes.LEFT, + Phaser.Input.Keyboard.KeyCodes.RIGHT, + Phaser.Input.Keyboard.KeyCodes.HOME, + Phaser.Input.Keyboard.KeyCodes.ENTER, + Phaser.Input.Keyboard.KeyCodes.ESC, + Phaser.Input.Keyboard.KeyCodes.DELETE, + ]; + const key = event.keyCode; + // // Check conditions before processing the button press. + if (!this.listening || this.buttonPressed !== null || blacklist.includes(key)) { + return; + } + 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 = getIconWithSettingName(activeConfig, this.target); + this.onInputDown(buttonIcon, null, "keyboard"); + } + + swapAction(): boolean { + const activeConfig = this.scene.inputController.getActiveConfig(Device.KEYBOARD); + if (this.scene.inputController.assignBinding(activeConfig, this.target, this.buttonPressed)) { + this.scene.gameData.saveMappingConfigs(this.getSelectedDevice(), activeConfig); + return true; + } + return false; + } + +} diff --git a/src/ui/settings/navigationMenu.ts b/src/ui/settings/navigationMenu.ts new file mode 100644 index 00000000000..843e9fd1f86 --- /dev/null +++ b/src/ui/settings/navigationMenu.ts @@ -0,0 +1,217 @@ +import BattleScene from "#app/battle-scene"; +import {Mode} from "#app/ui/ui"; +import {InputsIcons} from "#app/ui/settings/abstract-settings-ui-handler"; +import {addTextObject, setTextStyle, TextStyle} from "#app/ui/text"; +import {addWindow} from "#app/ui/ui-theme"; +import {Button} from "#app/enums/buttons"; + +/** + * Manages navigation and menus tabs within the setting menu. + */ +export class NavigationManager { + private static instance: NavigationManager; + public modes: Mode[]; + public selectedMode: Mode = Mode.SETTINGS; + public navigationMenus: NavigationMenu[] = new Array(); + public labels: string[]; + + /** + * Creates an instance of NavigationManager. + * To create a new tab in the menu, add the mode to the modes array and the label to the labels array. + * and instantiate a new NavigationMenu instance in your handler + * like: this.navigationContainer = new NavigationMenu(this.scene, 0, 0); + */ + constructor() { + this.modes = [ + Mode.SETTINGS, + Mode.SETTINGS_GAMEPAD, + Mode.SETTINGS_KEYBOARD, + ]; + this.labels = ["General", "Gamepad", "Keyboard"]; + } + + public reset() { + this.selectedMode = Mode.SETTINGS; + } + + /** + * Gets the singleton instance of the NavigationManager. + * @returns The singleton instance of NavigationManager. + */ + public static getInstance(): NavigationManager { + if (!NavigationManager.instance) { + NavigationManager.instance = new NavigationManager(); + } + return NavigationManager.instance; + } + + /** + * Navigates to the previous mode in the modes array. + * @param scene The current BattleScene instance. + */ + public navigateLeft(scene) { + const pos = this.modes.indexOf(this.selectedMode); + const maxPos = this.modes.length - 1; + if (pos === 0) { + this.selectedMode = this.modes[maxPos]; + } else { + this.selectedMode = this.modes[pos - 1]; + } + scene.ui.setMode(this.selectedMode); + this.updateNavigationMenus(); + } + + /** + * Navigates to the next mode in the modes array. + * @param scene The current BattleScene instance. + */ + public navigateRight(scene) { + const pos = this.modes.indexOf(this.selectedMode); + const maxPos = this.modes.length - 1; + if (pos === maxPos) { + this.selectedMode = this.modes[0]; + } else { + this.selectedMode = this.modes[pos + 1]; + } + scene.ui.setMode(this.selectedMode); + this.updateNavigationMenus(); + } + + /** + * Updates all navigation menus. + */ + public updateNavigationMenus() { + for (const instance of this.navigationMenus) { + instance.update(); + } + } + + /** + * Updates icons for all navigation menus. + */ + public updateIcons() { + for (const instance of this.navigationMenus) { + instance.updateIcons(); + } + } + +} + +export default class NavigationMenu extends Phaser.GameObjects.Container { + private navigationIcons: InputsIcons; + public scene: BattleScene; + protected headerTitles: Phaser.GameObjects.Text[] = new Array(); + + /** + * Creates an instance of NavigationMenu. + * @param scene The current BattleScene instance. + * @param x The x position of the NavigationMenu. + * @param y The y position of the NavigationMenu. + */ + constructor(scene: BattleScene, x: number, y: number) { + super(scene, x, y); + this.scene = scene; + + this.setup(); + } + + /** + * Sets up the NavigationMenu by adding windows, icons, and labels. + */ + setup() { + const navigationManager = NavigationManager.getInstance(); + const headerBg = addWindow(this.scene, 0, 0, (this.scene.game.canvas.width / 6) - 2, 24); + headerBg.setOrigin(0, 0); + this.add(headerBg); + this.width = headerBg.width; + this.height = headerBg.height; + + this.navigationIcons = {}; + + const iconPreviousTab = this.scene.add.sprite(8, 4, "keyboard"); + iconPreviousTab.setOrigin(0, -0.1); + iconPreviousTab.setPositionRelative(headerBg, 8, 4); + this.navigationIcons["BUTTON_CYCLE_FORM"] = iconPreviousTab; + + const iconNextTab = this.scene.add.sprite(0, 0, "keyboard"); + iconNextTab.setOrigin(0, -0.1); + iconNextTab.setPositionRelative(headerBg, headerBg.width - 20, 4); + this.navigationIcons["BUTTON_CYCLE_SHINY"] = iconNextTab; + + let relative: Phaser.GameObjects.Sprite | Phaser.GameObjects.Text = iconPreviousTab; + let relativeWidth: number = iconPreviousTab.width*6; + for (const label of navigationManager.labels) { + const labelText = addTextObject(this.scene, 0, 0, label, TextStyle.SETTINGS_LABEL); + labelText.setOrigin(0, 0); + labelText.setPositionRelative(relative, 6 + relativeWidth/6, 0); + this.add(labelText); + this.headerTitles.push(labelText); + relative = labelText; + relativeWidth = labelText.width; + } + + this.add(iconPreviousTab); + this.add(iconNextTab); + navigationManager.navigationMenus.push(this); + navigationManager.updateNavigationMenus(); + } + + /** + * Updates the NavigationMenu's header titles based on the selected mode. + */ + update() { + const navigationManager = NavigationManager.getInstance(); + const posSelected = navigationManager.modes.indexOf(navigationManager.selectedMode); + + for (const [index, title] of this.headerTitles.entries()) { + setTextStyle(title, this.scene, index === posSelected ? TextStyle.SETTINGS_SELECTED : TextStyle.SETTINGS_LABEL); + } + } + + /** + * Updates the icons in the NavigationMenu based on the latest input recorded. + */ + updateIcons() { + const specialIcons = { + "BUTTON_HOME": "HOME.png", + "BUTTON_DELETE": "DEL.png", + }; + for (const settingName of Object.keys(this.navigationIcons)) { + if (Object.keys(specialIcons).includes(settingName)) { + this.navigationIcons[settingName].setTexture("keyboard"); + this.navigationIcons[settingName].setFrame(specialIcons[settingName]); + this.navigationIcons[settingName].alpha = 1; + continue; + } + const icon = this.scene.inputController?.getIconForLatestInputRecorded(settingName); + if (icon) { + const type = this.scene.inputController?.getLastSourceType(); + this.navigationIcons[settingName].setTexture(type); + this.navigationIcons[settingName].setFrame(icon); + this.navigationIcons[settingName].alpha = 1; + } else { + this.navigationIcons[settingName].alpha = 0; + } + } + } + + /** + * Handles navigation based on the button pressed. + * @param button The button pressed for navigation. + * @returns A boolean indicating if the navigation was handled. + */ + navigate(button: Button): boolean { + const navigationManager = NavigationManager.getInstance(); + switch (button) { + case Button.CYCLE_FORM: + navigationManager.navigateLeft(this.scene); + return true; + break; + case Button.CYCLE_SHINY: + navigationManager.navigateRight(this.scene); + return true; + break; + } + return false; + } +} 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 3253ca8d325..8d2c534476a 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/settings-gamepad-ui-handler.ts b/src/ui/settings/settings-gamepad-ui-handler.ts new file mode 100644 index 00000000000..5389d4a1940 --- /dev/null +++ b/src/ui/settings/settings-gamepad-ui-handler.ts @@ -0,0 +1,168 @@ +import BattleScene from "../../battle-scene"; +import {addTextObject, TextStyle} from "../text"; +import {Mode} from "../ui"; +import { + setSettingGamepad, + SettingGamepad, + settingGamepadBlackList, + settingGamepadDefaults, + settingGamepadOptions +} from "../../system/settings-gamepad"; +import pad_xbox360 from "#app/configs/inputs/pad_xbox360"; +import pad_dualshock from "#app/configs/inputs/pad_dualshock"; +import pad_unlicensedSNES from "#app/configs/inputs/pad_unlicensedSNES"; +import {InterfaceConfig} from "#app/inputs-controller"; +import AbstractSettingsUiUiHandler from "#app/ui/settings/abstract-settings-ui-handler"; +import {Device} from "#app/enums/devices"; +import {truncateString} from "#app/utils"; + +/** + * Class representing the settings UI handler for gamepads. + * + * @extends AbstractSettingsUiUiHandler + */ + +export default class SettingsGamepadUiHandler extends AbstractSettingsUiUiHandler { + + /** + * Creates an instance of SettingsGamepadUiHandler. + * + * @param scene - The BattleScene instance. + * @param mode - The UI mode, optional. + */ + constructor(scene: BattleScene, mode?: Mode) { + super(scene, mode); + this.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"; + this.settingBlacklisted = settingGamepadBlackList; + } + + setSetting(scene: BattleScene, setting, value: integer): boolean { + return setSettingGamepad(scene, setting, value); + } + + /** + * Setup UI elements. + */ + setup() { + 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); + 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; + } + + /** + * Get the active configuration. + * + * @returns The active gamepad configuration. + */ + getActiveConfig(): InterfaceConfig { + return this.scene.inputController.getActiveConfig(Device.GAMEPAD); + } + + /** + * Get the gamepad settings from local storage. + * + * @returns The gamepad settings from local storage. + */ + getLocalStorageSetting(): object { + // Retrieve the gamepad settings from local storage or use an empty object if none exist. + const settings: object = localStorage.hasOwnProperty("settingsGamepad") ? JSON.parse(localStorage.getItem("settingsGamepad")) : {}; + return settings; + } + + /** + * Set the layout for the active configuration. + * + * @param activeConfig - The active gamepad configuration. + * @returns `true` if the layout was successfully applied, otherwise `false`. + */ + setLayout(activeConfig: InterfaceConfig): boolean { + // Check if there is no active configuration (e.g., no gamepad connected). + if (!activeConfig) { + // Retrieve the layout for when no gamepads are connected. + const layout = this.layout["noGamepads"]; + // Make the options container visible to show message. + layout.optionsContainer.setVisible(true); + // Return false indicating the layout application was not successful due to lack of gamepad. + return false; + } + + return super.setLayout(activeConfig); + } + + + /** + * Navigate to the left menu tab. + * + * @returns `true` indicating the navigation was successful. + */ + navigateMenuLeft(): boolean { + this.scene.ui.setMode(Mode.SETTINGS); + return true; + } + + /** + * Navigate to the right menu tab. + * + * @returns `true` indicating the navigation was successful. + */ + navigateMenuRight(): boolean { + this.scene.ui.setMode(Mode.SETTINGS_KEYBOARD); + return true; + } + + /** + * Update the display of the chosen gamepad. + */ + updateChosenGamepadDisplay(): void { + // Update any bindings that might have changed since the last update. + this.updateBindings(); + this.resetScroll(); + + // Iterate over the keys in the settingDevice enumeration. + for (const [index, key] of Object.keys(this.settingDevice).entries()) { + const setting = this.settingDevice[key]; // Get the actual setting value using the key. + + // Check if the current setting corresponds to the controller setting. + if (setting === this.settingDevice.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. + + // 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.selectedDevice[Device.GAMEPAD], 20)); + } + } + } + } + + /** + * Save the setting to local storage. + * + * @param setting - The setting to save. + * @param cursor - The cursor position to save. + */ + saveSettingToLocalStorage(setting, cursor): void { + if (this.settingDevice[setting] !== this.settingDevice.Controller) { + this.scene.gameData.saveGamepadSetting(setting, cursor); + } + } +} diff --git a/src/ui/settings/settings-keyboard-ui-handler.ts b/src/ui/settings/settings-keyboard-ui-handler.ts new file mode 100644 index 00000000000..3a7751c4522 --- /dev/null +++ b/src/ui/settings/settings-keyboard-ui-handler.ts @@ -0,0 +1,224 @@ +import BattleScene from "../../battle-scene"; +import {Mode} from "../ui"; +import cfg_keyboard_qwerty from "#app/configs/inputs/cfg_keyboard_qwerty"; +import { + setSettingKeyboard, + SettingKeyboard, + settingKeyboardBlackList, + settingKeyboardDefaults, + settingKeyboardOptions +} from "#app/system/settings-keyboard"; +import {reverseValueToKeySetting, truncateString} from "#app/utils"; +import AbstractSettingsUiUiHandler from "#app/ui/settings/abstract-settings-ui-handler"; +import {InterfaceConfig} from "#app/inputs-controller"; +import {addTextObject, TextStyle} from "#app/ui/text"; +import {deleteBind} from "#app/configs/inputs/configHandler"; +import {Device} from "#app/enums/devices"; +import {NavigationManager} from "#app/ui/settings/navigationMenu"; + +/** + * Class representing the settings UI handler for keyboards. + * + * @extends AbstractSettingsUiUiHandler + */ +export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandler { + /** + * Creates an instance of SettingsKeyboardUiHandler. + * + * @param scene - The BattleScene instance. + * @param mode - The UI mode, optional. + */ + constructor(scene: BattleScene, mode?: Mode) { + super(scene, mode); + this.titleSelected = "Keyboard"; + this.settingDevice = SettingKeyboard; + this.settingDeviceDefaults = settingKeyboardDefaults; + this.settingDeviceOptions = settingKeyboardOptions; + this.configs = [cfg_keyboard_qwerty]; + this.commonSettingsCount = 0; + this.textureOverride = "keyboard"; + this.localStoragePropertyName = "settingsKeyboard"; + this.settingBlacklisted = settingKeyboardBlackList; + + const deleteEvent = scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.DELETE); + const restoreDefaultEvent = scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.HOME); + deleteEvent.on("up", this.onDeleteDown, this); + restoreDefaultEvent.on("up", this.onHomeDown, this); + } + + setSetting(scene: BattleScene, setting, value: integer): boolean { + return setSettingKeyboard(scene, setting, value); + } + + /** + * Setup UI elements. + */ + 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); + + const iconDelete = this.scene.add.sprite(0, 0, "keyboard"); + iconDelete.setOrigin(0, -0.1); + iconDelete.setPositionRelative(this.actionsBg, this.navigationContainer.width - 260, 4); + this.navigationIcons["BUTTON_DELETE"] = iconDelete; + + const deleteText = addTextObject(this.scene, 0, 0, "Delete", TextStyle.SETTINGS_LABEL); + deleteText.setOrigin(0, 0.15); + deleteText.setPositionRelative(iconDelete, -deleteText.width/6-2, 0); + + this.settingsContainer.add(iconDelete); + this.settingsContainer.add(deleteText); + + + + // Map the 'noKeyboard' layout options for easy access. + this.layout["noKeyboard"].optionsContainer = optionsContainer; + this.layout["noKeyboard"].label = label; + } + + /** + * Handle the home key press event. + */ + onHomeDown(): void { + if (![Mode.SETTINGS_KEYBOARD, Mode.SETTINGS_GAMEPAD].includes(this.scene.ui.getMode())) { + return; + } + this.scene.gameData.resetMappingToFactory(); + NavigationManager.getInstance().updateIcons(); + } + + /** + * Handle the delete key press event. + */ + onDeleteDown(): void { + if (this.scene.ui.getMode() !== Mode.SETTINGS_KEYBOARD) { + return; + } + const cursor = this.cursor + this.scrollCursor; // Calculate the absolute cursor position. + const selection = this.settingLabels[cursor].text; + const key = reverseValueToKeySetting(selection); + const settingName = SettingKeyboard[key]; + const activeConfig = this.getActiveConfig(); + const success = deleteBind(this.getActiveConfig(), settingName); + if (success) { + this.saveCustomKeyboardMappingToLocalStorage(activeConfig); + this.updateBindings(); + NavigationManager.getInstance().updateIcons(); + } + } + + /** + * Get the active configuration. + * + * @returns The active keyboard configuration. + */ + getActiveConfig(): InterfaceConfig { + return this.scene.inputController.getActiveConfig(Device.KEYBOARD); + } + + /** + * Get the keyboard settings from local storage. + * + * @returns The keyboard settings from local storage. + */ + getLocalStorageSetting(): object { + // Retrieve the gamepad settings from local storage or use an empty object if none exist. + const settings: object = localStorage.hasOwnProperty("settingsKeyboard") ? JSON.parse(localStorage.getItem("settingsKeyboard")) : {}; + return settings; + } + + /** + * Set the layout for the active configuration. + * + * @param activeConfig - The active keyboard configuration. + * @returns `true` if the layout was successfully applied, otherwise `false`. + */ + setLayout(activeConfig: InterfaceConfig): boolean { + // Check if there is no active configuration (e.g., no gamepad connected). + if (!activeConfig) { + // Retrieve the layout for when no gamepads are connected. + const layout = this.layout["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); + } + + /** + * Navigate to the left menu tab. + * + * @returns `true` indicating the navigation was successful. + */ + navigateMenuLeft(): boolean { + this.scene.ui.setMode(Mode.SETTINGS_GAMEPAD); + return true; + } + + /** + * Navigate to the right menu tab. + * + * @returns `true` indicating the navigation was successful. + */ + navigateMenuRight(): boolean { + this.scene.ui.setMode(Mode.SETTINGS); + return true; + } + + /** + * Update the display of the chosen keyboard layout. + */ + updateChosenKeyboardDisplay(): void { + // Update any bindings that might have changed since the last update. + this.updateBindings(); + + // Iterate over the keys in the settingDevice enumeration. + for (const [index, key] of Object.keys(this.settingDevice).entries()) { + const setting = this.settingDevice[key]; // Get the actual setting value using the key. + + // Check if the current setting corresponds to the layout setting. + 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.selectedDevice[Device.KEYBOARD], 22)); + } + } + } + + } + + /** + * Save the custom keyboard mapping to local storage. + * + * @param config - The configuration to save. + */ + saveCustomKeyboardMappingToLocalStorage(config): void { + this.scene.gameData.saveMappingConfigs(this.scene.inputController?.selectedDevice[Device.KEYBOARD], config); + } + + /** + * Save the setting to local storage. + * + * @param settingName - The name of the setting to save. + * @param cursor - The cursor position to save. + */ + saveSettingToLocalStorage(settingName, cursor): void { + if (this.settingDevice[settingName] !== this.settingDevice.Default_Layout) { + this.scene.gameData.saveKeyboardSetting(settingName, cursor); + } + } +} diff --git a/src/ui/settings-ui-handler.ts b/src/ui/settings/settings-ui-handler.ts similarity index 69% rename from src/ui/settings-ui-handler.ts rename to src/ui/settings/settings-ui-handler.ts index ba6515ad0c4..44dc90dff88 100644 --- a/src/ui/settings-ui-handler.ts +++ b/src/ui/settings/settings-ui-handler.ts @@ -1,15 +1,18 @@ -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"; +import {InputsIcons} from "#app/ui/settings/abstract-settings-ui-handler"; +import NavigationMenu, {NavigationManager} from "#app/ui/settings/navigationMenu"; export default class SettingsUiHandler extends UiHandler { private settingsContainer: Phaser.GameObjects.Container; private optionsContainer: Phaser.GameObjects.Container; + private navigationContainer: NavigationMenu; private scrollCursor: integer; @@ -20,16 +23,20 @@ export default class SettingsUiHandler extends UiHandler { private settingLabels: Phaser.GameObjects.Text[]; private optionValueLabels: Phaser.GameObjects.Text[][]; + protected navigationIcons: InputsIcons; + private cursorObj: Phaser.GameObjects.NineSlice; private reloadRequired: boolean; private reloadI18n: boolean; + private rowsToDisplay: number; constructor(scene: BattleScene, mode?: Mode) { super(scene, mode); this.reloadRequired = false; this.reloadI18n = false; + this.rowsToDisplay = 8; } setup() { @@ -37,18 +44,36 @@ export default class SettingsUiHandler extends UiHandler { this.settingsContainer = this.scene.add.container(1, -(this.scene.game.canvas.height / 6) + 1); - this.settingsContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, this.scene.game.canvas.width / 6, this.scene.game.canvas.height / 6), Phaser.Geom.Rectangle.Contains); + this.settingsContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, this.scene.game.canvas.width / 6, this.scene.game.canvas.height / 6 - 20), Phaser.Geom.Rectangle.Contains); - const headerBg = addWindow(this.scene, 0, 0, (this.scene.game.canvas.width / 6) - 2, 24); - headerBg.setOrigin(0, 0); + this.navigationIcons = {}; - const headerText = addTextObject(this.scene, 0, 0, "Options", TextStyle.SETTINGS_LABEL); - headerText.setOrigin(0, 0); - headerText.setPositionRelative(headerBg, 8, 4); + this.navigationContainer = new NavigationMenu(this.scene, 0, 0); - 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 = addWindow(this.scene, 0, this.navigationContainer.height, (this.scene.game.canvas.width / 6) - 2, (this.scene.game.canvas.height / 6) - 16 - this.navigationContainer.height - 2); this.optionsBg.setOrigin(0, 0); + const actionsBg = addWindow(this.scene, 0, (this.scene.game.canvas.height / 6) - this.navigationContainer.height, (this.scene.game.canvas.width / 6) - 2, 22); + actionsBg.setOrigin(0, 0); + + const iconAction = this.scene.add.sprite(0, 0, "keyboard"); + iconAction.setOrigin(0, -0.1); + iconAction.setPositionRelative(actionsBg, this.navigationContainer.width - 32, 4); + this.navigationIcons["BUTTON_ACTION"] = iconAction; + + const actionText = addTextObject(this.scene, 0, 0, "Action", TextStyle.SETTINGS_LABEL); + actionText.setOrigin(0, 0.15); + actionText.setPositionRelative(iconAction, -actionText.width/6-2, 0); + + const iconCancel = this.scene.add.sprite(0, 0, "keyboard"); + iconCancel.setOrigin(0, -0.1); + iconCancel.setPositionRelative(actionsBg, this.navigationContainer.width - 100, 4); + this.navigationIcons["BUTTON_CANCEL"] = iconCancel; + + const cancelText = addTextObject(this.scene, 0, 0, "Cancel", TextStyle.SETTINGS_LABEL); + cancelText.setOrigin(0, 0.15); + cancelText.setPositionRelative(iconCancel, -cancelText.width/6-2, 0); + this.optionsContainer = this.scene.add.container(0, 0); this.settingLabels = []; @@ -91,10 +116,14 @@ export default class SettingsUiHandler extends UiHandler { this.optionCursors = Object.values(settingDefaults); - this.settingsContainer.add(headerBg); - this.settingsContainer.add(headerText); this.settingsContainer.add(this.optionsBg); + this.settingsContainer.add(this.navigationContainer); + this.settingsContainer.add(actionsBg); this.settingsContainer.add(this.optionsContainer); + this.settingsContainer.add(iconAction); + this.settingsContainer.add(iconCancel); + this.settingsContainer.add(actionText); + this.settingsContainer.add(cancelText); ui.add(this.settingsContainer); @@ -104,8 +133,30 @@ export default class SettingsUiHandler extends UiHandler { this.settingsContainer.setVisible(false); } + updateBindings(): void { + for (const settingName of Object.keys(this.navigationIcons)) { + if (settingName === "BUTTON_HOME") { + this.navigationIcons[settingName].setTexture("keyboard"); + this.navigationIcons[settingName].setFrame("HOME.png"); + this.navigationIcons[settingName].alpha = 1; + continue; + } + const icon = this.scene.inputController?.getIconForLatestInputRecorded(settingName); + if (icon) { + const type = this.scene.inputController?.getLastSourceType(); + this.navigationIcons[settingName].setTexture(type); + this.navigationIcons[settingName].setFrame(icon); + this.navigationIcons[settingName].alpha = 1; + } else { + this.navigationIcons[settingName].alpha = 0; + } + } + NavigationManager.getInstance().updateIcons(); + } + show(args: any[]): boolean { super.show(args); + this.updateBindings(); const settings: object = localStorage.hasOwnProperty("settings") ? JSON.parse(localStorage.getItem("settings")) : {}; @@ -133,12 +184,12 @@ export default class SettingsUiHandler 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; if (button === Button.CANCEL) { success = true; + NavigationManager.getInstance().reset(); // Reverts UI to its previous state on cancel. this.scene.ui.revertMode(); } else { @@ -154,17 +205,17 @@ export default class SettingsUiHandler extends UiHandler { } 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); + const successA = this.setCursor(this.rowsToDisplay - 1); // Then, adjust the scroll to display the bottommost elements of the menu. - const successB = this.setScrollCursor(this.optionValueLabels.length - rowsToDisplay); + const successB = this.setScrollCursor(this.optionValueLabels.length - this.rowsToDisplay); success = successA && successB; // success is just there to play the little validation sound effect } break; case Button.DOWN: if (cursor < this.optionValueLabels.length - 1) { - if (this.cursor < rowsToDisplay - 1) { // if the visual cursor is in the frame of 0 to 8 + if (this.cursor < this.rowsToDisplay - 1) {// if the visual cursor is in the frame of 0 to 8 success = this.setCursor(this.cursor + 1); - } else if (this.scrollCursor < this.optionValueLabels.length - rowsToDisplay) { + } else if (this.scrollCursor < this.optionValueLabels.length - this.rowsToDisplay) { success = this.setScrollCursor(this.scrollCursor + 1); } } else { @@ -177,7 +228,7 @@ export default class SettingsUiHandler extends UiHandler { } break; case Button.LEFT: - if (this.optionCursors[cursor]) { // Moves the option cursor left, if possible. + if (this.optionCursors[cursor]) {// Moves the option cursor left, if possible. success = this.setOptionCursor(cursor, this.optionCursors[cursor] - 1, true); } break; @@ -187,6 +238,10 @@ export default class SettingsUiHandler extends UiHandler { success = this.setOptionCursor(cursor, this.optionCursors[cursor] + 1, true); } break; + case Button.CYCLE_FORM: + case Button.CYCLE_SHINY: + success = this.navigationContainer.navigate(button); + break; } } @@ -263,7 +318,7 @@ export default class SettingsUiHandler extends UiHandler { this.optionsContainer.setY(-16 * this.scrollCursor); for (let s = 0; s < this.settingLabels.length; s++) { - const visible = s >= this.scrollCursor && s < this.scrollCursor + 9; + const visible = s >= this.scrollCursor && s < this.scrollCursor + this.rowsToDisplay; this.settingLabels[s].setVisible(visible); for (const option of this.optionValueLabels[s]) { option.setVisible(visible); diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index a8b75fe6357..32f5bffeb1b 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -742,7 +742,7 @@ export default class SummaryUiHandler extends UiHandler { allAbilityInfo.push(this.passiveContainer); // Sets up the pixel button prompt image - this.abilityPrompt = this.scene.add.image(0, 0, !this.scene.gamepadSupport ? "summary_profile_prompt_z" : "summary_profile_prompt_a"); + this.abilityPrompt = this.scene.add.image(0, 0, !this.scene.inputController?.gamepadSupport ? "summary_profile_prompt_z" : "summary_profile_prompt_a"); this.abilityPrompt.setPosition(8, 43); this.abilityPrompt.setVisible(true); this.abilityPrompt.setOrigin(0, 0); diff --git a/src/ui/text.ts b/src/ui/text.ts index 5c11f1cfbd1..3db4c37fe67 100644 --- a/src/ui/text.ts +++ b/src/ui/text.ts @@ -26,6 +26,7 @@ export enum TextStyle { STATS_VALUE, SETTINGS_LABEL, SETTINGS_SELECTED, + SETTINGS_LOCKED, TOOLTIP_TITLE, TOOLTIP_CONTENT, MOVE_INFO_CONTENT @@ -63,6 +64,15 @@ export function addTextObject(scene: Phaser.Scene, x: number, y: number, content return ret; } +export function setTextStyle(obj: Phaser.GameObjects.Text, scene: Phaser.Scene, style: TextStyle, extraStyleOptions?: Phaser.Types.GameObjects.Text.TextStyle) { + const [ styleOptions, shadowColor, shadowXpos, shadowYpos ] = getTextStyleOptions(style, (scene as BattleScene).uiTheme, extraStyleOptions); + obj.setScale(0.1666666667); + obj.setShadow(shadowXpos, shadowYpos, shadowColor); + if (!(styleOptions as Phaser.Types.GameObjects.Text.TextStyle).lineSpacing) { + obj.setLineSpacing(5); + } +} + export function addBBCodeTextObject(scene: Phaser.Scene, x: number, y: number, content: string, style: TextStyle, extraStyleOptions?: Phaser.Types.GameObjects.Text.TextStyle): BBCodeText { const [ styleOptions, shadowColor, shadowXpos, shadowYpos ] = getTextStyleOptions(style, (scene as BattleScene).uiTheme, extraStyleOptions); @@ -143,6 +153,7 @@ function getTextStyleOptions(style: TextStyle, uiTheme: UiTheme, extraStyleOptio break; case TextStyle.MESSAGE: case TextStyle.SETTINGS_LABEL: + case TextStyle.SETTINGS_LOCKED: case TextStyle.SETTINGS_SELECTED: styleOptions.fontSize = languageSettings[lang]?.summaryFontSize || "96px"; break; @@ -226,6 +237,7 @@ export function getTextColor(textStyle: TextStyle, shadow?: boolean, uiTheme: Ui case TextStyle.SUMMARY_GOLD: case TextStyle.MONEY: return !shadow ? "#e8e8a8" : "#a0a060"; + case TextStyle.SETTINGS_LOCKED: case TextStyle.SUMMARY_GRAY: return !shadow ? "#a0a0a0" : "#636363"; case TextStyle.STATS_LABEL: diff --git a/src/ui/title-ui-handler.ts b/src/ui/title-ui-handler.ts index 033327c0582..673d8c870d9 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 e7a38e2b7c3..7092f16a57c 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -12,12 +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 {addTextObject, TextStyle} from "./text"; +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"; @@ -38,6 +39,9 @@ import SessionReloadModalUiHandler from "./session-reload-modal-ui-handler"; import {Button} from "../enums/buttons"; import i18next, {ParseKeys} from "i18next"; import {PlayerGender} from "#app/system/game-data"; +import GamepadBindingUiHandler from "./settings/gamepad-binding-ui-handler"; +import SettingsKeyboardUiHandler from "#app/ui/settings/settings-keyboard-ui-handler"; +import KeyboardBindingUiHandler from "#app/ui/settings/keyboard-binding-ui-handler"; export enum Mode { MESSAGE, @@ -58,6 +62,10 @@ export enum Mode { MENU, MENU_OPTION_SELECT, SETTINGS, + SETTINGS_GAMEPAD, + GAMEPAD_BINDING, + SETTINGS_KEYBOARD, + KEYBOARD_BINDING, ACHIEVEMENTS, GAME_STATS, VOUCHERS, @@ -88,7 +96,11 @@ const noTransitionModes = [ Mode.OPTION_SELECT, 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, @@ -103,7 +115,7 @@ const noTransitionModes = [ export default class UI extends Phaser.GameObjects.Container { private mode: Mode; private modeChain: Mode[]; - private handlers: UiHandler[]; + public handlers: UiHandler[]; private overlay: Phaser.GameObjects.Rectangle; public achvBar: AchvBar; public savingIcon: SavingIconHandler; @@ -139,6 +151,10 @@ 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 GamepadBindingUiHandler(scene), + new SettingsKeyboardUiHandler(scene), + new KeyboardBindingUiHandler(scene), new AchvsUiHandler(scene), new GameStatsUiHandler(scene), new VouchersUiHandler(scene), diff --git a/src/utils.ts b/src/utils.ts index 43ce73f1ddb..adfe0b0df20 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -423,3 +423,49 @@ export function printContainerList(container: Phaser.GameObjects.Container): voi return {type: go.type, name: go.name}; })); } + + +/** + * Truncate a string to a specified maximum length and add an ellipsis if it exceeds that length. + * + * @param str - The string to be truncated. + * @param maxLength - The maximum length of the truncated string, defaults to 10. + * @returns The truncated string with an ellipsis if it was longer than maxLength. + */ +export function truncateString(str: String, maxLength: number = 10) { + // Check if the string length exceeds the maximum length + if (str.length > maxLength) { + // Truncate the string and add an ellipsis + return str.slice(0, maxLength - 3) + "..."; // Subtract 3 to accommodate the ellipsis + } + // Return the original string if it does not exceed the maximum length + return str; +} + +/** + * Perform a deep copy of an object. + * + * @param values - The object to be deep copied. + * @returns A new object that is a deep copy of the input. + */ +export function deepCopy(values: object): object { + // Convert the object to a JSON string and parse it back to an object to perform a deep copy + return JSON.parse(JSON.stringify(values)); +} + +/** + * Convert a space-separated string into a capitalized and underscored string. + * + * @param input - The string to be converted. + * @returns The converted string with words capitalized and separated by underscores. + */ +export function reverseValueToKeySetting(input) { + // Split the input string into an array of words + const words = input.split(" "); + // Capitalize the first letter of each word and convert the rest to lowercase + const capitalizedWords = words.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()); + // Join the capitalized words with underscores and return the result + return capitalizedWords.join("_"); +} + +