# InputState

> Talos next · Since 0.6; changed in 0.7

Present on `ctx.input` only for the player entity.

Action names come from the scene's action map, `meta.input` in the scene document (or the built-in map when the scene has none). A behavior reads actions and never keys, which is what makes rebinding a JSON edit and hot reload rebind a running scene. An action the live context does not bind reads neutral — never an error, so a behavior needs no guard.

Reached in a behaviour as `ctx.input`.

## Signature

```ts
export interface InputState {
  /**
   * `"move"` and `"look"` are always defined; each component is in -1..1. The mouse is folded in
   * as a rate, so for turning a view read {@link InputState.axisParts} instead (the built-in
   * camera rigs do).
   */
  axis(name: string): Vec2
  /**
   * The same axis split into its stick `rate` and its mouse `delta` (an angle in degrees). A
   * camera turns by `rate × speed × dt + delta`: the mouse then turns exactly as far as the hand
   * moved, with no top speed.
   */
  axisParts(name: string): AxisParts
  /**
   * Where the mouse points this tick, or `null`: `view` is the split-screen view under it (1-based) and `x`, `y`
   * are pixels of that view's own cell, in the viewport `ctx.camera.viewSize` reports. Exactly what
   * `ctx.camera.pick(p.x, p.y, { view: p.view })` and `screenToRay` take, so picking under the mouse is
   * `const p = ctx.input.pointer; if (p) ctx.camera.pick(p.x, p.y, { view: p.view })`.
   *
   * Unlike the camera's `viewSize`, this is **simulation input**: the engine records it with the rest of the
   * tick's input (replay format 3), so a replay plays the same pointer and a script may decide gameplay with
   * it. The pixel space still follows the window the run had; the recorded pointer is the one that run saw.
   * `null` when no mouse is over the window, on a touch-only device, and in a headless run that was never
   * given one (`POST /input` `pointer` gives one).
   */
  readonly pointer: InputPointer | null
  action(name: string): ActionState
  pressed(name: string): boolean
  held(name: string): boolean
  released(name: string): boolean
  /**
   * The live context of the scene's action map, e.g. `"gameplay"` or `"menu"`.
   *
   * Scene-wide, not per entity: "the menu is open" is a fact about the scene, so every
   * behavior sees the same answer.
   */
  readonly context: string
  /**
   * Switch the whole scene to another context. `false`, and nothing changes, when the map has
   * no context of that name. The switch lands on the next tick.
   */
  setContext(name: string): boolean
  /** Every action the live context binds, in name order. What a rebinding screen lists. */
  actions(): string[]
  /**
   * What the live context binds `action` to right now, or null when it binds no such action.
   * `context` asks another context of the map instead: a settings screen in a `menu` context shows
   * gameplay's keys with `bindings("jump", "gameplay")`. A context the map does not have throws an
   * `InputBindingError` (`input-unknown-context`).
   *
   * Reports the map as played, the player's own overrides included, so a rebinding screen and the
   * game can never disagree about which key jump is on.
   */
  bindings(action: string, context?: string): ActionBinding | null
  /**
   * Binds one action of the live context to something else, and remembers it.
   *
   * The change lands on the map at once, survives {@link SessionHandle.restart}, and is written to
   * the overrides file the runtime was launched with — `argus run --input-overrides <file>`, or
   * `<project>/.argus/input.json` under the editor's Play. A run given no file keeps the rebind
   * until the process exits and says so on stderr. The scene document is never touched: `meta.input`
   * is the author's map and a player remapping their keys is not an edit to it.
   *
   * **A name nothing answers to is refused.** A typo that bound nothing would leave the player with
   * a dead action and no way to find out why, so this throws an `Error` whose `name` is
   * `InputBindingError` and whose message opens with the reason — `input-unknown-key`,
   * `input-unknown-binding`, `input-unknown-action`, `input-unknown-context` or `input-binding-arity`.
   * Nothing changes when it throws.
   *
   * `context` rebinds an action of another context of the map, leaving the live one as it is: a
   * settings screen in a `menu` context remaps gameplay's jump with `rebind("jump", "K", "gameplay")`.
   * Left out, it is the live context.
   */
  rebind(action: string, binding: Binding, context?: string): void
  /** Drops every override, going back to the scene's own map, and empties the overrides file. */
  resetBindings(): void
  /**
   * Shake a connected gamepad: `rumble(strength, seconds)` or `rumble(strength, high, seconds)`.
   *
   * `strength` (heavy motor) and `high` (light motor) are 0..1, `seconds` 0..10; a newer call replaces the
   * last, 0 stops it. A no-op without a force-feedback pad, never an error; `GET /feel` `haptics` records it.
   */
  rumble(strength: number, seconds: number): void
  rumble(strength: number, high: number, seconds: number): void
}
```

Import: `import type { InputState } from "@argus/api"`

## Members

