From 41d1a84c767b9c389a3075001766188e637e4031 Mon Sep 17 00:00:00 2001 From: Flashfyre Date: Mon, 25 Dec 2023 15:03:50 -0500 Subject: [PATCH] Add touch controls for mobile support --- index.css | 157 ++++++++++++++++++++++++++++++++++++++++ index.html | 60 +++++++++------ src/battle-scene.ts | 4 + src/system/game-data.ts | 2 + src/system/settings.ts | 14 +++- src/touch-controls.js | 106 +++++++++++++++++++++++++++ src/ui/ui.ts | 3 + src/ui/window.ts | 10 +++ 8 files changed, 332 insertions(+), 24 deletions(-) create mode 100644 index.css create mode 100644 src/touch-controls.js diff --git a/index.css b/index.css new file mode 100644 index 00000000000..5001dc9f26b --- /dev/null +++ b/index.css @@ -0,0 +1,157 @@ +:root { + --color-base: hsl(0, 0%, 55%); + --color-light: hsl(0, 0%, 90%); + --color-dark: hsl(0, 0%, 10%); + --controls-size: 10vh; + --text-shadow-size: 0.65vh; +} + +@media (orientation: landscape) { + :root { + --controls-size: 20vh; + --text-shadow-size: 1.3vh; + } +} + +@font-face { + font-family: 'emerald'; + src: url('fonts/pokemon-emerald-pro.ttf') format('truetype'); +} + +@font-face { + font-family: 'pkmnems'; + src: url('fonts/pkmnems.ttf') format('truetype'); +} + +html { + touch-action: none; +} + +body { + margin: 0; + background: #484050; +} + +#app { + display: flex; + justify-content: center; +} + +#touchControls:not(.visible) { + display: none; +} + +#dpad, #apad { + position: fixed; + bottom: 1rem; + z-index: 3; +} + +#dpad { + left: 1rem; +} + +#apad { + right: 1rem; +} + +#dpad svg { + width: calc(2 * var(--controls-size)); + height: calc(2 * var(--controls-size)); + fill: var(--color-base); +} + +#dpad svg rect { + opacity: 0.4; +} + +#apad > * { + width: var(--controls-size); + height: var(--controls-size); +} + +#apad .apadBtn { + width: var(--controls-size); + height: var(--controls-size); + background-color: var(--color-base); + border-radius: 50%; +} + +#apad .apadLabel { + font-family: 'emerald'; + font-size: var(--controls-size); + text-shadow: var(--color-dark) var(--text-shadow-size) var(--text-shadow-size); + color: var(--color-light); + user-select: none; +} + +#apad .apadLabelSmall { + font-size: calc(var(--controls-size) / 3); + text-shadow: var(--color-dark) calc(var(--text-shadow-size) / 3) calc(var(--text-shadow-size) / 3); +} + +#apad #apadLabelAction, #apad #apadLabelCancel { + margin-left: calc(var(--controls-size) / 3); + line-height: 0.9; +} + +#apadLabelMenu { + margin-left: 10%; + line-height: 1.1; +} + +#apad > :nth-child(2) { + position: relative; + right: var(--controls-size); +} + +#apad .apadRectBtn { + position: relative; + border-radius: 10%; + margin-top: calc(var(--controls-size) * -0.4); + bottom: calc(var(--controls-size) * 0.05); + left: calc(var(--controls-size) * 0.21); + width: calc(var(--controls-size) * 0.6); + height: calc(var(--controls-size) * 0.4); +} + +#apad .apadSqBtn { + border-radius: 10%; + width: calc(var(--controls-size) * 0.3); + height: calc(var(--controls-size) * 0.3); +} + +#apad .apadBtnContainer { + position: relative; + display: flex; + flex-wrap: wrap; + justify-content: space-evenly; + align-items: center; + margin-bottom: calc(var(--controls-size) * -0.8); + top: calc(var(--controls-size) * -0.9); + left: calc(var(--controls-size) * 0.1); + width: calc(var(--controls-size) * 0.8); + height: calc(var(--controls-size) * 0.8); +} + +#touchControls:not([data-ui-mode='STARTER_SELECT']) #apad .apadBtnContainer { + display: none; +} + +#apad .apadRectBtn + .apadBtnContainer { + top: calc(var(--controls-size) * -1.9); + left: calc(var(--controls-size) * -0.9); +} + +#apad .apadBtnContainer .apadLabel { + margin-left: calc(var(--controls-size) / 12); + line-height: 0.8; +} + +#dpad path:not(.active), #apad .apadBtn:not(.active) { + opacity: 0.4; +} + +#layout:fullscreen #dpad, #layout:fullscreen #apad { + bottom: 6rem; +} \ No newline at end of file diff --git a/index.html b/index.html index 3f3ce7142f1..41329397142 100644 --- a/index.html +++ b/index.html @@ -7,32 +7,50 @@ Pokemon Rogue Battle - +
+
+
+ + + + + + + +
+ +
+
+ A +
+
+ B +
+
+ Menu +
+
+
+ R +
+
+ F +
+
+ G +
+
+ E +
+
+
+
+ diff --git a/src/battle-scene.ts b/src/battle-scene.ts index c45eb3eec46..a43c6006948 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -552,15 +552,19 @@ export default class BattleScene extends Phaser.Scene { [Button.SPEED_UP]: [keyCodes.PLUS], [Button.SLOW_DOWN]: [keyCodes.MINUS] }; + const mobileKeyConfig = {}; this.buttonKeys = []; for (let b of Utils.getEnumValues(Button)) { const keys: Phaser.Input.Keyboard.Key[] = []; if (keyConfig.hasOwnProperty(b)) { for (let k of keyConfig[b]) keys.push(this.input.keyboard.addKey(k)); + mobileKeyConfig[Button[b]] = keys[0]; } this.buttonKeys[b] = keys; } + + initTouchControls(mobileKeyConfig); } getParty(): PlayerPokemon[] { diff --git a/src/system/game-data.ts b/src/system/game-data.ts index ac76ce5a934..be82fd84d58 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -243,6 +243,8 @@ export class GameData { } private loadSettings(): boolean { + Object.values(Setting).map(setting => setting as Setting).forEach(setting => setSetting(this.scene, setting, settingDefaults[setting])); + if (!localStorage.hasOwnProperty('settings')) return false; diff --git a/src/system/settings.ts b/src/system/settings.ts index d153df7f943..ad648322299 100644 --- a/src/system/settings.ts +++ b/src/system/settings.ts @@ -7,7 +7,8 @@ export enum Setting { BGM_Volume = "BGM_VOLUME", SE_Volume = "SE_VOLUME", Show_Stats_on_Level_Up = "SHOW_LEVEL_UP_STATS", - Window_Type = "WINDOW_TYPE" + Window_Type = "WINDOW_TYPE", + Touch_Controls = "TOUCH_CONTROLS" } export interface SettingOptions { @@ -24,7 +25,8 @@ export const settingOptions: SettingOptions = { [Setting.BGM_Volume]: new Array(11).fill(null).map((_, i) => i ? (i * 10).toString() : 'Mute'), [Setting.SE_Volume]: new Array(11).fill(null).map((_, i) => i ? (i * 10).toString() : 'Mute'), [Setting.Show_Stats_on_Level_Up]: [ 'Off', 'On' ], - [Setting.Window_Type]: new Array(4).fill(null).map((_, i) => (i + 1).toString()) + [Setting.Window_Type]: new Array(4).fill(null).map((_, i) => (i + 1).toString()), + [Setting.Touch_Controls]: [ 'Auto', 'Disabled' ] }; export const settingDefaults: SettingDefaults = { @@ -33,7 +35,8 @@ export const settingDefaults: SettingDefaults = { [Setting.BGM_Volume]: 10, [Setting.SE_Volume]: 10, [Setting.Show_Stats_on_Level_Up]: 1, - [Setting.Window_Type]: 1 + [Setting.Window_Type]: 1, + [Setting.Touch_Controls]: 0 }; export function setSetting(scene: BattleScene, setting: Setting, value: integer): boolean { @@ -59,6 +62,11 @@ export function setSetting(scene: BattleScene, setting: Setting, value: integer) case Setting.Window_Type: updateWindowType(scene, parseInt(settingOptions[setting][value])); break; + case Setting.Touch_Controls: + const touchControls = document.getElementById('touchControls'); + if (touchControls) + touchControls.classList.toggle('visible', settingOptions[setting][value] !== 'Disabled' && hasTouchscreen()); + break; } return true; diff --git a/src/touch-controls.js b/src/touch-controls.js new file mode 100644 index 00000000000..1242800f2d9 --- /dev/null +++ b/src/touch-controls.js @@ -0,0 +1,106 @@ +const keys = new Map(); +const keysDown = new Map(); +let lastTouchedId; + +function initTouchControls(buttonMap) { + for (const button of document.querySelectorAll('[data-key]')) { + // @ts-ignore + bindKey(button, button.dataset.key, buttonMap); + } +} + +function hasTouchscreen() { + return window.matchMedia('(hover: none), (pointer: coarse)').matches; +} + +/** + * Simulate a keyboard event on the canvas + * + * @param {string} eventType Type of the keyboard event + * @param {string} button Button to simulate + * @param {object} buttonMap Map of buttons to key objects + */ +function simulateKeyboardEvent(eventType, button, buttonMap) { + const key = buttonMap[button]; + + switch (eventType) { + case 'keydown': + key.onDown({}); + break; + case 'keyup': + key.onUp({}); + break; + } +} + +/** + * Simulate a keyboard input from 'keydown' to 'keyup' + * + * @param {string} key Key to simulate + * @param {object} buttonMap Map of buttons to key objects + */ +function simulateKeyboardInput(key, buttonMap) { + simulateKeyboardEvent('keydown', key, buttonMap); + window.setTimeout(() => { + simulateKeyboardEvent('keyup', key, buttonMap); + }, 100); +} + +/** + * Bind a node by a specific key to simulate on touch + * + * @param {*} node The node to bind a key to + * @param {string} key Key to simulate + * @param {object} buttonMap Map of buttons to key objects + */ +function bindKey(node, key, buttonMap) { + keys.set(node.id, key); + + node.addEventListener('touchstart', event => { + event.preventDefault(); + simulateKeyboardEvent('keydown', key, buttonMap); + keysDown.set(event.target.id, node.id); + node.classList.add('active'); + }); + + node.addEventListener('touchend', event => { + event.preventDefault(); + + const pressedKey = keysDown.get(event.target.id); + if (pressedKey && keys.has(pressedKey)) { + const key = keys.get(pressedKey); + simulateKeyboardEvent('keyup', key, buttonMap); + } + + 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 => { + const { target, clientX, clientY } = event.changedTouches[0]; + const origTargetId = keysDown.get(target.id); + const nextTargetId = document.elementFromPoint(clientX, clientY).id; + if (origTargetId === nextTargetId) + return; + + if (origTargetId) { + const key = keys.get(origTargetId); + simulateKeyboardEvent('keyup', key, buttonMap); + keysDown.delete(target.id); + document.getElementById(origTargetId).classList.remove('active'); + } + + if (keys.has(nextTargetId)) { + const key = keys.get(nextTargetId); + simulateKeyboardEvent('keydown', key, buttonMap); + keysDown.set(target.id, nextTargetId); + lastTouchedId = nextTargetId; + document.getElementById(nextTargetId).classList.add('active'); + } + }); +} \ No newline at end of file diff --git a/src/ui/ui.ts b/src/ui/ui.ts index dc09be05f45..a21d9c01645 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -280,6 +280,9 @@ export default class UI extends Phaser.GameObjects.Container { if (chainMode && this.mode && !clear) this.modeChain.push(this.mode); this.mode = mode; + const touchControls = document.getElementById('touchControls'); + if (touchControls) + touchControls.dataset.uiMode = Mode[mode]; this.getHandler().show(args); } resolve(); diff --git a/src/ui/window.ts b/src/ui/window.ts index 55ecfdd6cc3..92a04297cbc 100644 --- a/src/ui/window.ts +++ b/src/ui/window.ts @@ -1,5 +1,12 @@ import BattleScene from "../battle-scene"; +const windowTypeControlColors = { + 0: [ '#706880', '#8888c8', '#484868' ], + 1: [ '#d04028', '#e0a028', '#902008' ], + 2: [ '#48b840', '#88d880', '#089040' ], + 3: [ '#2068d0', '#80b0e0', '#104888' ] +}; + export function addWindow(scene: BattleScene, x: number, y: number, width: number, height: number, mergeMaskTop?: boolean, mergeMaskLeft?: boolean, maskOffsetX?: number, maskOffsetY?: number): Phaser.GameObjects.NineSlice { const window = scene.add.nineslice(x, y, `window_${scene.windowType}`, null, width, height, 8, 8, 8, 8); window.setOrigin(0, 0); @@ -36,6 +43,9 @@ export function updateWindowType(scene: BattleScene, windowTypeIndex: integer): scene.windowType = windowTypeIndex; + const rootStyle = document.documentElement.style; + [ 'base', 'light', 'dark' ].map((k, i) => rootStyle.setProperty(`--color-${k}`, windowTypeControlColors[windowTypeIndex - 1][i])); + const windowKey = `window_${windowTypeIndex}`; for (let window of windowObjects)