Skip to content

Events

The simulation bus is sage-core's. Every simulation owns one, at simulation.bus, and every entity registers on it. The delivery rules are on the Events page; this page is the type and method reference.

typescript
import { EventBus } from '@skewedaspect/sage-core';
import type { EventMap, GameEvent, Subscriber } from '@skewedaspect/sage-core';

@skewedaspect/sage re-exports EventMap, EventSubscription, and GameEvent under the name GameEntityEvent. EventBus and Subscriber come from @skewedaspect/sage-core.

EventMap

An empty interface at sage-core's package root. A game augments it once, and the augmentation is the event vocabulary:

typescript
declare module '@skewedaspect/sage-core'
{
    interface EventMap
    {
        'door:opened' : { doorID : string };
        'action:jump' : boolean;
    }
}

export {};

The augmentation must name @skewedaspect/sage-core. A declare module merges only into the module that declares the interface, so augmenting @skewedaspect/sage does nothing.

GameEvent

typescript
type GameEvent = {
    [Type in keyof EventMap & string] : {
        type : Type;
        senderID : string;
        targetID ?: string;
        payload : EventMap[Type];
    }
}[keyof EventMap & string];

A union over every declared event, one member per type. Narrowing on type narrows payload. Until a game augments the map, GameEvent is never.

EventSubscription

An exact event type, or a wildcard pattern over a declared type's prefixes:

typescript
type EventSubscription = (keyof EventMap & string) | `${ EventPrefix<keyof EventMap & string> }:*`;

With door:opened and door:lock:opened declared, door:* and door:lock:* are valid subscriptions. A pattern spans every remaining segment, so door:* matches both.

Subscriber

typescript
interface Subscriber
{
    readonly id : string;
    receive(event : GameEvent) : boolean;
}

type Unsubscribe = () => void;

Every GameEntity is a Subscriber. Anything else holding the simulation may implement one and register it.

EventBus

MethodMeaning
register(subscriber, subscriptions)Registers under subscriber.id and answers with the teardown. A second registration under an ID already registered throws.
emit(event)Queues the event. Nothing is delivered inline.
drain()Delivers the queue, first in, first out, in passes. The simulation calls this at the end of each fixed step.

The teardown is idempotent, and a stale teardown never evicts a later registration that took the same ID.

typescript
const unsubscribe = simulation.bus.register(
    {
        id: 'door-log',
        receive(event)
        {
            console.log(event.type, event.senderID);
            return false;
        },
    },
    [ 'door:*' ]
);

simulation.bus.emit({ type: 'door:opened', senderID: 'level', payload: { doorID: 'door-3' } });

emit takes a whole event, sender included. GameEntity.emit(type, payload, targetID?) is the form that forces the entity's own ID as the sender.

Delivery

A targeted event goes to the subscriber registered under targetID, and to nobody else. A broadcast goes to every exact subscriber for the type in registration order, then every pattern subscriber whose pattern matches, in registration order, each subscriber once.

The drain

One drain pass delivers every event queued at the start of that pass; handler emissions form the next pass. A drain that exceeds drainPassLimit passes discards the remaining queue and raises. A handler that throws never stops delivery to the rest; the errors collect and are raised together as an AggregateError when the drain completes, with the drain pass error as one more entry when the limit was exceeded.

Entity subscriptions

A behavior declares its subscriptions in static events, and the entity registers once for all of them under its own ID. Within one entity, delivery is a consume chain in attachment order, stopped by an onEvent returning true. See Behaviors.

Released under the MIT License.