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
+
+
+
+
+ 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)