@vgai/engine 0.5.15 → 0.5.17

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 (220) hide show
  1. package/dist/adapter/adapter-module.d.ts +229 -33
  2. package/dist/adapter/adapter-module.d.ts.map +1 -1
  3. package/dist/adapter/adapter-module.js +222 -50
  4. package/dist/adapter/authoring-seam-contract.d.ts +530 -0
  5. package/dist/adapter/authoring-seam-contract.d.ts.map +1 -0
  6. package/dist/adapter/authoring-seam-contract.js +162 -0
  7. package/dist/adapter/authoring.d.ts +237 -50
  8. package/dist/adapter/authoring.d.ts.map +1 -1
  9. package/dist/adapter/authoring.js +2 -1
  10. package/dist/adapter/entrypoint-selection-source.d.ts +35 -0
  11. package/dist/adapter/entrypoint-selection-source.d.ts.map +1 -0
  12. package/dist/adapter/entrypoint-selection-source.js +151 -0
  13. package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts +7 -3
  14. package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts.map +1 -1
  15. package/dist/adapter/finders/scenes-from-entrypoint-selection.js +145 -27
  16. package/dist/adapter/first-party-systems.d.ts +13 -3
  17. package/dist/adapter/first-party-systems.d.ts.map +1 -1
  18. package/dist/adapter/first-party-systems.js +10 -3
  19. package/dist/adapter/index.d.ts +7 -2
  20. package/dist/adapter/index.d.ts.map +1 -1
  21. package/dist/adapter/index.js +4 -0
  22. package/dist/adapter/ingest/contract-system-adapters.d.ts +1 -1
  23. package/dist/adapter/ingest/contract-system-adapters.d.ts.map +1 -1
  24. package/dist/adapter/ingest/contract-system-adapters.js +13 -25
  25. package/dist/adapter/ingest/game-contract-seams.d.ts +214 -0
  26. package/dist/adapter/ingest/game-contract-seams.d.ts.map +1 -0
  27. package/dist/adapter/ingest/game-contract-seams.js +62 -0
  28. package/dist/adapter/ingest/game-contract.d.ts +5 -3
  29. package/dist/adapter/ingest/game-contract.d.ts.map +1 -1
  30. package/dist/adapter/ingest/game-contract.js +4 -1
  31. package/dist/adapter/ingest/merge-debug-adapters.d.ts +52 -0
  32. package/dist/adapter/ingest/merge-debug-adapters.d.ts.map +1 -0
  33. package/dist/adapter/ingest/merge-debug-adapters.js +141 -0
  34. package/dist/adapter/ingest/observation-debug-adapter.d.ts +56 -0
  35. package/dist/adapter/ingest/observation-debug-adapter.d.ts.map +1 -0
  36. package/dist/adapter/ingest/observation-debug-adapter.js +131 -0
  37. package/dist/adapter/ingest/scene-capture.d.ts +38 -13
  38. package/dist/adapter/ingest/scene-capture.d.ts.map +1 -1
  39. package/dist/adapter/ingest/scene-capture.js +101 -24
  40. package/dist/adapter/ingest/visible-capture-window.d.ts +22 -5
  41. package/dist/adapter/ingest/visible-capture-window.d.ts.map +1 -1
  42. package/dist/adapter/ingest/visible-capture-window.js +46 -7
  43. package/dist/adapter/native-debug-module.d.ts +43 -0
  44. package/dist/adapter/native-debug-module.d.ts.map +1 -0
  45. package/dist/adapter/native-debug-module.js +230 -0
  46. package/dist/adapter/object3d-authoring-subject.d.ts +32 -0
  47. package/dist/adapter/object3d-authoring-subject.d.ts.map +1 -0
  48. package/dist/adapter/object3d-authoring-subject.js +25 -0
  49. package/dist/adapter/root-adapter.d.ts +25 -56
  50. package/dist/adapter/root-adapter.d.ts.map +1 -1
  51. package/dist/adapter/root-adapter.js +4 -20
  52. package/dist/adapter/root-seam-contract.d.ts +129 -0
  53. package/dist/adapter/root-seam-contract.d.ts.map +1 -0
  54. package/dist/adapter/root-seam-contract.js +37 -0
  55. package/dist/adapter/seam-evidence.d.ts +118 -0
  56. package/dist/adapter/seam-evidence.d.ts.map +1 -0
  57. package/dist/adapter/seam-evidence.js +231 -0
  58. package/dist/adapter/setup-three-root-adapter.d.ts +2 -3
  59. package/dist/adapter/setup-three-root-adapter.d.ts.map +1 -1
  60. package/dist/adapter/setup-three-root-adapter.js +11 -12
  61. package/dist/adapter/system-adapter.d.ts +6 -1
  62. package/dist/adapter/system-adapter.d.ts.map +1 -1
  63. package/dist/adapter/system-seam-contract.d.ts +354 -0
  64. package/dist/adapter/system-seam-contract.d.ts.map +1 -0
  65. package/dist/adapter/system-seam-contract.js +95 -0
  66. package/dist/animation/xstate-animation-binding.d.ts +6 -0
  67. package/dist/animation/xstate-animation-binding.d.ts.map +1 -1
  68. package/dist/animation/xstate-animation-binding.js +142 -4
  69. package/dist/asset-formats/collider.d.ts +1 -1
  70. package/dist/asset-formats/mesh.d.ts +2 -2
  71. package/dist/asset-formats/particles.d.ts +1 -1
  72. package/dist/canvas-react/engine-bridge.d.ts +6 -6
  73. package/dist/canvas-react/engine-bridge.d.ts.map +1 -1
  74. package/dist/canvas-react/engine-bridge.js +2 -2
  75. package/dist/canvas-react/index.d.ts +2 -2
  76. package/dist/canvas-react/index.d.ts.map +1 -1
  77. package/dist/canvas-react/index.js +1 -1
  78. package/dist/canvas-react/pixi-react-adapter.d.ts +15 -9
  79. package/dist/canvas-react/pixi-react-adapter.d.ts.map +1 -1
  80. package/dist/canvas-react/pixi-react-adapter.js +129 -64
  81. package/dist/canvas-react/pixi-react-root-factory.d.ts.map +1 -1
  82. package/dist/canvas-react/pixi-react-root-factory.js +4 -0
  83. package/dist/canvas-react/world-context.d.ts +22 -12
  84. package/dist/canvas-react/world-context.d.ts.map +1 -1
  85. package/dist/canvas-react/world-context.js +12 -51
  86. package/dist/data/curve.d.ts +1 -1
  87. package/dist/data/curve.js +2 -2
  88. package/dist/dev/instruments.d.ts +4 -4
  89. package/dist/dev/instruments.js +4 -4
  90. package/dist/dev/register-render-vitals.d.ts +2 -2
  91. package/dist/dev/register-render-vitals.js +2 -2
  92. package/dist/dev/static-batch-advisor.d.ts +1 -1
  93. package/dist/dev/static-batch-advisor.js +2 -2
  94. package/dist/ecs/user-data.d.ts +5 -0
  95. package/dist/ecs/user-data.d.ts.map +1 -1
  96. package/dist/ecs/user-data.js +3 -0
  97. package/dist/input/input-manager.d.ts +64 -0
  98. package/dist/input/input-manager.d.ts.map +1 -1
  99. package/dist/input/input-manager.js +149 -0
  100. package/dist/manifest/load.d.ts +0 -3
  101. package/dist/manifest/load.d.ts.map +1 -1
  102. package/dist/manifest/load.js +0 -4
  103. package/dist/manifest/schema.d.ts +9 -11
  104. package/dist/manifest/schema.d.ts.map +1 -1
  105. package/dist/manifest/schema.js +2 -23
  106. package/dist/pixi/authoring.d.ts +33 -0
  107. package/dist/pixi/authoring.d.ts.map +1 -1
  108. package/dist/pixi/authoring.js +301 -2
  109. package/dist/pixi/ingest.d.ts +2 -1
  110. package/dist/pixi/ingest.d.ts.map +1 -1
  111. package/dist/pixi/ingest.js +4 -2
  112. package/dist/pixi/scene-capture.d.ts +9 -1
  113. package/dist/pixi/scene-capture.d.ts.map +1 -1
  114. package/dist/pixi/scene-capture.js +9 -6
  115. package/dist/react/world-state.d.ts +12 -0
  116. package/dist/react/world-state.d.ts.map +1 -1
  117. package/dist/react/world-state.js +14 -0
  118. package/dist/render/particles-factory.d.ts +13 -6
  119. package/dist/render/particles-factory.d.ts.map +1 -1
  120. package/dist/render/particles-factory.js +6 -3
  121. package/dist/render/soft-particle-depth.d.ts +3 -3
  122. package/dist/render/soft-particle-depth.d.ts.map +1 -1
  123. package/dist/render/soft-particle-depth.js +8 -13
  124. package/dist/runtime/create-runtime.d.ts +25 -62
  125. package/dist/runtime/create-runtime.d.ts.map +1 -1
  126. package/dist/runtime/create-runtime.js +58 -105
  127. package/dist/runtime/dev-build.d.ts +37 -0
  128. package/dist/runtime/dev-build.d.ts.map +1 -0
  129. package/dist/runtime/dev-build.js +49 -0
  130. package/dist/runtime/game.d.ts +38 -72
  131. package/dist/runtime/game.d.ts.map +1 -1
  132. package/dist/runtime/game.js +40 -55
  133. package/dist/runtime/mount-game.d.ts.map +1 -1
  134. package/dist/runtime/mount-game.js +12 -1
  135. package/dist/runtime/mount-manifest.d.ts +0 -13
  136. package/dist/runtime/mount-manifest.d.ts.map +1 -1
  137. package/dist/runtime/mount-manifest.js +5 -25
  138. package/dist/runtime/types.d.ts +18 -0
  139. package/dist/runtime/types.d.ts.map +1 -1
  140. package/dist/setup/setup-renderer.d.ts +3 -4
  141. package/dist/setup/setup-renderer.d.ts.map +1 -1
  142. package/dist/setup/setup-renderer.js +3 -4
  143. package/dist/world3d-react/engine-bridge.d.ts +1 -1
  144. package/dist/world3d-react/engine-bridge.d.ts.map +1 -1
  145. package/dist/world3d-react/engine-bridge.js +29 -4
  146. package/dist/world3d-react/index.d.ts +1 -1
  147. package/dist/world3d-react/index.js +1 -1
  148. package/dist/world3d-react/r3f-adapter.d.ts +7 -0
  149. package/dist/world3d-react/r3f-adapter.d.ts.map +1 -1
  150. package/dist/world3d-react/r3f-adapter.js +132 -28
  151. package/dist/world3d-react/r3f-root-factory.d.ts.map +1 -1
  152. package/dist/world3d-react/r3f-root-factory.js +14 -0
  153. package/dist/world3d-react/rapier-physics-bridge.d.ts +4 -4
  154. package/dist/world3d-react/rapier-physics-bridge.js +3 -3
  155. package/dist/world3d-react/world-context.d.ts +52 -3
  156. package/dist/world3d-react/world-context.d.ts.map +1 -1
  157. package/dist/world3d-react/world-context.js +94 -64
  158. package/dist-config/config.js.map +1 -1
  159. package/package.json +1 -1
  160. package/schemas/engine-capabilities.json +8 -8
  161. package/schemas/vgai-project.schema.json +1 -6
  162. package/src/adapter/adapter-module.ts +405 -72
  163. package/src/adapter/authoring-seam-contract.ts +211 -0
  164. package/src/adapter/authoring.ts +242 -50
  165. package/src/adapter/entrypoint-selection-source.ts +159 -0
  166. package/src/adapter/finders/scenes-from-entrypoint-selection.ts +162 -36
  167. package/src/adapter/first-party-systems.ts +17 -3
  168. package/src/adapter/index.ts +51 -1
  169. package/src/adapter/ingest/contract-system-adapters.ts +26 -30
  170. package/src/adapter/ingest/game-contract-seams.ts +85 -0
  171. package/src/adapter/ingest/game-contract.ts +9 -4
  172. package/src/adapter/ingest/merge-debug-adapters.ts +193 -0
  173. package/src/adapter/ingest/observation-debug-adapter.ts +158 -0
  174. package/src/adapter/ingest/scene-capture.ts +126 -23
  175. package/src/adapter/ingest/visible-capture-window.ts +63 -9
  176. package/src/adapter/native-debug-module.ts +307 -0
  177. package/src/adapter/object3d-authoring-subject.ts +50 -0
  178. package/src/adapter/root-adapter.ts +25 -56
  179. package/src/adapter/root-seam-contract.ts +57 -0
  180. package/src/adapter/seam-evidence.ts +336 -0
  181. package/src/adapter/setup-three-root-adapter.ts +13 -15
  182. package/src/adapter/system-adapter.ts +6 -1
  183. package/src/adapter/system-seam-contract.ts +121 -0
  184. package/src/animation/xstate-animation-binding.ts +151 -4
  185. package/src/canvas-react/engine-bridge.ts +6 -6
  186. package/src/canvas-react/index.ts +2 -1
  187. package/src/canvas-react/pixi-react-adapter.tsx +143 -77
  188. package/src/canvas-react/pixi-react-root-factory.tsx +4 -0
  189. package/src/canvas-react/world-context.ts +26 -101
  190. package/src/data/curve.ts +2 -2
  191. package/src/dev/instruments.ts +4 -4
  192. package/src/dev/register-render-vitals.ts +2 -2
  193. package/src/dev/static-batch-advisor.ts +2 -2
  194. package/src/ecs/user-data.ts +5 -0
  195. package/src/input/input-manager.ts +145 -0
  196. package/src/manifest/load.ts +0 -7
  197. package/src/manifest/schema.ts +2 -25
  198. package/src/pixi/authoring.ts +329 -2
  199. package/src/pixi/ingest.ts +9 -2
  200. package/src/pixi/scene-capture.ts +20 -5
  201. package/src/react/world-state.tsx +15 -0
  202. package/src/render/particles-factory.ts +16 -6
  203. package/src/render/soft-particle-depth.ts +8 -13
  204. package/src/runtime/create-runtime.ts +79 -158
  205. package/src/runtime/dev-build.ts +49 -0
  206. package/src/runtime/game.ts +72 -113
  207. package/src/runtime/mount-game.ts +19 -1
  208. package/src/runtime/mount-manifest.ts +5 -37
  209. package/src/runtime/types.ts +22 -0
  210. package/src/setup/setup-renderer.ts +3 -4
  211. package/src/world3d-react/engine-bridge.ts +29 -4
  212. package/src/world3d-react/index.ts +1 -1
  213. package/src/world3d-react/r3f-adapter.tsx +140 -31
  214. package/src/world3d-react/r3f-root-factory.tsx +14 -0
  215. package/src/world3d-react/rapier-physics-bridge.tsx +4 -4
  216. package/src/world3d-react/world-context.ts +111 -87
  217. package/dist/runtime/dev-layers.d.ts +0 -34
  218. package/dist/runtime/dev-layers.d.ts.map +0 -1
  219. package/dist/runtime/dev-layers.js +0 -46
  220. package/src/runtime/dev-layers.ts +0 -46
