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>
This commit is contained in:
parent
f62d83f488
commit
4f37adb4bd
13
README.md
13
README.md
@ -26,6 +26,19 @@ source of truth for the visual mesh *and* the collision, so what you see is what
|
|||||||
| **3 BLADES** | same park, one catch: **locals throw rubbish at you.** Tradition. Dodge the cans or eat pavement to a chorus of "FRUIT BOOTS!!" |
|
| **3 BLADES** | same park, one catch: **locals throw rubbish at you.** Tradition. Dodge the cans or eat pavement to a chorus of "FRUIT BOOTS!!" |
|
||||||
|
|
||||||
## Controls
|
## Controls
|
||||||
|
|
||||||
|
**Controller** (PS5 DualSense over Bluetooth, or any standard gamepad — press
|
||||||
|
any button after connecting so the browser picks it up):
|
||||||
|
```
|
||||||
|
left stick / dpad steer · up = push + up-variants · down = brake/varials
|
||||||
|
R2 push ✕ ollie / bunnyhop / hop off a grind
|
||||||
|
◻ kickflip|barspin ○ heelflip|tailwhip △ pop shove|x-up
|
||||||
|
R1 tre flip L1 fs shove L2 hardflip
|
||||||
|
OPTIONS start / run it back CREATE reset
|
||||||
|
```
|
||||||
|
Rumble on bails and bonks. On the title screen: dpad picks your discipline, ✕ drops in.
|
||||||
|
|
||||||
|
**Keyboard**
|
||||||
```
|
```
|
||||||
W / ↑ push SPACE ollie / bunnyhop / hop off a grind
|
W / ↑ push SPACE ollie / bunnyhop / hop off a grind
|
||||||
A D / ← → steer S / ↓ brake
|
A D / ← → steer S / ↓ brake
|
||||||
|
|||||||
@ -61,7 +61,7 @@
|
|||||||
land on rails and ledges to grind · hit the gaps · collect B·O·O·K·Q·U·O·Y<br>
|
land on rails and ledges to grind · hit the gaps · collect B·O·O·K·Q·U·O·Y<br>
|
||||||
R reset to the tennis-court entrance
|
R reset to the tennis-court entrance
|
||||||
</div>
|
</div>
|
||||||
<div class="go">press ENTER to drop in</div>
|
<div class="go">press ENTER (or ✕ on a controller) to drop in</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="over" class="panel">
|
<div id="over" class="panel">
|
||||||
|
|||||||
16
js/main.js
16
js/main.js
@ -7,6 +7,7 @@ import { buildLevel, GAPS, SPAWN, heightAt, PROPS, RAILS,
|
|||||||
import { KITS, RUBBISH_INSULTS, GAP_SCORE, LETTER_SCORE, ALL_LETTERS_SCORE } from './tricks.js';
|
import { KITS, RUBBISH_INSULTS, GAP_SCORE, LETTER_SCORE, ALL_LETTERS_SCORE } from './tricks.js';
|
||||||
import { Player } from './player.js';
|
import { Player } from './player.js';
|
||||||
import { Hud } from './hud.js';
|
import { Hud } from './hud.js';
|
||||||
|
import { Pad } from './pad.js';
|
||||||
|
|
||||||
const SESH_SECONDS = 120;
|
const SESH_SECONDS = 120;
|
||||||
const GRAV = 18;
|
const GRAV = 18;
|
||||||
@ -163,8 +164,9 @@ function setDiscipline(name) {
|
|||||||
onTrick: t => { if (mode === 'run') hud.addTrick(t.name, t.score); },
|
onTrick: t => { if (mode === 'run') hud.addTrick(t.name, t.score); },
|
||||||
onGrindTick: pts => { if (mode === 'run') hud.addGrindPoints(pts); },
|
onGrindTick: pts => { if (mode === 'run') hud.addGrindPoints(pts); },
|
||||||
onLand: () => { airGapsHit.clear(); landTimer = 0.9; },
|
onLand: () => { airGapsHit.clear(); landTimer = 0.9; },
|
||||||
onBail: () => { airGapsHit.clear(); hud.dropCombo(); },
|
onBail: () => { airGapsHit.clear(); hud.dropCombo(); pad.rumble(0.9, 0.5, 220); },
|
||||||
onBonk: c => {
|
onBonk: c => {
|
||||||
|
pad.rumble(1.0, 0.6, 260);
|
||||||
const what = (c.name || 'that').split('_')[0].toUpperCase();
|
const what = (c.name || 'that').split('_')[0].toUpperCase();
|
||||||
const lines = ['BONK!!', `STRAIGHT INTO THE ${what}`, `THE ${what} WON`, 'EYES UP!!'];
|
const lines = ['BONK!!', `STRAIGHT INTO THE ${what}`, `THE ${what} WON`, 'EYES UP!!'];
|
||||||
hud.flash(lines[(Math.random() * lines.length) | 0], '#ffb02e');
|
hud.flash(lines[(Math.random() * lines.length) | 0], '#ffb02e');
|
||||||
@ -178,12 +180,20 @@ function setDiscipline(name) {
|
|||||||
|
|
||||||
// ---------------------------------------------------------------- input
|
// ---------------------------------------------------------------- input
|
||||||
const keys = new Set();
|
const keys = new Set();
|
||||||
|
const pad = new Pad(id =>
|
||||||
|
hud.flash('CONTROLLER CONNECTED', '#7ee38a'));
|
||||||
|
|
||||||
addEventListener('keydown', e => {
|
addEventListener('keydown', e => {
|
||||||
if (mode !== 'run') {
|
if (mode !== 'run') {
|
||||||
if (e.code === 'Digit1') return setDiscipline('skate');
|
if (e.code === 'Digit1') return setDiscipline('skate');
|
||||||
if (e.code === 'Digit2') return setDiscipline('bmx');
|
if (e.code === 'Digit2') return setDiscipline('bmx');
|
||||||
if (e.code === 'Digit3') return setDiscipline('blades');
|
if (e.code === 'Digit3') return setDiscipline('blades');
|
||||||
if (e.code === 'Enter') startSesh();
|
if (e.code === 'ArrowLeft' || e.code === 'ArrowRight') { // pad dpad / stick
|
||||||
|
const order = ['skate', 'bmx', 'blades'];
|
||||||
|
const i = order.indexOf(discipline);
|
||||||
|
return setDiscipline(order[(i + (e.code === 'ArrowRight' ? 1 : 2)) % 3]);
|
||||||
|
}
|
||||||
|
if (e.code === 'Enter' || e.code === 'Space') startSesh(); // CROSS drops in too
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code === 'KeyR') { player.reset(SPAWN); return; }
|
if (e.code === 'KeyR') { player.reset(SPAWN); return; }
|
||||||
@ -321,6 +331,7 @@ function updateCamera(dt) {
|
|||||||
let last = performance.now(), perfNow = 0;
|
let last = performance.now(), perfNow = 0;
|
||||||
function frame(now) {
|
function frame(now) {
|
||||||
requestAnimationFrame(frame);
|
requestAnimationFrame(frame);
|
||||||
|
pad.poll(); // controller -> synthetic key events
|
||||||
const dt = Math.min((now - last) / 1000, 0.05); last = now;
|
const dt = Math.min((now - last) / 1000, 0.05); last = now;
|
||||||
perfNow = now / 1000;
|
perfNow = now / 1000;
|
||||||
if (player) {
|
if (player) {
|
||||||
@ -346,6 +357,7 @@ window.__step = (seconds = 1) => {
|
|||||||
const n = Math.round(seconds * 60);
|
const n = Math.round(seconds * 60);
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
const dt = 1 / 60; perfNow += dt;
|
const dt = 1 / 60; perfNow += dt;
|
||||||
|
pad.poll();
|
||||||
if (player) {
|
if (player) {
|
||||||
if (mode === 'run') { player.update(dt, keys); seshUpdate(dt); updateRubbish(dt, perfNow); }
|
if (mode === 'run') { player.update(dt, keys); seshUpdate(dt); updateRubbish(dt, perfNow); }
|
||||||
else { player.mixer.update(dt); }
|
else { player.mixer.update(dt); }
|
||||||
|
|||||||
76
js/pad.js
Normal file
76
js/pad.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
// 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user