Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/game/AsteroidsGame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ export class AsteroidsGame {
}

private updateFrame(timestampMs: number): void {
this.input.pollGamepad();
this.handleGlobalInput();

if (this.mode === "playing") {
Expand Down Expand Up @@ -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;
Expand Down
57 changes: 56 additions & 1 deletion src/game/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
tacticalnoot marked this conversation as resolved.
[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<string>();
private readonly pressed = new Set<string>();
// 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<string>();

handleKeyDown(event: KeyboardEvent): void {
if (!GAME_KEYS.has(event.code)) {
Expand Down Expand Up @@ -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<string>();

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 {
Expand All @@ -91,5 +145,6 @@ export class InputController {
reset(): void {
this.down.clear();
this.pressed.clear();
this.gamepadDown.clear();
}
}