# EventBus

> Talos 0.7 · Since 0.6; changed in 0.7

The global event bus.

Events emitted during a tick are delivered at the end of that tick, in order. Four names are published by the engine rather than by a behavior: `"trigger"` and `"collision"`, described here, `"ui-activated"` ({@link BusUiActivated}), which is a button press and has rules of its own, and `"hold-broken"` ({@link BusHoldBroken}), a hold ending, which follows the same rules as `"ui-activated"`. The first two carry {@link BusTriggerEvent} and {@link BusCollision}, with `by` always null because no behavior emitted them. They exist so a referee — a course brain, a scorer, a replay recorder — can watch contacts it is not a participant in. Two things about them are worth knowing before you rely on them: - The entity-targeted {@link Behavior.onTrigger} and {@link Behavior.onCollision} hooks run first and are unchanged. Subscribing here neither replaces nor suppresses them. - The published form does **not** reach {@link Behavior.onEvent}. Only an explicit `on`/`once` sees it, which is what keeps a scene that never subscribes from paying for it at all.

Reached in a behaviour as `ctx.events`.

## Signature

```ts
export interface EventBus {
  /**
   * Emit to every behavior's `onEvent`, every `on` listener, and the observability log. `data` must be JSON-serialisable.
   *
   * **Delivery order** (the same for `emit`, `emitTo` and the engine's own events):
   * - Nothing is delivered where it is sent. Events sent during `update` wait until every behaviour's
   *   `update` has run, then are delivered in the order they were sent. `emit` and `emitTo` share one
   *   queue, so a mix of both keeps the order you sent them in.
   * - One event is delivered completely before the next starts. For each receiving behaviour, in behaviour
   *   order (the order behaviours were attached: the scene's order, then spawns as they happened), its
   *   `onEvent` runs first, then its `on`/`once` listeners for that name in the order they were subscribed.
   *   So a single target hears several events in the order they were sent, never interleaved.
   * - An event sent while delivering (from `onEvent`, a listener or `onCollision`) does cascade within the
   *   same tick, but it does not jump the queue: it is delivered after every event already waiting, in
   *   rounds. A chain settles within the tick up to 8 rounds deep; past that the rest are dropped and a
   *   script error ("event cascade exceeded 8 rounds") says so.
   */
  emit(name: string, data?: unknown): void
  /**
   * Emit to one entity: only `target`'s behaviours hear it (their `onEvent` and the `on`/`once`
   * listeners they subscribed), at the end of this tick like `emit`, in the same order. The
   * observability log records it with `to`, the target's id. `target` is a handle or a scene id;
   * one that is not alive now is heard by nobody.
   *
   * What a damage or pickup message wants: one `emit` per hit reaches every behaviour in the
   * scene, so many hits in one tick cost hits × behaviours handler calls; `emitTo` costs one.
   *
   * ```ts
   * if (hit) ctx.events.emitTo(hit.entity, "damage", { amount: 10 })
   * onEvent(ctx, ev) { if (ev.name === "damage") ctx.state.hp -= (ev.data as { amount: number }).amount }
   * ```
   */
  emitTo(target: string | EntityHandle, name: string, data?: unknown): void
  /**
   * Subscribe. The engine's own names ({@link BusEventMap}) narrow `ev.data` to their payloads;
   * every other name carries whatever `emit` was given, as `unknown`.
   */
  on<N extends string>(name: N, handler: (ev: GameEvent<BusPayload<N>>) => void, opts?: ListenOptions): void
  /** Like `on` but removed after the first delivery. */
  once<N extends string>(name: N, handler: (ev: GameEvent<BusPayload<N>>) => void, opts?: ListenOptions): void
  /** Remove one handler, or all handlers for `name`. */
  off<N extends string>(name: N, handler?: (ev: GameEvent<BusPayload<N>>) => void): void
}
```

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

## Members

| Member | Signature | Description |
|---|---|---|
| `emit` | `emit(name: string, data?: unknown): void` | Emit to every behavior's `onEvent`, every `on` listener, and the observability log. `data` must be JSON-serialisable. **Delivery order** (the same for `emit`, `emitTo` and the engine's own events): - Nothing is delivered where it is sent. Events sent during `update` wait until every behaviour's   `update` has run, then are delivered in the order they were sent. `emit` and `emitTo` share one   queue, so a mix of both keeps the order you sent them in. - One event is delivered completely before the next starts. For each receiving behaviour, in behaviour   order (the order behaviours were attached: the scene's order, then spawns as they happened), its   `onEvent` runs first, then its `on`/`once` listeners for that name in the order they were subscribed.   So a single target hears several events in the order they were sent, never interleaved. - An event sent while delivering (from `onEvent`, a listener or `onCollision`) does cascade within the   same tick, but it does not jump the queue: it is delivered after every event already waiting, in   rounds. A chain settles within the tick up to 8 rounds deep; past that the rest are dropped and a   script error ("event cascade exceeded 8 rounds") says so. |
| `emitTo` | `emitTo(target: string \| EntityHandle, name: string, data?: unknown): void` | Emit to one entity: only `target`'s behaviours hear it (their `onEvent` and the `on`/`once` listeners they subscribed), at the end of this tick like `emit`, in the same order. The observability log records it with `to`, the target's id. `target` is a handle or a scene id; one that is not alive now is heard by nobody. What a damage or pickup message wants: one `emit` per hit reaches every behaviour in the scene, so many hits in one tick cost hits × behaviours handler calls; `emitTo` costs one. `ts if (hit) ctx.events.emitTo(hit.entity, "damage", { amount: 10 }) onEvent(ctx, ev) { if (ev.name === "damage") ctx.state.hp -= (ev.data as { amount: number }).amount } ``` |
| `on` | `on<N extends string>(name: N, handler: (ev: GameEvent<BusPayload<N>>) => void, opts?: ListenOptions): void` | Subscribe. The engine's own names ({@link BusEventMap}) narrow `ev.data` to their payloads; every other name carries whatever `emit` was given, as `unknown`. |
| `once` | `once<N extends string>(name: N, handler: (ev: GameEvent<BusPayload<N>>) => void, opts?: ListenOptions): void` | Like `on` but removed after the first delivery. |
| `off` | `off<N extends string>(name: N, handler?: (ev: GameEvent<BusPayload<N>>) => void): void` | Remove one handler, or all handlers for `name`. |

## Example

The tested fixture `fixtures/app/agent.ts`.

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

// Sends a nav agent to `props.target` once, then reports arrival through ctx.state so the
// test can read it without polling the crowd directly.
export default {
  init(ctx) {
    ctx.state.arrived = false
    ctx.nav?.goTo(ctx.props.target as [number, number, number])
  },
  update(ctx) {
    if (ctx.nav?.arrived) ctx.state.arrived = true
  },
} satisfies Behavior
```
