# Steamdeck Controls

> Steam Deck controller mapping for Phaser 3 HTML5 games. Covers W3C Gamepad API standard mapping, Phaser 3 gamepad integration, Steam Deck button layout, dual-input (keyboard+gamepad) support, and best practices for Steam Deck-optimized controls. Trigger: "steamdeck controls", "gamepad input", "controller mapping", "phaser gamepad", "steam deck phaser".

- Skill: `kadajett/steamdeck-controls` (Agent Skill)
- Install (CLI): `npx skillmds@latest add kadajett/steamdeck-controls`
- Raw SKILL.md: https://api.skillmd.com/api/skills/kadajett/steamdeck-controls/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: Kadajett (https://skillmd.com/u/kadajett)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/kadajett/steamdeck-controls

---


# Steam Deck Controller Mapping for Phaser 3

Ensure every generated game works with Steam Deck controls out of the box. The Steam Deck exposes its controller as a standard XInput (Xbox-layout) gamepad via the W3C Gamepad API in browsers.

## Steam Deck Physical Layout

```
         [L1/LB]   [R1/RB]          Buttons 4, 5
         [L2/LT]   [R2/RT]          Buttons 6, 7 (analog 0.0-1.0)

[L4] [L5]                   [R4] [R5]   (back grip buttons, NOT in standard mapping)

         ┌─────────────────┐
    LS   │  [View] [Menu]  │   RS       Buttons 8, 9
   (10)  │     [Steam]     │  (11)      Button 16
         │                 │
  D-pad  │                 │  [Y] (3)
 12-15   │     Screen      │ [X] [B]    2, 1
         │   (Touchscreen) │  [A] (0)
         │                 │
         └─────────────────┘

  [Left Trackpad]    [Right Trackpad]    (mapped as mouse or additional axes)
```

## W3C Standard Gamepad Button Index Mapping

The Steam Deck presents as a standard gamepad. These indices are guaranteed:

| Index | Button | Phaser Property | Use In Games |
|-------|--------|-----------------|-------------|
| 0 | A (bottom) | `gamepad.A` | Primary action (jump, select, confirm) |
| 1 | B (right) | `gamepad.B` | Secondary action (cancel, back, dodge) |
| 2 | X (left) | `gamepad.X` | Tertiary action (use item, interact) |
| 3 | Y (top) | `gamepad.Y` | Quaternary action (inventory, special) |
| 4 | LB / L1 | `gamepad.L1` | Left bumper (cycle left, previous) |
| 5 | RB / R1 | `gamepad.R1` | Right bumper (cycle right, next) |
| 6 | LT / L2 | `gamepad.L2` | Left trigger (aim, brake) - analog 0.0-1.0 |
| 7 | RT / R2 | `gamepad.R2` | Right trigger (shoot, accelerate) - analog 0.0-1.0 |
| 8 | View/Back | N/A (index) | Pause menu, scoreboard |
| 9 | Menu/Start | N/A (index) | Start game, open menu |
| 10 | Left Stick Press | N/A (index) | Sprint, lock-on |
| 11 | Right Stick Press | N/A (index) | Zoom, camera reset |
| 12 | D-pad Up | `gamepad.up` | Menu navigate up |
| 13 | D-pad Down | `gamepad.down` | Menu navigate down |
| 14 | D-pad Left | `gamepad.left` | Menu navigate left |
| 15 | D-pad Right | `gamepad.right` | Menu navigate right |
| 16 | Steam/Home | N/A | System button (do not use) |

## Axes Mapping

| Index | Axis | Phaser Property | Range |
|-------|------|-----------------|-------|
| axes[0] | Left Stick X | `gamepad.leftStick.x` | -1.0 (left) to 1.0 (right) |
| axes[1] | Left Stick Y | `gamepad.leftStick.y` | -1.0 (up) to 1.0 (down) |
| axes[2] | Right Stick X | `gamepad.rightStick.x` | -1.0 (left) to 1.0 (right) |
| axes[3] | Right Stick Y | `gamepad.rightStick.y` | -1.0 (up) to 1.0 (down) |

**IMPORTANT**: Y-axis is inverted from screen coordinates. Up on the stick = negative value.

## Steam Deck Specifics

### What IS available via Gamepad API in browser:
- All 17 standard buttons (indices 0-16)
- 4 axes (2 analog sticks)
- D-pad as buttons (not axes)
- Triggers as analog buttons (0.0-1.0)

### What is NOT available via standard Gamepad API:
- Back grip buttons (L4, L5, R4, R5) - only available via Steam Input remapping
- Trackpads - can be mapped to mouse via Steam Input, not to gamepad buttons
- Gyroscope/accelerometer - not exposed to browser Gamepad API
- Touchscreen - use standard DOM touch events, not gamepad

