actions reference
Map phone and physical controller input to game actions. Import from @controller-plus/sdk/actions or the package root. Positional IDs come from controls.
Bindings
DEFAULT_BINDINGS contains move, aim, primary, secondary, extra, special, pause, back, leftTrigger, rightTrigger. Move combines the left stick and D-pad; aim uses right stick. Primary/secondary/extra/special are south/east/west/north; pause/back are Start/Select; triggers are positive analog axes.
validateBindings(bindings): object returns a structured clone. Accepts at most 64 named actions. Names match [A-Za-z][A-Za-z0-9_.-]{0,47}. Unknown control IDs, kinds, or invalid deadzones throw Error. evaluateActions(state, bindings = DEFAULT_BINDINGS): object evaluates without validating; returns booleans, numbers or [x,y] vectors.
| Binding field | Behavior |
|---|---|
kind |
button, axis, vector2. |
buttons |
Up to 16 positional IDs; button is true if maximum pressure ≥ threshold (default 0.5). |
axes |
Up to two positional axis IDs. Axis action uses the first; vector uses both. |
positive, negative |
Up to 16 button IDs each; add/subtract their maximum pressure to an axis action. |
digital |
For vector2, [left, right, up, down] button IDs. |
deadzone |
0…0.8, default 0 during evaluation. |
invert, invertX, invertY |
Reverse an axis/vector component. |
Axis results clamp to −1…1. Vectors normalize diagonal length to at most 1. threshold and boolean flags are used as supplied; binding validation does not check their types. Supply finite sensible values.
ControllerManager
new ControllerManager({ bindings = DEFAULT_BINDINGS, timeout = 1000, now = () => performance.now() } = {}) validates bindings. timeout is milliseconds before input becomes stale. Public devices is a Map of ID to device; players maps a game player key to a Set of device IDs.
| Full signature | Return and behavior |
|---|---|
onChange(callback): () => boolean |
Register listener and return unsubscribe. |
emit(event): void |
Invoke change listeners locally; callback exceptions propagate. |
connect(id, { name = id, source = 'phone', player = null } = {}): Device |
String ID ≤128 characters; reuse or create device, mark connected, optionally assign. New physical devices use deadzone 0.12, others 0. |
assign(id, player): void |
Move device from other players to this key; null unassigns. Unknown device throws. Multiple devices may feed one player. |
update(id, input): void |
Create unknown device, validate 4 axes/16 buttons and ranges, copy input, refresh liveness. Does not retain motion/custom fields. |
configure(id, profile): void |
Merge/validate { deadzone, axes, buttons }; reset input to neutral. Axis map: 4 indices 0–3; button map: 16 indices 0–15; deadzone 0…0.8. Throws for unknown device/bad profile. |
disconnect(id): void |
Neutralize and mark disconnected; keep device and assignment. Unknown ID ignored. |
state(player): ControlState |
Neutral if no live assigned device. Apply profile once; choose strongest absolute axis and maximum button across live devices. No motion. |
actions(player): object |
Evaluate bindings against state(player). |
pollGamepads(gamepads = navigator?.getGamepads?.() || []): void |
Update physical devices as gamepad:N, autoassign initially to physical-N; disconnect missing pads. Call each game frame. |
bindSession(session): () => void |
Listen to host Session device-input, device-left, disconnected; autoassign phone IDs to same player keys. Return cleanup function. Not a RelayRoom event adapter. |
neutralize(): void |
Zero all device input without changing connections/assignments. |
Device shape: { id, name, source, input, lastSeen, connected, profile: { deadzone, axes, buttons } }. Connect can emit { type: 'connected', device }, assignment emits { type: 'assigned', id, player }, disconnect emits { type: 'disconnected', id }. There is no input-update change event.
Do not feed change-only RelayRoom events into a one-second manager timeout without handling freshness separately: a held physical pad can emit no changes for much longer. Use bindSession for the direct host's refresh stream, or keep RelayRoom state in your own Map and clear it on leave/disconnect.
Map an action
import { ControllerManager, DEFAULT_BINDINGS, validateBindings, evaluateActions,
NEUTRAL } from '@controller-plus/sdk';
const bindings = validateBindings({ ...DEFAULT_BINDINGS,
boost: { kind: 'button', buttons: ['face.south'] },
});
const manager = new ControllerManager({ bindings });
const off = manager.onChange(event => console.log(event.type));
manager.connect('phone-1', { name: 'Ana', player: 'driver' });
const input = NEUTRAL(); input.buttons[0] = 1;
manager.update('phone-1', input);
console.log(manager.actions('driver').boost, evaluateActions(input).primary); // true, true
manager.configure('phone-1', { deadzone: 0 });
manager.assign('phone-1', 'copilot');
manager.pollGamepads([]);
manager.disconnect('phone-1'); manager.neutralize(); off();