Skip to content
Moku Labs
@moku-labs/game pages

API reference

Every export of @moku-labs/game and its fourteen other entries, with signatures.

On this page

Entries

ImportRuns inFor
@moku-labs/gameanywhereThe engine: createApp, defineGame, the plugins, the helpers and the components.
@moku-labs/game/testinganywhereHeadless and isolated games, the fake clock, the in-memory save. No node: import.
@moku-labs/game/appanywheredefineGameApp, startMoment and the types of a game's index.ts and config.ts.
@moku-labs/game/app/pagethe browserstartPage, the page the generated .moku/main.ts calls.
@moku-labs/game/app/systemthe browser, the native shellThe system shell over the optional peer @moku-labs/system.
@moku-labs/game/cliNode and BunrunCli, the bin moku-game, and the dev page seams of the editor.
@moku-labs/game/visualNode and BunVisual tests: baselines of state, describe and pixels.
@moku-labs/game/assetsNode and BunAsset keys, the manifest, strings, the pack. The bin moku-game-assets.
@moku-labs/game/inspectanywhere, production includedRead a running game.
@moku-labs/game/controldev and debug buildsDrive a running game.
@moku-labs/game/hotthe Bun dev serverThe Bun plugin that hot swaps views.
@moku-labs/game/lintoxlintThe game lint rules, as an oxlint JS plugin.
@moku-labs/game/projectNode and BunThe project index. The bin moku-game-index.
@moku-labs/game/jsx-runtimeanywhereThe JSX runtime "jsxImportSource": "@moku-labs/game" resolves to.
@moku-labs/game/jsx-dev-runtimeanywhereThe same, for dev builds.

