@vgai/engine 0.5.2 → 0.5.4

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.
Files changed (185) hide show
  1. package/README.md +18 -11
  2. package/package.json +10 -6
  3. package/schemas/engine-api.json +1 -68
  4. package/schemas/engine-api.md +1 -32
  5. package/schemas/engine-capabilities.json +30 -42
  6. package/schemas/{vgai-game.schema.json → vgai-project.schema.json} +52 -34
  7. package/src/adapter/adapter-surface.ts +5 -5
  8. package/src/adapter/authoring.ts +168 -226
  9. package/src/adapter/colyseus-networking-adapter.ts +44 -5
  10. package/src/adapter/first-party-systems.ts +156 -42
  11. package/src/adapter/host-context.ts +177 -46
  12. package/src/adapter/index.ts +47 -51
  13. package/src/adapter/ingest/game-contract.ts +2 -2
  14. package/src/adapter/ingest/scene-capture.ts +18 -19
  15. package/src/adapter/ingest/structural-ids.ts +127 -0
  16. package/src/adapter/ingest/upstream-pin.ts +9 -12
  17. package/src/adapter/loop-gate-report.ts +11 -11
  18. package/src/adapter/rapier-physics-adapter.ts +27 -9
  19. package/src/adapter/root-adapter.ts +217 -0
  20. package/src/adapter/{vgai-scene-game-adapter.ts → setup-three-root-adapter.ts} +173 -351
  21. package/src/adapter/system-adapter.ts +80 -63
  22. package/src/ai/navigation.ts +1 -1
  23. package/src/animation/animation-clock.ts +1 -1
  24. package/src/animation/camera-ownership.ts +1 -2
  25. package/src/animation/cubic-spline-interpolant.ts +132 -0
  26. package/src/animation/theatre-clock-binding.ts +2 -2
  27. package/src/animation/theatre-object-binding.ts +4 -4
  28. package/src/animation/xstate-animation-binding.ts +75 -5
  29. package/src/{scene/schema → asset-formats}/camera.ts +2 -5
  30. package/src/{scene/schema → asset-formats}/collider.ts +2 -5
  31. package/src/asset-formats/index.ts +54 -0
  32. package/src/{scene/schema → asset-formats}/instances.ts +10 -6
  33. package/src/{scene/schema → asset-formats}/light.ts +3 -6
  34. package/src/{scene/schema → asset-formats}/material.ts +4 -7
  35. package/src/{scene/schema → asset-formats}/mesh.ts +4 -7
  36. package/src/asset-formats/parse.ts +39 -0
  37. package/src/{scene/schema → asset-formats}/particles.ts +3 -6
  38. package/src/{scene/schema/environment.ts → asset-formats/render-env.ts} +23 -86
  39. package/src/{scene/schema → asset-formats}/tuples.ts +1 -1
  40. package/src/{scene/asset-loaders.ts → asset-loaders.ts} +8 -9
  41. package/src/asset-parse-error.ts +33 -0
  42. package/src/{scene/asset-registry.ts → asset-registry.ts} +1 -1
  43. package/src/assets.ts +1 -1
  44. package/src/audio/wav-encode.ts +9 -9
  45. package/src/canvas-react/engine-bridge.ts +59 -0
  46. package/src/canvas-react/index.ts +50 -0
  47. package/src/canvas-react/pixi-primitive.tsx +202 -0
  48. package/src/canvas-react/pixi-react-adapter.tsx +290 -0
  49. package/src/canvas-react/pixi-react-root-factory.tsx +88 -0
  50. package/src/canvas-react/world-context.ts +328 -0
  51. package/src/core/frame-pacing.ts +100 -0
  52. package/src/core/game-loop.ts +50 -28
  53. package/src/core/seeded-random.ts +7 -7
  54. package/src/core/sim-clock.ts +388 -0
  55. package/src/core/system-runner.ts +17 -63
  56. package/src/core/types.ts +34 -15
  57. package/src/data/data-asset.ts +3 -3
  58. package/src/data/data-check-core.ts +6 -7
  59. package/src/data/data-ref.ts +11 -11
  60. package/src/data/vite-plugin-data.ts +10 -10
  61. package/src/{scene/defaults.ts → defaults.ts} +18 -40
  62. package/src/dev/render-debug-adapter.ts +1 -1
  63. package/src/dev/webgl-frame-capture.ts +1 -1
  64. package/src/ecs/scene-index.ts +439 -0
  65. package/src/ecs/scene-query.ts +43 -0
  66. package/src/{scene → ecs}/user-data.ts +17 -36
  67. package/src/index.ts +7 -9
  68. package/src/input/input-manager.ts +29 -32
  69. package/src/input/input-types.ts +2 -2
  70. package/src/input/schema.ts +5 -5
  71. package/src/loader.ts +57 -0
  72. package/src/manifest/editor-port.ts +69 -0
  73. package/src/manifest/filename.ts +49 -0
  74. package/src/manifest/index.ts +8 -2
  75. package/src/manifest/load-file.ts +11 -0
  76. package/src/manifest/load.ts +65 -77
  77. package/src/manifest/locate.ts +55 -0
  78. package/src/manifest/schema.ts +400 -233
  79. package/src/{scene → physics}/collider-dimensions.ts +3 -3
  80. package/src/physics/physics-registry.ts +1 -1
  81. package/src/{world2d/authoring-2d.ts → pixi/authoring.ts} +24 -11
  82. package/src/pixi/index.ts +43 -0
  83. package/src/{world2d/ingest-iframe-2d.ts → pixi/ingest-iframe.ts} +9 -9
  84. package/src/{world2d/ingest2d.ts → pixi/ingest.ts} +28 -28
  85. package/src/{world2d/physics2d-registry.ts → pixi/physics-registry.ts} +1 -1
  86. package/src/{world2d/scene-capture-2d.ts → pixi/scene-capture.ts} +3 -3
  87. package/src/{world2d/system-adapters-2d.ts → pixi/system-adapters.ts} +2 -2
  88. package/src/react/unmanaged-root-detector.ts +26 -1
  89. package/src/react/use-data.ts +5 -5
  90. package/src/react/use-selection.tsx +15 -42
  91. package/src/react/{game-state.tsx → world-state.tsx} +44 -46
  92. package/src/render/auto-batcher.ts +1 -2
  93. package/src/{scene → render}/instance-mesh.ts +1 -1
  94. package/src/{scene → render}/light-camera-factory.ts +14 -13
  95. package/src/render/lod.ts +17 -0
  96. package/src/{scene → render}/material-factory.ts +8 -7
  97. package/src/{scene → render}/particles-factory.ts +62 -12
  98. package/src/render/render-batch-system.ts +14 -41
  99. package/src/render/render-features.ts +1 -1
  100. package/src/render/render-settings.ts +1 -2
  101. package/src/render/spark-renderer-lifecycle.ts +1 -1
  102. package/src/runtime/create-runtime.ts +352 -463
  103. package/src/runtime/debug-bridge.ts +148 -93
  104. package/src/runtime/debug-registry.ts +79 -54
  105. package/src/runtime/dev-layers.ts +40 -0
  106. package/src/runtime/frame-selector-cache.ts +4 -4
  107. package/src/runtime/game.ts +618 -369
  108. package/src/runtime/gameplay-rng-trap.ts +6 -7
  109. package/src/runtime/input-router.ts +11 -11
  110. package/src/runtime/mount-game.ts +54 -55
  111. package/src/runtime/mount-manifest.ts +154 -150
  112. package/src/runtime/presentation.ts +141 -0
  113. package/src/runtime/render-audio-control.ts +64 -53
  114. package/src/runtime/render-control.ts +45 -51
  115. package/src/runtime/render-seed.ts +3 -4
  116. package/src/runtime/state-bridge.ts +17 -18
  117. package/src/runtime/types.ts +94 -61
  118. package/src/setup/setup-renderer.ts +11 -6
  119. package/src/world3d-react/engine-bridge.ts +46 -33
  120. package/src/world3d-react/index.ts +31 -26
  121. package/src/world3d-react/r3f-adapter.tsx +211 -96
  122. package/src/world3d-react/r3f-root-factory.tsx +91 -0
  123. package/src/world3d-react/renderer-config.ts +137 -0
  124. package/src/world3d-react/world-context.ts +141 -111
  125. package/schemas/entity2d.schema.json +0 -468
  126. package/schemas/prefab.schema.json +0 -9992
  127. package/schemas/scn2d.schema.json +0 -494
  128. package/schemas/vscn.schema.json +0 -10851
  129. package/src/adapter/game-adapter.ts +0 -164
  130. package/src/adapter/ingest/overlay-applier.ts +0 -207
  131. package/src/adapter/ingest/overlay-apply.ts +0 -168
  132. package/src/adapter/ingest/overlay-file.ts +0 -126
  133. package/src/adapter/ingest/overlay-report.ts +0 -176
  134. package/src/animation/gsap-registration.ts +0 -184
  135. package/src/audio/audio-introspection.ts +0 -290
  136. package/src/audio/index.ts +0 -39
  137. package/src/audio/tone-clock-binding.ts +0 -98
  138. package/src/audio/tone-context.ts +0 -175
  139. package/src/audio/tone-offline-render.ts +0 -167
  140. package/src/ecs/component-manager.ts +0 -814
  141. package/src/ecs/game-component.ts +0 -260
  142. package/src/ecs/hmr-swap-report.ts +0 -65
  143. package/src/physics/trigger-dispatch.ts +0 -97
  144. package/src/react/root-adapter.tsx +0 -49
  145. package/src/scene/asset-paths.ts +0 -121
  146. package/src/scene/asset-ref-check.ts +0 -248
  147. package/src/scene/component-registry.ts +0 -51
  148. package/src/scene/parse.ts +0 -204
  149. package/src/scene/scene-apply.ts +0 -407
  150. package/src/scene/scene-diff-schema.ts +0 -115
  151. package/src/scene/scene-diff-types.ts +0 -29
  152. package/src/scene/scene-loader.ts +0 -1526
  153. package/src/scene/scene-query.ts +0 -63
  154. package/src/scene/scene-types.ts +0 -33
  155. package/src/scene/scene-version.ts +0 -40
  156. package/src/scene/schema/animation.ts +0 -46
  157. package/src/scene/schema/audio.ts +0 -25
  158. package/src/scene/schema/entity-ref.ts +0 -78
  159. package/src/scene/schema/entity.ts +0 -189
  160. package/src/scene/schema/index.ts +0 -51
  161. package/src/scene/schema/joint.ts +0 -26
  162. package/src/scene/schema/physics.ts +0 -49
  163. package/src/scene/schema/scene-file.ts +0 -292
  164. package/src/scene/schema/shadow.ts +0 -24
  165. package/src/scene/schema/spline.ts +0 -21
  166. package/src/world2d/asset-paths2d.ts +0 -44
  167. package/src/world2d/capture-to-scene2d.ts +0 -52
  168. package/src/world2d/collision-2d.ts +0 -99
  169. package/src/world2d/entity2d-asset.ts +0 -22
  170. package/src/world2d/index.ts +0 -91
  171. package/src/world2d/physics2d-transform.ts +0 -173
  172. package/src/world2d/physics2d-units.ts +0 -10
  173. package/src/world2d/pixi-game-adapter.ts +0 -439
  174. package/src/world2d/pixi-surface.ts +0 -78
  175. package/src/world2d/scene2d-identity.ts +0 -49
  176. package/src/world2d/scene2d-loader.ts +0 -433
  177. package/src/world2d/schema/entity2d.ts +0 -163
  178. package/src/world2d/schema/physics2d.ts +0 -64
  179. package/src/world2d/schema/sprite.ts +0 -99
  180. package/src/world2d/schema/tilemap.ts +0 -39
  181. package/src/world2d/schema/tuples2d.ts +0 -25
  182. package/src/world2d/transform-writer-2d.ts +0 -42
  183. package/src/world2d/types.ts +0 -74
  184. package/src/world3d-react/behavior.tsx +0 -146
  185. /package/src/{scene → render}/mesh-shadow.ts +0 -0
