# HudHandle

> Talos next · Since 0.6; changed in 0.7

HUD overlay.

Text values are keys into `meta.strings`; a missing key renders as `[[key]]`.

Reached in a behaviour as `ctx.hud`.

## Signature

```ts
export interface HudHandle {
  /**
   * Set one field. With no `meta.hud` spec a field reads as a label and the value ("Lap 2"); `options`
   * give it a label, a unit and fixed decimals: `ctx.hud.set("speed", 92, { unit: "km/h" })` reads
   * "Speed 92 km/h". An unknown option, or decimals that are not a whole number 0..6, throws.
   */
  set(field: HudField, value: string | number | null, options?: HudFieldOptions): void
  objective(textKey: string): void
  /** A contextual prompt ("Press E"); null hides it. */
  prompt(textKey: string | null): void
  /** Countdown in seconds, shown as a clock with tenths (`0:02.0`, `1:23.3`) unless `meta.hud` styles it; null hides it. */
  timer(seconds: number | null): void
  score(value: number): void
  /**
   * Let one HUD row answer the hit-test and take focus.
   *
   * The HUD is click-through, so nothing on it can be clicked or tabbed to and it can never
   * quietly eat a gameplay click. This is the opt-in, and it grants both halves at the node:
   * the row is hit-testable *and* focusable, because §2.9 states the two together.
   *
   * In practice that is the pointer half. Tab, Enter and Space go to the topmost screen that
   * is not click-through, and the HUD is click-through so Escape still reaches the game over
   * a bare one — so an opted-in row is clickable rather than tabbable. A HUD node that has to
   * take the keyboard belongs on a screen that takes input.
   *
   * The field need not have a row yet — the opt-in is remembered and applies the moment `set`
   * writes one. It is not a {@link UiProp}: a binding cannot change what a node is, so this
   * re-registers the stock screen, once per opt-in rather than once per frame.
   */
  hittable(field: HudField | string, on?: boolean): void
}
```

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

## Members

| Member | Signature | Description |
|---|---|---|
| `set` | `set(field: HudField, value: string \| number \| null, options?: HudFieldOptions): void` | Set one field. With no `meta.hud` spec a field reads as a label and the value ("Lap 2"); `options` give it a label, a unit and fixed decimals: `ctx.hud.set("speed", 92, { unit: "km/h" })` reads "Speed 92 km/h". An unknown option, or decimals that are not a whole number 0..6, throws. |
| `objective` | `objective(textKey: string): void` |  |
| `prompt` | `prompt(textKey: string \| null): void` | A contextual prompt ("Press E"); null hides it. |
| `timer` | `timer(seconds: number \| null): void` | Countdown in seconds, shown as a clock with tenths (`0:02.0`, `1:23.3`) unless `meta.hud` styles it; null hides it. |
| `score` | `score(value: number): void` |  |
| `hittable` | `hittable(field: HudField \| string, on?: boolean): void` | Let one HUD row answer the hit-test and take focus. The HUD is click-through, so nothing on it can be clicked or tabbed to and it can never quietly eat a gameplay click. This is the opt-in, and it grants both halves at the node: the row is hit-testable *and* focusable, because §2.9 states the two together. In practice that is the pointer half. Tab, Enter and Space go to the topmost screen that is not click-through, and the HUD is click-through so Escape still reaches the game over a bare one — so an opted-in row is clickable rather than tabbable. A HUD node that has to take the keyboard belongs on a screen that takes input. The field need not have a row yet — the opt-in is remembered and applies the moment `set` writes one. It is not a {@link UiProp}: a binding cannot change what a node is, so this re-registers the stock screen, once per opt-in rather than once per frame. |

## Example

The tested fixture `fixtures/recipes/jump-pad-and-elevator/scenes/behaviors/camera-zone.ts`.

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

/**
 * A trigger volume that retunes the camera while the player is inside it.
 *
 * The engine has no camera-zone concept and needs none: a volume, a `trigger` component and this.
 * Here the player's behaviour owns the camera (it re-sends its params when the view is turned), so
 * the zone tells it what to change rather than calling `ctx.camera.set` itself. In a scene where
 * nothing else writes the camera, the zone can call `ctx.camera.set(kind, params)` directly.
 */
interface Props {
  /** `ctx.camera.set` params to merge in on the way in, and on the way out. */
  inside?: Record<string, unknown>
  outside?: Record<string, unknown>
}

export default {
  onTrigger(ctx, trigger) {
    const params = trigger.entering ? ctx.props.inside : ctx.props.outside
    if (params) ctx.events.emit("camera-zone", { target: trigger.other.id, params })
  },
} satisfies Behavior<Record<string, never>, Props>
```
