diff --git a/src/game/AsteroidsGame.ts b/src/game/AsteroidsGame.ts index ef2e2c7..68c22ce 100644 --- a/src/game/AsteroidsGame.ts +++ b/src/game/AsteroidsGame.ts @@ -394,6 +394,7 @@ export class AsteroidsGame { } private updateFrame(timestampMs: number): void { + this.input.pollGamepad(); this.handleGlobalInput(); if (this.mode === "playing") { @@ -479,6 +480,10 @@ export class AsteroidsGame { if (this.mode === "menu" || this.mode === "game-over") { this.audio.enable(); this.startNewGame(); + } else if (this.mode === "playing") { + this.mode = "paused"; + this.pauseFromHidden = false; + this.audio.pauseMusic(); } else if (this.mode === "paused") { this.mode = "playing"; this.pauseFromHidden = false; diff --git a/src/game/input.ts b/src/game/input.ts index d65d8d5..b3e208e 100644 --- a/src/game/input.ts +++ b/src/game/input.ts @@ -36,9 +36,26 @@ function isEditableTarget(target: EventTarget | null): boolean { return false; } +// Xbox controller button index → key code mapping (Standard Gamepad API layout) +const GAMEPAD_BUTTON_MAP: Array<[number, string]> = [ + [1, "Escape"], // B → Return to menu + [6, "ArrowUp"], // LT → Ignition (thrust) + [7, "Space"], // RT → Fire + [9, "Enter"], // Start → Start / resume / pause + [12, "ArrowUp"], // D-Pad Up → Thrust + [14, "ArrowLeft"], // D-Pad Left → Turn left + [15, "ArrowRight"],// D-Pad Right → Turn right +]; + +// Thumbstick deadzone threshold +const GAMEPAD_AXIS_THRESHOLD = 0.5; + export class InputController { private readonly down = new Set(); private readonly pressed = new Set(); + // Tracks which codes are currently held via gamepad (kept separate from + // keyboard state so releasing one input source never clears the other). + private readonly gamepadDown = new Set(); handleKeyDown(event: KeyboardEvent): void { if (!GAME_KEYS.has(event.code)) { @@ -74,8 +91,45 @@ export class InputController { this.down.delete(event.code); } + /** + * Poll all connected gamepads and synthesize key presses/releases. + * Must be called once at the start of each animation frame. + */ + pollGamepad(): void { + if (typeof navigator === "undefined" || !navigator.getGamepads) return; + + const gamepads = navigator.getGamepads(); + const newDown = new Set(); + + for (const pad of gamepads) { + if (!pad?.connected) continue; + + for (const [idx, code] of GAMEPAD_BUTTON_MAP) { + if (pad.buttons[idx]?.pressed) newDown.add(code); + } + + // Left thumbstick axes (index 0 = X, index 1 = Y) + const lx = pad.axes[0] ?? 0; + const ly = pad.axes[1] ?? 0; + if (lx < -GAMEPAD_AXIS_THRESHOLD) newDown.add("ArrowLeft"); + if (lx > GAMEPAD_AXIS_THRESHOLD) newDown.add("ArrowRight"); + if (ly < -GAMEPAD_AXIS_THRESHOLD) newDown.add("ArrowUp"); + } + + // Newly pressed this frame → register in shared pressed set + for (const code of newDown) { + if (!this.gamepadDown.has(code)) { + this.pressed.add(code); + } + } + + // Update gamepadDown to reflect current hardware state + this.gamepadDown.clear(); + for (const code of newDown) this.gamepadDown.add(code); + } + isDown(code: string): boolean { - return this.down.has(code); + return this.down.has(code) || this.gamepadDown.has(code); } consumePress(code: string): boolean { @@ -91,5 +145,6 @@ export class InputController { reset(): void { this.down.clear(); this.pressed.clear(); + this.gamepadDown.clear(); } }