Add touch controls for mobile support
This commit is contained in:
parent
de29ea9c05
commit
41d1a84c76
|
@ -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;
|
||||
}
|
60
index.html
60
index.html
|
@ -7,32 +7,50 @@
|
|||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Pokemon Rogue Battle</title>
|
||||
<style type="text/css">
|
||||
@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');
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: #484050;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" type="text/css" href="index.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="touchControls">
|
||||
<div id="dpad" class="unselectable">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72">
|
||||
<path id="dpadUp" data-key="UP" d="M48,5.8C48,2.5,45.4,0,42,0H29.9C26.6,0,24,2.4,24,5.8V24h24V5.8z" />
|
||||
<path id="dpadRight" data-key="RIGHT" d="M66.2,24H48v24h18.2c3.3,0,5.8-2.7,5.8-6V29.9C72,26.5,69.5,24,66.2,24z" />
|
||||
<path id="dpadDown" data-key="DOWN" d="M24,66.3c0,3.3,2.6,5.7,5.9,5.7H42c3.3,0,6-2.4,6-5.7V48H24V66.3z" />
|
||||
<path id="dpadLeft" data-key="LEFT" d="M5.7,24C2.4,24,0,26.5,0,29.9V42c0,3.3,2.3,6,5.7,6H24V24H5.7z" />
|
||||
<rect id="dpadCenter" x="24" y="24" width="24" height="24" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div id="apad" class="unselectable">
|
||||
<div id="apadAction" class="apadCircBtn apadBtn" data-key="ACTION">
|
||||
<text id="apadLabelAction" class="apadLabel">A</text>
|
||||
</div>
|
||||
<div id="apadCancel" class="apadCircBtn apadBtn" data-key="CANCEL">
|
||||
<text id="apadLabelCancel" class="apadLabel">B</text>
|
||||
</div>
|
||||
<div id="apadMenu" class="apadRectBtn apadBtn" data-key="MENU">
|
||||
<text id="apadLabelMenu" class="apadLabel apadLabelSmall">Menu</text>
|
||||
</div>
|
||||
<div class="apadBtnContainer">
|
||||
<div id="apadCycleShiny" class="apadSqBtn apadBtn" data-key="CYCLE_SHINY">
|
||||
<text class="apadLabel apadLabelSmall">R</text>
|
||||
</div>
|
||||
<div id="apadCycleForm" class="apadSqBtn apadBtn" data-key="CYCLE_FORM">
|
||||
<text class="apadLabel apadLabelSmall">F</text>
|
||||
</div>
|
||||
<div id="apadCycleGender" class="apadSqBtn apadBtn" data-key="CYCLE_GENDER">
|
||||
<text class="apadLabel apadLabelSmall">G</text>
|
||||
</div>
|
||||
<div id="apadCycleAbility" class="apadSqBtn apadBtn" data-key="CYCLE_ABILITY">
|
||||
<text class="apadLabel apadLabelSmall">E</text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="src/main.ts"></script>
|
||||
<script src="src/touch-controls.js"></script>
|
||||
<script src="src/debug.js"></script>
|
||||
</body>
|
||||
|
||||
|
|
|
@ -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[] {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue