import { Container, Application } from 'pixi.js'; 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; } /** One entry of the game's asset manifest, as offered by the asset picker. */ interface InspectorAsset { alias: string; path: string; kind?: 'image' | 'audio' | string; } interface SceneInspectorOptions { handle: SceneHandle; container: HTMLElement; /** Route for edits (host may add logging/saving); defaults to handle.patch. */ applyPatch?: (patch: ScenePatch) => void; /** Current orientation — decides which effective layout rule is shown/edited. */ getOrientation?: () => Orientation; /** Selection made in the panel (host mirrors it on the canvas). */ onSelect?: (id: string | null) => void; /** Shown as a "Calibrate inner" button for nodes declaring props.inner (host mounts the gizmo). */ onCalibrate?: (id: string) => void; /** Hide the built-in outliner (a host — e.g. the standalone IDE — provides its own). */ showTree?: boolean; /** * The game's assets, for `kind:'asset'` fields. Without it those fields stay a text box — * which is exactly the dead end a fresh sprite used to hit. */ assets?: () => InspectorAsset[]; /** Map an asset path to a previewable URL (the IDE prefixes the game's origin). */ assetUrl?: (path: string) => string; } interface SceneInspector { select(id: string | null): void; selected(): string | null; refresh(): void; destroy(): void; } declare function createSceneInspector(opts: SceneInspectorOptions): SceneInspector; type PropControlKind = 'number' | 'text' | 'boolean' | 'color' | 'json'; /** Infer an editor control for a props value (schema-less fallback). */ declare function propControlKind(value: unknown): PropControlKind; interface LayoutFieldSpec { /** Dot-path inside the rule ('x', 'pxNudge.x', 'anchor.0', 'offset.1'). */ path: string; label: string; kind: 'number' | 'text' | 'boolean' | 'readonly'; value: number | string | boolean; /** Input step — fractions and anchors get fine steps, px fields get 1. */ step?: number; } /** Flatten a layout rule into editable field specs (mode itself is structural → readonly). */ declare function layoutFieldSpecs(rule: LayoutRule): LayoutFieldSpec[]; /** Immutable set of a dot-path inside a (JSON-ish) rule object. */ declare function setRulePath(rule: LayoutRule, path: string, value: unknown): LayoutRule; 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[]; } 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; } interface FlowTimeline { show(trace: FlowTrace): void; clear(): void; destroy(): void; } interface FlowTimelineOptions { container: HTMLElement; /** Row click = scrub: host re-poses the scene via runner.replay(event, entryIndex). */ onScrub?: (event: string, entryIndex: number) => void; } declare function createFlowTimeline(opts: FlowTimelineOptions): FlowTimeline; interface FlowEditorOptions { doc: FlowDoc; container: HTMLElement; /** Step kinds offered in the add-menu (pass registry kinds incl. plugins). */ kinds?: string[]; /** Called after every mutation — persist (e.g. POST /__write flow.json) and log here. */ onChange?: () => void; } interface FlowEditor { refresh(): void; destroy(): void; } declare function createFlowEditor(opts: FlowEditorOptions): FlowEditor; type HandleId = 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | 'rotate' | 'body'; interface TransformGizmo { attach(nodeId: string | null): void; refresh(): void; /** Run a full handle drag programmatically (agent/tests). Screen coords. */ drag(handle: HandleId, from: { x: number; y: number; }, to: { x: number; y: number; }): void; /** * Move the selection by a discrete amount (arrow keys). Deliberately NOT a synthetic drag: * a drag ignores travel below the click/drag threshold, which would swallow a 1px nudge. */ nudge(dxScreen: number, dyScreen: number): void; destroy(): void; } /** A ctx field the flow editor offers in its `$`-value and `when` pickers (from the spec). */ interface CtxField { path: string; type: 'number' | 'string' | 'boolean' | 'array' | 'object'; /** Optional sample value used when the spin console fires without a real result. */ sample?: unknown; } /** The flow surface a game hands the bridge so the IDE can edit and run its flow-IR. */ interface IdeFlowSurface { /** The live FlowDoc (mutated by flow patches). */ doc: FlowDoc; runner: FlowRunner; /** Writable path for the flow doc (idePlugin allowlist), e.g. 'src/scene/flow.json'. */ file?: string; /** Step kinds available (built-ins + plugins) for the add-step menu. */ /** @deprecated the runner reports its own kinds WITH schemas — kept for older hosts. */ stepKinds?: string[]; /** ctx fields for the `$`/`when` pickers — ideally derived from the game's result schema. */ ctx?: CtxField[]; /** Build a ctx to fire an event with when the console has no real result (spike/dev). */ sampleCtx?: (event: string) => Record; } /** One scene in the stage graph (file-referenced; the host swaps scenes by navigation). */ interface StageSceneRef { key: string; title?: string; /** Scene doc file (idePlugin-writable), e.g. 'src/scene/scene.json'. */ file: string; /** Transition into this scene, as data on the edge (host animates it at runtime). */ transition?: 'none' | 'fade' | 'slide-left' | 'slide-right'; /** Skipped on replay launches (intro idiom). */ skipOnReplay?: boolean; } /** The multi-scene graph the IDE Scenes panel lists and edits — data, not a live scene. */ interface StageManifest { scenes: StageSceneRef[]; } /** A game's stage surface: the manifest, which scene this page shows, and how to navigate. */ interface IdeStageSurface { manifest: StageManifest; /** Which scene key THIS page is currently rendering. */ activeKey: string; /** Writable path for the manifest (idePlugin allowlist), e.g. 'src/scene/stage.json'. */ file?: string; /** Query param the page reads to pick a scene (default 'scene' → ?scene=). */ sceneParam?: string; } interface IdeBridgeOptions { handle: SceneHandle; /** Pixi app — the bridge draws the selection outline on its stage and drives it on the ticker. */ app: Application; /** Relative path the doc persists to (idePlugin allowlist), e.g. 'src/scene/scene.json'. */ sceneFile?: string; /** Re-layout hook after structural edits (host owns design-unit conversion). */ relayout?: () => void; /** Current orientation, for the transform gizmo's orientation-aware patches. */ getOrientation?: () => Orientation; /** Design-units-per-screen-pixel, so a move drag converts to pxNudge. */ designPerPixel?: () => number; /** Optional flow-IR surface — enables the IDE's Flow tab + spin console. */ flow?: IdeFlowSurface; /** Optional multi-scene stage — enables the IDE's Scenes panel. */ stage?: IdeStageSurface; /** * The game's texture aliases as `alias -> public path`. THIS is what `props.src` holds. * Scanning public/assets only yields file names, and for most games those are not the * aliases the game registered (paper-duel loads `back.webp` as `th_back`), so a picker * built from the file scan alone would write aliases the game cannot resolve. Pass the * game's own manifest and the editor offers exactly the values that will work. */ assetAliases?: () => Record; targetOrigin?: string; } interface IdeBridge { /** Push a fresh snapshot to the IDE (call after external changes). */ refresh(): void; select(id: string | null): void; /** The on-canvas transform manipulator (exposed for keyboard/agent/tests). */ gizmo: TransformGizmo; destroy(): void; } declare function mountIdeBridge(opts: IdeBridgeOptions): IdeBridge; export { createFlowEditor, createFlowTimeline, createSceneInspector, layoutFieldSpecs, mountIdeBridge, propControlKind, setRulePath }; export type { CtxField, FlowEditor, FlowEditorOptions, FlowTimeline, FlowTimelineOptions, IdeBridge, IdeBridgeOptions, IdeFlowSurface, IdeStageSurface, LayoutFieldSpec, PropControlKind, SceneInspector, SceneInspectorOptions, StageManifest, StageSceneRef };