Bun only, ESM only. pixi.js is a peer dependency. fonts/* is a file pattern, not a module: it holds the MSDF body font and its licence.

Signatures below are simplified for reading. The installed dist/*.d.mts files are the source.

createApp and createPlugin

const createApp: (options?: {
  plugins?: readonly AnyPluginInstance[];
  config?: { orientation?: "portrait" | "landscape"; referenceSide?: number; referenceLong?: number };
  pluginConfigs?: { [pluginName: string]: Partial<PluginConfig> };
  onReady?: (ctx: AppCallbackContext) => void;
  onError?: (error: Error, ctx: AppCallbackContext) => void;
  onStart?: (ctx: AppCallbackContext) => void | Promise<void>;
  onStop?: (ctx: AppCallbackContext) => void | Promise<void>;
}) => App;

const createPlugin: (name: string, spec: PluginSpec) => PluginInstance;

createApp composes the logic plugins time, lifecycle, model, clock, flow, and debug, on the core plugins log and env. The screen set and the opt-in plugins come in plugins. A game made with defineGameApp never calls it: the shell does.

Global configDefaultDescription
orientation"portrait"The orientation the game is designed for.
referenceSide1080Short side of the reference resolution.
referenceLong1920The long side the layout needs inside the safe area.

createPlugin writes a game plugin bound to the engine's config and events. Types are inferred from the spec; never pass them. ctx.log and ctx.env come from @moku-labs/common.

defineGame

function defineGame<Types extends Flow.GameTypes>(): GameKit<Types>;

type GameTypes = {
  player: Json;
  session: Json;
  assets: string;
  strings: Record<string, unknown>;
  bundles?: string;
  scenes?: string;
  textStyles?: string;
  textFormats?: string;
  emitters?: string;
  hud?: string;
};

Returns the authoring helpers, typed by the game. A texture key the game does not have, or an effect id it did not declare, does not compile.

Kit memberTyped by
defineNode, defineFlow, defineFeatureplayer, session, scenes. Not root exports: they come from the kit.
projection, systemplayer, session.
sprite, Sprite, NineSlice, frames, Frames, sfx, music, defineStyle, Displacementassets.
defineBundles, load, ready, defineSceneassets, bundles, player, session.
tr, keystrings.
label, bind, defineTextStyles, defineTextFormatstextStyles, textFormats.
defineEmitter, Emitteremitters.
Overlay, HudParthud.
volumesplayer: the audio config's reader.
defineAnimation, play, defineComponent, defineTokens, popup, intrinsicsThe same helpers, from the kit.

The graph

function type<Payload = void>(): TypeTag<Payload>;
function exit<const Name extends string>(outcome: Name): Exit<Name>;
function to<const Target extends string, Payload, Output>(target: Target, map: (payload: Payload) => Output): Mapped<Target, Payload, Output>;
function slot<const Name extends string, const Tags extends OutcomeTags = {}>(name: Name, outcomes?: Tags): SlotNode<Tags, Name>;
function contribution(slot: SlotNode, spec: { flow: AnyFlow; order: number; when?: (snapshot: Snapshot) => boolean }): Record<string, ContributionSpec>;
function defineFeature(name: string, description: FeatureDescription): FeaturePlugin;
function flattenFeatures(list: readonly FeatureTree[]): FeatureTree[];

function schedule(moment: number | undefined): Descriptor<void>;
function guide(options: GuideOptions): Descriptor<void>;
function hint(kind: string, payload?: Json): Hint;
function defineEffect<Payload, Result>(kind: string, options?: { fast?: "run" | "skip"; cosmetic?: boolean }): Effect<Payload, Result>;

class SaveUnreadableError extends Error {
  savedVersion: number;
  schemaVersion: number;
}
ExportWhat it does
typeThe type tag of a payload: outcomes: { done: type<{ id: string }>() }.
exit, to, slotEdge targets: leave a sub-flow with an outcome, map a payload into a node, a node features contribute to.
contributionBuilds a contribute entry and checks the flow's outcomes against the slot at compile time.
defineFeatureTurns a feature description into a plugin. feature.logicOnly keeps only the logic keys, for headless tests.
flattenFeaturesThe plugin list of a feature tree, a parent before its children.
schedule, guide, hintEffect descriptors: arm the clock, narrow the gate for a tutorial step, a cosmetic hint for fx.emit.
defineEffectA game's own typed effect. effect.handle(fx, handler) registers its one handler.
SaveUnreadableErrorThrown by model.store.load() when the save cannot be read.

See Nodes and flows and Features.

Plugins

const timePlugin: PluginInstance<"time">;
const lifecyclePlugin: PluginInstance<"lifecycle">;
const modelPlugin: PluginInstance<"model">;
const clockPlugin: PluginInstance<"clock">;
const flowPlugin: PluginInstance<"flow">;
const worldPlugin: PluginInstance<"world">;
const rendererPlugin: PluginInstance<"renderer">;
const inputPlugin: PluginInstance<"input">;
const assetsPlugin: PluginInstance<"assets">;
const scenesPlugin: PluginInstance<"scenes">;
const animPlugin: PluginInstance<"anim">;
const i18nPlugin: PluginInstance<"i18n">;
const textPlugin: PluginInstance<"text">;
const uiPlugin: PluginInstance<"ui">;
const effectsPlugin: PluginInstance<"effects">;
const audioPlugin: PluginInstance<"audio">;
const platformPlugin: PluginInstance<"platform">;
const debugPlugin: PluginInstance<"debug">;
const screen: readonly [worldPlugin, rendererPlugin, inputPlugin, assetsPlugin, scenesPlugin, animPlugin, i18nPlugin, textPlugin, uiPlugin];

The instances are for plugins, depends and ctx.require in game plugins. Each has its page: time, lifecycle, model, clock, flow, world, renderer, input, assets, scenes, anim, i18n, text, ui, effects, audio, platform.

screen is the list of the nine screen plugins a game spreads in: plugins: [...screen, effectsPlugin, audioPlugin, platformPlugin], platform last.

debugPlugin is the editor's overlay from the game. It is always registered and a game never lists it; the name debug is reserved. app.debug.overlay has open({ mode?: "pill" | "card" }), close(), toggle() and isOpen(). Each answers true when an overlay took the call; without the agent flag, in a release build or a test, every member answers false.

World

function component<Value extends object>(name: string, defaults: Value, options?: { owned?: readonly (keyof Value)[] }): ComponentType<Value>;
function tag(name: string): TagType;
function resource<Value extends object>(name: string, defaults: Value | (() => Value)): ResourceType<Value>;
function mut<Value extends object>(component: ComponentHandle<Value>): Mut<Value>;
function system<const Terms extends readonly QueryTerm[]>(definition: {
  name: string;
  phase: "input" | "animate" | "layout" | "sync";
  query: Terms;
  scenes?: readonly string[];
  run(entities: Iterable<QueryTuple<Terms>>, ctx: { world; res; snapshot; time; resumed }): void;
}): SystemDefinition<Terms>;
function projection<Item>(spec: ProjectionSpec<Item>): ProjectionSpec<Item>;

const Layer: ComponentType<{ name: string }>;
const Order: ComponentType<{ value: number }>;
const Tree: ComponentType<{ node: DescriptionNode }>;
const Exiting: TagType;
const Now: ResourceType<{ now: number }>;
ExportWhat it is
component, tag, resourceDeclare a component with defaults, a tag with no data, a resource of every world.
mutA query term whose values are writable; every yielded entity is marked changed.
systemA system: one run a frame in its phase, skipped while its query is empty.
projectionTypes a projection spec: committed state to keyed views.
Layer, Order, TreeEngine components: the layer of a view, its draw order, the description of a JSX root.
ExitingThe tag of a view that plays its exit.
NowThe frame's moment: const { now } = res(Now) in a system.

See world.

Renderer

const Transform: ComponentType<{ x: number; y: number; rotation: number; scale: number }>;
const Sprite: ComponentType<SpriteValue>;
const NineSlice: ComponentType<NineSliceValue>;
const Shape: ComponentType<ShapeValue>;
const Parent: ComponentType<{ entity: Entity }>;
const Display: ComponentType<DisplayValue>;
const Overlay: ComponentType<OverlayValue>;
const HudPart: ComponentType<{ key: string }>;
function sprite(options: SpriteOptions): [SpriteValue, TransformValue];

function frameAt(ring: FrameRing, age: number): FrameRecord | undefined;
const PERF_LOG_VERSION = 1;
const PERF_GAP_BUCKETS = 81;
const PERF_CAUSES: readonly ["gap", "js", "gpu", "compile", "upload", "longFrame", "gc?"];
const MONITOR_SECTIONS: readonly ["gpu", "passes", "shaders", "problems", "memory", "systems", "nodes", "longFrames"];
const DEMAND_REASONS: readonly ["always", "first", "resize", "restore", "layers", "debug", "wake", "capture", "request", "mark", "verify", "none", "overlay", "retake"];
const PASS_KINDS: readonly ["canvas", "texture", "filter"];
const CANVAS_RESTARTS: readonly ["none", "first", "filter", "mask", "depth", "empty"];
const SHADER_WHATS: readonly ["compile", "module"];
const PROBLEM_RULES: readonly ["filter-still", "target-churn", "retakes"];
ExportWhat it is
Transform, Sprite, NineSlice, Shape, Parent, DisplayThe display components. Shape draws a rounded rectangle, a triangle or an arc.
OverlayOn the root of an overlay: keeps what is under it as one picture, draws the dim.
HudPartTags a HUD view an overlay keeps shown or lets fade.
spriteThe Sprite and Transform of a view in one call.
frameAtOne record of the performance monitor's frame ring.
PERF_LOG_VERSION, PERF_GAP_BUCKETS, PERF_CAUSES, MONITOR_SECTIONS, DEMAND_REASONS, PASS_KINDS, CANVAS_RESTARTS, SHADER_WHATS, PROBLEM_RULESThe constant lists of the performance log of an agent build, for tools that read it.

See renderer.

Input

const Tappable: ComponentType<{ intent: string; payload: Json }>;
const Pressable: ComponentType<{ intent: string; payload: Json }>;
const Draggable: ComponentType<{ payload: Json; carry?: unknown; aim?: { x: number; y: number } | null }>;
const DropTarget: ComponentType<{ intent: string; payload: Json }>;
const Swipeable: ComponentType<{ intent: string; payload: Json }>;
const Traceable: ComponentType<{ intent: string; payload: Json }>;
const Touchable: TagType;
const Held: TagType;
const Hovered: TagType;
const PointerOver: TagType;
const Pressed: TagType;
const Traced: TagType;
const Pointer: ResourceType<PointerValue>;

Tappable, Pressable, Draggable, DropTarget, Swipeable and Traceable are gestures as data. Touchable reaches the tap listeners and answers nothing. Held, Hovered, PointerOver, Pressed and Traced are tags the engine sets. Pointer is the followed pointer. See input.

Assets and scenes

function defineBundles<Key extends string>(map: Partial<Record<Key, { tier: Tier; files?: readonly string[]; release?: "budget" | "leave"; mipmaps?: true | readonly string[] }>>): BundleMap<Key>;
function load(bundle: string | readonly string[]): Descriptor<LoadResult | undefined>;
function ready(bundle: string | readonly string[]): Descriptor<boolean | undefined>;
function defineScene(id: string, scene: { bundle: string; bundles?: (state) => string[]; layers: LayerMap; projections: readonly Projection[]; music?: string; leave?: SceneLeave | false; enter?: SceneEnter }): SceneDefinition;

defineBundles describes the bundles of a feature. load and ready are effects a node awaits. defineScene declares a scene. See assets and scenes.

Animation

function defineAnimation<Tags extends SlotTags>(id: string, spec: { slots: Tags; build: (slots, tools: { at }) => Step }): AnimationDefinition<Tags>;
function sequence(...steps: readonly Step[]): Step;
function parallel(...steps: readonly Step[]): Step;
function stagger<Item>(items: readonly Item[], staggerMs: number, build: (item: Item, index: number) => Step): Step;
function tween<Value>(target: Target, component: ComponentHandle<Value>, to: Partial<Value>, options: { ms: number; ease?: Ease; delayMs?: number; additive?: boolean; space?: "local" | "root"; reduced?: "cut" | "keep"; segments?; repeat? }): Step;
function set<Value>(target: Target, component: ComponentHandle<Value>, patch: Partial<Value>): Step;
function wait(durationMs: number): Step;
function mark(name: string): Step;
function frames(target: Target, options: { keys: readonly string[]; fps: number; loop?: boolean }): Step;
function sfx(key: string, options?: { bus?: string; id?: string; fadeMs?: number }): SfxDescriptor;
function stopSfx(id: string, options?: { fadeMs?: number }): StopSfxDescriptor;
function haptic(kind: HapticKind): HapticDescriptor;
function use<Tags>(animation: AnimationDefinition<Tags>, slots: SlotValues<Tags>, tools?: BuildTools): Step;
function spawn(id: string, components: readonly ComponentValue[], options?: { layer?: string; order?: number }): Step;
function spawned(id: string): { spawned: string };
function play<Tags>(animation: AnimationDefinition<Tags>, slots: SlotValues<Tags>): PlayDescriptor;
function external(player: ExternalPlayer, clip: string): never;
function defineMotion(spec: { states?; transition?; on: { enter?; exit?; change? }; when?: "always" | "change" }): ProjectionMotion;
function cubicBezier(x1: number, y1: number, x2: number, y2: number): (t: number) => number;

const Animation: ComponentType<AnimationValue>;
const Frames: ComponentType<{ keys: readonly string[]; fps: number; loop: boolean; playing: boolean }>;
const AnimPlayer: ResourceType<{ play(animation, slots): PlayHandle }>;
ExportWhat it does
defineAnimationAnimation as frozen data, built from typed slots.
sequence, parallel, stagger, tween, set, wait, mark, frames, useSteps of a timeline.
sfx, stopSfx, hapticDescriptors audio and platform handle: as steps, or as effects a node awaits.
spawn, spawnedA temporary entity the timeline despawns when it ends, and a target for it.
playThe effect a node awaits: await fx(play(animation, slots)).
externalThrows until Spine arrives.
defineMotionThe enter, exit and change hooks of a projection or a ui tag.
cubicBezierA CSS-style easing curve for tween's ease.
Animation, FramesComponents: the engine's animation slot, and a sprite's frame loop for its whole life.
AnimPlayerThe resource a system plays through: res(AnimPlayer).play(animation, slots).

See anim.

Strings and text

function tr(key: string, params?: Record<string, Argument>): Message;
function messageArgument(value: unknown, intl: IntlKit): Part;
function messageDuration(ms: number): Partial<Record<Intl.DurationFormatUnit, number>>;

const Text: ComponentType<TextValue>;
function label(options: { text: string | Message; style: string; at: Point; anchor?: Point }): [TextValue, TransformValue];
function defineTextStyles(map: Record<string, TextStyleInput>): TextStyles;
function defineTextFormats(map: Readonly<Record<string, (value: number) => { content; style }>>): TextFormats;
function bind<Value extends object>(component: ComponentHandle<Value>, field: BindField<Value>, options?: { format?: "int" | "mm:ss" | "h:mm:ss" | "duration" }): TextBind;
const Countdown: ComponentType<{ until: number; left: number }>;
ExportWhat it does
trBuilds a frozen Message. No locale is read at the call site.
messageArgument, messageDurationCalled by the compiled strings modules. messageDuration(95_000) gives { minutes: 1, seconds: 35 }.
Text, labelWords on the screen, and the Text with its Transform in one call.
defineTextStyles, defineTextFormatsThe text styles and number formats of a feature.
bind, CountdownShow a numeric component field; count down to a clock moment.

The root also exports the types BindOptions, CountdownValue, TextFormat, TextFormats, TextFormatAnswer, TextFormatFunction and FormattedNumber. See i18n and text.

Interface

function defineComponent<Properties, Local, Outcomes>(name: string, spec: { local?: Local; outcomes?: Outcomes; view: (props: Properties, local: Local) => DescriptionNode }): ComponentDefinition;
function popup(component: PopupComponent, props: Properties, options?: { over?: string }): Descriptor<Answer>;
function defineStyle<const Given extends Style>(style: Given): Readonly<Given>;
function defineTokens<const Given extends Tokens>(tokens: Given): Readonly<Given>;
function resolve(style: Style | undefined, flags: StyleFlags, viewport: ViewportSize): ResolvedStyle;
function music(key: string | null, options?: { fadeMs?: number }): Descriptor<void>;

const Box: ComponentType<{ x: number; y: number; w: number; h: number }>;
const LocalWrite: ComponentType<LocalWriteValue>;
const ScrollState: ComponentType<{ offset: number; max: number; moving: boolean }>;
const DebugTrigger: TagType;
ExportWhat it does
defineComponentAn interface component. With outcomes it is a popup.
popupThe effect that opens a popup and resolves with the pressed button's answer.
defineStyle, defineTokens, resolveThe style vocabulary, shared values, and a style computed for given flags.
musicThe effect that switches the music track.
Box, LocalWrite, ScrollStateComponents of ui elements: the rect, a write into local state, a scroll's offset.
DebugTriggerFive taps on that view open the editor's overlay on a page with the agent.

See ui and audio.

Effects

function defineEmitter(id: string, config: EmitterConfig): EmitterDefinition;
const Emitter: ComponentType<{ effect: string; active: boolean }>;
function defineFilter<Uniforms>(id: string, spec: { wgsl: string; glsl: string; uniforms?: Uniforms; passes?; padding?; inputs?; fallback?; cache?; area? }): FilterComponent;
const Glow: FilterComponent<GlowValue>;
const Outline: FilterComponent<OutlineValue>;
const Blur: ComponentType<BlurValue>;
const ColorMatrix: ComponentType<ColorMatrixValue>;
const Noise: ComponentType<NoiseValue>;
const Displacement: ComponentType<DisplacementValue>;
const Alpha: ComponentType<AlphaValue>;

defineEmitter and Emitter are particles; defineFilter makes a filter component from a WGSL body and its GLSL twin. Glow, Outline, Blur, ColorMatrix, Noise, Displacement and Alpha are the built-in filters. All of them need effectsPlugin. See effects.

Platform

const HAPTIC_KINDS: readonly ["light", "medium", "heavy", "selection", "success", "warning", "error"];

HAPTIC_KINDS is the frozen list of haptic kinds; HapticKind is its type. The types PlatformProvider and BackResult are root exports too. See platform.

Type namespaces

Time, Lifecycle, Model, Clock, Flow, World, Renderer, Input, Assets, Scenes, Anim, I18n, TextTypes, Ui, Audio, Effects, Platform and Debug hold all public types of one plugin. Text is the component, so the text types are TextTypes. At run time each is an empty object.

types.ts
import type { Anim, Clock, Flow, I18n, Lifecycle, Model, Time } from "@moku-labs/game";

export type Step = Flow.RouteStep; // { at, intent, payload? } or { at, result }
export type Context = Flow.NodeContext;
export type Save = Model.Json;
export type Provider = Model.PlayerStateProvider;
export type Source = Clock.ClockSource;
export type Phase = Time.Phase;
export type Reason = Lifecycle.PauseReason;
export type Target = Anim.Target;
export type Text = I18n.Message;

@moku-labs/game/testing

function createHeadless(app: HeadlessApp): Promise<HeadlessGame>;
function runRepro(app: HeadlessApp, repro: { player: Json; session?: Json; rng?: RngState; checkpoint?: string; route: Flow.RouteStep[] }): Promise<{ path: string[]; player: Json; session: Json }>;
function stepFrames(app: HeadlessApp, count: number, deltaMs: number): void;
function fakeClock(start?: number): ClockSource & { advance(ms: number): void; set(moment: number): void };
function fakeFx(options?: { answer?: (descriptor: Descriptor) => unknown }): { fx; awaited; started; emitted };
function memory(fixture?: { state: SaveDoc; version: number }): PlayerStateProvider & { calls: ProviderCall[] };
function saveOf(player: Json, seed?: number): SaveDoc;
function isolate(feature: FeaturePlugin, options: { flow: AnyFlow; player: Json; as?: string; shared?; stubs?; plugins?; session?: Json; seed?: number }): (seams?) => App;
function stub<const Name extends string>(outcome: Name, payload?: Json): Stub<Name>;
ExportWhat it does
createHeadlessTakes an app that is not started, sets fast mode, starts it and runs the graph. Resolves at the first rest. HeadlessGame has walk, answer, state, history, stop.
runReproReplays a route from a given state. The app keeps running.
stepFramesCalls app.time.step(deltaMs) count times.
fakeClockA clock source for tests. advance fires due timers synchronously.
fakeFxThe fx of a node context for a unit test of one node.
memory, saveOfThe in-memory save provider with calls, and a save document for a fixture.
isolate, stubPlay one feature without the rest of the game; stand in for a node it does not run.

See Testing.

@moku-labs/game/app

function defineGameApp(definition: {
  flow: AnyFlow;
  player: Json;
  session: Json;
  safeNode?: string;
  orientation?: "portrait" | "landscape";
  referenceSide?: number;
  referenceLong?: number;
  seed?: number;
  shared?: FeaturePlugin;
  features?: readonly FeaturePlugin[];
  plugins?: readonly AnyPluginInstance[];
  headless?: { features?: readonly FeaturePlugin[]; plugins?: readonly AnyPluginInstance[] };
  pluginConfigs?: GamePluginConfigs;
}): GameApp;
const startMoment = 1_000_000;

defineGameApp returns the game, { headless(seams?), screen(seams?) }, and creates no app. Each call gives a fresh { app, clock, provider }, the app not started, on fakeClock(startMoment) and a fresh memory() save. startMoment is the start of that default fake clock. The types GameConfig, GameDefinition, GameApp, GameHandle, GamePluginConfigs, HeadlessSeams, ScreenSeams, MemoryProvider, Scenario, PageAgent, SaveKind and SystemName come from this entry. See The game shell.

@moku-labs/game/app/page

function startPage(game: GameApp, config: GameConfig, options?: { scenarios?; system?; agents?; devModules? }): Promise<StartedPage>;

startPage is the page the generated .moku/main.ts calls. It resolves the config, builds the system shell and the save, makes game.screen() with the canvas on #game, starts the app, runs the graph, and flushes the save on pagehide. It reads ?player=<name> scenarios and ?renderer=webgl.

@moku-labs/game/app/system

function systemShellOf(modules: SystemModules): SystemShellFactory;
function fromSystem(system: SystemSlice): PlatformProvider;
function storeSave(store: StoreSlice, key: string, report: (problem: string) => void): PlayerStateProvider;
function createSystemApp(names: readonly SystemName[], namespace: string, modules: SystemModules): Promise<SystemApp>;

systemShellOf takes one loader per system plugin the system list of config.ts names. The generated main.ts passes them, so a web-only page bundles no system code. fromSystem turns the system app into the PlatformProvider of platform, storeSave the system store into the save provider of save: "store", and createSystemApp builds the system app. See Native.

@moku-labs/game/cli

function runCli(argv: readonly string[]): Promise<number>;
function preparePage(root: string, options?: { agents?: readonly string[]; preload?: readonly string[]; servePlugins?: readonly string[] }): Promise<{ html: string; bunfig: string; engine: EngineFingerprint }>;
function watchKeys(root: string, options?: { onError?: (message: string) => void }): Promise<{ close(): void }>;
function watchEngine(root: string, options?: { onChange?: (change: { before; after; restart: boolean }) => void; onError? }): { close(): void };
ExportWhat it does
runCliThe bin moku-game: dev, build, native, keys, pack, visual, help.
preparePageWrites the dev page of moku-game dev into <root>/.moku/, for a server the caller starts. The editor calls it.
watchKeysKeeps generated/ fresh while that page is open.
watchEngineTells when the installed engine changed: restart the page server, or run bun install.

@moku-labs/game/visual

function defineVisualTest(name: string, test: { start: VisualStart; steps: readonly VisualStep[]; webgl?: boolean }): VisualTest;
function runVisualTests(setup: VisualSetup, tests: readonly VisualTest[], options?: VisualOptions): Promise<VisualReport>;
function parseVisualArgv(argv: readonly string[]): VisualFlags;

defineVisualTest describes one test: a start and steps, each a /control command or { checkpoint: name }. runVisualTests runs them against the baselines. parseVisualArgv reads the flags of moku-game visual. See Testing.

@moku-labs/game/assets

function scanAssets(options: ScanOptions): Promise<ScanResult>;
function emitKeys(manifest: Manifest): string;
function emitManifest(manifest: Manifest): string;
function compileStrings(root: string, out: string, options?: CompileOptions): Promise<CompileReport>;
function checkStrings(root: string, out: string): Promise<boolean>;
function exportStrings(root: string, dir: string, options?: ExchangeOptions): Promise<ExportReport>;
function importStrings(root: string, dir: string, options?: ImportOptions): Promise<ImportReport>;
function packAssets(options: PackOptions): Promise<PackResult>;
function runCli(argv: string[], strings: StringsTools, ui?: ScanUi): Promise<number>;
ExportWhat it does
scanAssetsScans the assets/ of every feature and layer into a manifest.
emitKeys, emitManifestThe typed keys module and the manifest file, as text.
compileStrings, checkStringsCompile the strings/<locale>.json files; check the output is up to date.
exportStrings, importStringsExchange strings with translators.
packAssetsThe production build: atlas pages, content-hashed names.
runCliThe bin moku-game-assets. A game with config.ts runs moku-game keys and moku-game pack instead.

See Assets.

@moku-labs/game/inspect

function read<Output>(app: App, source: Source<Output>, input?: Input): Output;
function watch<Output>(app: App, source: Source<Output>, input: Input, fn: (value: Output) => void): () => void;
function defineSource(source: { id: string; title: string; input: InputSchema; changes: "frame" | "commit" | "edge"; read(app, input): Output }): Source;
const sources: Readonly<{ graph; position; history; tainted; cheats; model; entities; projections; ui; locate; render; sounds; audioMuted; effects; filters; assets; log; explain; diff; schema; at }>;

read reads a source once. watch reads on the signals phase when the source changes and returns the stop function. defineSource declares a game's own source. sources is the frozen catalogue; each id is game.<key>. Every source only reads, so the door is safe in a production build. See Doors for the editor.

@moku-labs/game/control

function run<Output>(app: App, command: Command<Output>, input?: Input): Promise<{ value: Output; state: { path: string; frame: number; tainted: boolean } }>;
function defineCommand(command: { id: string; title: string; input: InputSchema; effect: "read" | "route" | "cosmetic" | "cheat" | "raw"; run(app, input); overlay?: "toggle" | "button" }): Command;
function controlRefused(): ControlRefusedError;
function controlReadOnly(id: string): ControlReadOnlyError;
const commands: Readonly<{ answer; tap; drag; key; walk; bookmark; restore; saveExport; saveImport; step; pause; resume; capture; debug; reducedMotion; fill; scroll; timeScale; frameRate; mute; trace; filter }>;

run runs a command and resolves { value, state }. Commands run in dev and debug builds only. defineCommand declares a game's own command. controlRefused() is the error a command body throws outside a dev build; controlReadOnly(id) the error of a read-only debug page. commands is the frozen catalogue. A cheat or raw command taints the session. See Doors for the editor.

@moku-labs/game/hot

function hot(options?: { include?; exclude? }): HotPlugin;

hot makes the Bun plugin that hot swaps view files in a running page, for a game with a server of its own and another layout: export default hot({ include, exclude }). The default export is the plugin itself: a bunfig.toml lists @moku-labs/game/hot under [serve.static] plugins. moku-game dev lists it for you. See Hot swap.

@moku-labs/game/lint

function aliasTargetsOf(tsconfig: string, root: string, specifier: string): string[];
function globToRegExp(glob: string): RegExp;

The default export is the oxlint JS plugin moku-game with eleven rules: lazy-imports, native-imports, dev-imports, no-module-state, determinism, rules-siblings, static-keys, layer-imports, feature-door, sub-features, test-suffix. aliasTargetsOf answers what the layout rules read for an import alias. globToRegExp turns a glob into the expression a rule tests a path against. See Lint.

@moku-labs/game/project

function openProject(options: { root: string; manifest?: string; tsconfig?: string; debounceMs?: number }): Promise<ProjectApi>;
function runCli(argv: readonly string[], ui?: IndexUi): Promise<number>;

openProject indexes where every engine id of a game is defined: index, find(key), watch(onIndex), changed(path), close(). It needs the optional peer typescript. runCli is the bin moku-game-index.

JSX runtime

function jsx(type: unknown, props: JsxProperties, key?: string): DescriptionNode;
const jsxs: typeof jsx;
function jsxDEV(type: unknown, props: JsxProperties, key?: string, isStatic?: boolean, source?: unknown, self?: unknown): DescriptionNode;
function Fragment(props: JsxProperties): DescriptionNode;

jsx, jsxs and Fragment come from /jsx-runtime; /jsx-dev-runtime adds jsxDEV. They build plain description nodes, and the entry also carries the JSX namespace with the intrinsic tags. A game never imports them by hand: it sets "jsx": "react-jsx" and "jsxImportSource": "@moku-labs/game".

Example

A headless test of a whole game, from the definition to the committed state.

dice.test.ts
import { defineGame, type } from "@moku-labs/game";
import { defineGameApp, startMoment } from "@moku-labs/game/app";
import { createHeadless } from "@moku-labs/game/testing";

const { defineNode, defineFlow } = defineGame<{
  player: { coins: number };
  session: { rolls: number };
  assets: string;
  strings: Record<string, unknown>;
}>();

const home = defineNode({ outcomes: { roll: type() }, rest: true, checkpoint: true });
const roll = defineNode({
  outcomes: { done: type() },
  run: ({ player, session, rng, out }) => {
    player.coins += rng.stream("dice").range(1, 6);
    session.rolls += 1;
    return out.done();
  }
});

const mainFlow = defineFlow("main", {
  nodes: { home, roll },
  start: "home",
  edges: { home: { roll: "roll" }, roll: { done: "home" } }
});

const game = defineGameApp({ flow: mainFlow, safeNode: "home", player: { coins: 0 }, session: { rolls: 0 } });

const { app, clock } = game.headless();
const run = await createHeadless(app);

clock.now() === startMoment; // true
await run.walk([{ at: "home", intent: "roll" }, { at: "home", intent: "roll" }]);
app.model.store.snapshot().session; // { rolls: 2 }
await run.stop();