81 lines
4.7 KiB
JavaScript
81 lines
4.7 KiB
JavaScript
/* Touch controls for phones and tablets: an arrow pad on the left, a MOBA-style action cluster on the right.
|
|
They feed the same key handlers as the keyboard (window.__game.input), so every rule (double-tap run, double jump,
|
|
combo queue, cooldowns) behaves exactly like WASD + Space/I/O/P. Only active when <html> has the .touch class. */
|
|
(() => {
|
|
'use strict';
|
|
if (!document.documentElement.classList.contains('touch')) return;
|
|
const root = document.getElementById('touch-controls');
|
|
if (!root) return;
|
|
const game = () => window.__game;
|
|
const capture = (el, id) => { try { el.setPointerCapture(id); } catch (_) { /* capture is a nicety; input still works without it */ } };
|
|
const input = (key, down) => game()?.input?.(key, down);
|
|
|
|
// Inside the mobile shell the first touch asks the shell to go fullscreen and lock landscape (Android).
|
|
if (window.parent !== window) {
|
|
let sent = false;
|
|
addEventListener('pointerdown', () => { if (sent) return; sent = true; try { if (window.parent.aetherActivate) { window.parent.aetherActivate(); return; } } catch (_) {} window.parent.postMessage({ type: 'aether-activate' }, location.origin); }, { capture: true });
|
|
}
|
|
// No long-press menus, selection or double-tap zoom anywhere on the game.
|
|
addEventListener('contextmenu', e => e.preventDefault());
|
|
|
|
// Arrow pad: left / down / right in a row, up above down. Each finger holds the arrow under it and can slide to a
|
|
// neighbour without lifting (left <-> right to turn, down -> up to jump). Up is W (jump; again in the air = double jump).
|
|
const pad = root.querySelector('.dpad');
|
|
const fingers = new Map(); // pointerId -> key
|
|
function refresh() {
|
|
const down = new Set(fingers.values());
|
|
for (const arrow of pad.querySelectorAll('.dbtn')) arrow.classList.toggle('pressed', down.has(arrow.dataset.key));
|
|
}
|
|
function setFinger(id, key) {
|
|
const old = fingers.get(id);
|
|
if (old === key) return;
|
|
if (old) { fingers.delete(id); if (![...fingers.values()].includes(old)) input(old, false); }
|
|
if (key) { const already = [...fingers.values()].includes(key); fingers.set(id, key); if (!already) input(key, true); }
|
|
refresh();
|
|
}
|
|
pad.addEventListener('pointerdown', e => {
|
|
const arrow = e.target.closest?.('.dbtn');
|
|
if (!arrow) return;
|
|
e.preventDefault(); capture(pad, e.pointerId); setFinger(e.pointerId, arrow.dataset.key);
|
|
});
|
|
pad.addEventListener('pointermove', e => {
|
|
if (!fingers.has(e.pointerId)) return;
|
|
const arrow = document.elementFromPoint(e.clientX, e.clientY)?.closest?.('.dbtn');
|
|
if (arrow && pad.contains(arrow)) setFinger(e.pointerId, arrow.dataset.key);
|
|
});
|
|
const lift = e => { if (fingers.has(e.pointerId)) setFinger(e.pointerId, null); };
|
|
for (const type of ['pointerup', 'pointercancel', 'lostpointercapture']) pad.addEventListener(type, lift);
|
|
|
|
// Action buttons: big basic attack, three small skills around it. Each finger holds its own button.
|
|
const buttons = [...root.querySelectorAll('.tbtn')].map(el => ({ el, key: el.dataset.key, slot: el.dataset.slot, img: el.querySelector('img'), wipe: el.querySelector('.cooldown-wipe'), num: el.querySelector('.cooldown-num'), pointer: null }));
|
|
for (const b of buttons) {
|
|
b.el.addEventListener('pointerdown', e => {
|
|
e.preventDefault();
|
|
if (b.pointer !== null) return;
|
|
b.pointer = e.pointerId; capture(b.el, e.pointerId); b.el.classList.add('pressed'); input(b.key, true);
|
|
});
|
|
const end = e => { if (e.pointerId !== b.pointer) return; b.pointer = null; b.el.classList.remove('pressed'); input(b.key, false); };
|
|
for (const type of ['pointerup', 'pointercancel', 'lostpointercapture']) b.el.addEventListener(type, end);
|
|
}
|
|
|
|
// Mirror the HUD skill cards (icon, cooldown wipe, active/recharged state) onto the round buttons every frame.
|
|
function sync() {
|
|
const g = game();
|
|
// The game clears its keys when a menu opens; forget the held arrows too.
|
|
if (document.body.classList.contains('in-menu') && fingers.size) { fingers.clear(); refresh(); }
|
|
for (const b of buttons) {
|
|
const card = document.querySelector(`.skill-card[data-action="${b.slot}"]`);
|
|
if (card) {
|
|
const src = card.querySelector('img')?.getAttribute('src');
|
|
if (src && b.img.getAttribute('src') !== src) b.img.setAttribute('src', src);
|
|
for (const cls of ['on-cooldown', 'active', 'recharged']) b.el.classList.toggle(cls, card.classList.contains(cls));
|
|
const wipe = card.querySelector('.cooldown-wipe');
|
|
if (wipe && b.wipe) b.wipe.style.background = wipe.style.background;
|
|
}
|
|
if (b.num) { const cd = g?.hero?.cooldowns?.[b.slot] || 0; b.num.textContent = cd > 0 ? String(Math.ceil(cd)) : ''; }
|
|
}
|
|
requestAnimationFrame(sync);
|
|
}
|
|
requestAnimationFrame(sync);
|
|
})();
|