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))
|
addEventListener('keyup', (e) => keys.delete(e.code))
|
||||||
const held = (...codes: string[]) => codes.some((c) => keys.has(c))
|
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 ----
|
// ---- state ----
|
||||||
let grounded = false
|
let grounded = false
|
||||||
let coyote = 0 // seconds of "still counts as grounded" after leaving ground
|
let coyote = 0 // seconds of "still counts as grounded" after leaving ground
|
||||||
@ -103,13 +139,17 @@ export function createBlobController(
|
|||||||
fwd.normalize()
|
fwd.normalize()
|
||||||
right.set(-fwd.z, 0, fwd.x) // forward × up
|
right.set(-fwd.z, 0, fwd.x) // forward × up
|
||||||
|
|
||||||
|
pollGamepad()
|
||||||
move.set(0, 0, 0)
|
move.set(0, 0, 0)
|
||||||
if (held('KeyW', 'ArrowUp')) move.add(fwd)
|
if (held('KeyW', 'ArrowUp')) move.add(fwd)
|
||||||
if (held('KeyS', 'ArrowDown')) move.sub(fwd)
|
if (held('KeyS', 'ArrowDown')) move.sub(fwd)
|
||||||
if (held('KeyD', 'ArrowRight')) move.add(right)
|
if (held('KeyD', 'ArrowRight')) move.add(right)
|
||||||
if (held('KeyA', 'ArrowLeft')) move.sub(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
|
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 v = body.linvel()
|
||||||
const mass = body.mass()
|
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 })
|
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)
|
// debug handle (dev builds are the only builds right now)
|
||||||
;(window as unknown as { BLOBBO: unknown }).BLOBBO = { world, blob, skin, course }
|
;(window as unknown as { BLOBBO: unknown }).BLOBBO = { world, blob, skin, course }
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user