Controller+BETADownload

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();