Add Xbox/gamepad support via Gamepad API — left stick and D-pad to walk, A to confirm/talk, B to advance
This commit is contained in:
@@ -256,9 +256,11 @@ class Game {
|
|||||||
this._walkLoop = null;
|
this._walkLoop = null;
|
||||||
this._introClick = null;
|
this._introClick = null;
|
||||||
this._introKey = null;
|
this._introKey = null;
|
||||||
|
this._gpLoop = null;
|
||||||
|
|
||||||
this._bindController();
|
this._bindController();
|
||||||
this._bindGlobalKeys();
|
this._bindGlobalKeys();
|
||||||
|
this._bindGamepad();
|
||||||
this.boot();
|
this.boot();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -583,8 +585,113 @@ class Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ══════════════════════════════════════════════════════════
|
/* ══════════════════════════════════════════════════════════
|
||||||
CONTROLLER VISIBILITY
|
XBOX / GAMEPAD SUPPORT (Gamepad API)
|
||||||
|
Polls every animation frame. Works with Xbox, PS, and most
|
||||||
|
USB/Bluetooth gamepads that the browser recognises.
|
||||||
|
|
||||||
|
Xbox mapping:
|
||||||
|
Axes 0/1 = left stick X/Y
|
||||||
|
Button 0 = A (confirm / talk)
|
||||||
|
Button 1 = B (talk / back)
|
||||||
|
Button 12 = D-pad up
|
||||||
|
Button 13 = D-pad down
|
||||||
|
Button 14 = D-pad left
|
||||||
|
Button 15 = D-pad right
|
||||||
|
Button 9 = Start (confirm)
|
||||||
══════════════════════════════════════════════════════════ */
|
══════════════════════════════════════════════════════════ */
|
||||||
|
_bindGamepad() {
|
||||||
|
/* Track previous button state to detect press edges */
|
||||||
|
this._gpPrev = {};
|
||||||
|
this._gpActive = false;
|
||||||
|
|
||||||
|
window.addEventListener('gamepadconnected', (e) => {
|
||||||
|
console.log('Gamepad connected:', e.gamepad.id);
|
||||||
|
this._gpActive = true;
|
||||||
|
if (!this._gpLoop) this._startGpLoop();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('gamepaddisconnected', () => {
|
||||||
|
const gamepads = navigator.getGamepads ? navigator.getGamepads() : [];
|
||||||
|
this._gpActive = [...gamepads].some(g => g);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_startGpLoop() {
|
||||||
|
const poll = () => {
|
||||||
|
this._gpLoop = requestAnimationFrame(poll);
|
||||||
|
this._pollGamepad();
|
||||||
|
};
|
||||||
|
this._gpLoop = requestAnimationFrame(poll);
|
||||||
|
}
|
||||||
|
|
||||||
|
_pollGamepad() {
|
||||||
|
const gamepads = navigator.getGamepads ? navigator.getGamepads() : [];
|
||||||
|
const gp = [...gamepads].find(g => g);
|
||||||
|
if (!gp) return;
|
||||||
|
|
||||||
|
const DEAD = 0.25; /* analog stick deadzone */
|
||||||
|
const axes = gp.axes;
|
||||||
|
const btns = gp.buttons;
|
||||||
|
|
||||||
|
/* ── Directional input ─────────────────────────────── */
|
||||||
|
const gpDirs = {
|
||||||
|
up: (axes[1] < -DEAD) || (btns[12]?.pressed),
|
||||||
|
down: (axes[1] > DEAD) || (btns[13]?.pressed),
|
||||||
|
left: (axes[0] < -DEAD) || (btns[14]?.pressed),
|
||||||
|
right: (axes[0] > DEAD) || (btns[15]?.pressed),
|
||||||
|
};
|
||||||
|
|
||||||
|
const s = this.state.screen;
|
||||||
|
|
||||||
|
Object.entries(gpDirs).forEach(([dir, active]) => {
|
||||||
|
const key = `axis_${dir}`;
|
||||||
|
if (active && !this._gpPrev[key]) {
|
||||||
|
/* Pressed this frame */
|
||||||
|
if (s === 'map' || s === 'battle') {
|
||||||
|
this._heldKeys.add(dir);
|
||||||
|
if (s === 'map' && !this._walkLoop) this._startWalkLoop();
|
||||||
|
if (s === 'battle') this._moveCursor(dir);
|
||||||
|
}
|
||||||
|
} else if (!active && this._gpPrev[key]) {
|
||||||
|
/* Released this frame */
|
||||||
|
this._heldKeys.delete(dir);
|
||||||
|
if (this._heldKeys.size === 0) this._stopWalkLoop();
|
||||||
|
}
|
||||||
|
this._gpPrev[key] = active;
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── Button press edges (fire once on press) ───────── */
|
||||||
|
const pressedNow = (i) => btns[i]?.pressed;
|
||||||
|
const wasPressed = (i) => !!this._gpPrev[`btn_${i}`];
|
||||||
|
const justPressed = (i) => pressedNow(i) && !wasPressed(i);
|
||||||
|
|
||||||
|
/* A button (0) or Start (9) = confirm / talk */
|
||||||
|
if (justPressed(0) || justPressed(9)) {
|
||||||
|
this._pressA();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* B button (1) = back / talk */
|
||||||
|
if (justPressed(1)) {
|
||||||
|
this._pressB();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Quick-pick answers with X=2, Y=3, LB=4, RB=5 on battle screen */
|
||||||
|
if (s === 'battle' && !this.state.answering) {
|
||||||
|
const quickMap = { 2:0, 3:1, 4:2, 5:3 };
|
||||||
|
Object.entries(quickMap).forEach(([btn, idx]) => {
|
||||||
|
if (justPressed(+btn)) {
|
||||||
|
this.state.cursor = idx;
|
||||||
|
this._renderCursor();
|
||||||
|
setTimeout(() => this._confirmCursor(), 80);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Store current button states for next frame */
|
||||||
|
for (let i = 0; i < btns.length; i++) {
|
||||||
|
this._gpPrev[`btn_${i}`] = btns[i]?.pressed;
|
||||||
|
}
|
||||||
|
}
|
||||||
_showController(visible) {
|
_showController(visible) {
|
||||||
const ctrl = document.getElementById('gba-controller');
|
const ctrl = document.getElementById('gba-controller');
|
||||||
if (!ctrl) return;
|
if (!ctrl) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user