@@ -0,0 +1,141 @@
1
+ /**
2
+ * runtime/presentation.ts — how a game with a DECLARED resolution is presented in a host panel.
3
+ *
4
+ * `vgai.project.json`'s `resolution` is the game's own LOGICAL size: the coordinate space its
5
+ * cameras frame, its DOM overlay lays out in, and its 2D worlds author positions against. A host
6
+ * panel is whatever size the user dragged it to, and the two are not the same number.
7
+ *
8
+ * The rule, and it is the whole module:
9
+ *
10
+ * **A declared resolution is rendered AT that resolution, and the RESULT is scaled uniformly to
11
+ * fit the panel, centered, letterboxed.**
12
+ *
13
+ * That is presentation scaling — an OS scaling a game window — and deliberately NOT a stretch mode.
14
+ * Nothing here reflows the game, changes a camera's aspect, or re-anchors a HUD: the game renders
15
+ * one shape and the host draws that shape smaller or larger. Every presented root of one project
16
+ * (a three canvas, a pixi canvas, a dom overlay) therefore shares ONE scaled rect, which is what
17
+ * keeps a HUD registered over the world beneath it.
18
+ *
19
+ * A project that declares no `resolution` is unchanged: the game fills the panel and its cameras
20
+ * follow the panel's aspect ({@link PRESENTATION_FILL}).
21
+ *
22
+ * The math is here, alone and pure, because it had already been written twice inline (the Game
23
+ * panel's device-preview branch and the story preview's artboard) and a third copy would have been
24
+ * the one that drifted.
25
+ */
26
+
27
+ /** A width/height pair in whatever unit the caller is working in. */
28
+ export interface PresentationSize {
29
+ readonly width: number;
30
+ readonly height: number;
31
+ }
32
+
33
+ /** Where a logical rect lands inside a container, scaled uniformly and centered. */
34
+ export interface PresentationRect {
35
+ /** Uniform logical→container scale factor. Always finite and > 0. */
36
+ readonly scale: number;
37
+ /** The scaled rect's size in container units. */
38
+ readonly width: number;
39
+ readonly height: number;
40
+ /** The letterbox margins — how far in from the container's top-left the scaled rect starts. */
41
+ readonly offsetX: number;
42
+ readonly offsetY: number;
43
+ }
44
+
45
+ function positiveOrZero(value: number): number {
46
+ return Number.isFinite(value) && value > 0 ? value : 0;
47
+ }
48
+
49
+ /**
50
+ * Fit `logical` inside `container`, preserving aspect and centering the result.
51
+ *
52
+ * `allowUpscale` is the difference between the two callers this replaces and it is not cosmetic.
53
+ * A DEVICE PREVIEW must never upscale — the point is to see the device's own pixels, so a phone
54
+ * frame in a big panel stays phone-sized. A game's DECLARED resolution must upscale, or a 720x540
55
+ * game sits as a postage stamp in the middle of a maximized panel and the feature reads as broken.
56
+ *
57
+ * A degenerate input (zero, negative, NaN — a panel mid-collapse, a manifest with a 0) yields
58
+ * `scale: 1` and a zero-offset rect rather than an Infinity that would propagate into a CSS
59
+ * transform.
60
+ */
61
+ export function fitPresentation(
62
+ container: PresentationSize,
63
+ logical: PresentationSize,
64
+ options: { readonly allowUpscale?: boolean } = {},
65
+ ): PresentationRect {
66
+ const cw = positiveOrZero(container.width);
67
+ const ch = positiveOrZero(container.height);
68
+ const lw = positiveOrZero(logical.width);
69
+ const lh = positiveOrZero(logical.height);
70
+ if (cw === 0 || ch === 0 || lw === 0 || lh === 0) {
71
+ return { scale: 1, width: lw, height: lh, offsetX: 0, offsetY: 0 };
72
+ }
73
+ const raw = Math.min(cw / lw, ch / lh);
74
+ const scale = options.allowUpscale === true ? raw : Math.min(raw, 1);
75
+ const width = lw * scale;
76
+ const height = lh * scale;
77
+ return {
78
+ scale,
79
+ width,
80
+ height,
81
+ offsetX: (cw - width) / 2,
82
+ offsetY: (ch - height) / 2,
83
+ };
84
+ }
85
+
86
+ /** The game fills its panel: no declared resolution, no device preset, today's behaviour. */
87
+ export interface PresentationFill {
88
+ readonly kind: 'fill';
89
+ }
90
+
91
+ /** The game renders at a fixed logical size and the host scales the result. */
92
+ export interface PresentationLogical {
93
+ readonly kind: 'logical';
94
+ readonly width: number;
95
+ readonly height: number;
96
+ /** `device` — a preview preset the user picked; `manifest` — the project's own `resolution`. */
97
+ readonly source: 'device' | 'manifest';
98
+ /** Device previews never upscale; a declared resolution does. See {@link fitPresentation}. */
99
+ readonly allowUpscale: boolean;
100
+ }
101
+
102
+ export type PresentedSize = PresentationFill | PresentationLogical;
103
+
104
+ export const PRESENTATION_FILL: PresentationFill = { kind: 'fill' };
105
+
106
+ /**
107
+ * Decide what size a game is presented at, from the two things that can ask for one.
108
+ *
109
+ * PRECEDENCE: an explicitly PICKED device resolution wins over the manifest's declared one. The
110
+ * pick is a live act by whoever is looking at the panel ("show me this on a phone"), and a project
111
+ * setting must not silently override it — otherwise every declared-resolution project would make
112
+ * the device-preview picker do nothing.
113
+ */
114
+ export function resolvePresentedSize(
115
+ picked: { readonly width: number | null; readonly height: number | null },
116
+ declared?: PresentationSize | undefined,
117
+ ): PresentedSize {
118
+ if (picked.width !== null && picked.height !== null) {
119
+ return {
120
+ kind: 'logical',
121
+ width: picked.width,
122
+ height: picked.height,
123
+ source: 'device',
124
+ allowUpscale: false,
125
+ };
126
+ }
127
+ if (
128
+ declared !== undefined &&
129
+ positiveOrZero(declared.width) > 0 &&
130
+ positiveOrZero(declared.height) > 0
131
+ ) {
132
+ return {
133
+ kind: 'logical',
134
+ width: declared.width,
135
+ height: declared.height,
136
+ source: 'manifest',
137
+ allowUpscale: true,
138
+ };
139
+ }
140
+ return PRESENTATION_FILL;
141
+ }
@@ -1,39 +1,51 @@
1
1
  /**
2
- * Render-mode AUDIO control seam (I6, `docs/AI-NATIVE-AUTHORING-
3
- * IMPLEMENTATION-SPEC.md` §15 I6 "Encode and Mux" + the audio-capture half of
4
- * §13 G4). This is `render-control.ts`'s sibling for the audio side: where
5
- * that module publishes `window.__vgaiRender` (exact-time seeks + composited
6
- * frame passes), THIS module publishes `window.__vgaiRenderAudio` an
7
- * OPT-IN surface a render-mode page installs only when it declares a Tone
8
- * score, so `vgai render-cinematic` (`packages/vgai-cli/src/render-
9
- * cinematic.ts`) can detect "this cinematic has a deterministic audio track"
10
- * with a single property check (`__vgaiRenderAudio?.hasAudio === true`)
11
- * rather than needing a schema field threaded through every fixture.
2
+ * Render-mode AUDIO control seam (I6, "Encode and Mux" + the audio-capture
3
+ * half of §13 G4). This is `render-control.ts`'s sibling for the audio side:
4
+ * where that module publishes `window.__vgaiRender` (exact-time seeks +
5
+ * composited frame passes), THIS module publishes `window.__vgaiRenderAudio`
6
+ * an OPT-IN surface a render-mode page installs only when it declares a score,
7
+ * so `vgai render-cinematic` (`packages/vgai-cli/src/render-cinematic.ts`) can
8
+ * detect "this cinematic has a deterministic audio track" with a single
9
+ * property check (`__vgaiRenderAudio?.hasAudio === true`) rather than needing
10
+ * a schema field threaded through every fixture.
12
11
  *
13
12
  * Deliberately a SEPARATE global from `__vgaiRender`/`VgaiRenderHarness`
14
13
  * (`render-control.ts`), not an extra method bolted onto that interface:
15
14
  * every existing render-mode page (I0's `render-cinematic` fixture, I8's
16
- * `reference-cinematic`) has no Tone score at all, and this keeps their
15
+ * `reference-cinematic`) has no score at all, and this keeps their
17
16
  * bundles/tests completely untouched — `render-control.ts` itself is not
18
17
  * modified by this unit.
19
18
  *
20
- * ## Why `renderAudio` takes `(start, end)` and `compose` is a FACTORY
19
+ * ## What this module is, and what it deliberately is not
21
20
  *
22
- * `Tone.Offline` (via `audio/tone-offline-render.ts`) gives the composition
23
- * callback LOCAL time 0 at whatever `start` was requested — an offline
24
- * render of `[10, 10.72)` schedules its first event at LOCAL time 0, not
25
- * absolute time 10 (see that module's own doc comment). A render-mode page
26
- * authors its score in ABSOLUTE/canonical time (e.g. "a note at the same
27
- * t=1.2s the video's Director cut happens"), so the actual Tone-native
28
- * compose callback passed to `renderToneOffline` must subtract the
29
- * requested range's `start` from every scheduled event time. Rather than
30
- * have every fixture re-derive that subtraction inline, this module's
31
- * {@link RenderAudioControlOptions.compose} is a FACTORY: called once per
32
- * `renderAudio(start, end)` invocation with `start`, returning the actual
33
- * `Tone.Offline`-native callback closed over that offset. This is the
34
- * concrete fix for the G12 review follow-up named in this unit's build
35
- * brief ("renderToneOffline gives the composition LOCAL time 0 at `start`,
36
- * so your composition must close over the range/offset").
21
+ * `window.__vgaiRenderAudio` is a PROTOCOL the surface the Node-side capture
22
+ * driver reads through `page.evaluate`, the audio counterpart to a debug
23
+ * adapter. That is why it stays in the engine. Producing the samples is not a
24
+ * protocol, so it does not: {@link RenderAudioControlOptions.render} is
25
+ * supplied by the caller and hands back a native `AudioBuffer`. This module
26
+ * owns the render-mode gate, the WAV encoding, and the base64 wire format.
27
+ *
28
+ * It used to call `renderToneOffline` itself, which put `tone` in the engine's
29
+ * dependency list for a page that merely wanted to publish a global. The
30
+ * offline renderer moved to the `music` capability (`src/lib/music/
31
+ * tone-offline-render.ts`); a fixture composes the two in one line, and a page
32
+ * scoring with anything else a pre-rendered WAV decoded into an
33
+ * `AudioBuffer`, a WebAudio `OfflineAudioContext` graph, another library
34
+ * satisfies the same contract without touching Tone at all.
35
+ *
36
+ * ## Why `renderAudio` takes `(start, end)` — the offset trap
37
+ *
38
+ * An offline render of `[10, 10.72)` schedules its first event at LOCAL time
39
+ * 0, not absolute time 10. A render-mode page authors its score in
40
+ * ABSOLUTE/canonical time (e.g. "a note at the same t=1.2s the video's
41
+ * Director cut happens"), so whatever the caller's `render` does internally
42
+ * must subtract the requested range's `start` from every scheduled event time.
43
+ * With Tone that means building the `Tone.Offline` callback per call, closed
44
+ * over `start` — see `renderToneOffline`'s own doc. This was the G12 review
45
+ * follow-up named in the unit's build brief ("renderToneOffline gives the
46
+ * composition LOCAL time 0 at `start`, so your composition must close over the
47
+ * range/offset"); it is now the caller's to honour, and the capability's
48
+ * fixture shows it.
37
49
  *
38
50
  * ## Wire format — why WAV bytes cross as base64, not a raw sample array
39
51
  *
@@ -48,22 +60,27 @@
48
60
  * logic duplicated on the Node side at all.
49
61
  */
