@energy8platform/game-engine 0.33.4 → 0.33.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/scene.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { Container, Texture } from 'pixi.js';
1
+ import { Container, Filter, Texture, Application } from 'pixi.js';
2
+ import { SlotSpinResultBase } from '@energy8platform/platform-core/slot-result';
2
3
 
3
4
  type Orientation = 'landscape' | 'portrait';
4
5
  interface SceneDoc {
@@ -31,6 +32,8 @@ interface SceneNode {
31
32
  states?: Record<string, NodeOverride>;
32
33
  /** Id of another node in this doc used as this node's mask. */
33
34
  mask?: string;
35
+ /** Filters applied to the node's view; kinds resolve through the registry (core: blur). */
36
+ filters?: FilterSpec[];
34
37
  /**
35
38
  * Tiny condition over runtime vars, e.g. `"mode === 'free_spins'"`. Supported form:
36
39
  * `<var> <op> <literal>` with `=== !== >= <= > <`. Anything richer belongs in flow/code.
@@ -41,6 +44,11 @@ interface SceneNode {
41
44
  /** Order = z-order. */
42
45
  children?: SceneNode[];
43
46
  }
47
+ /** A filter instance: kind + kind-specific params (validated by the contribution). */
48
+ interface FilterSpec {
49
+ kind: string;
50
+ [param: string]: unknown;
51
+ }
44
52
  /** Partial node applied on top of the base for an orientation or a named state. */
45
53
  interface NodeOverride {
46
54
  layout?: LayoutRule;
@@ -57,6 +65,13 @@ interface LayoutCommon {
57
65
  };
58
66
  /** Static rotation in radians (choreographed rotation belongs to flow/animation). */
59
67
  rotation?: number;
68
+ /**
69
+ * `'viewport'` resolves this rule against the LIVE viewport even when the node lives
70
+ * inside a nested coordinate space — the engine converts the result into the space's
71
+ * coordinates. The paper-duel logo idiom: screen-pinned, but rendered at a diorama
72
+ * z-depth.
73
+ */
74
+ space?: 'viewport';
60
75
  }
61
76
  /** Fixed design-space coordinates. */
62
77
  interface AbsoluteLayout extends LayoutCommon {
@@ -153,6 +168,28 @@ type ScenePatch = {
153
168
  op: 'set-var';
154
169
  name: string;
155
170
  value: unknown;
171
+ }
172
+ /** Insert `node` under `parent` at `index` (default: append). Ids must be unused. */
173
+ | {
174
+ op: 'add-node';
175
+ parent: string;
176
+ node: SceneNode;
177
+ index?: number;
178
+ } | {
179
+ op: 'remove-node';
180
+ id: string;
181
+ }
182
+ /** Reparent/reorder `id` under `parent` at `index` (same parent = pure reorder). */
183
+ | {
184
+ op: 'move-node';
185
+ id: string;
186
+ parent: string;
187
+ index?: number;
188
+ }
189
+ /** Deep-copy `id` (all descendant ids suffixed to stay unique) as its next sibling. */
190
+ | {
191
+ op: 'duplicate-node';
192
+ id: string;
156
193
  };
157
194
  /** Outliner row — the tree the inspector and the agent both read. */
158
195
  interface OutlineNode {
@@ -252,6 +289,23 @@ interface SymbolView extends Container {
252
289
  /** Game-supplied factory: build the view for a symbol id (sprite / layered sprites / Spine / composite). */
253
290
  type SymbolResolver = (symbolId: string) => SymbolView | null;
254
291
 
292
+ interface IScene {
293
+ /** Root display container for this scene */
294
+ readonly container: Container;
295
+ /** @internal GameApplication reference — set by SceneManager */
296
+ __engineApp?: any;
297
+ /** Called when the scene is entered */
298
+ onEnter?(data?: unknown): Promise<void> | void;
299
+ /** Called when the scene is exited */
300
+ onExit?(): Promise<void> | void;
301
+ /** Called every frame */
302
+ onUpdate?(dt: number): void;
303
+ /** Called when viewport resizes */
304
+ onResize?(width: number, height: number): void;
305
+ /** Called when the scene is destroyed */
306
+ onDestroy?(): void;
307
+ }
308
+
255
309
  interface NodeCreateContext {
256
310
  registry: SceneRegistry;
257
311
  /** Resolve a texture alias (defaults to Pixi `Texture.from`; tests/labs inject). */
@@ -274,12 +328,22 @@ interface NodeInstance {
274
328
  };
275
329
  destroy?(): void;
276
330
  }
331
+ /** Skeleton a palette/agent uses to spawn a fresh node of this kind (id is assigned by the caller). */
332
+ interface NodeDefaults {
333
+ /** Human label for the palette button (defaults to the kind). */
334
+ label?: string;
335
+ name?: string;
336
+ props?: Record<string, unknown>;
337
+ layout?: SceneNode['layout'];
338
+ }
277
339
  interface NodeTypeContribution {
278
340
  kind: string;
279
341
  /** JSON-Schema-ish description of `props` — validation, inspector autogen, agent docs. */
280
342
  schema?: Record<string, unknown>;
281
343
  /** 3–10 lines for the agent: when to use this kind, patterns, anti-patterns. */
282
344
  agentDoc?: string;
345
+ /** Palette skeleton — omit to hide the kind from the add-menu (e.g. `prefab` dispatch). */
346
+ defaults?: NodeDefaults;
283
347
  create(node: SceneNode, ctx: NodeCreateContext): NodeInstance;
284
348
  }
285
349
  /** Reusable game component (meters, HUDs) addressed as `{type:'prefab', props:{prefab:'name'}}`. */
@@ -287,22 +351,47 @@ interface PrefabContribution {
287
351
  name: string;
288
352
  schema?: Record<string, unknown>;
289
353
  agentDoc?: string;
354
+ /** Palette skeleton (props merged onto `{prefab:name}`, plus a starter layout). */
355
+ defaults?: NodeDefaults;
290
356
  create(props: Record<string, unknown>, ctx: NodeCreateContext): NodeInstance;
291
357
  }
358
+ /** A filter kind: `{kind:'blur', strength: 3}` in a node's `filters` resolves through this. */
359
+ interface FilterKindContribution {
360
+ kind: string;
361
+ schema?: Record<string, unknown>;
362
+ agentDoc?: string;
363
+ create(spec: FilterSpec): Filter;
364
+ }
292
365
  interface ScenePlugin {
293
366
  id: string;
294
367
  nodeTypes?: NodeTypeContribution[];
295
368
  prefabs?: PrefabContribution[];
369
+ filterKinds?: FilterKindContribution[];
370
+ }
371
+ /** One palette entry — a spawnable node kind or prefab, with its skeleton. */
372
+ interface PaletteEntry {
373
+ kind: string;
374
+ label: string;
375
+ /** 'prefab' entries spawn `{type:'prefab', props:{prefab:kind}}`; others spawn `{type:kind}`. */
376
+ isPrefab: boolean;
377
+ agentDoc?: string;
378
+ defaults: NodeDefaults;
296
379
  }
297
380
  interface SceneRegistry {
298
381
  nodeType(kind: string): NodeTypeContribution | undefined;
299
382
  prefab(name: string): PrefabContribution | undefined;
383
+ filterKind(kind: string): FilterKindContribution | undefined;
300
384
  kinds(): string[];
301
385
  prefabNames(): string[];
386
+ filterKindNames(): string[];
387
+ /** Spawnable palette: node kinds with `defaults` + all registered prefabs. */
388
+ palette(): PaletteEntry[];
302
389
  }
303
390
  /** Merge built-ins with plugin contributions; later plugins may override earlier kinds. */
304
- declare function createSceneRegistry(plugins?: ScenePlugin[], builtins?: NodeTypeContribution[]): SceneRegistry;
391
+ declare function createSceneRegistry(plugins?: ScenePlugin[], builtins?: NodeTypeContribution[], builtinFilters?: FilterKindContribution[]): SceneRegistry;
305
392
 
393
+ /** Core filter kinds; heavier looks (bloom etc.) arrive as plugins carrying their deps. */
394
+ declare const BUILTIN_FILTER_KINDS: FilterKindContribution[];
306
395
  declare const BUILTIN_NODE_TYPES: NodeTypeContribution[];
307
396
 
308
397
  interface SceneValidationError {
@@ -336,9 +425,524 @@ interface SceneHandle {
336
425
  patch(patch: ScenePatch): void;
337
426
  /** The live doc (mutated by patches) — serialize this to persist the scene. */
338
427
  doc(): SceneDoc;
428
+ /** Spawnable palette (node kinds with defaults + prefabs) — feeds the editor add-menu. */
429
+ palette(): PaletteEntry[];
430
+ /**
431
+ * Re-apply every node's doc-effective props, wiping transient presentation writes
432
+ * (flow tweens/count-ups). "Settle = the doc" — scrub replays call this first.
433
+ */
434
+ resetProps(): void;
339
435
  destroy(): void;
340
436
  }
341
437
  declare function createSceneFromDoc(doc: SceneDoc, opts?: CreateSceneOptions): SceneHandle;
342
438
 
343
- export { BUILTIN_NODE_TYPES, createSceneFromDoc, createSceneRegistry, dependencyOf, edgePoint, effectiveNode, evalVisibleWhen, orientationOf, placementBounds, resolveLayoutRule, validateSceneDoc };
344
- export type { AbsoluteLayout, CoverLayout, CreateSceneOptions, FrameFractionLayout, GridCellLayout, LayoutContext, LayoutResolution, LayoutRule, NodeCreateContext, NodeInstance, NodeOverride, NodeTypeContribution, Orientation, OutlineNode, PinEdge, PinLayout, Placement, PrefabContribution, Rect, SceneDoc, SceneHandle, SceneNode, ScenePatch, ScenePlugin, SceneRegistry, SceneValidationError, Size, ViewportFractionLayout };
439
+ interface InnerFracs {
440
+ left: number;
441
+ top: number;
442
+ width: number;
443
+ height: number;
444
+ }
445
+ type InnerEdge = 'left' | 'right' | 'top' | 'bottom';
446
+ /** Pure edge-drag math: `frac` is the pointer position normalized to the frame rect axis. */
447
+ declare function dragInnerEdge(inner: InnerFracs, edge: InnerEdge, frac: number): InnerFracs;
448
+ interface InnerCalibratorOptions {
449
+ handle: SceneHandle;
450
+ /** The node whose props.inner is being calibrated (reelFrame or any inner-declaring node). */
451
+ nodeId: string;
452
+ /** Patch route (host may add logging/saving); defaults to handle.patch. */
453
+ applyPatch?: (patch: ScenePatch) => void;
454
+ onChange?: (inner: InnerFracs) => void;
455
+ }
456
+ interface InnerCalibrator {
457
+ /** Add to the stage ABOVE the scene view (canvas coordinates). */
458
+ view: Container;
459
+ /** Redraw from current bounds/props (call after resize or external patches). */
460
+ refresh(): void;
461
+ destroy(): void;
462
+ }
463
+ declare function createInnerCalibrator(opts: InnerCalibratorOptions): InnerCalibrator;
464
+
465
+ /** Which size lever a rule exposes — decides how a resize drag is interpreted. */
466
+ declare function sizeLever(rule: LayoutRule): 'wh-frac' | 'wh-box' | 'scale' | 'none';
467
+ /**
468
+ * Multiply a rule's size lever by (factorX, factorY). Corners pass equal factors (uniform);
469
+ * edges pass 1 on the untouched axis. Uniform levers (`scale`) use factorX.
470
+ */
471
+ declare function resizeRule(rule: LayoutRule, factorX: number, factorY: number): LayoutRule;
472
+ declare function setRuleRotation(rule: LayoutRule, radians: number): LayoutRule;
473
+ /** Nudge a rule's final pixel offset by a screen delta already converted to design units. */
474
+ declare function nudgeRule(rule: LayoutRule, dxDesign: number, dyDesign: number): LayoutRule;
475
+ type HandleId = 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | 'rotate' | 'body';
476
+ interface Box {
477
+ x: number;
478
+ y: number;
479
+ width: number;
480
+ height: number;
481
+ }
482
+ /** Screen position of each handle for a bounding box. */
483
+ declare function handlePoint(box: Box, id: Exclude<HandleId, 'body'>): {
484
+ x: number;
485
+ y: number;
486
+ };
487
+ /**
488
+ * Resize factors from dragging a handle, using the OPPOSITE edge/corner as the fixed
489
+ * reference — the intuitive "grab a corner, drag out, it grows" feel.
490
+ */
491
+ declare function resizeFactors(startBox: Box, handle: HandleId, pointer: {
492
+ x: number;
493
+ y: number;
494
+ }): {
495
+ factorX: number;
496
+ factorY: number;
497
+ };
498
+
499
+ interface TransformGizmoOptions {
500
+ handle: SceneHandle;
501
+ app: Application;
502
+ applyPatch?: (patch: ScenePatch) => void;
503
+ getOrientation?: () => Orientation;
504
+ /** Override screen→design conversion for move drags. Default: the node's parent world scale. */
505
+ designPerPixel?: (nodeId: string) => number;
506
+ }
507
+ interface TransformGizmo {
508
+ attach(nodeId: string | null): void;
509
+ refresh(): void;
510
+ /** Run a full handle drag programmatically (keyboard/agent/tests). Screen coords. */
511
+ drag(handle: HandleId, from: {
512
+ x: number;
513
+ y: number;
514
+ }, to: {
515
+ x: number;
516
+ y: number;
517
+ }): void;
518
+ destroy(): void;
519
+ }
520
+ declare function createTransformGizmo(opts: TransformGizmoOptions): TransformGizmo;
521
+
522
+ /**
523
+ * Base class for all scenes.
524
+ * Provides a root PixiJS Container and lifecycle hooks.
525
+ *
526
+ * @example
527
+ * ```ts
528
+ * class MenuScene extends Scene {
529
+ * async onEnter() {
530
+ * const bg = Sprite.from('menu-bg');
531
+ * this.container.addChild(bg);
532
+ * }
533
+ *
534
+ * onUpdate(dt: number) {
535
+ * // per-frame logic
536
+ * }
537
+ *
538
+ * onResize(width: number, height: number) {
539
+ * // reposition UI
540
+ * }
541
+ * }
542
+ * ```
543
+ */
544
+ declare abstract class Scene implements IScene {
545
+ readonly container: Container;
546
+ constructor();
547
+ /** Called when this scene becomes active. Override in subclass. */
548
+ onEnter?(data?: unknown): Promise<void> | void;
549
+ /** Called when this scene is deactivated. Override in subclass. */
550
+ onExit?(): Promise<void> | void;
551
+ /** Called every frame with delta time (in seconds). Override in subclass. */
552
+ onUpdate?(dt: number): void;
553
+ /** Called when the viewport resizes. Override in subclass. */
554
+ onResize?(width: number, height: number): void;
555
+ /** Cleanup — called when the scene is permanently removed. */
556
+ onDestroy?(): void;
557
+ }
558
+
559
+ /** Everything a scene needs to render one segment. The host builds it per segment. */
560
+ interface RenderContext {
561
+ /** Bet for this round (major units). Stable for the whole round. */
562
+ bet: number;
563
+ /** Trigger action in the game's own vocabulary ('spin' | 'ante' | 'buy_bonus' | …). */
564
+ action: string;
565
+ /** Stake bet-mode of the round ('BASE' | 'ANTE' | 'BONUS' | …). */
566
+ mode: string;
567
+ /** Currency-aware money formatter. */
568
+ formatAmount(value: number): string;
569
+ /** LIVE turbo level (0 = off, 1..3 = escalating speed). Read at access. */
570
+ readonly turbo: number;
571
+ /** Only meaningful in `onEnterMode`: true when RETURNING to a suspended parent bonus after a
572
+ * nested sub-bonus finished (e.g. back to free spins after an adventure), false on a fresh
573
+ * entry. Lets a scene restore vs rebuild. Undefined outside `onEnterMode`. */
574
+ resumed?: boolean;
575
+ /** Aborted when the player skips this segment (double-tap). The scene's async pacing can race or
576
+ * cancel on it; on abort the scene must collapse to the segment's final visual state. */
577
+ signal: AbortSignal;
578
+ }
579
+ /** Playback-only audio handle. Volume/mute are shell settings → host, never the scene. */
580
+ interface SceneAudio {
581
+ play(alias: string, opts?: {
582
+ volume?: number;
583
+ loop?: boolean;
584
+ speed?: number;
585
+ }): void;
586
+ playMusic(alias: string, fadeMs?: number): void;
587
+ stopMusic(): void;
588
+ duck(factor: number): void;
589
+ unduck(): void;
590
+ }
591
+ interface OverlayShowOptions {
592
+ /** Draw the overlay content into `container` (sized to the canvas). */
593
+ build(container: Container, size: {
594
+ width: number;
595
+ height: number;
596
+ }): void;
597
+ /** Re-layout the same `container` when the canvas resizes while the overlay is open.
598
+ * Receives the content container from `build` and the new size. Optional — omit for
599
+ * overlays that self-center or don't care about resize. */
600
+ onResize?(container: Container, size: {
601
+ width: number;
602
+ height: number;
603
+ }): void;
604
+ /** Auto-close after N ms (combine with closeOn — whichever fires first). */
605
+ autoCloseMs?: number;
606
+ /** Dismiss on a single tap. Default 'tap'. Set false to require an explicit close(). */
607
+ closeOn?: 'tap' | false;
608
+ /** Optional host-drawn backdrop alpha (0..1). Default: none (game draws its own). */
609
+ dim?: number;
610
+ }
611
+ /** Single host-owned layer above scene + shell. Eats pointer events so shell controls are
612
+ * unreachable while open. */
613
+ interface SceneOverlay {
614
+ /** Resolves when the overlay closes. Rejects if one is already open. */
615
+ show(opts: OverlayShowOptions): Promise<void>;
616
+ close(): void;
617
+ }
618
+ interface SceneShell {
619
+ /** Live insets (px). `bottom` = the shell bar height; read inside onResize. */
620
+ readonly safeArea: {
621
+ top: number;
622
+ right: number;
623
+ bottom: number;
624
+ left: number;
625
+ };
626
+ }
627
+ interface AutoplaySceneState {
628
+ running: boolean;
629
+ remaining: number;
630
+ }
631
+ /** Stable capabilities injected once via onCreate. */
632
+ interface SceneApi {
633
+ audio: SceneAudio;
634
+ overlay: SceneOverlay;
635
+ shell: SceneShell;
636
+ formatAmount(value: number): string;
637
+ readonly bet: number;
638
+ readonly mode: string;
639
+ readonly turbo: number;
640
+ }
641
+ /** The contract a slot scene implements. The HOST owns the play→present→ack→drain loop and the
642
+ * shell; the scene only renders + reacts. The core spin-lifecycle hooks are REQUIRED (implement
643
+ * them — empty bodies are fine where a game has nothing to do); the incidental reactions below
644
+ * stay optional. */
645
+ interface SlotSceneController<T extends SlotSpinResultBase = SlotSpinResultBase> {
646
+ /** Injected ONCE before the first round — capabilities, subscriptions, one-time setup. */
647
+ onCreate(api: SceneApi): void;
648
+ /** Fires once per round when the player presses spin (before the network result). */
649
+ onSpinStart(): void;
650
+ /** Render ONE segment (a spin or one free spin). Await your own pacing. */
651
+ onSpin(result: T, ctx: RenderContext): Promise<void>;
652
+ /** Fires when a bonus LEVEL begins. With nested bonuses this fires once per level (free spins,
653
+ * then adventure, …) — check `ctx.mode` for which. `ctx.resumed` is true when returning to a
654
+ * suspended parent after a nested sub-bonus finished, so a scene can restore instead of rebuild.
655
+ * A single-bonus round fires it exactly once (as before). */
656
+ onEnterMode(result: T, ctx: RenderContext): Promise<void>;
657
+ /** Fires when a bonus LEVEL ends — popping a nested sub-bonus back to its parent, or unwinding
658
+ * the last level back to BASE. `ctx.mode` is the level being left. Fires once per level. */
659
+ onExitMode(result: T, ctx: RenderContext): Promise<void>;
660
+ /** Fires once per round after the full drain (controls unlocked). */
661
+ onSpinEnd(result: T, ctx: RenderContext): void;
662
+ /** Shell events (may fire while idle) — optional. */
663
+ onBetChanged?(bet: number): void;
664
+ onTurboChanged?(level: number): void;
665
+ onAutoplayChanged?(state: AutoplaySceneState): void;
666
+ /** Double-tap skip during an active onSpin (gated by the skipGesture setting). */
667
+ onSkip?(): void;
668
+ /** Tab focus lost / regained. */
669
+ onPause?(): void;
670
+ onResume?(): void;
671
+ }
672
+
673
+ interface FlowDoc {
674
+ version: 1;
675
+ id: string;
676
+ /** Sound cue registry: alias → definition. Steps refer to cues, never to files. */
677
+ cues?: Record<string, CueDef>;
678
+ /** Event name (host lifecycle: spinStart/reelStop/result/enterMode/…) → steps. */
679
+ on: Record<string, Step[]>;
680
+ }
681
+ interface CueDef {
682
+ /** One source alias, or several — with `rotate` the runner cycles them (anti-repetition). */
683
+ src: string | string[];
684
+ channel?: 'music' | 'sfx';
685
+ loop?: boolean;
686
+ rotate?: boolean;
687
+ /** ± random playback-rate jitter (0.06 = ±6%). */
688
+ jitter?: number;
689
+ volume?: number;
690
+ }
691
+ /** Core step vocabulary; plugins contribute additional `do` kinds through the registry. */
692
+ type Step = TweenStep | SoundStep | SetStateStep | SetVarStep | SetPropsStep | CountUpStep | WaitStep | IfStep | ParallelStep | SeqStep | ForEachStep | CodeStep | PluginStep;
693
+ interface TweenStep {
694
+ do: 'tween';
695
+ node: string;
696
+ /** Target numeric view properties (alpha, x, y, scale, rotation, …). */
697
+ to: Record<string, number>;
698
+ ms: number;
699
+ ease?: string;
700
+ }
701
+ interface SoundStep {
702
+ do: 'sound';
703
+ cue: string;
704
+ action?: 'play' | 'stop';
705
+ }
706
+ interface SetStateStep {
707
+ do: 'setState';
708
+ node: string;
709
+ state: string | null;
710
+ }
711
+ interface SetVarStep {
712
+ do: 'setVar';
713
+ name: string;
714
+ value: unknown;
715
+ }
716
+ /** Transient prop write on the live instance (presentation) — does NOT touch the doc. */
717
+ interface SetPropsStep {
718
+ do: 'setProps';
719
+ node: string;
720
+ props: Record<string, unknown>;
721
+ }
722
+ interface CountUpStep {
723
+ do: 'countUp';
724
+ node: string;
725
+ /** Instance prop receiving the formatted value (default 'value' — badge prefabs). */
726
+ prop?: string;
727
+ from?: number | string;
728
+ /** Number or a '$path' reference into the fire() ctx (e.g. '$win'). */
729
+ to: number | string;
730
+ ms?: number;
731
+ format?: 'int' | 'space';
732
+ }
733
+ interface WaitStep {
734
+ do: 'wait';
735
+ ms?: number;
736
+ until?: 'tap';
737
+ }
738
+ interface IfStep {
739
+ do: 'if';
740
+ /** Micro-expression over the fire() ctx: `win >= 100`, `mode === 'fs'`, or a bare truthy name. */
741
+ when: string;
742
+ then: Step[];
743
+ else?: Step[];
744
+ }
745
+ interface ParallelStep {
746
+ do: 'parallel';
747
+ /** Independent tracks, awaited together. */
748
+ steps: Step[][];
749
+ }
750
+ interface SeqStep {
751
+ do: 'seq';
752
+ steps: Step[];
753
+ }
754
+ /**
755
+ * Iterate a ctx collection — THE dynamic fan-out every studied game needs (staggered jar
756
+ * flights, per-cell transmutes, cascade histories). Nested steps see the current element
757
+ * as `$<as>` (default `$item`) and its index as `$<as>Index`.
758
+ */
759
+ interface ForEachStep {
760
+ do: 'forEach';
761
+ /** '$path' into the ctx (or an inline array). */
762
+ items: string | unknown[];
763
+ as?: string;
764
+ mode?: 'sequential' | 'parallel';
765
+ /** Per-item delay: parallel start stagger / sequential inter-item gap (turbo-scaled). */
766
+ staggerMs?: number;
767
+ steps: Step[];
768
+ }
769
+ /** Escape hatch: a named code choreography registered on the runner. */
770
+ interface CodeStep {
771
+ do: 'code';
772
+ ref: string;
773
+ args?: Record<string, unknown>;
774
+ }
775
+ interface PluginStep {
776
+ do: string;
777
+ [key: string]: unknown;
778
+ }
779
+ interface TraceEntry {
780
+ /** ms since fire() (real time, unscaled). */
781
+ t: number;
782
+ do: string;
783
+ node?: string;
784
+ info?: string;
785
+ }
786
+ interface FlowTrace {
787
+ event: string;
788
+ turbo: number;
789
+ skipped: boolean;
790
+ /** Set on scrub replays: the run executed instantly and halted after this entry index. */
791
+ haltedAtEntry?: number;
792
+ durationMs: number;
793
+ entries: TraceEntry[];
794
+ }
795
+
796
+ /** What the audio adapter receives — cue already resolved (rotation/jitter applied). */
797
+ interface ResolvedCue {
798
+ cue: string;
799
+ src: string;
800
+ channel: 'music' | 'sfx';
801
+ loop: boolean;
802
+ rate: number;
803
+ volume: number;
804
+ }
805
+ interface FlowAudioAdapter {
806
+ play(cue: ResolvedCue): void;
807
+ stop(cueName: string): void;
808
+ }
809
+ interface CreateFlowRunnerOptions {
810
+ scene: SceneHandle;
811
+ audio?: FlowAudioAdapter;
812
+ plugins?: FlowPlugin[];
813
+ /** Named code choreographies for `{do:'code', ref}` escape hatches. */
814
+ code?: Record<string, (rt: FlowRuntime, args: Record<string, unknown>) => Promise<void> | void>;
815
+ /** Live trace feed (the log/timeline view); the full trace is also returned by fire(). */
816
+ onTrace?: (event: string, entry: TraceEntry) => void;
817
+ log?: (msg: string) => void;
818
+ }
819
+ interface FireOptions {
820
+ /** Scrub replay: run instantly (no waits, anims jump, audio muted) and halt after entry N. */
821
+ replay?: {
822
+ untilEntry?: number;
823
+ };
824
+ }
825
+ interface FlowRunner {
826
+ fire(event: string, ctx?: Record<string, unknown>, opts?: FireOptions): Promise<FlowTrace>;
827
+ /** Scrub: re-fire `event` with its LAST ctx, instantly, halting after trace entry N. */
828
+ replay(event: string, untilEntry: number): Promise<FlowTrace>;
829
+ /** Complete every active run in zero time (settle). */
830
+ skip(): void;
831
+ /** 1 = normal; 2 = twice as fast. Applies to waits, tweens, count-ups. */
832
+ setTurbo(factor: number): void;
833
+ /** Host reports a user tap (releases `wait until:'tap'`). */
834
+ tap(): void;
835
+ events(): string[];
836
+ destroy(): void;
837
+ }
838
+ /** Per-run runtime handed to step executors (core and plugin alike). */
839
+ interface FlowRuntime {
840
+ scene: SceneHandle;
841
+ ctx: Record<string, unknown>;
842
+ audio: FlowAudioAdapter;
843
+ /** Turbo-scaled, skip-aware sleep. */
844
+ wait(ms: number): Promise<void>;
845
+ waitTap(): Promise<void>;
846
+ /** Execute a nested step list (if/parallel/seq bodies). */
847
+ run(steps: Step[]): Promise<void>;
848
+ /** True once skip() hit this run — executors should jump straight to end states. */
849
+ readonly skipped: boolean;
850
+ /** True for scrub replays — like skipped (jump to end states), plus audio stays muted. */
851
+ readonly instant: boolean;
852
+ readonly turbo: number;
853
+ /** Resolve '$path' references against ctx; other values pass through. */
854
+ resolve(value: unknown): unknown;
855
+ /** Evaluate an if-condition against ctx. */
856
+ when(expr: string): boolean;
857
+ /** A runtime whose ctx is overlaid with extra values (forEach exposes `$item` this way). */
858
+ child(ctxPatch: Record<string, unknown>): FlowRuntime;
859
+ trace(entry: Omit<TraceEntry, 't'>): void;
860
+ /** The node's live view (tween targets). */
861
+ view(id: string): Container | undefined;
862
+ log(msg: string): void;
863
+ codeRef(ref: string): ((rt: FlowRuntime, args: Record<string, unknown>) => Promise<void> | void) | undefined;
864
+ playCue(name: string, action: 'play' | 'stop'): void;
865
+ }
866
+
867
+ interface FlowStepContribution {
868
+ kind: string;
869
+ schema?: Record<string, unknown>;
870
+ /** 3–10 lines for the agent: when to use the step, patterns, anti-patterns. */
871
+ agentDoc?: string;
872
+ run(step: Step, rt: FlowRuntime): Promise<void> | void;
873
+ }
874
+ interface FlowPlugin {
875
+ id: string;
876
+ steps?: FlowStepContribution[];
877
+ }
878
+
879
+ /** The multi-scene graph as data: which docs exist and how the player moves between them. */
880
+ interface StageDoc {
881
+ version: 1;
882
+ scenes: Array<{
883
+ key: string;
884
+ scene: SceneDoc;
885
+ flow?: FlowDoc;
886
+ /** Host skips this scene on replay launches (intro idiom). */
887
+ skipOnReplay?: boolean;
888
+ /** A tap anywhere advances to this scene key (intro → game). */
889
+ advanceOnTap?: string;
890
+ }>;
891
+ }
892
+ /** Build the createSlotGame `scenes` list from a StageDoc — every entry a DocScene. */
893
+ declare function buildDocScenes<T extends SlotSpinResultBase = SlotSpinResultBase>(stage: StageDoc, shared: Omit<DocSceneOptions<T>, 'scene' | 'flow' | 'advanceOnTap'>): Array<{
894
+ key: string;
895
+ scene: DocScene<T>;
896
+ skipOnReplay?: boolean;
897
+ }>;
898
+ interface DocSceneOptions<T extends SlotSpinResultBase = SlotSpinResultBase> {
899
+ scene: SceneDoc;
900
+ flow?: FlowDoc;
901
+ /** A tap anywhere advances to this scene key (uses the host-injected goto). */
902
+ advanceOnTap?: string;
903
+ plugins?: ScenePlugin[];
904
+ flowPlugins?: FlowPlugin[];
905
+ code?: CreateFlowRunnerOptions['code'];
906
+ resolveSymbol?: SymbolResolver;
907
+ texture?: (alias: string) => Texture;
908
+ vars?: Record<string, unknown>;
909
+ /**
910
+ * Build the flow ctx for result-bearing events. Default: the result object spread +
911
+ * `{ win: totalWin, mode, action, bet }`. Games with richer normalized data override.
912
+ */
913
+ resultCtx?: (result: T, ctx: RenderContext) => Record<string, unknown>;
914
+ /** Map the shell turbo level (0..3) to a flow time factor. Default 1 + level. */
915
+ turboFactor?: (level: number) => number;
916
+ onTrace?: CreateFlowRunnerOptions['onTrace'];
917
+ log?: (msg: string) => void;
918
+ }
919
+ declare class DocScene<T extends SlotSpinResultBase = SlotSpinResultBase> extends Scene implements SlotSceneController<T> {
920
+ private opts;
921
+ private api;
922
+ private handle_;
923
+ private runner_;
924
+ private chain;
925
+ private lastSize;
926
+ private log;
927
+ constructor(opts: DocSceneOptions<T>);
928
+ /** The live scene handle (inspector/agent attachment). Null before onEnter. */
929
+ get scene(): SceneHandle | null;
930
+ get flow(): FlowRunner | null;
931
+ onEnter(data?: unknown): void;
932
+ onResize(width: number, height: number): void;
933
+ onDestroy(): void;
934
+ private fire;
935
+ private ctxOf;
936
+ onCreate(api: SceneApi): void;
937
+ onSpinStart(): void;
938
+ onSpin(result: T, ctx: RenderContext): Promise<void>;
939
+ onEnterMode(result: T, ctx: RenderContext): Promise<void>;
940
+ onExitMode(result: T, ctx: RenderContext): Promise<void>;
941
+ onSpinEnd(result: T, ctx: RenderContext): void;
942
+ onSkip(): void;
943
+ onTurboChanged(level: number): void;
944
+ }
945
+ declare function createDocScene<T extends SlotSpinResultBase = SlotSpinResultBase>(opts: DocSceneOptions<T>): DocScene<T>;
946
+
947
+ export { BUILTIN_FILTER_KINDS, BUILTIN_NODE_TYPES, DocScene, buildDocScenes, createDocScene, createInnerCalibrator, createSceneFromDoc, createSceneRegistry, createTransformGizmo, dependencyOf, dragInnerEdge, edgePoint, effectiveNode, evalVisibleWhen, handlePoint, nudgeRule, orientationOf, placementBounds, resizeFactors, resizeRule, resolveLayoutRule, setRuleRotation, sizeLever, validateSceneDoc };
948
+ export type { AbsoluteLayout, Box, CoverLayout, CreateSceneOptions, DocSceneOptions, FilterKindContribution, FilterSpec, FrameFractionLayout, GridCellLayout, HandleId, InnerCalibrator, InnerCalibratorOptions, InnerEdge, InnerFracs, LayoutContext, LayoutResolution, LayoutRule, NodeCreateContext, NodeDefaults, NodeInstance, NodeOverride, NodeTypeContribution, Orientation, OutlineNode, PaletteEntry, PinEdge, PinLayout, Placement, PrefabContribution, Rect, SceneDoc, SceneHandle, SceneNode, ScenePatch, ScenePlugin, SceneRegistry, SceneValidationError, Size, StageDoc, TransformGizmo, TransformGizmoOptions, ViewportFractionLayout };