| Member | Signature | Description |
|---|---|---|
| `axis` | `axis(name: string): Vec2` | `"move"` and `"look"` are always defined; each component is in -1..1. The mouse is folded in as a rate, so for turning a view read {@link InputState.axisParts} instead (the built-in camera rigs do). |
| `axisParts` | `axisParts(name: string): AxisParts` | The same axis split into its stick `rate` and its mouse `delta` (an angle in degrees). A camera turns by `rate × speed × dt + delta`: the mouse then turns exactly as far as the hand moved, with no top speed. |
| `pointer` | `readonly pointer: InputPointer \| null` | Where the mouse points this tick, or `null`: `view` is the split-screen view under it (1-based) and `x`, `y` are pixels of that view's own cell, in the viewport `ctx.camera.viewSize` reports. Exactly what `ctx.camera.pick(p.x, p.y, { view: p.view })` and `screenToRay` take, so picking under the mouse is `const p = ctx.input.pointer; if (p) ctx.camera.pick(p.x, p.y, { view: p.view })`. Unlike the camera's `viewSize`, this is **simulation input**: the engine records it with the rest of the tick's input (replay format 3), so a replay plays the same pointer and a script may decide gameplay with it. The pixel space still follows the window the run had; the recorded pointer is the one that run saw. `null` when no mouse is over the window, on a touch-only device, and in a headless run that was never given one (`POST /input` `pointer` gives one). |
| `action` | `action(name: string): ActionState` |  |
| `pressed` | `pressed(name: string): boolean` |  |
| `held` | `held(name: string): boolean` |  |
| `released` | `released(name: string): boolean` |  |
| `context` | `readonly context: string` | The live context of the scene's action map, e.g. `"gameplay"` or `"menu"`. Scene-wide, not per entity: "the menu is open" is a fact about the scene, so every behavior sees the same answer. |
| `setContext` | `setContext(name: string): boolean` | Switch the whole scene to another context. `false`, and nothing changes, when the map has no context of that name. The switch lands on the next tick. |
| `actions` | `actions(): string[]` | Every action the live context binds, in name order. What a rebinding screen lists. |
| `bindings` | `bindings(action: string, context?: string): ActionBinding \| null` | What the live context binds `action` to right now, or null when it binds no such action. `context` asks another context of the map instead: a settings screen in a `menu` context shows gameplay's keys with `bindings("jump", "gameplay")`. A context the map does not have throws an `InputBindingError` (`input-unknown-context`). Reports the map as played, the player's own overrides included, so a rebinding screen and the game can never disagree about which key jump is on. |
| `rebind` | `rebind(action: string, binding: Binding, context?: string): void` | Binds one action of the live context to something else, and remembers it. The change lands on the map at once, survives {@link SessionHandle.restart}, and is written to the overrides file the runtime was launched with — `argus run --input-overrides <file>`, or `<project>/.argus/input.json` under the editor's Play. A run given no file keeps the rebind until the process exits and says so on stderr. The scene document is never touched: `meta.input` is the author's map and a player remapping their keys is not an edit to it. **A name nothing answers to is refused.** A typo that bound nothing would leave the player with a dead action and no way to find out why, so this throws an `Error` whose `name` is `InputBindingError` and whose message opens with the reason — `input-unknown-key`, `input-unknown-binding`, `input-unknown-action`, `input-unknown-context` or `input-binding-arity`. Nothing changes when it throws. `context` rebinds an action of another context of the map, leaving the live one as it is: a settings screen in a `menu` context remaps gameplay's jump with `rebind("jump", "K", "gameplay")`. Left out, it is the live context. |
| `resetBindings` | `resetBindings(): void` | Drops every override, going back to the scene's own map, and empties the overrides file. |
| `rumble` | `rumble(strength: number, seconds: number): void` | Shake a connected gamepad: `rumble(strength, seconds)` or `rumble(strength, high, seconds)`. `strength` (heavy motor) and `high` (light motor) are 0..1, `seconds` 0..10; a newer call replaces the last, 0 stops it. A no-op without a force-feedback pad, never an error; `GET /feel` `haptics` records it. |

## Example

The tested fixture `fixtures/recipes/best-score-save/scenes/behaviors/best-score-save.ts`.

```ts
import type { Behavior } from "@argus/api"

/**
 * A best score that outlives the run, kept with `ctx.save`.
 *
 * Space scores, Return ends the round, and the best is saved before the round restarts.
 *
 * `ctx.state` is wiped by `ctx.session.restart()`, by `loadScene` and by quitting; `ctx.save` is
 * not. So the best is read from the save in `init` — which runs again after every restart — and
 * written there the moment it is beaten, never kept only in state.
 */
interface State {
  score: number
  best: number
}

/** One store per application, so a key is worth namespacing: 1–64 of `A-Z a-z 0-9 . _ -`. */
const BEST = "best-score.best"

export default {
  init(ctx) {
    ctx.state.score = 0
    ctx.state.best = Number(ctx.save.get(BEST) ?? 0) || 0
    ctx.hud.score(0)
    ctx.hud.set("best", ctx.state.best)
  },

  update(ctx) {
    if (ctx.input !== null && ctx.input.pressed("score")) {
      ctx.state.score += 1
      ctx.hud.score(ctx.state.score)
    }
    if (ctx.input !== null && ctx.input.pressed("finish")) {
      if (ctx.state.score > ctx.state.best) {
        ctx.state.best = ctx.state.score
        ctx.save.set(BEST, ctx.state.best)
      }
      // Lands at the end of this tick; the save is written at the end of it too, first.
      ctx.session.restart()
    }
  },
} satisfies Behavior<State>
```
