# controls reference

Read controls by position, independent of the letters painted on the player's controller. Import from `@controller-plus/sdk/controls`, the package root, or `/sdk/src/controls.mjs` on your controller origin.

## Input and constants

`ControlState` is `{ axes: number[4], buttons: number[16], motion?: { gyro?: number[3], accel?: number[3], orientation?: number[4] }, custom?: Record<string, number> }`. Axes are −1…1; buttons are 0…1. Right/down are positive. `custom` is a local helper field, not part of normal wire input. Motion vectors are bounded to −100…100 and quaternion components to −1…1 by the JSON protocol; units must match your sensor adapter.

| Index | `BUTTONS[index]` | Classic label | Standard label |
| --- | --- | --- | --- |
| 0 | `face.south` | B | A |
| 1 | `face.east` | A | B |
| 2 | `face.west` | Y | X |
| 3 | `face.north` | X | Y |
| 4 | `shoulder.left` | L | L |
| 5 | `shoulder.right` | R | R |
| 6 | `trigger.left` | L2 | L2 |
| 7 | `trigger.right` | R2 | R2 |
| 8 | `menu.select` | SELECT | SELECT |
| 9 | `menu.start` | START | START |
| 10 | `stick.left.press` | Left stick press | Left stick press |
| 11 | `stick.right.press` | Right stick press | Right stick press |
| 12 | `dpad.up` | Up | Up |
| 13 | `dpad.down` | Down | Down |
| 14 | `dpad.left` | Left | Left |
| 15 | `dpad.right` | Right | Right |

`BUTTONS` and `AXES` are frozen arrays. `AXES` is `['stick.left.x', 'stick.left.y', 'stick.right.x', 'stick.right.y']`. `CLASSIC_LABELS` and `STANDARD_LABELS` are frozen maps of the four face positions to the letters above. They do not contain the other labels in the table.

## Functions

| Full signature | Parameters, defaults and return |
| --- | --- |
| `NEUTRAL(): ControlState` | New zero-filled arrays on every call; no motion. |
| `clamp(value, low = -1, high = 1): number` | Clamp a number inclusively. Does not validate finite numbers; `NaN` remains `NaN`. |
| `deadzone(value, threshold = 0.12): number` | Return zero inside the threshold; rescale the remaining range to full travel. Supply a finite threshold below 1. Does not clamp or validate. |
| `controlValue(input, id): number` | Find a positional button/axis, then `input.custom[id]`; missing values return zero. Requires an input object. |
| `movement(input): [number, number]` | Add left stick and D-pad, clamping each axis to −1…1. Does not normalize diagonal length. |
| `fromGamepad(gamepad, threshold = 0.12): ControlState` | Read four axes and sixteen `.buttons[i].value` values from a browser Gamepad-like object; clamp and apply the axis deadzone. Missing entries become zero. |
| `quantize(input): ControlState` | Round supplied axes/buttons to three decimals and clone optional motion. Does not validate length, range or finite values; does not copy `custom`. |

These are pure helpers with no events or network traffic. They do not throw custom errors; invalid argument shapes can cause ordinary JavaScript errors. Use [protocol validation](sdk-protocol.md#input-validation) at a trust boundary.

## Read input

<!-- example: controls -->
```js
import { NEUTRAL, BUTTONS, AXES, CLASSIC_LABELS, STANDARD_LABELS,
  movement, controlValue, clamp, deadzone, fromGamepad, quantize
} from '@controller-plus/sdk/controls';

const input = NEUTRAL();
input.buttons[15] = 1;
input.buttons[0] = 1;
console.log(movement(input));                 // [1, 0]
console.log(controlValue(input, 'face.south')); // 1
console.log(BUTTONS[0], AXES[0], CLASSIC_LABELS, STANDARD_LABELS);
console.log(clamp(2), deadzone(0.05));          // 1, 0
const pad = fromGamepad({ axes: [0.5], buttons: [{ value: 1 }] });
console.log(quantize(pad));
```

Keep input per `playerId` and sample it in the simulation loop. See [Read and map input](input-api.md) for edge-triggered actions, disconnects, physical controllers and motion.