### Steam Input Remapping
Users can remap back grips and trackpads via Steam Input overlay. Do NOT rely on these for core gameplay. Treat them as optional convenience bindings only.

---

## Phaser 3 Gamepad Integration

### Enable Gamepad Input in Config

```typescript
const config: Phaser.Types.Core.GameConfig = {
  type: Phaser.AUTO,
  width: 1280,
  height: 800,
  input: {
    gamepad: true,    // REQUIRED: enable gamepad support
  },
  // ... rest of config
};
```

### Detect Gamepad Connection

```typescript
class GameScene extends Phaser.Scene {
  private pad: Phaser.Input.Gamepad.Gamepad | null = null;

  create(): void {
    if (this.input.gamepad) {
      // If already connected
      if (this.input.gamepad.total > 0) {
        this.pad = this.input.gamepad.getPad(0);
      }

      // Listen for new connections
      this.input.gamepad.once(
        "connected",
        (pad: Phaser.Input.Gamepad.Gamepad) => {
          this.pad = pad;
        }
      );
    }
  }
}
```

### Read Input in Update Loop

```typescript
update(time: number, delta: number): void {
  if (!this.pad) return;

  // Analog sticks (apply deadzone)
  const DEADZONE = 0.15;
  const lx = Math.abs(this.pad.leftStick.x) > DEADZONE ? this.pad.leftStick.x : 0;
  const ly = Math.abs(this.pad.leftStick.y) > DEADZONE ? this.pad.leftStick.y : 0;

  // Face buttons (boolean)
  if (this.pad.A) { /* primary action */ }
  if (this.pad.B) { /* secondary action */ }
  if (this.pad.X) { /* tertiary action */ }
  if (this.pad.Y) { /* quaternary action */ }

  // Shoulder buttons
  if (this.pad.L1) { /* left bumper */ }
  if (this.pad.R1) { /* right bumper */ }

  // Triggers (analog 0.0 to 1.0)
  const leftTrigger = this.pad.L2;   // float
  const rightTrigger = this.pad.R2;  // float

  // D-pad (boolean)
  if (this.pad.up) { /* d-pad up */ }
  if (this.pad.down) { /* d-pad down */ }
  if (this.pad.left) { /* d-pad left */ }
  if (this.pad.right) { /* d-pad right */ }

  // Buttons by index (for View/Menu/Stick press)
  if (this.pad.isButtonDown(8)) { /* View/Back */ }
  if (this.pad.isButtonDown(9)) { /* Menu/Start */ }
  if (this.pad.isButtonDown(10)) { /* Left Stick Press */ }
  if (this.pad.isButtonDown(11)) { /* Right Stick Press */ }
}
```

---

## Dual-Input System (Keyboard + Gamepad)

Every game MUST support both keyboard and gamepad simultaneously. Use this unified input pattern:

### InputState Interface

The `InputState` type is exported directly from `@sdr/engine`. Import and use it:

```typescript
import { InputManager } from "@sdr/engine";
import type { InputState } from "@sdr/engine";

// In create():
this.inputManager = new InputManager(this);
this.inputManager.setup(); // handles gamepad, keyboard, AND touch (virtual joystick)

// In onUpdate():
const input: InputState = this.inputManager.getState();
```

The `InputState` interface:

```typescript
interface InputState {
  moveX: number;          // -1.0 to 1.0 (horizontal movement)
  moveY: number;          // -1.0 to 1.0 (vertical movement)
  aimX: number;           // -1.0 to 1.0 (right stick horizontal)
  aimY: number;           // -1.0 to 1.0 (right stick vertical)
  action1: boolean;       // A / Space / touch-A — primary action
  action2: boolean;       // B / Shift / touch-B — secondary action
  action3: boolean;       // X / E — tertiary action
  action4: boolean;       // Y / Q — quaternary action
  bumperLeft: boolean;    // LB / Tab
  bumperRight: boolean;   // RB / R
  triggerLeft: number;    // LT (0.0 to 1.0)
  triggerRight: number;   // RT (0.0 to 1.0)
  pause: boolean;         // Start / Escape
  lastDevice: "keyboard" | "gamepad" | "touch";
}
```

### Touch Controls (Mobile)

`InputManager.setup()` automatically detects touch devices and creates:
- Virtual joystick in the lower-left corner (provides `moveX`/`moveY`)
- A button (lower-right, provides `action1`)
- B button (upper-right of A, provides `action2`)

No extra setup needed — works transparently alongside keyboard/gamepad.

### Default Control Scheme

