Talos docs

EventBus

Since 0.6Changed in 0.7

The global event bus.

Plain text

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

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

MemberSignatureDescription
emitemit(name: string, data?: unknown): voidEmit 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.
emitToemitTo(target: string | EntityHandle, name: string, data?: unknown): voidEmit 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 }
onon<N extends string>(name: N, handler: (ev: GameEvent<BusPayload<N>>) => void, opts?: ListenOptions): voidSubscribe. The engine's own names ({@link BusEventMap}) narrow ev.data to their payloads; every other name carries whatever emit was given, as unknown.
onceonce<N extends string>(name: N, handler: (ev: GameEvent<BusPayload<N>>) => void, opts?: ListenOptions): voidLike on but removed after the first delivery.
offoff<N extends string>(name: N, handler?: (ev: GameEvent<BusPayload<N>>) => void): voidRemove one handler, or all handlers for name.

Example

The tested fixture fixtures/app/agent.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