bookquoy/js/pad.js
type-two 4f37adb4bd PS5 controller support: DualSense over Bluetooth via Gamepad API
js/pad.js synthesizes the same KeyboardEvents the keyboard produces —
one input path, zero new handler code. Stick/dpad steer (+variants),
R2 push, CROSS ollie, SQUARE/CIRCLE/TRIANGLE flip/heel/shove, R1 tre,
L1 fs shove, L2 hardflip, OPTIONS start, CREATE reset. Dual-rumble on
bails and bonks. Title screen: dpad cycles discipline, CROSS drops in
(Space now works there for keyboards too). Verified with a stubbed
DualSense through __step: push 7.6m/s, steer, ollie->kickflip x2 (220).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 22:26:34 +10:00

77 lines
2.8 KiB
JavaScript

// BOOKQUOY — controller support (PS5 DualSense over Bluetooth, or anything
// exposing the standard Gamepad mapping). Zero new input paths: the pad
// synthesizes the SAME KeyboardEvents the keyboard produces, so every trick,
// variant, and menu action flows through the one proven handler in main.js.
//
// left stick / dpad steer · up = push + up-variants · down = brake/varials
// R2 push
// CROSS ollie / bunnyhop / hop off a grind (Space)
// SQUARE kickflip | barspin (J)
// CIRCLE heelflip | tailwhip (K)
// TRIANGLE pop shove | x-up (L)
// R1 tre flip · L1 fs shove · L2 hardflip
// OPTIONS start / run it back (Enter) · CREATE reset (R)
const DEAD = 0.3;
const BTN = { 0: 'Space', 1: 'KeyK', 2: 'KeyJ', 3: 'KeyL', 4: 'KeyF', 5: 'KeyT',
6: 'KeyH', 8: 'KeyR', 9: 'Enter', 12: 'ArrowUp', 13: 'ArrowDown',
14: 'ArrowLeft', 15: 'ArrowRight' };
export class Pad {
constructor(onConnect) {
this.held = new Set();
this.index = null;
addEventListener('gamepadconnected', e => {
this.index = e.gamepad.index;
if (onConnect) onConnect(e.gamepad.id);
});
addEventListener('gamepaddisconnected', e => {
if (e.gamepad.index === this.index) this.index = null;
this._sync(new Set()); // release everything, no stuck keys
});
}
_gp() {
const pads = navigator.getGamepads ? navigator.getGamepads() : [];
if (this.index !== null && pads[this.index]) return pads[this.index];
for (const p of pads) if (p) { this.index = p.index; return p; }
return null;
}
poll() {
const gp = this._gp();
if (!gp) return;
const want = new Set();
const ax = gp.axes[0] || 0, ay = gp.axes[1] || 0;
if (ax < -DEAD) want.add('ArrowLeft');
if (ax > DEAD) want.add('ArrowRight');
if (ay < -DEAD) want.add('ArrowUp');
if (ay > DEAD) want.add('ArrowDown');
if ((gp.buttons[7]?.value || 0) > 0.15) want.add('KeyW'); // R2 push
for (const [i, code] of Object.entries(BTN)) {
const b = gp.buttons[+i];
if (b && (b.pressed || b.value > 0.5)) want.add(code);
}
this._sync(want);
}
_sync(want) {
for (const code of want)
if (!this.held.has(code)) {
this.held.add(code);
dispatchEvent(new KeyboardEvent('keydown', { code }));
}
for (const code of [...this.held])
if (!want.has(code)) {
this.held.delete(code);
dispatchEvent(new KeyboardEvent('keyup', { code }));
}
}
rumble(strong = 0.8, weak = 0.4, ms = 180) {
const gp = this._gp();
gp?.vibrationActuator?.playEffect?.('dual-rumble',
{ duration: ms, strongMagnitude: strong, weakMagnitude: weak });
}
}