| Action | Gamepad | Keyboard (Player 1) | Description |
|--------|---------|---------------------|-------------|
| Move | Left Stick | WASD | Character movement |
| Aim/Look | Right Stick | Mouse (if available) | Camera or aim direction |
| Primary Action | A | Space | Jump, select, confirm |
| Secondary Action | B | Shift / Right-Click | Dodge, cancel, back |
| Tertiary Action | X | E | Interact, use item |
| Quaternary Action | Y | Q | Inventory, special ability |
| Bumper Left | LB | Tab | Cycle left, previous weapon |
| Bumper Right | RB | R | Cycle right, next weapon |
| Trigger Left | LT | (none) | Aim down sights, brake |
| Trigger Right | RT | Left-Click | Shoot, accelerate |
| Pause | Menu/Start | Escape | Pause menu |
| D-pad | D-pad | Arrow keys | Menu navigation, quick select |

### Using InputManager (ALWAYS use this, never roll your own)

`InputManager` from `@sdr/engine` handles all three input sources and returns a unified `InputState`. It also creates the virtual joystick on touch devices automatically.

```typescript
// In create():
this.inputManager = new InputManager(this);
this.inputManager.setup();

// In onUpdate():
const input = this.inputManager.getState();
// input.moveX, input.moveY, input.action1, input.lastDevice, etc.
```

Do NOT write your own `readInput` function or manage raw gamepad/keyboard objects. Use `InputManager`.

---

## Required Patterns for All Generated Games

### 1. Always Apply Deadzone to Analog Sticks

```typescript
// BAD: Raw stick values drift when idle
const x = gamepad.leftStick.x;

// GOOD: Apply deadzone
const DEADZONE = 0.15;
const x = Math.abs(gamepad.leftStick.x) > DEADZONE ? gamepad.leftStick.x : 0;
```

### 2. Always Handle Missing Gamepad Gracefully

```typescript
// BAD: Assumes gamepad exists
const x = this.pad!.leftStick.x;

// GOOD: Null-safe access
const x = this.pad?.leftStick.x ?? 0;
```

### 3. Always Support Both Input Methods

Every game must be fully playable with EITHER keyboard OR gamepad. Never require both. Never require ONLY gamepad.

### 4. Use Delta Time for All Movement

```typescript
// BAD: Frame-rate dependent
player.x += speed;

// GOOD: Frame-rate independent
player.x += speed * dt;
```

### 5. Screen Resolution

Always target **1280x800** (Steam Deck native resolution). Use `Phaser.Scale.FIT` with `CENTER_BOTH` to handle different aspect ratios.

```typescript
scale: {
  mode: Phaser.Scale.FIT,
  autoCenter: Phaser.Scale.CENTER_BOTH,
  width: 1280,
  height: 800,
}
```

### 6. Readable UI at Steam Deck Size

The Steam Deck has a 7" 1280x800 screen. Minimum text size should be 18px. Buttons and interactive elements should have at least 48x48px touch targets. Use high-contrast colors.

### 7. Normalize Diagonal Movement

```typescript
// Prevent faster diagonal movement
let dx = state.moveX;
let dy = state.moveY;
const mag = Math.sqrt(dx * dx + dy * dy);
if (mag > 1) {
  dx /= mag;
  dy /= mag;
}
```

### 8. Button Prompts

When showing button prompts in-game, detect whether the last input was from gamepad or keyboard and show the appropriate icon/text:

```typescript
// Track last input device
let lastDevice: "keyboard" | "gamepad" = "keyboard";

// In update:
if (pad && (Math.abs(pad.leftStick.x) > 0.1 || pad.A || pad.B)) {
  lastDevice = "gamepad";
}
if (anyKeyDown) {
  lastDevice = "keyboard";
}

// In UI:
const actionLabel = lastDevice === "gamepad" ? "[A]" : "[SPACE]";
```

---

## Common Pitfalls

1. **Do not use button index 16 (Steam/Home)** - This is the system button. It opens the Steam overlay.
2. **Do not require back grip buttons (L4/L5/R4/R5)** - They are not exposed to the standard Gamepad API.
3. **Do not require trackpad input** - It may be mapped to mouse, or disabled entirely.
4. **Do not forget the deadzone** - Steam Deck sticks have slight drift. Always use >= 0.15 deadzone.
5. **Do not assume gamepad is always connected** - The browser may not report it until the user presses a button.
6. **Do not use `gamepad.vibration`** - Steam Deck supports haptics but browser API support is inconsistent on Linux.
7. **Do not hard-code button indices** - Use Phaser's named properties (`.A`, `.B`, `.L1`, etc.) when available.

