readonly camera: CameraControllerThe gestures-to-camera policy. iso owns where the camera may be; this owns where the
player is trying to put it.
readonly profile: Readonly<GestureProfile>The thresholds in force right now, defaults filled in and every override validated.
A live read, not the object handed to the constructor: after setProfile this is the
new one. Frozen, so a game that wants a different threshold changes it through setProfile
rather than by writing to a shared object three other things are reading.
setProfile(overrides: ProfileOverrides | undefined): Readonly<GestureProfile>Replace every threshold, and keep every handler.
input.setProfile({ tapSlopPx: { touch: 14 } }); // handlers, scopes and camera all survive
A full replacement of the override set, resolved against the defaults exactly as
construction does — not a patch onto the profile in force. setProfile({}) therefore
returns to the defaults, and a game that keeps its overrides in one object and re-passes it
gets a profile that depends only on that object and not on the order the sliders were moved.
A patching version would make the thresholds path-dependent, and a path-dependent value is
one a recorded log's fingerprint cannot be reasoned about.
The recognizer is rebuilt behind the seam — its tick counts, its velocity rings and its
pointer slots are all sized from the profile — but the buffer's slot pool, the camera
controller, every handler, every child scope and the DOM binding are the same objects
afterwards. That is what makes this cheap enough to put behind a slider: retuning one
threshold used to mean dispose, recreate and re-register every handler.
Every live gesture ends first, under the old thresholds: each drag gets its dragend
and each held key its release, exactly as dispose does it, for the same reason — a
recognizer replaced mid-drag is a placement ghost stuck to the cursor and a camera that pans
for ever.
ThrowsRangeError if any override is out of range, before anything changes; if called
from inside a handler, because the bucket being delivered was recognized under the old
thresholds and the samples behind it would meet a recognizer that never saw their press;
if a recording is running, because the profile fingerprint is a third of a log's identity
and a log that changed rules half way through describes no session that can be replayed;
or if the system has been disposed.
setActions(actions: ActionMap<A>): voidRebind every action, and keep every handler.
input.setActions({ collect: ['tap', 'key:Space'], build: ['key:KeyN'] }); // was KeyB
A full replacement of the map, compiled exactly as construction compiles it — the same
validator, the same errors, the same unknown-key-code diagnostic. A binding an action had
and this map does not name is gone; there is no patch form, for setProfile's reason.
The names are identity; only the bindings move
Passing a map whose names are not exactly the declared ones throws. A was inferred from
the constructor's map and has already been handed out — every onAction handler is keyed to
one of those names, actionNames has been read into a shortcut sheet, and the type of this
very argument is derived from it. A name that appeared would have no handler list and no way
to acquire one; a name that vanished would take a live handler with it and look, from the
game's side, exactly like a handler that stopped being called. Adding an action is a new
system. Which key produces build is the thing a settings screen moves, and that is what
this method is for.
Unlike setProfile this ends nothing first, because an action map holds no live
state: actions fire on the press edge, so every press that has already fired has already
been delivered under the map that was in force when it fired. held is answered
through the new map from the next call onward, which is the honest reading of "is the key
bound to build down".
ThrowsRangeError if a binding is malformed, if the map names an action that was not
declared or omits one that was — before anything changes; if called from inside a
handler, because half of the bucket being delivered would dispatch through each map; if a
recording is running; or if the system has been disposed.
The recording refusal is the one worth reading twice, because the reason is not
setProfile's. A log stores RawSamples, and actions is not in the
compatibility triple — so a mid-recording rebind changes nothing about what the log says
and everything about what a replay of it does, behind a triple that still matches
exactly. setProfile refuses to keep the log's declared identity true; this refuses
because there is no declared identity here to keep true, and the alternative is a
divergence report that is confidently wrong. See docs/rfc/live-options.md §6b.
readonly terrain: TerrainOption | undefinedThe ground in force right now, exactly as it was declared, or undefined if it never
was.
A live read, not the object handed to the constructor: after setTerrain this is the
new one. It is the same object the caller passed — not a copy — so a HUD that wants to show
the march ceiling reads it here instead of keeping a second copy that drifts.
setTerrain(terrain: TerrainOption): voidDeclare the ground, or change it. Keeps every handler, every scope and the camera.
input.setTerrain({ field: hill, maxHeightPx: hill.tallestPx }); // after the map generated
input.setTerrain('flat'); // the tunnel level
Settable rather than baked, and the readback rule's three questions are why. Identity:
nothing allocated or handed out depends on it — every coordinate is resolved from the
pointer at the moment it is read, so there is no derived value to invalidate. Record: a
log stores RawSamples, which are screen pixels; gx/gy have never been in one, any
more than the camera position they equally depend on is, so no recording is made invalid by
this. Cost: what the hot path reads is the field itself, which is exactly what a game
with deformable ground needs to be live.
The march ceiling moving under a slider is the case that settled it — examples/terraces
ships that slider — and a game whose map is generated after its input system is bound is the
case that made it necessary at all.
It does not bump the epoch setProfile and setActions bump, and a recording
does not refuse it. Those two replace recognition and dispatch rules, which a log's samples
were produced under; this replaces the surface a coordinate is measured against, which is
game state and moves during ordinary play — examples/clay deforms it every frame. A cursor
that refused here would refuse every session in which a player dug a hole.
ThrowsTypeError / RangeError for a malformed declaration, naming the field that is wrong,
before anything changes; RangeError if called from inside a handler, because half of
the bucket being delivered would then have resolved on a different surface from the other
half; or if the system has been disposed.
readonly stepMs: numberThe fixed step every duration is counted in. Fixed for the life of the system: changing it
would re-time every gesture and invalidate every log, which is a new system, not a knob.
readonly actionNames: readonly A[]Every declared action, in declaration order. A live read: after setActions the
order is the new map's, and the set is necessarily the same one.
bindings(action: A): readonly ActionBinding[]What is bound to an action right now.
Exists so a keyboard-shortcut sheet is rendered from the map rather than transcribed
beside it — and so that a sheet re-rendered after setActions shows the new keys
without the game keeping a second copy of the map to read them from.
ThrowsRangeError naming an action that was never declared.
tick(index: number): voidClose the sample buffer and deliver everything in it as simulation tick index.
The only place handlers run. Call it once per fixed step, before the game's own update.
A pump with no ticks loses nothing; a pump with five delivers the backlog to the first and
leaves the other four empty, which is correct — they are catch-up for time that already
passed, and a tap did not happen five times.
ThrowsRangeError if index is not an integer, or is not greater than the previous one.
A repeated index makes the log ambiguous — two buckets under one key — and a regression
makes it unreplayable, and both are silent until a replay reports a confident wrong
answer months later.
frame(nowMs: number): voidAdvance the view: the camera's glide, and nothing else.
Called once per rendered frame, before drawing. Delivers nothing and calls no handler.
ThrowsRangeError if nowMs is not finite. A NaN here propagates into the camera and
the screen goes blank a hundred frames from the mistake.
submit(sample: RawSample): voidFeed the recognizer directly. The DOM binding is a producer of these and nothing more.
The sample is copied, so a producer may reuse one object for every event it makes.
ThrowsRangeError for a tick sample — tick(index) produces those, and one submitted by
hand would put a marker in the log at a position no tick closed — or for a coordinate
that is not a finite number.
held(action: A): booleanIs any binding of this action currently held? Continuous input is a query, not a stream.
keyHeld(code: string): booleanEscape hatch for a key with no action, e.g. a debug overlay. KeyboardEvent.code.
hoverTile(out: GridPoint): booleanThe tile under the pointer, for a hover highlight.
A query, answered from the newest position submitted and through the live camera —
so a ghost following a finger is smooth at display rate even when ticks are slow. Querying
is safe outside a tick precisely because it cannot mutate simulation state.
Returns false when there is no pointer over the world — which is every touch device,
always, between taps. A control that only appears on hover does not exist on a phone; this
signature exists to make that impossible to forget.
On a system with HeadlessInputOptions.terrain it also returns false when the
pointer is over the sky or past the edge of the field, and leaves out untouched: a ghost
with nowhere to stand should not be drawn on the shore instead.
pointerScreen(out: Vec2): booleanThe pointer's screen position, same contract as hoverTile.
readonly buffered: numberSamples waiting for the next tick. A number a stall diagnostic can watch.