Gamepad support: analog stick + d-pad move, A/B jump, Start respawn, connect toast
Left stick is radial-deadzone analog (magnitude preserved — gentle stick = gentle roll) feeding the same camera-relative move vector as WASD; jump goes through the buffered-edge path as Space. Verified headless with a stubbed navigator.getGamepads: full/half-stick speeds, single edge jump, respawn. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
a5f48b9405
commit
a966e65bde
@ -43,6 +43,42 @@ export function createBlobController(
|
||||
addEventListener('keyup', (e) => keys.delete(e.code))
|
||||
const held = (...codes: string[]) => codes.some((c) => keys.has(c))
|
||||
|
||||
// ---- gamepad (poll API — read fresh each fixed step) ----
|
||||
// Left stick = analog camera-relative move (radial deadzone, magnitude kept
|
||||
// so gentle stick = gentle roll), d-pad = digital, A/Cross or B/Circle = jump
|
||||
// through the same buffered-edge path as Space.
|
||||
const pad = { x: 0, z: 0 }
|
||||
let padJumpHeld = false
|
||||
function pollGamepad(): void {
|
||||
pad.x = 0
|
||||
pad.z = 0
|
||||
let jump = false
|
||||
const pads = typeof navigator !== 'undefined' && navigator.getGamepads
|
||||
? navigator.getGamepads() : null
|
||||
if (pads) {
|
||||
for (const p of pads) {
|
||||
if (!p || !p.connected) continue
|
||||
const dz = 0.15
|
||||
const ax = p.axes[0] ?? 0
|
||||
const ay = p.axes[1] ?? 0
|
||||
const mag = Math.hypot(ax, ay)
|
||||
if (mag > dz) {
|
||||
const scale = Math.min(1, (mag - dz) / (1 - dz)) / mag
|
||||
pad.x = ax * scale
|
||||
pad.z = ay * scale
|
||||
}
|
||||
if (p.buttons[14]?.pressed) pad.x = -1 // d-pad left
|
||||
if (p.buttons[15]?.pressed) pad.x = 1 // d-pad right
|
||||
if (p.buttons[12]?.pressed) pad.z = -1 // d-pad up
|
||||
if (p.buttons[13]?.pressed) pad.z = 1 // d-pad down
|
||||
jump = !!(p.buttons[0]?.pressed || p.buttons[1]?.pressed)
|
||||
break // first connected pad wins
|
||||
}
|
||||
}
|
||||
if (jump && !padJumpHeld) jumpBuffer = 0.12
|
||||
padJumpHeld = jump
|
||||
}
|
||||
|
||||
// ---- state ----
|
||||
let grounded = false
|
||||
let coyote = 0 // seconds of "still counts as grounded" after leaving ground
|
||||
@ -103,13 +139,17 @@ export function createBlobController(
|
||||
fwd.normalize()
|
||||
right.set(-fwd.z, 0, fwd.x) // forward × up
|
||||
|
||||
pollGamepad()
|
||||
move.set(0, 0, 0)
|
||||
if (held('KeyW', 'ArrowUp')) move.add(fwd)
|
||||
if (held('KeyS', 'ArrowDown')) move.sub(fwd)
|
||||
if (held('KeyD', 'ArrowRight')) move.add(right)
|
||||
if (held('KeyA', 'ArrowLeft')) move.sub(right)
|
||||
if (pad.x !== 0 || pad.z !== 0) {
|
||||
move.addScaledVector(fwd, -pad.z).addScaledVector(right, pad.x) // stick up = away from camera
|
||||
}
|
||||
const hasInput = move.lengthSq() > 1e-6
|
||||
if (hasInput) move.normalize()
|
||||
if (move.lengthSq() > 1) move.normalize() // keep sub-unit analog magnitudes
|
||||
|
||||
const v = body.linvel()
|
||||
const mass = body.mass()
|
||||
|
||||
26
src/game.ts
26
src/game.ts
@ -110,6 +110,32 @@ export function installGame(world: World) {
|
||||
if (e.code === 'KeyC') world.events.emit('paint:request-cleanse', { fraction: 0.5 })
|
||||
})
|
||||
|
||||
// ---- gamepad niceties (movement/jump live in the blob controller) ----
|
||||
let startHeld = false
|
||||
world.addSystem({
|
||||
update() {
|
||||
const pads = navigator.getGamepads?.()
|
||||
if (!pads) return
|
||||
for (const p of pads) {
|
||||
if (!p || !p.connected) continue
|
||||
const s = !!p.buttons[9]?.pressed // Start/Options = respawn
|
||||
if (s && !startHeld) respawn()
|
||||
startHeld = s
|
||||
break
|
||||
}
|
||||
},
|
||||
})
|
||||
addEventListener('gamepadconnected', (e) => {
|
||||
const toast = document.createElement('div')
|
||||
toast.textContent = `🎮 ${(e as GamepadEvent).gamepad.id.slice(0, 40)} connected — stick to roll, A to jump, Start to respawn`
|
||||
toast.style.cssText =
|
||||
'position:fixed;bottom:16px;left:50%;transform:translateX(-50%);' +
|
||||
'font:13px ui-monospace,Menlo,monospace;color:#fff;background:rgba(20,24,34,.88);' +
|
||||
'padding:9px 14px;border-radius:9px;z-index:20;pointer-events:none'
|
||||
document.body.appendChild(toast)
|
||||
setTimeout(() => toast.remove(), 3200)
|
||||
})
|
||||
|
||||
// debug handle (dev builds are the only builds right now)
|
||||
;(window as unknown as { BLOBBO: unknown }).BLOBBO = { world, blob, skin, course }
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user