| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| const PAD_DEADZONE = 0.15; |
| const PAD_ALPHA = 0.12; |
| const dz = (v) => (Math.abs(v) < PAD_DEADZONE ? 0 : v); |
|
|
| |
| const BTN_X = 2, BTN_Y = 3, BTN_LB = 4, BTN_RB = 5, BTN_LT = 6, BTN_RT = 7; |
| const BTN_R3 = 11, BTN_DPAD_UP = 12, BTN_DPAD_DOWN = 13; |
|
|
| const DPAD_UP_HOLD_MS = 1000; |
|
|
| export class GamepadSource { |
| id = "gamepad"; |
| connected = false; |
| command = new Float32Array(3); |
| axes = { jaw: 0, orbitX: 0, orbitY: 0 }; |
| pressed = { |
| x: false, y: false, rb: false, lb: false, r3: false, |
| dpadDown: false, dpadUp: false, |
| }; |
| onAction = () => {}; |
|
|
| #getVelocityLimits; |
| #active = false; |
| #dpadUpAt = 0; |
| #dpadUpFired = false; |
| #rtArmed = true; |
|
|
| constructor({ getVelocityLimits }) { |
| this.#getVelocityLimits = getVelocityLimits; |
| } |
|
|
| init() {} |
| dispose() {} |
|
|
| isActive() { |
| return this.#active; |
| } |
|
|
| poll() { |
| const prev = this.pressed; |
| const gp = [...(navigator.getGamepads?.() ?? [])].find((p) => p && p.connected); |
| this.connected = !!gp; |
| if (!gp) { |
| if (this.#active) { |
| this.#active = false; |
| this.command.fill(0); |
| this.axes.jaw = 0; |
| } |
| this.axes.orbitX = 0; |
| this.axes.orbitY = 0; |
| return; |
| } |
| const now = performance.now(); |
|
|
| |
| |
| const lx = dz(gp.axes[0] ?? 0), ly = dz(gp.axes[1] ?? 0); |
| const up = -ly; |
| const [limF, limB, limA] = this.#getVelocityLimits(); |
| const target = [ |
| up >= 0 ? up * limF : up * -limB, |
| 0, |
| -lx * limA, |
| ]; |
| for (let i = 0; i < 3; i++) this.command[i] += PAD_ALPHA * (target[i] - this.command[i]); |
| |
| |
| |
| const stickInput = lx !== 0 || ly !== 0; |
| if (stickInput) this.#active = true; |
| else if ( |
| this.#active && |
| Math.abs(this.command[0]) + Math.abs(this.command[1]) + Math.abs(this.command[2]) < 0.01 |
| ) { |
| this.#active = false; |
| this.command.fill(0); |
| } |
|
|
| |
| |
| |
| this.axes.orbitX = dz(gp.axes[2] ?? 0); |
| this.axes.orbitY = dz(gp.axes[3] ?? 0); |
|
|
| |
| const r3 = !!gp.buttons[BTN_R3]?.pressed; |
| if (r3 && !prev.r3) this.onAction("chaseToggle"); |
| prev.r3 = r3; |
|
|
| const x = !!gp.buttons[BTN_X]?.pressed; |
| if (x && !prev.x) this.onAction("roll"); |
| prev.x = x; |
|
|
| const y = !!gp.buttons[BTN_Y]?.pressed; |
| if (y && !prev.y) this.onAction("spawnBall"); |
| prev.y = y; |
|
|
| const rb = !!gp.buttons[BTN_RB]?.pressed; |
| if (rb && !prev.rb) this.onAction("kickR"); |
| prev.rb = rb; |
| const lb = !!gp.buttons[BTN_LB]?.pressed; |
| if (lb && !prev.lb) this.onAction("kickL"); |
| prev.lb = lb; |
|
|
| const dpadDown = !!gp.buttons[BTN_DPAD_DOWN]?.pressed; |
| if (dpadDown && !prev.dpadDown) this.onAction("sitToggle"); |
| prev.dpadDown = dpadDown; |
|
|
| |
| const dpadUp = !!gp.buttons[BTN_DPAD_UP]?.pressed; |
| if (dpadUp && !prev.dpadUp) { |
| this.#dpadUpAt = now; |
| this.#dpadUpFired = false; |
| this.onAction("walk"); |
| } |
| if (dpadUp && !this.#dpadUpFired && now - this.#dpadUpAt >= DPAD_UP_HOLD_MS) { |
| this.#dpadUpFired = true; |
| this.onAction("locoToggle"); |
| } |
| prev.dpadUp = dpadUp; |
|
|
| |
| |
| const rt = gp.buttons[BTN_RT]?.value ?? 0; |
| const lt = gp.buttons[BTN_LT]?.value ?? 0; |
| this.axes.jaw = Math.max(rt, lt); |
| if (this.#rtArmed && rt >= 0.35) { |
| this.onAction("quack"); |
| this.#rtArmed = false; |
| } else if (!this.#rtArmed && rt < 0.2) { |
| this.#rtArmed = true; |
| } |
| } |
| } |
|
|