import { Container } from 'pixi.js'; interface FlowDoc { version: 1; id: string; /** Sound cue registry: alias → definition. Steps refer to cues, never to files. */ cues?: Record; /** Event name (host lifecycle: spinStart/reelStop/result/enterMode/…) → steps. */ on: Record; } interface CueDef { /** One source alias, or several — with `rotate` the runner cycles them (anti-repetition). */ src: string | string[]; channel?: 'music' | 'sfx'; loop?: boolean; rotate?: boolean; /** ± random playback-rate jitter (0.06 = ±6%). */ jitter?: number; volume?: number; } /** * Fields every step carries regardless of kind. * * `id` exists because steps used to be addressed purely by position — three different * positional schemes across the codebase — so any insert or delete invalidated every * sibling's path, and a trace entry could not say WHICH step it came from. It lives on the * step rather than in a document-level map on purpose: the editor→game doc push copies only * `on` and `cues`, so a new top-level key would be silently dropped and never saved. */ interface StepCommon { /** Stable identity. Assigned by `ensureStepIds` when a hand-written doc omits it. */ id?: string; /** Editor-only: where the node sits on the flow canvas. Absent → auto-laid-out. */ ui?: { x: number; y: number; }; } /** Core step vocabulary; plugins contribute additional `do` kinds through the registry. */ type Step = TweenStep | SoundStep | SetStateStep | SetVarStep | SetPropsStep | CountUpStep | WaitStep | IfStep | ParallelStep | SeqStep | ForEachStep | CodeStep | PluginStep; interface TweenStep extends StepCommon { do: 'tween'; node: string; /** Target numeric view properties (alpha, x, y, scale, rotation, …). */ to: Record; ms: number; ease?: string; } interface SoundStep extends StepCommon { do: 'sound'; cue: string; action?: 'play' | 'stop'; } interface SetStateStep extends StepCommon { do: 'setState'; node: string; state: string | null; } interface SetVarStep extends StepCommon { do: 'setVar'; name: string; value: unknown; } /** Transient prop write on the live instance (presentation) — does NOT touch the doc. */ interface SetPropsStep extends StepCommon { do: 'setProps'; node: string; props: Record; } interface CountUpStep extends StepCommon { do: 'countUp'; node: string; /** Instance prop receiving the formatted value (default 'value' — badge prefabs). */ prop?: string; from?: number | string; /** Number or a '$path' reference into the fire() ctx (e.g. '$win'). */ to: number | string; ms?: number; format?: 'int' | 'space'; } interface WaitStep extends StepCommon { do: 'wait'; ms?: number; until?: 'tap'; } interface IfStep extends StepCommon { do: 'if'; /** Micro-expression over the fire() ctx: `win >= 100`, `mode === 'fs'`, or a bare truthy name. */ when: string; then: Step[]; else?: Step[]; } interface ParallelStep extends StepCommon { do: 'parallel'; /** Independent tracks, awaited together. */ steps: Step[][]; } interface SeqStep extends StepCommon { do: 'seq'; steps: Step[]; } /** * Iterate a ctx collection — THE dynamic fan-out every studied game needs (staggered jar * flights, per-cell transmutes, cascade histories). Nested steps see the current element * as `$` (default `$item`) and its index as `$Index`. */ interface ForEachStep extends StepCommon { do: 'forEach'; /** '$path' into the ctx (or an inline array). */ items: string | unknown[]; as?: string; mode?: 'sequential' | 'parallel'; /** Per-item delay: parallel start stagger / sequential inter-item gap (turbo-scaled). */ staggerMs?: number; steps: Step[]; } /** Escape hatch: a named code choreography registered on the runner. */ interface CodeStep extends StepCommon { do: 'code'; ref: string; args?: Record; } /** A plugin-contributed kind: open shape, but it still carries the common fields. */ interface PluginStep extends StepCommon { do: string; [key: string]: unknown; } interface TraceEntry { /** ms since fire() (real time, unscaled). */ t: number; do: string; /** * Which step produced this entry. Without it the trace only says a `tween` ran — two * identical tweens are indistinguishable — so a canvas cannot highlight what is running * and a scrub cannot point at a step. */ stepId?: string; node?: string; info?: string; } interface FlowTrace { event: string; turbo: number; skipped: boolean; /** Set on scrub replays: the run executed instantly and halted after this entry index. */ haltedAtEntry?: number; durationMs: number; entries: TraceEntry[]; } type Orientation = 'landscape' | 'portrait'; interface SceneDoc { version: 1; /** Stable document id (usually the game id). */ id: string; /** * Another scene this one builds on. The resolved doc is base ⊕ this, merged by node id, * so a per-mode scene carries only its delta instead of a copy of the whole board. * Resolved by `resolveExtends` before the doc reaches the engine. */ extends?: string; /** Design space the layout rules are authored in (viewport arrives in these units). */ design: { width: number; height: number; }; root: SceneNode; } interface SceneNode { /** Stable id — flow, code and patches address the node by it. Unique per doc. */ id: string; /** Node kind resolved through the registry (built-in or plugin-contributed). */ type: string; /** Human label for the outliner; never used for addressing. */ name?: string; /** Kind-specific properties (validated by the contribution's schema). */ props?: Record; layout?: LayoutRule; /** Per-orientation override merged over the base node before layout. */ responsive?: Partial>; /** * Named variants toggled at runtime (flow `setState`, editor, code) — e.g. a reel * frame's `anticipation` / `bonus` looks. A state is an override, not a new node. */ states?: Record; /** Id of another node in this doc used as this node's mask. */ mask?: string; /** Filters applied to the node's view; kinds resolve through the registry (core: blur). */ filters?: FilterSpec[]; /** * Tiny condition over runtime vars, e.g. `"mode === 'free_spins'"`. Supported form: * ` ` with `=== !== >= <= > <`. Anything richer belongs in flow/code. */ visibleWhen?: string; /** Entry in the shared anchor vocabulary flow/animation use to address the node. */ anchorName?: string; /** Order = z-order. */ children?: SceneNode[]; } /** A filter instance: kind + kind-specific params (validated by the contribution). */ interface FilterSpec { kind: string; [param: string]: unknown; } /** Partial node applied on top of the base for an orientation or a named state. */ interface NodeOverride { layout?: LayoutRule; props?: Record; visible?: boolean; } interface LayoutCommon { /** Content anchor [ax, ay] in 0..1 (applied via pivot on the node's natural size). */ anchor?: [number, number]; /** Final hand-tuned nudge in design px, applied after the rule resolves. */ pxNudge?: { x?: number; y?: number; }; /** Static rotation in radians (choreographed rotation belongs to flow/animation). */ rotation?: number; /** * `'viewport'` resolves this rule against the LIVE viewport even when the node lives * inside a nested coordinate space — the engine converts the result into the space's * coordinates. The paper-duel logo idiom: screen-pinned, but rendered at a diorama * z-depth. */ space?: 'viewport'; } /** Fixed design-space coordinates. */ interface AbsoluteLayout extends LayoutCommon { mode: 'absolute'; x: number; y: number; scale?: number; rotation?: number; } /** * Position as a fraction of the live viewport. Sizing, strongest first: * `widthFrac`/`heightFrac` size the node to that fraction of the viewport (contain when * both are given) — the "frame is 92% of the screen in portrait" idiom; otherwise * `scaleWith` multiplies `scale` by the viewport/design ratio; otherwise plain `scale`. */ interface ViewportFractionLayout extends LayoutCommon { mode: 'viewport-fraction'; xFrac: number; yFrac: number; scale?: number; /** Multiply `scale` by viewport/design ratio: width | height | min | max. */ scaleWith?: 'width' | 'height' | 'min' | 'max'; /** Size the node to this fraction of the viewport width (scale derived from natural size). */ widthFrac?: number; /** Size the node to this fraction of the viewport height. */ heightFrac?: number; /** With both fracs: contain (default) keeps aspect, stretch distorts to fill exactly. */ fit?: 'contain' | 'stretch'; } /** * Center-crop fill of the current space (backgrounds, diorama layers). `scale` multiplies * the computed cover scale (the paper-duel `e` idiom); `pxNudge` offsets from center. */ interface CoverLayout extends LayoutCommon { mode: 'cover'; scale?: number; } /** * Place inside another node's laid-out rect using fractions of that rect. * `use: 'inner'` reads the target's declared `props.inner` fractions (reel frames * declare where their hollow is) instead of spelling the fractions twice. * When a box (`wFrac`/`hFrac` or `use`) is given the node is contain-fitted into it * (`fit: 'stretch'` distorts instead). */ interface FrameFractionLayout extends LayoutCommon { mode: 'frame-fraction'; frame: string; xFrac?: number; yFrac?: number; wFrac?: number; hFrac?: number; use?: 'inner'; fit?: 'contain' | 'stretch'; } /** * Center of a reel-grid cell (badges, per-cell VFX slots). `grid` defaults to the sole * reelGrid. By default the node scales with the grid (sizes authored in grid-design px); * `scaleWithGrid: false` keeps it in absolute design units. */ interface GridCellLayout extends LayoutCommon { mode: 'grid-cell'; grid?: string; col: number; row: number; scale?: number; scaleWithGrid?: boolean; } /** Pin to an edge/center of another node's laid-out rect. */ interface PinLayout extends LayoutCommon { mode: 'pin'; to: string; edge: PinEdge; offset?: [number, number]; scale?: number; } type PinEdge = 'top-left' | 'top-center' | 'top-right' | 'center-left' | 'center' | 'center-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'; type LayoutRule = AbsoluteLayout | ViewportFractionLayout | CoverLayout | FrameFractionLayout | GridCellLayout | PinLayout; type ScenePatch = { op: 'set-props'; id: string; props: Record; } /** * Targets `orientation` (responsive) when given, else the base rule. `layout: null` clears * that slot — which is how undo removes an override the edit created. */ | { op: 'set-layout'; id: string; layout: LayoutRule | null; orientation?: Orientation; } /** Rename the outliner label. `name` is presentation only — addressing is always by id. */ | { op: 'set-name'; id: string; name: string | undefined; } | { op: 'set-state'; id: string; state: string | null; } | { op: 'set-var'; name: string; value: unknown; } /** Insert `node` under `parent` at `index` (default: append). Ids must be unused. */ | { op: 'add-node'; parent: string; node: SceneNode; index?: number; } | { op: 'remove-node'; id: string; } /** Reparent/reorder `id` under `parent` at `index` (same parent = pure reorder). */ | { op: 'move-node'; id: string; parent: string; index?: number; } /** Deep-copy `id` (all descendant ids suffixed to stay unique) as its next sibling. */ | { op: 'duplicate-node'; id: string; }; /** * Outcome of applying a patch. Rejections used to be console-only warnings inside the * game, so an editor (or agent) saw "ok" for a patch that never applied — the reason now * travels back to the caller. */ type PatchResult = { ok: true; /** * The patch that undoes this one. Applying it restores the pre-patch state exactly, * which is what the editor's undo stack replays. Absent only when the op had no effect. */ inverse?: ScenePatch; } | { ok: false; error: string; }; /** Outliner row — the tree the inspector and the agent both read. */ interface OutlineNode { id: string; type: string; name?: string; anchorName?: string; visible: boolean; state: string | null; children: OutlineNode[]; } type FieldKind = 'number' | 'text' | 'boolean' | 'color' /** One of `options`. */ | 'enum' /** A texture/audio alias from the game's asset manifest — the editor offers a picker. */ | 'asset' /** The id of another node in this doc — the editor offers the node list. */ | 'nodeRef' /** Nested record described by `fields`. */ | 'object' /** Anything else: edited as raw JSON. */ | 'json'; interface FieldSchema { kind: FieldKind; /** Human label (defaults to the key). */ label?: string; /** What the field does — inspector tooltip AND the agent's documentation. */ doc?: string; /** * Value the runtime uses when the field is absent. The inspector shows it as the * effective value so "not set" never looks like "zero". */ default?: unknown; /** `enum`: the allowed values. */ options?: readonly string[]; /** `number`: bounds and input step. */ min?: number; max?: number; step?: number; /** `asset`: which kind of media this field accepts (filters the picker). */ accept?: 'image' | 'audio' | 'spritesheet'; /** `nodeRef`: restrict the picker to nodes of this type (e.g. `reelFrame`). */ ofType?: string; /** `object`: the nested fields. */ fields?: PropsSchema; /** Lower sorts first in the inspector; unordered fields follow, alphabetically. */ order?: number; } /** A node type's (or prefab's) authorable props, keyed by prop name. */ type PropsSchema = Record; interface Size { width: number; height: number; } /** Live counterpart of one scene node, produced by its contribution. */ interface NodeInstance { view: Container; /** Natural (unscaled) content size — drives anchor, cover and box fitting. */ measure(): Size; /** Apply a props patch in place; absent → the engine rebuilds the node instead. */ applyProps?(props: Record): void; /** Scene-local center of a grid cell (reel-grid kinds only; enables `grid-cell` layout). */ gridCell?(col: number, row: number): { x: number; y: number; }; destroy?(): void; } /** Skeleton a palette/agent uses to spawn a fresh node of this kind (id is assigned by the caller). */ interface NodeDefaults { /** Human label for the palette button (defaults to the kind). */ label?: string; name?: string; props?: Record; layout?: SceneNode['layout']; } /** One palette entry — a spawnable node kind or prefab, with its skeleton. */ interface PaletteEntry { kind: string; label: string; /** 'prefab' entries spawn `{type:'prefab', props:{prefab:kind}}`; others spawn `{type:kind}`. */ isPrefab: boolean; agentDoc?: string; defaults: NodeDefaults; } interface SceneHandle { view: Container; node(id: string): Container | undefined; instance(id: string): NodeInstance | undefined; anchor(name: string): Container | undefined; tree(): OutlineNode; layout(width: number, height: number): void; setVar(name: string, value: unknown): void; setState(id: string, state: string | null): void; /** Apply an edit. Returns `{ok:false, error}` when the patch was rejected. */ patch(patch: ScenePatch): PatchResult; /** The live doc (mutated by patches) — serialize this to persist the scene. */ doc(): SceneDoc; /** Spawnable palette (node kinds with defaults + prefabs) — feeds the editor add-menu. */ palette(): PaletteEntry[]; /** * Props schemas by node kind (prefabs keyed `prefab:`) — what the inspector renders * its controls from, and what an agent reads as the authoring contract. */ schemas(): Record; /** * Re-apply every node's doc-effective props, wiping transient presentation writes * (flow tweens/count-ups). "Settle = the doc" — scrub replays call this first. */ resetProps(): void; destroy(): void; } /** What the audio adapter receives — cue already resolved (rotation/jitter applied). */ interface ResolvedCue { cue: string; src: string; channel: 'music' | 'sfx'; loop: boolean; rate: number; volume: number; } interface FlowAudioAdapter { play(cue: ResolvedCue): void; stop(cueName: string): void; } interface CreateFlowRunnerOptions { scene: SceneHandle; audio?: FlowAudioAdapter; plugins?: FlowPlugin[]; /** Named code choreographies for `{do:'code', ref}` escape hatches. */ code?: Record) => Promise | void>; /** Live trace feed (the log/timeline view); the full trace is also returned by fire(). */ onTrace?: (event: string, entry: TraceEntry) => void; log?: (msg: string) => void; } interface FireOptions { /** Scrub replay: run instantly (no waits, anims jump, audio muted) and halt after entry N. */ replay?: { untilEntry?: number; }; } interface FlowRunner { fire(event: string, ctx?: Record, opts?: FireOptions): Promise; /** Scrub: re-fire `event` with its LAST ctx, instantly, halting after trace entry N. */ replay(event: string, untilEntry: number): Promise; /** Complete every active run in zero time (settle). */ skip(): void; /** 1 = normal; 2 = twice as fast. Applies to waits, tweens, count-ups. */ setTurbo(factor: number): void; /** Host reports a user tap (releases `wait until:'tap'`). */ tap(): void; events(): string[]; /** * The step kinds this runner accepts, with their authoring metadata. The editor lives in * another origin and cannot reach the registry, so the contract travels through here — * before this, field lists were hardcoded in the editor (twice, and already drifting). */ stepKinds(): Array<{ kind: string; schema?: PropsSchema; agentDoc?: string; }>; destroy(): void; } /** Per-run runtime handed to step executors (core and plugin alike). */ interface FlowRuntime { scene: SceneHandle; ctx: Record; audio: FlowAudioAdapter; /** Turbo-scaled, skip-aware sleep. */ wait(ms: number): Promise; waitTap(): Promise; /** Execute a nested step list (if/parallel/seq bodies). */ run(steps: Step[]): Promise; /** True once skip() hit this run — executors should jump straight to end states. */ readonly skipped: boolean; /** True for scrub replays — like skipped (jump to end states), plus audio stays muted. */ readonly instant: boolean; readonly turbo: number; /** Resolve '$path' references against ctx; other values pass through. */ resolve(value: unknown): unknown; /** Evaluate an if-condition against ctx. */ when(expr: string): boolean; /** A runtime whose ctx is overlaid with extra values (forEach exposes `$item` this way). */ child(ctxPatch: Record): FlowRuntime; trace(entry: Omit): void; /** The node's live view (tween targets). */ view(id: string): Container | undefined; log(msg: string): void; codeRef(ref: string): ((rt: FlowRuntime, args: Record) => Promise | void) | undefined; playCue(name: string, action: 'play' | 'stop'): void; } declare function validateFlowDoc(doc: FlowDoc, registry: FlowStepRegistry): string[]; declare function createFlowRunner(doc: FlowDoc, opts: CreateFlowRunnerOptions): FlowRunner; interface FlowStepContribution { kind: string; /** Authorable fields — drives the editor's controls and documents the step for an agent. */ schema?: PropsSchema; /** 3–10 lines for the agent: when to use the step, patterns, anti-patterns. */ agentDoc?: string; run(step: Step, rt: FlowRuntime): Promise | void; } interface FlowPlugin { id: string; steps?: FlowStepContribution[]; } interface FlowStepRegistry { step(kind: string): FlowStepContribution | undefined; kinds(): string[]; } declare function createFlowStepRegistry(plugins?: FlowPlugin[], builtins?: FlowStepContribution[]): FlowStepRegistry; declare const BUILTIN_FLOW_STEPS: FlowStepContribution[]; /** Child lists a container step owns, by kind. `parallel` holds a list PER track. */ declare function containerTracks(step: Step): Step[][]; /** Human label for a container's Nth track (`if` branches are then/else, parallel is 1..N). */ declare function trackLabel(step: Step, index: number): string; declare const isContainer: (step: Step) => boolean; /** * Give every step a stable id, in place-ish (returns the same doc object). Ids are only * assigned where missing, so re-running is a no-op and saved ids survive. */ declare function ensureStepIds(doc: FlowDoc, prefix?: string): FlowDoc; /** Find a step by id, with the list and index that hold it (so edits can splice). */ declare function locateStep(doc: FlowDoc, id: string): { step: Step; list: Step[]; index: number; event: string; } | undefined; interface GraphNode { id: string; kind: string; step: Step; x: number; y: number; width: number; height: number; /** Nesting depth (0 = top level of the event). */ depth: number; /** Container this node lives in, if any. */ parentId?: string; /** Label of the track it sits on inside its container ('then', 'track 2', …). */ track?: string; /** True when the node is a container drawn as a box around its children. */ container: boolean; /** Set when the position came from the step's own `ui`, not the auto-layout. */ pinned: boolean; } interface GraphEdge { from: string; to: string; /** A container's edge to the first node of one of its tracks. */ kind: 'next' | 'into'; label?: string; } interface FlowGraph { nodes: GraphNode[]; edges: GraphEdge[]; width: number; height: number; } declare const NODE_W = 168; declare const NODE_H = 44; /** * Lay one event out left-to-right. Containers are measured from their contents, so a * `parallel` with two long tracks grows tall and wide instead of overlapping its siblings. */ declare function layoutEvent(doc: FlowDoc, event: string): FlowGraph; /** One-line summary of a step's most telling fields, for the node body. */ declare function summarize(step: Step): string; export { BUILTIN_FLOW_STEPS, NODE_H, NODE_W, containerTracks, createFlowRunner, createFlowStepRegistry, ensureStepIds, isContainer, layoutEvent, locateStep, summarize, trackLabel, validateFlowDoc }; export type { CodeStep, CountUpStep, CreateFlowRunnerOptions, CueDef, FireOptions, FlowAudioAdapter, FlowDoc, FlowGraph, FlowPlugin, FlowRunner, FlowRuntime, FlowStepContribution, FlowStepRegistry, FlowTrace, ForEachStep, GraphEdge, GraphNode, IfStep, ParallelStep, PluginStep, ResolvedCue, SeqStep, SetPropsStep, SetStateStep, SetVarStep, SoundStep, Step, TraceEntry, TweenStep, WaitStep };