50
62
 
51
- import { renderToneOffline, type ToneOfflineComposeFn } from '../audio/tone-offline-render';
52
63
  import { encodeWav16, pcmStats } from '../audio/wav-encode';
53
64
  import { isRenderModeRequested } from './render-control';
54
65
 
55
- /** Builds the actual `Tone.Offline`-native compose callback for a render
56
- * starting at `rangeStart` (absolute/canonical seconds)see the module
57
- * doc's "closes over the range/offset" section above. */
58
- export type RenderAudioComposeFactory = (rangeStart: number) => ToneOfflineComposeFn;
66
+ /** What a caller's renderer hands back: real PCM in a native `AudioBuffer`.
67
+ * `AudioBuffer` not a vgai type is the point: any offline audio path in
68
+ * the platform already produces one. */
69
+ export interface RenderedAudio {
70
+ readonly buffer: AudioBuffer;
71
+ readonly durationSeconds: number;
72
+ }
73
+
74
+ /** Render `[start, end)` (absolute/canonical seconds) offline and
75
+ * deterministically. The offset trap in the module doc is the renderer's to
76
+ * honour: identical `(start, end)` on an unchanged score must produce
77
+ * identical samples, since the capture driver relies on it. */
78
+ export type OfflineAudioRenderer = (start: number, end: number) => Promise<RenderedAudio>;
59
79
 