@@ -31,12 +31,11 @@ import type { PlaytestContext } from './types';
31
31
  * by `createGameRuntime` and every headless test harness
32
32
  * (`test/game-root.test.ts`, `test/frame-order.test.ts`,
33
33
  * `test/game-two-roots.test.ts`) so there is exactly one code path for this
34
- * wiring (T7.1 slice 1, id-generalized in slice 3 for the two-world proof — stage
35
- * 3) — `physics`/`collisions`/`camera`/ `frame` are populated from the mount's
34
+ * wiring. `physics`/`collisions`/`camera`/`frame` are populated from the mount's
36
35
  * first-party `GameContext`/ `MountedSetupThreeRoot` when available, left
37
36
  * `undefined` otherwise (an external adapter's mount has none of these
38
37
  * first-party handles — `frame` in particular is what makes
39
- * `GameInternal.runFrame` (T7.1 slice 2) fall back to a single opaque
38
+ * `GameInternal.runFrame` fall back to a single opaque
40
39
  * `mounted.update` call per substep for such a world).
41
40
  */
42
41
  function firstPartyExtras(
@@ -53,10 +52,8 @@ function firstPartyExtras(
53
52
 
54
53
  /**
55
54
  * Register a three world onto the Game shell. `opts.id` defaults to
56
- * `'main'` — the single-world callers (`createGameRuntime`, slice-1/2 tests)
57
- * are unaffected; a second/third world (T7.1 slice 3's two-world proof, and
58
- * real multi-world manifests later) pass an explicit id. This is the ONE
59
- * registration code path for a three world, whatever its id.
55
+ * `'main'`; multi-root callers pass an explicit id. This is the one
56
+ * registration path for a three world, whatever its id.
60
57
  */
61
58
  export function registerThreeRoot(
62
59
  game: GameInternal,
@@ -104,13 +101,13 @@ export function registerPixiRoot(
104
101
  }
105
102
 
106
103
  /**
107
- * Register a react world onto the Game shell (T6.2 slice 1) — the react
104
+ * Register a React world onto the Game shell — the DOM
108
105
  * analog of {@link registerThreeRoot}/{@link registerPixiRoot}. A react
109
106
  * world has no `frame` hooks (react's own
110
107
  * `createRoot` schedules its commits; `GameInternal.runFrame` correctly
111
108
  * leaves a world with no `frame` untouched by its opaque-`update` fallback
112
109
  * too, since `MountedReactGame` declares no `update`). `adapter` is typed
113
- * as the real {@link ReactRootAdapter} shape (T7.5, same reasoning as
110
+ * as the real {@link ReactRootAdapter} shape (same reasoning as
114
111
  * `registerPixiRoot`'s doc comment above) — `RootInstance.adapter` only
115
112
  * needs `.id`, so this passes through with zero cast.
116
113
  */
@@ -134,12 +131,12 @@ export function registerReactRoot(
134
131
  }
135
132
 
136
133
  /**
137
- * One world to mount in the {@link RootsRuntimeConfig} roots path (T6.1
138
- * slice 1) — the host-facing mirror of `manifest/load.ts`'s
139
- * `ResolvedAdapterRoot` (same `id`/`zOrder`/`pausable`/`loop` fields; the
134
+ * One world to mount in the universal host the host-facing mirror of
135
+ * `manifest/load.ts`'s `ResolvedAdapterRoot` (same identity and presentation
136
+ * fields; the
140
137
  * manifest-to-host translation itself is the editor's job, not this file's).
141
138
  *
142
- * `react` (T6.2 slice 1) mounts as a DOM-root layer `<div>` in the SAME
139
+ * A `dom` root mounts as a DOM layer `<div>` in the same
143
140
  * stack instead of a canvas — see {@link ReactRootMountSpec}/{@link
144
141
  * ReactRootAdapter} below.
145
142
  */
@@ -152,28 +149,6 @@ export interface RootMountSpecBase {
152
149
  readonly zOrder?: number | undefined;
153
150
  /** Whether play-mode pause/step applies to this world (D10). Defaults to `true`. */
154
151
  readonly pausable?: boolean | undefined;
155
- /**
156
- * `vgai.project.json`'s `dev: true` — this root is a DEV LAYER (the game's
157
- * own dev GUI), not shipped game content. Two things happen here: the
158
- * surface is stacked ABOVE every non-dev root (a dev layer sits topmost),
159
- * and it is marked `data-vgai-dev-layer="true"` so the play compositor can
160
- * leave it out of a capture (`composite-screenshot.ts`). Whether a dev root
161
- * reaches this function at all is the CALLER's gate —
162
- * `mountManifestRoots` asks `devLayersEnabled()` before building the spec.
163
- *
164
- * It steals no input from the game while closed for a reason that predates
165
- * this flag and needs no new mechanism: a dev layer is a `dom` root, DOM
166
- * layers are `pointer-events:none` (see `mountOneReactRoot`) with the
167
- * mounted tree opting individual elements back in, and the input router
168
- * never sees DOM roots at all.
169
- */
170
- readonly dev?: boolean | undefined;
171
- /** `'gated'` (host-driven, default) or `'self-driven'` (this world drives
172
- * its own loop — D5's "composited, unsynchronized" tier). Carried through
173
- * for parity with `ResolvedAdapterRoot`; T6.1 slice 1 does not yet
174
- * validate it against the mounted adapter's actual `drivesOwnLoop` (that
175
- * cross-check, if ever needed, is T7.6's loop-gate surface). */
176
- readonly loop?: 'gated' | 'self-driven' | undefined;
177
152
  /**
178
153
  * Optional claim predicate for the delegating input router (D5 §2a), over
179
154
  * a point RELATIVE TO THE CONTAINER. Absent means: this world claims only
@@ -199,8 +174,8 @@ export interface PixiRootMountSpec extends RootMountSpecBase {
199
174
  }
200
175
 
201
176
  /**
202
- * A live, mounted react world (T6.2 slice 1) — the react analog of a mounted
203
- * canvas world. React roots render from game state via the T7.4 bridge
177
+ * A live, mounted React world — the DOM analog of a mounted canvas world.
178
+ * React roots render from game state
204
179
  * instead of a per-frame `update`, so this shape carries no `update`/`fixedUpdate`/
205
180
  * `ctx`/`frame` — `drivesOwnLoop` is always `false` (react's `createRoot`
206
181
  * schedules its OWN commits; the host's fixed-step loop never drives it,
@@ -217,10 +192,10 @@ export interface PixiRootMountSpec extends RootMountSpecBase {
217
192
  * `kind: 'dom'` (§7.1-15): a react world has no `observe` BY DESIGN — that
218
193
  * hook is scoped to `useRootObservation` (the ingested/foreign-world case),
219
194
  * not `useWorldState` (`ui/game-state.tsx`), which a react world's own
220
- * mounted tree uses instead — it reads `Game.state` directly (T7.4's actual
221
- * bridge for first-party-observable state), never a per-world `observe`.
195
+ * mounted tree uses instead — it reads `Game.state` directly, never a
196
+ * per-world `observe`.
222
197
  *
223
- * `kind`/`container` (T7.5) satisfy `MountedReactRoot` (`adapter/
198
+ * `kind`/`container` satisfy `MountedReactRoot` (`adapter/
224
199
  * root-adapter.ts`) — `container` is the SAME `DomHostContext.container` the
225
200
  * adapter's `mount` was handed (identity matters, mirroring `threeScene()`/
226
201
  * `pixiStage()`'s "same instance the adapter mounted" contract); every
@@ -236,7 +211,7 @@ export interface MountedReactGame extends MountedReactRoot {
236
211
  * A react-shaped adapter — the structural contract `RootMountSpec`'s
237
212
  * `react` variant requires. Deliberately
238
213
  * NOT tied to a concrete implementer here so an editor-resolved adapter (the
239
- * `default-react` resolver branch, T6.2's editor-side follow-up) can satisfy
214
+ * `default-react` resolver branch) can satisfy
240
215
  * this shape without this file importing react-dom or any editor code.
241
216
  *
242
217
  * A genuine `RootAdapter<'dom'>` refinement (`HostContextFor<'dom'>` =
@@ -328,37 +303,25 @@ export interface RootsRuntimeConfig {
328
303
  *
329
304
  * This is the GENERIC host handle — it has no first-party concepts (no
330
305
  * `GameContext`, no `GameSetupFn`). First-party features (the live `GameContext`,
331
- * warm-restart hot reload) are reached by casting `mounted` to `MountedSetupThreeRoot`
306
+ * warm-restart hot reload) are reached through `game.defaultRoot.mounted`
307
+ * and narrowed to `MountedSetupThreeRoot`
332
308
  * (the editor does this for HMR/physics-sync — those are inherently first-party).
333
309
  *
334
- * `scene`/`camera`/`mounted` alias the Game's `defaultRoot`: it is the first
335
- * three root (else the first
336
- * world) by DECLARATION order, per `Game.defaultRoot`'s existing rule —
337
- * independent of `zOrder`/canvas stacking, which is a rendering-only concern.
310
+ * Root access is explicit through `game.roots`, `game.world(id)`, and
311
+ * `game.defaultRoot`; the session does not project a surface-specific alias.
338
312
  */
339
313
  export interface GameSession {
340
314
  stop(): void;
315
+ /** Resolves after every root has finished the cleanup initiated by stop(). */
316
+ readonly stopComplete: Promise<void>;
341
317
  pause(): void;
342
318
  resume(): void;
343
319
  step(): void;
344
- /** Resize every world's render buffer. `pixelRatio` (W2c device preview)
345
- * optionally re-pins the three renderers' DPR in the same pass; omitted,
346
- * the mount-time ratio is left untouched (legacy behavior). */
320
+ /** Resize every world's render buffer. `pixelRatio` optionally re-pins the
321
+ * three renderers' DPR in the same pass. */
347
322
  resize(width: number, height: number, pixelRatio?: number): void;
348
- readonly scene: THREE.Scene;
349
- readonly camera: THREE.PerspectiveCamera;
350
- /** The default world's mounted root, as the honest `MountedRoot` union —
351
- * narrow on `.kind` before reaching for a surface's own members (the same
352
- * discriminant `scene`/`camera` above use). This deliberately does NOT
353
- * claim `MountedThreeRoot`: a roots-path session whose default world is
354
- * canvas or dom returns that world's mount, and the old
355
- * `as MountedThreeRoot` cast made every such value a type-level lie. For a
356
- * first-party three mount, narrow to `'three'` and cast to
357
- * `MountedSetupThreeRoot` for the first-party extras. */
358
- readonly mounted: MountedRoot;
359
- /** The Game root (T6.1 slice 1) — the multi-world entry point
360
- * (`game.roots`/`game.world(id)`/`game.queryByComponent`) for callers
361
- * that need more than the default-world aliases above. */
323
+ /** The multi-root entry point. Root access stays explicit through
324
+ * `game.roots`, `game.world(id)`, and `game.defaultRoot`. */
362
325
  readonly game: Game;
363
326
  }
364
327
 
@@ -374,7 +337,7 @@ export async function createGameRuntime(config: RootsRuntimeConfig): Promise<Gam
374
337
  }
375
338
 
376
339
  // ---------------------------------------------------------------------------
377
- // Worlds path (T6.1 slice 1)
340
+ // Universal root host
378
341
  // ---------------------------------------------------------------------------
379
342
 
380
343
  /** A stand-in `THREE.WebGLRenderer` for headless (`headless:true`) three
@@ -397,7 +360,7 @@ function createHeadlessRendererStub(): THREE.WebGLRenderer {
397
360
 
398
361
  /** One already-mounted world, tracked for disposal + the router. `element`
399
362
  * is the world's stacked surface — an `HTMLCanvasElement` for three/
400
- * canvas, or the DOM-root layer `<div>` for react (T6.2 slice 1) — kept
363
+ * canvas, or the DOM-root layer `<div>` for React — kept
401
364
  * under one field name so `fullCleanup`'s disposal loop stays kind-generic
402
365
  * (`container.removeChild(entry.element)` needs no branch). */
403
366
  interface MountedAdapterRoot {
@@ -477,11 +440,8 @@ async function mountOneThreeRoot(
477
440
  // resize-staleness (the roots-path `resize()` below always calls
478
441
  // `setSize(rw, rh, false)`, so nothing else was ever going to update this
479
442
  // canvas' CSS after mount). `createHostRenderer`'s own default
480
- // (`updateStyle:true`) is intentionally left alone the LEGACY
481
- // single-canvas path (editor play-mode's non-multi-world branch) still
482
- // relies on that construction-time stamp + its own later `updateStyle:true`
483
- // resizes for byte-identical behavior; this fix touches only the
484
- // roots-path canvas, after the fact.
443
+ // (`updateStyle:true`) is intentionally left alone; the host owns CSS layout
444
+ // and reasserts the container-relative size after renderer construction.
485
445
  canvas.style.width = '100%';
486
446
  canvas.style.height = '100%';
487
447
  renderer.setPixelRatio(dpr);
@@ -559,7 +519,7 @@ async function mountOnePixiRoot(
559
519
  }
560
520
 
561
521
  /**
562
- * Mount one react `RootMountSpec` (T6.2 slice 1) — the react sibling of
522
+ * Mount one React `RootMountSpec` — the DOM sibling of
563
523
  * `mountOneThreeRoot`/`mountOnePixiRoot`. Unlike its canvas-backed siblings
564
524
  * this returns NO `routerEntry`: a react world's DOM-root layer participates in
565
525
  * D5's z-order/box stacking (the caller still creates and positions its `<div>`
@@ -592,7 +552,7 @@ async function mountOneReactRoot(
592
552
  // An absolutely-positioned layer with no width/height collapses to zero
593
553
  // content size, so a child's
594
554
  // own position:absolute offsets resolve against a degenerate containing
595
- // block (found by the T6.2 slice-3 e2e — clicks landed outside the game).
555
+ // block; otherwise clicks land outside the game.
596
556
  layer.style.width = '100%';
597
557
  layer.style.height = '100%';
598
558
  const reactHost: DomHostContext = { container: layer, game };
@@ -613,8 +573,8 @@ async function mountOneReactRoot(
613
573
  }
614
574
 
615
575
  /**
616
- * The roots-path implementer behind {@link createGameRuntime} (T6.1 slice
617
- * 1; react roots added T6.2 slice 1). Builds ONE surface per world — a
576
+ * The universal host implementer behind {@link createGameRuntime}. It builds
577
+ * one surface per world — a
618
578
  * canvas for three/canvas, a DOM-root `<div>` layer for react — stacked
619
579
  * per D5 §1, z-order/ties exactly matching
620
580
  * `manifest/load.ts`'s sort, ONE `Game`, and registers every world onto it
@@ -628,16 +588,11 @@ async function mountOneReactRoot(
628
588
 
629
589
  /**
630
590
  * Dev/e2e-only `window.__vgaiScene`/`__vgaiCamera` exposure for the roots
631
- * path's DEFAULT world (E4) — split out of `createRootsGameRuntime` purely
632
- * to keep that function's own cyclomatic complexity down. Mirrors the
633
- * legacy single-world path's identical exposure (above, in this same file),
634
- * using the SAME "first three world, else none" default-world rule the
635
- * `GameSession.scene`/`.camera` getters alias (`Game.defaultRoot`): a
636
- * non-threejs default world (or none at all) publishes neither global,
637
- * exactly like those getters return `undefined` in that case. Returns a
638
- * retraction callback — identity-guarded (a newer session's globals must
639
- * never be clobbered by an older session's stop, same reason the legacy
640
- * path's own cleanup guards it) — that is a no-op when nothing was
591
+ * path's default world — split out of `createRootsGameRuntime` purely
592
+ * to keep that function's own cyclomatic complexity down. It uses the
593
+ * "first three world, else none" rule: a non-Three default world publishes
594
+ * neither global. Returns an identity-guarded retraction callback so an
595
+ * older session's stop cannot clobber a newer session's globals. It is a no-op when nothing was
641
596
  * published (non-DEV build, or non-threejs default world).
642
597
  */
643
598
  function installDefaultRootDevGlobals(game: GameInternal): () => void {
@@ -804,26 +759,15 @@ async function createRootsGameRuntime(config: RootsRuntimeConfig): Promise<GameS
804
759
  // --- Surface stack (D5 §1): DOM/z-order follows zOrder, ties -> array order
805
760
  // — computed FIRST (bottom -> top) so both the z-index assignment below and
806
761
  // the router's default-claim rule share one definition. A react world's
807
- // DOM-root layer shares this SAME stacking pass (T6.2 slice 1 "one
808
- // stacking model, no special case") even though it is a `<div>`, not a
762
+ // DOM-root layer shares this SAME stacking pass even though it is a `<div>`, not a
809
763
  // canvas, and carries no `hitTest` (the router never sees react entries at
810
764
  // all — see the dispatch loop below). ---
811
- // Dev layers sit TOPMOST, above every non-dev root, whatever zOrder either
812
- // declares: a dev GUI the game can paint over is not a dev GUI. Expressed as
813
- // two independent `stackOrder` passes concatenated rather than a magic
814
- // zOrder value, so `stackOrder`'s one rule (ascending zOrder, ties -> array
815
- // order) still governs WITHIN each band and nothing has to reason about what
816
- // number is "high enough".
817
765
  const claimEntries = mountSpecs.map((spec) => ({
818
766
  id: spec.id,
819
767
  zOrder: spec.zOrder ?? 0,
820
768
  hitTest: spec.hitTest,
821
- dev: spec.dev === true,
822
769
  }));
823
- const stacked = [
824
- ...stackOrder(claimEntries.filter((e) => !e.dev)),
825
- ...stackOrder(claimEntries.filter((e) => e.dev)),
826
- ];
770
+ const stacked = stackOrder(claimEntries);
827
771
  const bottomId = stacked[0]?.id;
828
772
  const kindById = new Map(mountSpecs.map((spec) => [spec.id, spec.kind] as const));
829
773
 
@@ -839,11 +783,6 @@ async function createRootsGameRuntime(config: RootsRuntimeConfig): Promise<GameS
839
783
  // surface.ts`, which is the one door the capture/staleness/screenshot
840
784
  // sites now ask (ARCHITECTURE-CORE §The editor protocol, zero inference).
841
785
  surface.dataset['vgaiRootId'] = entry.id;
842
- // The capture's marker (`composite-screenshot.ts` reads exactly this
843
- // attribute): a dev layer is excluded from a play capture unless the
844
- // capture asks for it, because a contaminated "does the game look right"
845
- // frame is worthless.
846
- if (entry.dev) surface.dataset['vgaiDevLayer'] = 'true';
847
786
  if (!isReact) {
848
787
  const canvas = surface as unknown as HTMLCanvasElement;
849
788
  canvas.width = w;
@@ -862,7 +801,7 @@ async function createRootsGameRuntime(config: RootsRuntimeConfig): Promise<GameS
862
801
  // too (not just in `mountOneThreeRoot`) means every surface, whatever
863
802
  // kind, starts container-relative from its very first paint, before any
864
803
  // per-kind mount work has even run.
865
- // `contain:layout paint` makes each surface the CONTAINING BLOCK for
804
+ // `contain:layout paint` makes each surface the containing block for
866
805
  // `position:fixed` descendants (and clips overflow to the world's
867
806
  // rectangle): full-screen game UI written the natural way (`fixed;
868
807
  // inset:0`) then fills the WORLD, not the page. Without it, `fixed` UI
@@ -936,17 +875,7 @@ async function createRootsGameRuntime(config: RootsRuntimeConfig): Promise<GameS
936
875
  started = true;
937
876
  loop.start();
938
877
 
939
- // Expose scene & camera for dev tools / e2e tests — mirrors the legacy
940
- // single-world path's identical dev-only exposure above, generalized to
941
- // the roots path's DEFAULT world (E4): `mountGameFromManifest`/
942
- // `mountManifestRoots` route every caller (including a
943
- // single-threejs-world scaffold project) through THIS path, so a caller
944
- // migrating off the legacy `{canvas, adapter}` call must not silently
945
- // lose `window.__vgaiScene`/`__vgaiCamera` — real e2e/dev tooling
946
- // depends on them
947
- // (`packages/editor/e2e/tests/04-standalone-game.spec.ts`). Split into
948
- // its own top-level helper (with its retraction counterpart below)
949
- // purely to keep this function's own cyclomatic complexity down.
878
+ // Expose the default Three root's scene and camera for dev/e2e tooling.
950
879
  const retractDevGlobals = installDefaultRootDevGlobals(game);
951
880
 
952
881
  // G3/FT-11 — publish `window.__vgaiRender` for the deterministic capture
@@ -959,39 +888,49 @@ async function createRootsGameRuntime(config: RootsRuntimeConfig): Promise<GameS
959
888
  ? installSessionRenderHarness(game, renderModeLocation, config.renderControl?.target)
960
889
  : undefined;
961
890
 
891
+ let resolveStopComplete!: () => void;
892
+ const stopComplete = new Promise<void>((resolve) => {
893
+ resolveStopComplete = resolve;
894
+ });
895
+ let stopping = false;
896
+
962
897
  function fullCleanup(): void {
963
- loop.stop();
964
- router.dispose();
965
- for (const entry of mountedEntries) {
966
- entry.mounted.dispose();
967
- entry.renderer?.dispose();
968
- entry.renderer?.forceContextLoss();
969
- // Remove from the element's CURRENT parent, not the mount-time
970
- // `container`: the editor's Game panel re-parents the live surfaces
971
- // when its mount element swaps (fill <-> device preset, W2c), and
972
- // `container.removeChild` would throw NotFoundError after such a move.
973
- // `container` remains the fallback for hosts whose element stand-ins
974
- // never wire `parentNode` (headless unit fixtures); an element already
975
- // detached by such a host is a no-op via the catch.
976
- try {
977
- (entry.element.parentNode ?? container).removeChild(entry.element);
978
- } catch {
979
- /* already detached — nothing to remove */
898
+ if (stopping) return;
899
+ stopping = true;
900
+ try {
901
+ loop.stop();
902
+ router.dispose();
903
+ for (const entry of mountedEntries) {
904
+ entry.mounted.dispose();
905
+ entry.renderer?.dispose();
906
+ entry.renderer?.forceContextLoss();
907
+ // Remove from the element's CURRENT parent, not the mount-time
908
+ // `container`: the editor's Game panel re-parents the live surfaces
909
+ // when its mount element swaps (fill <-> device preset, W2c), and
910
+ // `container.removeChild` would throw NotFoundError after such a move.
911
+ // `container` remains the fallback for hosts whose element stand-ins
912
+ // never wire `parentNode` (headless unit fixtures); an element already
913
+ // detached by such a host is a no-op via the catch.
914
+ try {
915
+ (entry.element.parentNode ?? container).removeChild(entry.element);
916
+ } catch {
917
+ /* already detached — nothing to remove */
918
+ }
980
919
  }
920
+ game.dispose();
921
+ retractDevGlobals();
922
+ retractRenderHarness?.();
923
+ } finally {
924
+ void Promise.allSettled(
925
+ mountedEntries.map((entry) => entry.mounted.disposeComplete ?? Promise.resolve()),
926
+ ).then(() => resolveStopComplete());
981
927
  }
982
- game.dispose();
983
- retractDevGlobals();
984
- retractRenderHarness?.();
985
928
  }
986
929
 
987
930
  return {
988
931
  stop: fullCleanup,
989
- // D10/T7.6: `Game.play` fans out per-world pausable/loop-gate/audio-gate
990
- // semantics itself now (see the legacy path's identical comment above) —
991
- // a `pausable: false` world (a menu/HUD world) keeps ticking while every
992
- // other world freezes, which the old blind `for (const entry of
993
- // mountedEntries) entry.mounted.setPaused?.(true)` fan-out (with no
994
- // `pausable` check at all) could never express.
932
+ stopComplete,
933
+ // `Game.play` owns per-world pause, loop-gate, and audio-gate semantics.
995
934
  pause() {
996
935
  game.play.pause();
997
936
  },
@@ -1014,24 +953,6 @@ async function createRootsGameRuntime(config: RootsRuntimeConfig): Promise<GameS
1014
953
  entry.mounted.resize?.(safeWidth, safeHeight);
1015
954
  }
1016
955
  },
1017
- // `GameSession.scene`/`.camera`/`.mounted` alias `Game.defaultRoot` (T6.1
1018
- // slice 1's "first three world, else first world" rule) — a legacy
1019
- // convenience shaped for the threejs-only past. T7.5 narrows the read via
1020
- // the `kind` discriminant instead of a blind `.scene`/`.camera` cast
1021
- // through a nonexistent property (identical behavior to before: still
1022
- // `undefined` for a roots-path session whose default world isn't
1023
- // three — real per-world surface routing for that case is T7.6's).
1024
- get scene() {
1025
- const m = game.defaultRoot.mounted;
1026
- return (m.kind === 'three' ? m.scene : undefined) as THREE.Scene;
1027
- },
1028
- get camera() {
1029
- const m = game.defaultRoot.mounted;
1030
- return (m.kind === 'three' ? m.camera : undefined) as THREE.PerspectiveCamera;
1031
- },
1032
- get mounted() {
1033
- return game.defaultRoot.mounted;
1034
- },
1035
956
  game,
1036
957
  };
1037
958
  }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * THE one owner of "is this a development context". Every reader that has to
3
+ * answer that question calls {@link devBuildEnabled}; there is deliberately NO
4
+ * second source of truth — not a module-level cached boolean, not a
5
+ * `globalThis` flag, not a per-host copy of the `import.meta.env` read below.
6
+ * Instrumentation that ships to players because two places disagreed about
7
+ * what "dev" means is exactly the failure this single owner exists to make
8
+ * impossible.
9
+ *
10
+ * Ownership, stated in one place (the build rule):
11
+ * - OWNER: this function. It resolves the answer; nothing else derives it.
12
+ * - SHARERS: the two three-root adapters (`adapter/setup-three-root-adapter.ts`
13
+ * and `world3d-react/r3f-adapter.tsx`), which seed live render vitals only
14
+ * under it, and the `static-batch` capability's mutation watch and `Frozen`
15
+ * warning. Any future dev-only instrument calls this too, with its own
16
+ * `override`.
17
+ * - TEARDOWN: none. This is a pure predicate over build config and one
18
+ * caller-supplied argument — it owns no resource, allocates nothing, and
19
+ * has no lifecycle to end.
20
+ *
21
+ * The three inputs, highest precedence first:
22
+ * 1. `override` — the explicit per-call answer. A headless test, a capture
23
+ * harness, or a host that knows better passes `true`/`false` and gets
24
+ * exactly that. Passing `undefined` (or omitting it) means "decide for
25
+ * me" and falls through. The editor's own preview mount is the worked
26
+ * case: it is a dev session by definition even when the editor SPA it
27
+ * runs inside is a production build.
28
+ * 2. A dev build — `import.meta.env.DEV`. The ordinary local/editor case.
29
+ * 3. A production build's EXPLICIT opt-in — `VITE_VGAI_DEV_BUILD=true`.
30
+ * Instrumenting a production bundle is a real, legitimate choice (an
31
+ * internal playtest build, a QA build), and it must be an opt-in someone
32
+ * had to type, never something a default drifts into.
33
+ *
34
+ * Anything else — a production build with no opt-in — is `false`.
35
+ */
36
+ export function devBuildEnabled(override?: boolean | undefined): boolean {
37
+ if (override !== undefined) return override;
38
+ // `import.meta` is cast whole, not just its `.env`: this module is reachable
39
+ // from programs whose tsconfig does not pull in `vite/client` (the session
40
+ // client's, for one, which reaches the three adapter transitively), and there
41
+ // `ImportMeta` has no declared `env` at all. The cast keeps the single owner
42
+ // of the dev answer importable from ANY program rather than forcing every
43
+ // downstream tsconfig to adopt Vite's ambient types.
44
+ const env = (import.meta as unknown as { env?: unknown }).env as
45
+ | { DEV?: boolean | undefined; VITE_VGAI_DEV_BUILD?: string | undefined }
46
+ | undefined;
47
+ if (env?.DEV === true) return true;
48
+ return env?.VITE_VGAI_DEV_BUILD === 'true';
49
+ }