60
80
  export interface RenderAudioControlOptions {
61
- /** See {@link RenderAudioComposeFactory}. */
62
- readonly compose: RenderAudioComposeFactory;
63
- /** Output channel count. Default 2 (stereo) — same default as `tone-offline-render.ts`. */
64
- readonly channels?: number;
65
- /** Output sample rate. Default 44100 — same default as `tone-offline-render.ts`. */
66
- readonly sampleRate?: number;
81
+ /** See {@link OfflineAudioRenderer}. With the `music` capability this is one
82
+ * line over `renderToneOffline`; nothing here knows or cares that it is. */
83
+ readonly render: OfflineAudioRenderer;
67
84
  /** Where to publish the harness. Defaults to the real `window` — override in a unit test. */
68
85
  readonly target?: Record<string, unknown>;
69
86
  /** Where to read `?vgai-render=1` from. Defaults to `window.location`. */
@@ -88,17 +105,17 @@ export interface RenderAudioResult {
88
105
 
89
106
  /** The `window.__vgaiRenderAudio` surface. Presence of this global (guarded
90
107
  * by `hasAudio: true`, never a bare `undefined`/`false` value on a present
91
- * object) IS the "this cinematic declares a Tone score" signal `render-
108
+ * object) IS the "this cinematic declares a score" signal `render-
92
109
  * cinematic.ts` checks — see this module's own top doc comment. */
93
110
  export interface VgaiRenderAudioHarness {
94
111
  readonly hasAudio: true;
95
112
  /** Render `[start, end)` (absolute/canonical seconds, exactly the same
96
- * range `window.__vgaiRender`'s frame walk covers) offline via
97
- * `Tone.Offline`, returning a complete WAV file. Deterministic: identical
98
- * `(start, end)` on an unchanged score produces byte-identical
99
- * `wavBase64` every call (the same `Tone.Offline` determinism
100
- * `tone-offline-render.ts`/G2 already proves this module adds no
101
- * additional source of nondeterminism). */
113
+ * range `window.__vgaiRender`'s frame walk covers), returning a complete
114
+ * WAV file. Deterministic: identical `(start, end)` on an unchanged score
115
+ * produces byte-identical `wavBase64` every call this module adds no
116
+ * source of nondeterminism, so the property is exactly whatever the
117
+ * supplied {@link OfflineAudioRenderer} guarantees (with the `music`
118
+ * capability, the `Tone.Offline` determinism G2 already proves). */
102
119
  renderAudio(start: number, end: number): Promise<RenderAudioResult>;
103
120
  }
104
121
 
@@ -119,18 +136,12 @@ export function installRenderAudioHarness(
119
136
  if (!isRenderModeRequested(location)) return undefined;
120
137
 
121
138
  const target = opts.target ?? (window as unknown as Record<string, unknown>);
122
- const { compose, channels, sampleRate } = opts;
139
+ const { render } = opts;
123
140
 
124
141
  const harness: VgaiRenderAudioHarness = {
125
142
  hasAudio: true,
126
143
  async renderAudio(start: number, end: number): Promise<RenderAudioResult> {
127
- const rendered = await renderToneOffline({
128
- start,
129
- end,
130
- ...(channels !== undefined ? { channels } : {}),
131
- ...(sampleRate !== undefined ? { sampleRate } : {}),
132
- compose: compose(start),
133
- });
144
+ const rendered = await render(start, end);
134
145
  const channelData: Float32Array[] = [];
135
146
  for (let ch = 0; ch < rendered.buffer.numberOfChannels; ch++) {
136
147
  channelData.push(rendered.buffer.getChannelData(ch));
@@ -1,20 +1,19 @@
1
1
  /**
2
- * Render-control runtime seam (I2, `docs/AI-NATIVE-AUTHORING-IMPLEMENTATION-SPEC.md`
3
- * §15 I2). This is the harness a deterministic-capture host (Playwright/CDP,
4
- * I0/I3/I4) drives: exact-time seeks against the canonical
5
- * `AnimationClock` (`animation/animation-clock.ts`), and single
6
- * `preRender`+`render` phase passes across every world on a `Game`
7
- * (`runtime/game.ts`) WITHOUT ever advancing gameplay fixed-step substeps
8
- * (`input`/`prePhysics`/`physics`/`postPhysics`/`gameLogic`/`animation` are
9
- * never invoked by this seam; `--simulate` (I5) is a different, later, path
10
- * that deliberately drives `game.runFrame(fixedDt)` instead).
2
+ * Render-control runtime seam (I2). This is the harness a deterministic-capture host
3
+ * (Playwright/CDP, I0/I3/I4) drives: exact-time seeks against the canonical
4
+ * `AnimationClock` (`animation/animation-clock.ts`), and single `preRender`+`render`
5
+ * phase passes across every world on a `Game` (`runtime/game.ts`) WITHOUT ever
6
+ * advancing gameplay fixed-step substeps
7
+ * (`input`/`prePhysics`/`physics`/`postPhysics`/`gameLogic`/`animation` are never
8
+ * invoked by this seam; `--simulate` (I5) is a different, later, path that
9
+ * deliberately drives `game.runFrame(fixedDt)` instead).
11
10
  *
12
11
  * This module does NOT reuse `PlayState.step()` (`Game.play.step()`) — that
13
12
  * surface only advances the currently-FROZEN (paused) set of roots and is a
14
13
  * whole-call no-op while unpaused (see its doc comment in `runtime/game.ts`);
15
14
  * the spec review explicitly flagged it as the wrong seam. `renderOnce`
16
15
  * instead walks `game.roots` directly and calls each world's own
17
- * `WorldFrameHooks.runPhase` for exactly two phases.
16
+ * `RootFrameHooks.runPhase` for exactly two phases.
18
17
  *
19
18
  * Also does NOT install anything by default — a caller must explicitly call
20
19
  * {@link installRenderControlHarness}, and even then it only actually
@@ -36,7 +35,7 @@
36
35
  */
37
36
 
38
37
  import { SystemPhase } from '../core/types';
39
- import type { Game, GameInternal, WorldFrameHooks } from './game';
38
+ import type { Game, GameInternal, RootFrameHooks } from './game';
40
39
  import { RENDER_MODE_QUERY_PARAM } from './render-seed';
41
40
 
42
41
  /** Per-subsystem readiness verdict (I2 AC: "Runtime reports asset, shader,
@@ -193,8 +192,8 @@ export interface VgaiRenderHarness {
193
192
  * page-declared list (e.g. "live Colyseus networking", "non-seeded ambient
194
193
  * audio") this fixture/game names about itself, and (2) any world this
195
194
  * seam itself could not frame-gate (a `drivesOwnLoop` world, or a
196
- * host-driven world with no `WorldFrameHooks` — the SAME two conditions
197
- * {@link renderOnce}'s `renderableWorlds` skips, described here as policy
195
+ * host-driven world with no `RootFrameHooks` — the SAME two conditions
196
+ * {@link renderOnce}'s `renderableRoots` skips, described here as policy
198
197
  * rather than merely warned about once to the console). Declared entries
199
198
  * come first, in declaration order; world-exclusion entries follow, in
200
199
  * `game.roots` declaration order.
@@ -222,7 +221,7 @@ export interface VgaiRenderHarness {
222
221
  * `PerformanceProfiler` (`dev/performance-profiler.ts`) enabled, sampling
223
222
  * it after every frame, then restores the profiler's prior enabled state.
224
223
  * Returns per-frame CPU/phase timings + render counters, plus a
225
- * structural node/entity count of every threejs/pixijs world's live scene
224
+ * structural node/entity count of every three/canvas world's live scene
226
225
  * graph. NOT a parallel instrumentation layer: every number here comes
227
226
  * from the existing profiler (timings, render counters) or the live world
228
227
  * roots themselves (counts).
@@ -257,7 +256,7 @@ export interface PerfFrameSample {
257
256
  }
258
257
 
259
258
  /** Per-world structural counts from {@link VgaiRenderHarness.perfSample}. */
260
- export interface PerfWorldCount {
259
+ export interface PerfRootCount {
261
260
  readonly id: string;
262
261
  readonly kind: string;
263
262
  /** Total scene-graph descendants of the world root (exclusive of the root
@@ -271,9 +270,9 @@ export interface PerfWorldCount {
271
270
 
272
271
  export interface PerfSampleReport {
273
272
  readonly frames: readonly PerfFrameSample[];
274
- /** threejs/pixijs worlds only — a react/DOM world has no scene-graph node
273
+ /** three/canvas worlds only — a react/DOM world has no scene-graph node
275
274
  * count; it is omitted rather than fabricated. */
276
- readonly worlds: readonly PerfWorldCount[];
275
+ readonly worlds: readonly PerfRootCount[];
277
276
  }
278
277
 
279
278
  export interface RenderControlHarnessOptions {
@@ -360,12 +359,12 @@ const RENDER_ONLY_PHASES = [SystemPhase.PRE_RENDER, SystemPhase.RENDER] as const
360
359
  * than silently omitted, mirroring this file's "fail loudly, never a silent
361
360
  * no-op" convention. A host-driven world with no `frame` (an opaque/foreign
362
361
  * mount) is skipped the same way — there is no phase hook to call. Both
363
- * checks happen ONCE, up front (see {@link renderableWorlds}), not per
362
+ * checks happen ONCE, up front (see {@link renderableRoots}), not per
364
363
  * phase — a world unrenderable in `preRender` is unrenderable in `render`
365
364
  * too, so warning twice would just be noise.
366
365
  */
367
366
  async function renderOnce(game: Game): Promise<void> {
368
- const roots = renderableWorlds(game);
367
+ const roots = renderableRoots(game);
369
368
  for (const phase of RENDER_ONLY_PHASES) {
370
369
  // Game-scoped systems first, then per-world frame hooks — the SAME order
371
370
  // GameInternal.runFrame uses (runtime/game.ts). See doc comment above.
@@ -377,24 +376,20 @@ async function renderOnce(game: Game): Promise<void> {
377
376
  await compositionFence();
378
377
  }
379
378
 
380
- interface RenderableWorld {
379
+ interface RenderableRoot {
381
380
  readonly id: string;
382
- readonly frame: WorldFrameHooks;
381
+ readonly frame: RootFrameHooks;
383
382
  }
384
383
 
385
384
  /** Filter `game.roots` down to the ones {@link renderOnce} can actually
386
- * drive (host-driven, with real `WorldFrameHooks`) — see that function's
385
+ * drive (host-driven, with real `RootFrameHooks`) — see that function's
387
386
  * doc comment for why an excluded world is warned about loudly, not
388
387
  * silently dropped. */
389
- function renderableWorlds(game: Game): RenderableWorld[] {
390
- const result: RenderableWorld[] = [];
388
+ function renderableRoots(game: Game): RenderableRoot[] {
389
+ const result: RenderableRoot[] = [];
391
390
  for (const world of game.roots) {
392
391
  if (world.mounted.drivesOwnLoop) {
393
- warnUnrenderable(
394
- world.id,
395
- 'drives its own loop — renderOnce() cannot frame-gate it ' +
396
- '(docs/AI-NATIVE-AUTHORING-IMPLEMENTATION-SPEC.md §15 I5 names this policy)',
397
- );
392
+ warnUnrenderable(world.id, 'drives its own loop — renderOnce() cannot frame-gate it');
398
393
  continue;
399
394
  }
400
395
  if (!world.frame) {
@@ -416,19 +411,19 @@ function warnUnrenderable(worldLabel: string, reason: string): void {
416
411
  }
417
412
 
418
413
  /**
419
- * I5 AC 5 policy text for the two conditions {@link renderableWorlds} skips —
414
+ * I5 AC 5 policy text for the two conditions {@link renderableRoots} skips —
420
415
  * shared so {@link VgaiRenderHarness.excludedFromDeterminism} names the SAME
421
416
  * roots `renderOnce()` warns about, in the SAME words, rather than
422
417
  * maintaining a second, driftable description of the same two checks.
423
418
  */
424
- function computeWorldExclusionReasons(game: Game): string[] {
419
+ function computeRootExclusionReasons(game: Game): string[] {
425
420
  const reasons: string[] = [];
426
421
  for (const world of game.roots) {
427
422
  if (world.mounted.drivesOwnLoop) {
428
423
  reasons.push(
429
424
  `world "${world.id}" drives its own loop — cannot be frame-gated for deterministic ` +
430
- 'capture (docs/AI-NATIVE-AUTHORING-IMPLEMENTATION-SPEC.md §15 I5 names this policy: ' +
431
- 'FAIL LOUDLY or be excluded only by an explicit declared policy).',
425
+ 'capture (the policy is to FAIL LOUDLY, or be excluded only by an explicit declared ' +
426
+ 'policy).',
432
427
  );
433
428
  continue;
434
429
  }
@@ -442,11 +437,11 @@ function computeWorldExclusionReasons(game: Game): string[] {
442
437
  return reasons;
443
438
  }
444
439
 
445
- /** Structural scene-graph walk shared by threejs (`Object3D`) and pixijs
446
- * (`Container`) roots — both expose a `children` array, and threejs nodes
440
+ /** Structural scene-graph walk shared by three (`Object3D`) and canvas
441
+ * (`Container`) roots — both expose a `children` array, and three nodes
447
442
  * additionally carry `userData` (where the loader/editor entity tag lives).
448
443
  * Deliberately duck-typed so this file keeps its type-only three/pixi rule. */
449
- function countWorldGraph(root: { readonly children?: readonly unknown[] }): {
444
+ function countRootGraph(root: { readonly children?: readonly unknown[] }): {
450
445
  nodes: number;
451
446
  entities: number;
452
447
  } {
@@ -465,16 +460,16 @@ function countWorldGraph(root: { readonly children?: readonly unknown[] }): {
465
460
  return { nodes, entities };
466
461
  }
467
462
 
468
- /** See {@link PerfSampleReport.worlds} — threejs/pixijs worlds only; a
463
+ /** See {@link PerfSampleReport.worlds} — three/canvas worlds only; a
469
464
  * react/DOM world has no scene-graph node count and is omitted, never
470
465
  * fabricated. */
471
- function countWorlds(game: Game): PerfWorldCount[] {
472
- const counts: PerfWorldCount[] = [];
466
+ function countRoots(game: Game): PerfRootCount[] {
467
+ const counts: PerfRootCount[] = [];
473
468
  for (const world of game.roots) {
474
- if (world.kind === 'threejs') {
475
- counts.push({ id: world.id, kind: world.kind, ...countWorldGraph(world.threeRoot()) });
476
- } else if (world.kind === 'pixijs') {
477
- counts.push({ id: world.id, kind: world.kind, ...countWorldGraph(world.pixiRoot()) });
469
+ if (world.kind === 'three') {
470
+ counts.push({ id: world.id, kind: world.kind, ...countRootGraph(world.threeScene()) });
471
+ } else if (world.kind === 'canvas') {
472
+ counts.push({ id: world.id, kind: world.kind, ...countRootGraph(world.pixiStage()) });
478
473
  }
479
474
  }
480
475
  return counts;
@@ -581,7 +576,7 @@ export function installRenderControlHarness(
581
576
  }
582
577
  },
583
578
  excludedFromDeterminism(): string[] {
584
- return [...declaredExclusions, ...computeWorldExclusionReasons(game)];
579
+ return [...declaredExclusions, ...computeRootExclusionReasons(game)];
585
580
  },
586
581
  simulateFixedDt(): number {
587
582
  return simulateFixedDt;
@@ -606,7 +601,7 @@ export function installRenderControlHarness(
606
601
  } finally {
607
602
  profiler.enabled = wasEnabled;
608
603
  }
609
- return { frames, worlds: countWorlds(game) };
604
+ return { frames, worlds: countRoots(game) };
610
605
  },
611
606
  };
612
607
 
@@ -617,10 +612,9 @@ export function installRenderControlHarness(
617
612
  /**
618
613
  * Defensive assertion for the I2 AC "Pixi roots are pinned to the WebGL
619
614
  * renderer (not WebGPU) in render mode". In THIS engine that invariant
620
- * already holds unconditionally — `world2d/pixi-surface.ts`'s
621
- * `createPixiSurface` hardcodes `preference: 'webgl'` on every
622
- * `Application.init()` call it makes, and `setup/setup-renderer.ts` never
623
- * constructs a `WebGPURenderer` for threejs roots either — so there is no
615
+ * already holds unconditionally — no in-repo mount constructs a Pixi
616
+ * `Application` with a WebGPU preference, and `setup/setup-renderer.ts` never
617
+ * constructs a `WebGPURenderer` for three roots either so there is no
624
618
  * live WebGPU code path for a first-party render-mode page to disable.
625
619
  * This helper exists for a render-mode installer that mounts a CUSTOM/
626
620
  * foreign pixi adapter (`create-runtime.ts`'s `{ module }` pixi path) this
@@ -631,13 +625,13 @@ export function installRenderControlHarness(
631
625
  * type-only-pixi rule `runtime/game.ts`/`create-runtime.ts` document for
632
626
  * themselves).
633
627
  */
634
- export function assertPixiWorldIsWebGL(rendererType: unknown, worldId: string): void {
628
+ export function assertPixiRootIsWebGL(rendererType: unknown, worldId: string): void {
635
629
  const isWebGPU =
636
630
  rendererType === 2 || (typeof rendererType === 'string' && /webgpu/i.test(rendererType));
637
631
  if (isWebGPU) {
638
632
  throw new Error(
639
633
  `[render-control] world "${worldId}": Pixi renderer is WebGPU in render mode — ` +
640
- 'render-control requires WebGL (docs/AI-NATIVE-AUTHORING-IMPLEMENTATION-SPEC.md §15 I2).',
634
+ 'render-control requires WebGL.',
641
635
  );
642
636
  }
643
637
  }
@@ -1,8 +1,7 @@
1
1
  /**
2
- * Deterministic `Math.random` seeding for render-control mode (I2 AC,
3
- * `docs/AI-NATIVE-AUTHORING-IMPLEMENTATION-SPEC.md` §15 I2: "`Math.random`
4
- * is deterministically seeded/stubbed page-wide before any game module
5
- * executes").
2
+ * Deterministic `Math.random` seeding for render-control mode (I2 AC:
3
+ * "`Math.random` is deterministically seeded/stubbed page-wide before any
4
+ * game module executes").
6
5
  *
7
6
  * Deliberately a ZERO-IMPORT module (no `three`, no other engine file) so
8
7
  * its own evaluation is instantaneous — a render-mode entry page loads this
@@ -1,36 +1,35 @@
1
1
  /**
2
- * Frame-versioned game state bridge (T7.4 slice 1 `docs/REACT-STATE-BRIDGE.md`
3
- * §2, the D7 remainder). Components mutate state on `this` freely during
4
- * ticks there is no proxy, no dirty tracking, no event per mutation (Track
5
- * A: no mirror, no sync layer). So the unit of change a subscriber can
6
- * observe is the FRAME: `GameInternal.runFrame` bumps `frameVersion` and
7
- * notifies subscribers at most once, at its tail, AFTER every phase of every
8
- * world and every world's `endFrame` hook has run (see the wiring in
9
- * `runtime/game.ts`) mirroring the D7 "from `gameLogic` onward, every
10
- * world's post-step state is readable" guarantee, extended to "after the
2
+ * Frame-versioned game state bridge (T7.4 slice 1, the D7 remainder). Components
3
+ * mutate state on `this` freely during ticks — there is no proxy, no dirty
4
+ * tracking, no event per mutation (Track A: no mirror, no sync layer). So the
5
+ * unit of change a subscriber can observe is the FRAME: `GameInternal.runFrame`
6
+ * bumps `frameVersion` and notifies subscribers at most once, at its tail, AFTER
7
+ * every phase of every world and every world's `endFrame` hook has run (see the
8
+ * wiring in `runtime/game.ts`) mirroring the D7 "from `gameLogic` onward,
9
+ * every world's post-step state is readable" guarantee, extended to "after the
11
10
  * frame, ALL state is readable".
12
11
  *
13
12
  * This file is plain, react-free TypeScript any consumer could use — no
14
13
  * react import here, or anywhere under `runtime/` (the react-facing
15
- * `useGameState` hook, which DOES import React, lives at
16
- * `packages/engine/src/react/game-state.tsx`).
14
+ * `useWorldState` hook, which DOES import React, lives at
15
+ * `packages/engine/src/react/world-state.tsx`).
17
16
  *
18
- * SECOND trigger for `bump()` (`Game.registerWorld`, `runtime/game.ts`): the
19
- * `roots` list is also state a `useGameState` selector can read
17
+ * SECOND trigger for `bump()` (`Game.registerRoot`, `runtime/game.ts`): the
18
+ * `roots` list is also state a `useWorldState` selector can read
20
19
  * (`g.roots.map(...)`, the T6.2 two-world-compose gate), and it changes
21
- * OUTSIDE the frame loop — `registerWorld` is called as each world finishes
20
+ * OUTSIDE the frame loop — `registerRoot` is called as each world finishes
22
21
  * mounting, which can happen well before the host's loop has ticked even
23
22
  * once. Without an immediate notification here, a selector that first reads
24
23
  * `g.roots` before every world has registered stays stuck on that stale
25
24
  * snapshot until the next completed frame — which, on a slow/contended host
26
- * or a backgrounded tab, may be arbitrarily delayed. See `registerWorld`'s
25
+ * or a backgrounded tab, may be arbitrarily delayed. See `registerRoot`'s
27
26
  * own doc comment for the concrete CI failure this closes.
28
27
  */
29
28
 
30
- /** Frame-versioned, react-free subscription surface — `docs/REACT-STATE-BRIDGE.md` §2. */
29
+ /** Frame-versioned, react-free subscription surface. */
31
30
  export interface GameStateBridge {
32
31
  /** Monotonic; bumped once per completed `runFrame`, AND once per
33
- * `Game.registerWorld` call (world-list changes are observable state too
32
+ * `Game.registerRoot` call (world-list changes are observable state too
34
33
  * — see this file's module doc comment). Starts at 0 before the first of
35
34
  * either has happened. Never treat this as a literal frame COUNT — only
36
35
  * "did it change" is a contract any consumer may rely on. */
@@ -41,7 +40,7 @@ export interface GameStateBridge {
41
40
  }
42
41
 
43
42
  /** Host-internal extension of {@link GameStateBridge}: adds `bump`, called
44
- * both by `GameInternal.runFrame` at its tail AND by `Game.registerWorld`
43
+ * both by `GameInternal.runFrame` at its tail AND by `Game.registerRoot`
45
44
  * when the world list changes (see this file's module doc comment). NOT
46
45
  * part of the game-facing `Game.state` surface (typed as the narrower
47
46
  * `GameStateBridge` there) — games/components/hooks never call `bump`