@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
@@ -1,25 +1,11 @@
1
1
  /**
2
- * Game root T7.1 slice 1 ("introduce Game internally") + slice 2 ("world
3
- * registry + ordered frame algorithm"), extended in T7.3 slice 1 with real
4
- * `'canvas'`-kind `RootInstance` support (`stage`/`physics2d`, a real
5
- * `pixiStage()`) so a canvas adapter can register its world onto a real `Game`
6
- * instead of driving its own loop, and in T7.3 slice 2 with a
7
- * construction-time stage check (symmetric with the three/scene check).
8
- * T6.2 slice 1 adds real `'react'`-kind
9
- * `RootInstance` support (`container`, a real `reactRoot()`) — the DOM-root
10
- * world surface `runtime/create-runtime.ts`'s roots path now mounts, with
11
- * `physics`/`collisions`/`camera`/`frame` `undefined` for them exactly
12
- * like an opaque/foreign mount.
13
- *
14
- * (D6), particularly §4 (the frame algorithm) and §8 stage 2, for the full
15
- * target shape. This file holds the Game/ RootInstance shell, the NEW
16
- * game-scoped `SystemRunner`, and the host-facing `runFrame` frame executor.
17
- * The public surface is NOT frozen yet — do not add these exports to a
18
- * barrel/index; only the in-scope runtime/adapter files import this module
19
- * directly. The `gpu` per-world cache is the one remaining slice-1 omission.
2
+ * The surface-neutral Game root and its declaration-ordered root registry.
3
+ * Three, Canvas, and DOM roots share one game-scoped loop, input manager,
4
+ * system runner, state bridge, debug registry, clock, and seeded RNG. Each
5
+ * RootInstance retains its own mounted surface and optional capabilities.
20
6
  */
21
7
 
22
- // TYPE-ONLY pixi import (T7.3 slice 1): `game.ts` must never value-import
8
+ // TYPE-ONLY pixi import: `game.ts` must never value-import
23
9
  // `pixi.js` — a value import would pull the Pixi runtime into 3D-only
24
10
  // bundles that never touch a canvas root. Every pixi-typed field below is erased
25
11
  // at compile time; nothing here constructs or calls into Pixi.
@@ -48,7 +34,6 @@ import type { PhysicsRegistry } from '../physics/physics-registry';
48
34
  // TYPE-ONLY (same rule as the pixi import above): this lives under `pixi/`,
49
35
  // but `game.ts` only ever names its TYPE.
50
36
  import type { Physics2DRegistry } from '../pixi/physics-registry';
51
- import type { AudioContext as GameAudio } from '../setup/setup-audio';
52
37
  import {
53
38
  createDebugRegistry,
54
39
  DebugError,
@@ -89,10 +74,7 @@ const DISPLAY_RATE_PHASES: readonly SystemPhaseName[] = PHASE_ORDER.filter(
89
74
  * `runFrame`. `GameLoop.timeScale` (unaffected by this) remains the orthogonal
90
75
  * "speed up/slow down" axis — pausing never touches it, so the host's
91
76
  * accumulator/rAF loop keeps ticking at its normal cadence, which is what makes
92
- * "paused still renders" possible (the previous `GameSession.pause()`
93
- * implementation set `loop.timeScale = 0`, which starves the accumulator — and,
94
- * before WO-P13, stopped rendering with it; that was the bug this control
95
- * surface replaces, not a compatible behavior to keep).
77
+ * "paused still renders" possible.
96
78
  */
97
79
  export interface PlayState {
98
80
  /** Whether the game is currently paused (game-level — see the per-world
@@ -131,11 +113,9 @@ export interface PlayState {
131
113
 
132
114
  /**
133
115
  * The kinds of render surface a world can be. `'three'` and `'canvas'`
134
- * roots are both real; `'react'` is named
135
- * here so the type is already shaped for T7.4 and no caller has to widen a
136
- * union later.
116
+ * roots are canvas-backed; `'dom'` roots own a DOM layer.
137
117
  *
138
- * Re-exported from `adapter/adapter-surface.ts` (T7.5) — moved there so
118
+ * Re-exported from `adapter/adapter-surface.ts` so
139
119
  * `adapter/root-adapter.ts`'s kind-tagged `MountedRoot` types can name it
140
120
  * without an import cycle back to this file. This re-export keeps every
141
121
  * existing `import type { AdapterSurface } from '../runtime/game'` call site
@@ -144,7 +124,7 @@ export interface PlayState {
144
124
  export type AdapterSurface = AdapterSurfaceLeaf;
145
125
 
146
126
  /**
147
- * A world's per-phase frame hooks (T7.1 slice 2).
127
+ * A world's per-phase frame hooks.
148
128
  * Populated on a `RootInstance` only for first-party mounts — an opaque/
149
129
  * foreign mount has no phase-partitioned entry point, so it stays
150
130
  * `undefined` and the Game's frame executor (`GameInternal.runFrame`) falls
@@ -153,7 +133,7 @@ export type AdapterSurface = AdapterSurfaceLeaf;
153
133
  export interface RootFrameHooks {
154
134
  /** Run this world's engine systems + component ticks + world-bound game
155
135
  * systems for one phase. For a first-party world this delegates to the
156
- * SAME `SystemRunner.runPhase` its (legacy) `mounted.update` uses. */
136
+ * same `SystemRunner.runPhase` as its direct `mounted.update` entry. */
157
137
  runPhase(phase: SystemPhaseName, dt: number): void;
158
138
  /** Run once per substep, after ALL phases have run for ALL roots this
159
139
  * substep (mirrors where `mounted.update`'s post-`systems.run` work sat
@@ -164,14 +144,12 @@ export interface RootFrameHooks {
164
144
 
165
145
  /**
166
146
  * A single world instance: the unit of adaptation.
167
- * Slice-1 subset — `gpu` (per-world GPU resource cache, §6.2) is omitted
168
- * until the slice that builds it.
169
147
  */
170
148
  export interface RootInstance {
171
- /** Manifest id (T3.1). Slice 1 always registers exactly one: `'main'`. */
149
+ /** Manifest root id. */
172
150
  readonly id: string;
173
151
  readonly kind: AdapterSurface;
174
- /** Per-world play/pause semantics (D10, T7.6). Slice 1 always `true`. */
152
+ /** Per-world play/pause semantics. */
175
153
  readonly pausable: boolean;
176
154
  /** The RootAdapter that produced `mounted` — first-party or external.
177
155
  * Deliberately narrower than `RootAdapter<K>` (T7.5): this field is only
@@ -195,7 +173,7 @@ export interface RootInstance {
195
173
  * descriptively for a non-pixijs world, or a canvas world built without
196
174
  * a `stage` (see `RootInstanceInit.stage`). */
197
175
  pixiStage(): PIXI.Container;
198
- /** Kind-narrowed accessor for react roots (T6.2 slice 1): returns the
176
+ /** Kind-narrowed accessor for DOM roots: returns the
199
177
  * DOM-root layer `<div>` the host mounted this world's react tree into
200
178
  * (the SAME element passed as `container` to `createRootInstance` —
201
179
  * identity matters, mirroring `threeScene()`/`pixiStage()`'s "same
@@ -214,7 +192,7 @@ export interface RootInstance {
214
192
  readonly physics2d?: Physics2DRegistry | undefined;
215
193
  /** Kind-typed via `mounted` in T7.5; `unknown` here deliberately. */
216
194
  readonly camera?: unknown;
217
- /** Phase-partitioned frame entry point (T7.1 slice 2) — present only for
195
+ /** Phase-partitioned frame entry point — present only for
218
196
  * first-party mounts. `undefined` for an opaque/foreign mount, which
219
197
  * `GameInternal.runFrame` drives via its single `mounted.update` call
220
198
  * instead (unless it `drivesOwnLoop`, in which case it isn't ticked at
@@ -399,8 +377,8 @@ export function createRootInstance(init: RootInstanceInit): RootInstance {
399
377
  /**
400
378
  * Type guard for whether a `MountedThreeRoot` is a first-party
401
379
  * `SetupThreeRootAdapter` mount (has a live `GameContext` at `.ctx`). Used to
402
- * decide whether a world's `physics`/`collisions`/`camera` (and, on `Game`,
403
- * `input`/`audio`) can be populated from it — an external
380
+ * decide whether a world's `physics`/`collisions`/`camera` can be populated
381
+ * from it — an external
404
382
  * adapter's mount has none of these first-party handles.
405
383
  *
406
384
  * Checks the `firstParty: true` brand (checklist item 1), NOT `'ctx' in
@@ -505,13 +483,9 @@ function debrisOwnersOf(world: RootInstance): DebrisOwner[] {
505
483
  }
506
484
 
507
485
  /**
508
- * The Game root. Owns the one loop, the raw-asset
509
- * cache, the world registry, and (T7.1 slice 2) the game-scoped
510
- * `SystemRunner`. `input`/`audio` remain slice-1 late
511
- * additions: they delegate to the default world's first-party mount so
512
- * existing single-world call sites keep working; hoisting them to true
513
- * Game ownership (the target shape) is a later slice's
514
- * work, not this one's.
486
+ * The Game root. Owns the loop, raw-asset cache, world registry, input,
487
+ * and game-scoped `SystemRunner`. Surface-specific capabilities remain on
488
+ * their mounted roots or in the aggregated `SystemAdapters` contract.
515
489
  */
516
490
  export interface Game {
517
491
  readonly loop: GameLoop;
@@ -521,18 +495,16 @@ export interface Game {
521
495
  /** Per-game diagnostic store. Disabled by default; the editor enables it on demand. */
522
496
  readonly profiler: PerformanceProfiler;
523
497
  /**
524
- * The game-scoped `SystemRunner` (T7.1 slice 2) a
525
- * NEW bucket, separate from any world's own runner. Within each phase,
498
+ * The game-scoped `SystemRunner`, separate from any world's own runner.
499
+ * Within each phase,
526
500
  * `GameInternal.runFrame` runs THIS runner's `runPhase` first, before any
527
501
  * world's engine systems/component ticks/world-bound game systems (e.g.
528
502
  * `ctx.systems.add`, which stays world-bound to the default world — see
529
503
  * `runtime/types.ts`). Empty for every existing game (nothing registers
530
- * against it yet), so `runFrame`'s behavior for a single-world game is
531
- * unchanged by its presence.
504
+ * against it), so an empty runner has no frame cost beyond dispatch.
532
505
  */
533
506
  readonly systems: SystemRunner;
534
- /** Declaration-ordered. Slice 1 registers exactly one (the default
535
- * three world) — this is the SAME array reference `registerRoot`
507
+ /** Declaration-ordered. This is the same array reference `registerRoot`
536
508
  * mutates, not a snapshot, so holders (e.g. `GameContext.roots`) observe
537
509
  * later registrations. */
538
510
  readonly roots: ReadonlyArray<RootInstance>;
@@ -568,9 +540,6 @@ export interface Game {
568
540
  subscribeSystemAdapters?(listener: () => void): () => void;
569
541
  /** The one game-owned `InputManager`, shared by every first-party root. */
570
542
  readonly input: InputManager;
571
- /** Delegates to the default world's first-party audio context. Throws
572
- * when the default world is not a first-party mount. */
573
- readonly audio: GameAudio;
574
543
  /**
575
544
  * Frame-versioned state bridge (T7.4 slice 1). Bumped once per completed
576
545
  * `runFrame`, after all phases of all roots and all `endFrame` hooks (see
@@ -653,28 +622,18 @@ export interface GameInternal extends Game {
653
622
  * else: world.mounted.update?.(dt) // opaque world fallback
654
623
  * ```
655
624
  *
656
- * For today's single first-party world this is byte-identical to the
657
- * legacy `mounted.update(dt)` (`game.systems` is empty; the one world's
658
- * `frame.runPhase` delegates to the SAME `SystemRunner.runPhase` its
659
- * `update` used; `endFrame` is the same `postFrame` call). A
660
- * `drivesOwnLoop` world is never ticked here at all — matching its
661
- * exclusion from the legacy `!mountedRef.drivesOwnLoop` guard. An opaque
662
- * host-driven world (no `frame`) gets exactly one `update(dt)` call per
663
- * substep, after the phase loop — unchanged cadence from today.
625
+ * A single first-party world and its direct `mounted.update(dt)` entry run
626
+ * the same `SystemRunner` and `postFrame` work. A `drivesOwnLoop` world is
627
+ * never ticked here. An opaque host-driven world (no `frame`) gets exactly
628
+ * one `update(dt)` call per substep, after the phase loop.
664
629
  *
665
630
  * D10/T7.6 play-state addendum: when `Game.play.paused` is true, every
666
631
  * `pausable` (and non-`drivesOwnLoop`) world skips every phase EXCEPT
667
632
  * `render` (still called, every substep, with `dt` forced to `0`) and skips
668
633
  * its `endFrame`/opaque-`update` call entirely — a `pausable: false` world
669
- * is completely unaffected. `opts.ignorePause` runs this call as if nothing
670
- * were paused, regardless of the live `paused` flag; kept for external
671
- * byte-compatibility (no first-party caller passes it today grep finds
672
- * none) but is no longer how `Game.play.step()` works (§7.1-2 fix,
673
- * probe4: the old `ignorePause` full-frame re-run double-ticked every
674
- * already-running `pausable: false` world, since D10's ordinary loop never
675
- * stops ticking them). `Game.play.step()` now drives this function via the
676
- * internal-only `onlyFrozen` mode instead (see `runFrameImpl` — not part of
677
- * this public, host-facing signature): it ticks EXACTLY the currently
634
+ * is completely unaffected. `Game.play.step()` drives this function via the
635
+ * internal-only `onlyFrozen` mode (see `runFrameImpl` not part of this
636
+ * public, host-facing signature): it ticks exactly the currently
678
637
  * frozen set (host-driven, `pausable`, and `paused`) through every phase +
679
638
  * `endFrame` with the real `dt` (not the render-phase's forced `0`), and
680
639
  * touches no other world at all — a natural no-op while not paused, since
@@ -690,7 +649,7 @@ export interface GameInternal extends Game {
690
649
  * calls `runFrame(fixedDt)` with no opts) is frame-exact and unchanged.
691
650
  * `runTicks` sets it per tick for its `render: 'none' | 'last'` modes.
692
651
  */
693
- runFrame(dt: number, opts?: { ignorePause?: boolean; skipRenderPhases?: boolean }): void;
652
+ runFrame(dt: number, opts?: { skipRenderPhases?: boolean }): void;
694
653
  /**
695
654
  * WO-P13 — run ONE display frame's presentation pass: the registered
696
655
  * `onRenderStep` callbacks, then the `preRender` and `render` phases across
@@ -775,20 +734,18 @@ export interface GameInternal extends Game {
775
734
  * the conventional `/inputmaps/default.inputmap.json` (or the adapter's
776
735
  * configured `inputMapPath`); runtime additions go through
777
736
  * `ctx.input.registerAction` instead.
737
+ *
738
+ * `{ optional: true }` is the CONVENTIONAL-PATH probe a root uses when the
739
+ * project never named a map: an absent file resolves quietly (a brand-new
740
+ * scaffold declares no actions yet), pins nothing — so a sibling root may
741
+ * still declare its own map — and yields to a map another root already
742
+ * owns. A map that exists but fails to parse is as loud either way.
778
743
  */
779
- loadInputMap(path: string): Promise<void>;
744
+ loadInputMap(path: string, options?: { optional?: boolean }): Promise<void>;
780
745
  /** Release game-owned resources after every mounted root has disposed. */
781
746
  dispose(): void;
782
747
  }
783
748
 
784
- function describeMismatch(handle: 'input' | 'audio', world: RootInstance): string {
785
- return (
786
- `Game.${handle}: default world "${world.id}" (kind: ${world.kind}) is not a first-party ` +
787
- "mount — hoisting these to true Game ownership is a later slice's work; available only " +
788
- 'via a first-party default world today'
789
- );
790
- }
791
-
792
749
  /**
793
750
  * Construct the (host-internal) Game shell. Callers: `createGameRuntime`
794
751
  * builds this BEFORE mounting its one adapter, then registers the default
@@ -879,7 +836,7 @@ export function createGame(opts: {
879
836
  getTick: () => tick,
880
837
  getSimT: () => simT,
881
838
  // D15/T-D15.5 — "the manifest's first/default world" for the debug
882
- // registry's world-addressed input-target compatibility surface. Every
839
+ // registry's world-addressed input-target surface. Every
883
840
  // first-party root now registers the SAME game-owned InputManager, but the
884
841
  // stable default id still keeps explicit/implicit debug routing coherent.
885
842
  // Use the SAME "first three world, else first world" rule
@@ -905,6 +862,10 @@ export function createGame(opts: {
905
862
  input.setDebugEmit((event, detail) => debugRegistry.forRoot('(game)').emit(event, detail));
906
863
  let inputMapPath: string | null = null;
907
864
  let inputMapLoad: Promise<void> | null = null;
865
+ /** In-flight/settled conventional-path probes (`loadInputMap(path, {optional})`),
866
+ * so two roots probing the same path share one fetch. Separate from
867
+ * `inputMapLoad` because a probe that found nothing declares nothing. */
868
+ const inputMapProbes = new Map<string, Promise<void>>();
908
869
 
909
870
  function requireDefaultRoot(): RootInstance {
910
871
  if (roots.length === 0) {
@@ -913,14 +874,6 @@ export function createGame(opts: {
913
874
  return roots.find((w) => w.kind === 'three') ?? roots[0]!;
914
875
  }
915
876
 
916
- function requireFirstPartyCtx(handle: 'input' | 'audio') {
917
- const world = requireDefaultRoot();
918
- if (!isFirstPartyMounted(world.mounted)) {
919
- throw new Error(describeMismatch(handle, world));
920
- }
921
- return world.mounted.ctx;
922
- }
923
-
924
877
  // --- Game.systemAdapters aggregation (§7.1-3, probe1) --------------------
925
878
  // Warn-once-per-colliding-key state, scoped to this Game instance (a fresh
926
879
  // Game gets a fresh warn history) — deliberately NOT reset by anything
@@ -1023,7 +976,7 @@ export function createGame(opts: {
1023
976
  // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: the frame algorithm (now with D10's per-world pause gate + the onlyFrozen step()-only mode) is one cohesive nested loop over phases/roots — splitting it would obscure the ordering contract documented on GameInternal.runFrame
1024
977
  function runFrameImpl(
1025
978
  dt: number,
1026
- frameOpts?: { ignorePause?: boolean; onlyFrozen?: boolean; skipRenderPhases?: boolean },
979
+ frameOpts?: { onlyFrozen?: boolean; skipRenderPhases?: boolean },
1027
980
  ): void {
1028
981
  // D15 (T-D15.3) — brackets the ENTIRE frame body (every phase, every
1029
982
  // world, both the `onlyFrozen` and normal branches below converge on the
@@ -1033,7 +986,6 @@ export function createGame(opts: {
1033
986
  // declare `determinism.seededRandom`).
1034
987
  if (rngTrapEnabled) rngTrap.enable();
1035
988
  profiler.beginFrame();
1036
- const ignorePause = frameOpts?.ignorePause ?? false;
1037
989
  // D15/T-D15.4 + WO-P13: the render-phase skip. Two callers set it, for the
1038
990
  // same reason — this substep is not the thing that paints. `runTicks`'s
1039
991
  // `render: 'none'|'last'` fast-forward sets it on every tick it doesn't
@@ -1048,9 +1000,6 @@ export function createGame(opts: {
1048
1000
  // one and only caller. It ticks EXACTLY the currently-frozen set (host-driven,
1049
1001
  // `pausable`, and `paused`) through every phase + `endFrame`, with the REAL
1050
1002
  // `dt` (not the render-phase's forced `0`), and touches no other world at all
1051
- // — §7.1-2 fix, probe4: the previous `ignorePause` full-frame re-run ticked
1052
- // EVERY host-driven world, double-ticking every already-running
1053
- // `pausable: false` world (D10's ordinary loop never stops ticking them).
1054
1003
  const onlyFrozen = frameOpts?.onlyFrozen ?? false;
1055
1004
  // Checklist item 7: snapshot the world count ONCE at entry and iterate
1056
1005
  // by index in both loops below. A world registered mid-frame (e.g. from
@@ -1068,10 +1017,9 @@ export function createGame(opts: {
1068
1017
  break;
1069
1018
  }
1070
1019
  } else if (
1071
- ignorePause ||
1072
- (world.mounted.drivesOwnLoop
1020
+ world.mounted.drivesOwnLoop
1073
1021
  ? !paused || !world.pausable || !world.mounted.setPaused
1074
- : !paused || !world.pausable)
1022
+ : !paused || !world.pausable
1075
1023
  ) {
1076
1024
  inputFrameActive = true;
1077
1025
  break;
@@ -1169,13 +1117,12 @@ export function createGame(opts: {
1169
1117
  for (let i = 0; i < n; i++) {
1170
1118
  const world = roots[i]!;
1171
1119
  if (world.mounted.drivesOwnLoop) continue;
1172
- // D10/T7.6: a `pausable` world under an active (non-ignored) pause
1120
+ // D10/T7.6: a `pausable` world under an active pause
1173
1121
  // skips every phase except `render` — its render still runs, every
1174
1122
  // substep, but with `dt` forced to `0` (deterministic: no
1175
1123
  // time-based render effect silently keeps animating a "frozen"
1176
- // scene). A `pausable: false` world (or any world while
1177
- // `ignorePause`) is unaffected.
1178
- const frozen = !ignorePause && paused && world.pausable;
1124
+ // scene). A `pausable: false` world is unaffected.
1125
+ const frozen = paused && world.pausable;
1179
1126
  if (frozen && phase !== SystemPhase.RENDER) continue;
1180
1127
  const phaseDt = frozen ? 0 : dt;
1181
1128
  // Checklist item 2: isolate each world's per-phase work — one
@@ -1199,7 +1146,7 @@ export function createGame(opts: {
1199
1146
  if (world.mounted.drivesOwnLoop) continue;
1200
1147
  // A fully-frozen world gets no `endFrame`/opaque-`update` call either —
1201
1148
  // there is nothing to "end the frame" of when nothing ran this substep.
1202
- const frozen = !ignorePause && paused && world.pausable;
1149
+ const frozen = paused && world.pausable;
1203
1150
  if (frozen) continue;
1204
1151
  advanced = true;
1205
1152
  if (world.frame) {
@@ -1346,9 +1293,6 @@ export function createGame(opts: {
1346
1293
  for (const listener of systemAdapterListeners) listener();
1347
1294
  },
1348
1295
  input,
1349
- get audio() {
1350
- return requireFirstPartyCtx('audio').audio;
1351
- },
1352
1296
  state: stateBridge,
1353
1297
  play: {
1354
1298
  get paused() {
@@ -1387,11 +1331,7 @@ export function createGame(opts: {
1387
1331
  }
1388
1332
  }
1389
1333
  }
1390
- // §7.1-2 fix (probe4): tick EXACTLY the frozen (host-driven, pausable,
1391
- // paused) set — not `{ ignorePause: true }`, which re-ran a FULL
1392
- // extra frame for every host-driven world (including already-running
1393
- // `pausable: false` ones — a double-tick, since D10's ordinary loop
1394
- // never stops ticking them). See `runFrameImpl`'s `onlyFrozen` mode.
1334
+ // Tick exactly the frozen (host-driven, pausable, paused) set.
1395
1335
  runFrameImpl(dt, { onlyFrozen: true });
1396
1336
  },
1397
1337
  },
@@ -1486,7 +1426,7 @@ export function createGame(opts: {
1486
1426
  );
1487
1427
  }
1488
1428
  },
1489
- runFrame(dt: number, frameOpts?: { ignorePause?: boolean; skipRenderPhases?: boolean }): void {
1429
+ runFrame(dt: number, frameOpts?: { skipRenderPhases?: boolean }): void {
1490
1430
  runFrameImpl(dt, frameOpts);
1491
1431
  },
1492
1432
  runRenderFrame(alpha: number, displayDt: number): void {
@@ -1538,7 +1478,26 @@ export function createGame(opts: {
1538
1478
  runFrameImpl(fixedDt, { skipRenderPhases });
1539
1479
  }
1540
1480
  },
1541
- loadInputMap(path: string): Promise<void> {
1481
+ loadInputMap(path: string, options?: { optional?: boolean }): Promise<void> {
1482
+ if (options?.optional === true) {
1483
+ // The conventional-path probe. It never becomes the game's declared
1484
+ // map unless a file was actually there, so a root that DOES name a
1485
+ // map (a sibling canvas root, a later mount) is never pre-empted by a
1486
+ // probe that found nothing — the competing-path throw below would
1487
+ // otherwise fire on a project that declared exactly one map.
1488
+ if (inputMapPath) return inputMapLoad ?? Promise.resolve();
1489
+ let probe = inputMapProbes.get(path);
1490
+ if (!probe) {
1491
+ probe = input.loadMapIfPresent(path).then((loaded) => {
1492
+ if (loaded && !inputMapPath) {
1493
+ inputMapPath = path;
1494
+ inputMapLoad = Promise.resolve();
1495
+ }
1496
+ });
1497
+ inputMapProbes.set(path, probe);
1498
+ }
1499
+ return probe;
1500
+ }
1542
1501
  if (inputMapPath && inputMapPath !== path) {
1543
1502
  throw new Error(
1544
1503
  `Game.loadInputMap: input map is already "${inputMapPath}"; root requested "${path}". ` +
@@ -17,6 +17,11 @@
17
17
  // even the 'three' convenience export is a plain function the CALLER
18
18
  // registers, not something this module wires up automatically.
19
19
 
20
+ import {
21
+ installNativeDebugBindings,
22
+ type NativeDebugBinding,
23
+ nativeDebugBindingFromEntryModule,
24
+ } from '../adapter/native-debug-module';
20
25
  import type { RootAdapter } from '../adapter/root-adapter';
21
26
  import type { ResolvedAdapterRoot } from '../manifest/load';
22
27
  import type { GameSession } from './create-runtime';
@@ -209,6 +214,15 @@ function worldIsSelfSufficient(_world: ResolvedAdapterRoot): boolean {
209
214
  return false;
210
215
  }
211
216
 
217
+ function nativeDebugForEntry(
218
+ world: ResolvedAdapterRoot,
219
+ entryModule: unknown,
220
+ ): NativeDebugBinding[] {
221
+ if (entryModule === undefined || world.adapter.type !== 'builtin') return [];
222
+ const binding = nativeDebugBindingFromEntryModule(world.id, entryModule);
223
+ return binding ? [binding] : [];
224
+ }
225
+
212
226
  /**
213
227
  * Mount every world declared by a `vgai.project.json` manifest onto `host
214
228
  * .container`, resolving each world's kind through the `registerAdapter`
@@ -240,6 +254,7 @@ export async function mountGameFromManifest(
240
254
  assertExportedOrInEditor();
241
255
  const manifest = resolveManifest(manifestInput);
242
256
  const entries: Record<string, MountEntry> = { ...opts.entries };
257
+ const nativeDebug: NativeDebugBinding[] = [];
243
258
 
244
259
  for (const world of manifest.roots) {
245
260
  if (entries[world.id] !== undefined) continue; // explicit entry always wins
@@ -250,6 +265,7 @@ export async function mountGameFromManifest(
250
265
  world.entry !== undefined && host.loadEntryModule
251
266
  ? await host.loadEntryModule(world.entry)
252
267
  : undefined;
268
+ nativeDebug.push(...nativeDebugForEntry(world, entryModule));
253
269
  entries[world.id] = await factory(world, { host, entryModule });
254
270
  continue;
255
271
  }
@@ -265,7 +281,7 @@ export async function mountGameFromManifest(
265
281
  );
266
282
  }
267
283
 
268
- return mountManifestRoots({
284
+ const session = await mountManifestRoots({
269
285
  manifest,
270
286
  container: host.container,
271
287
  entries,
@@ -273,4 +289,6 @@ export async function mountGameFromManifest(
273
289
  height: host.height,
274
290
  headless: host.headless,
275
291
  });
292
+ installNativeDebugBindings(session.game, nativeDebug);
293
+ return session;
276
294
  }
@@ -20,8 +20,7 @@
20
20
  // `entries[id].adapter` (the caller's own module graph built it). Threejs is
21
21
  // different: `three`/
22
22
  // `SetupThreeRootAdapter` are already unconditional dependencies of every
23
- // caller of `createGameRuntime`'s legacy path (this same file's sibling,
24
- // immediately below in this directory), so wrapping a `setup` with
23
+ // caller of `createGameRuntime`, so wrapping a `setup` with
25
24
  // `fromSetup` here adds no NEW bundle weight — only pixi/react are avoided.
26
25
 
27
26
  import { assertNever } from '../adapter/adapter-surface';
@@ -37,7 +36,6 @@ import {
37
36
  import { createGameRuntime, type GameSession, type RootMountSpec } from './create-runtime';
38
37
  import { type DebugBridgeWindowTarget, maybeInstallDebugBridge } from './debug-bridge';
39
38
  import { getDebugRegistry } from './debug-registry';
40
- import { devLayersEnabled } from './dev-layers';
41
39
  import { getGameplayRngTrapControl } from './gameplay-rng-trap';
42
40
  import { RENDER_SEED_QUERY_PARAM } from './render-seed';
43
41
  import type { GameSetupFn, PlaytestContext } from './types';
@@ -157,15 +155,6 @@ export interface MountManifestOptions {
157
155
  readonly seed?: number | undefined;
158
156
  /** Host identity for a private play run or coordinated Team Test. */
159
157
  readonly playtest?: PlaytestContext | null | undefined;
160
- /**
161
- * The explicit per-mount answer to "are dev layers enabled" — forwarded
162
- * verbatim to `devLayersEnabled` (`runtime/dev-layers.ts`), the ONE owner of
163
- * that question. Omit it and the build decides (dev build ⇒ on; production
164
- * build ⇒ off unless `VITE_VGAI_DEV_LAYERS=true`). A root declaring
165
- * `dev: true` is simply not mounted when the answer is `false`, so a shipped
166
- * build carries no dev GUI at all.
167
- */
168
- readonly devLayers?: boolean | undefined;
169
158
  /** Where to read `?vgai-seed=` from for the boot-time seed reader. Same
170
159
  * override precedent as `debugBridge.url` (defaults to `window.location`
171
160
  * when a real `window` exists; a headless caller with no override gets no
@@ -275,8 +264,9 @@ function resolveThreeAdapter(
275
264
  if (world.adapter.type === 'ingest') {
276
265
  throw new Error(
277
266
  `mountManifestRoots: world "${world.id}" (three) declares an { ingest } adapter — ` +
278
- "ingest roots require the editor's dev-server-backed mount machinery (an EditorStore " +
279
- "plus in-realm scene capture, see adapter-resolver.ts's resolveIngestThreeAdapter) and are " +
267
+ "captured ingest roots require the editor's dev-server-backed mount machinery (an EditorStore " +
268
+ 'plus in-realm scene capture, see resolveIngestDescriptor + ' +
269
+ 'mountThreeIngestRootFromManifest) and are ' +
280
270
  'not supported by mountManifestRoots (no porting aids, no hosted ingest routes).',
281
271
  );
282
272
  }
@@ -346,8 +336,6 @@ function buildRootMountSpec(
346
336
  id: world.id,
347
337
  zOrder: world.zOrder,
348
338
  pausable: world.pausable,
349
- dev: world.dev,
350
- loop: world.loop,
351
339
  };
352
340
 
353
341
  // `entry`'s `kind` is guaranteed to agree with `world.kind` past the guard
@@ -391,10 +379,6 @@ function buildRootMountSpec(
391
379
  * ingest }` adapter with no caller-supplied adapter, or an empty manifest all
392
380
  * throw a named `Error` identifying the world id, its kind, and what was
393
381
  * expected — never a silent skip or a partially-mounted session.
394
- *
395
- * The ONE root that IS skipped silently is a `dev: true` dev layer when dev
396
- * layers are disabled (`devLayersEnabled`) — that skip is the feature: it is
397
- * what makes a dev GUI safe to leave in the game.
398
382
  */
399
383
  export async function mountManifestRoots(opts: MountManifestOptions): Promise<GameSession> {
400
384
  const manifest = resolveManifest(opts.manifest);
@@ -403,23 +387,7 @@ export async function mountManifestRoots(opts: MountManifestOptions): Promise<Ga
403
387
  }
404
388
  const entries = opts.entries ?? {};
405
389
 
406
- // The SHIP GATE. A `dev: true` root is the game's own dev GUI; it mounts
407
- // only when dev layers are enabled, so a production build carries no dev
408
- // GUI. The answer comes from `devLayersEnabled` and nowhere else — see that
409
- // function's header for why there is exactly one owner. Dropping the root
410
- // here (rather than mounting it hidden) is deliberate: nothing of the dev
411
- // layer is constructed, so its entry module's cost and its DOM never exist
412
- // in a shipped build.
413
- const devEnabled = devLayersEnabled(opts.devLayers);
414
- const mountable = devEnabled ? manifest.roots : manifest.roots.filter((world) => !world.dev);
415
- if (mountable.length === 0) {
416
- throw new Error(
417
- 'mountManifestRoots: every root in this manifest is a dev layer (`dev: true`) and dev ' +
418
- 'layers are disabled for this build — there is no game to mount.',
419
- );
420
- }
421
-
422
- const roots: RootMountSpec[] = mountable.map((world) =>
390
+ const roots: RootMountSpec[] = manifest.roots.map((world) =>
423
391
  buildRootMountSpec(world, entries[world.id]),
424
392
  );
425
393
 
@@ -259,6 +259,28 @@ export interface GameContext {
259
259
  playtest?: PlaytestContext | null | undefined;
260
260
  }
261
261
 
262
+ /**
263
+ * Services shared by component-authored roots, independent of their render
264
+ * surface. This is deliberately an explicit pick from {@link GameContext}:
265
+ * adding a subsystem to the imperative Three setup context never silently
266
+ * exposes it to R3F or Canvas components.
267
+ *
268
+ * `input` is nullable because it belongs to the optional `Game` host. Normal
269
+ * vgai project mounts always provide it; bare harnesses and foreign hosts do
270
+ * not. Component code therefore sees the absence in its type instead of a
271
+ * getter that first throws from inside a frame callback.
272
+ *
273
+ * `clock` is surface-specific and intentionally absent here. The imperative
274
+ * and R3F clocks can dispose `THREE.Object3D` debris; a Canvas context narrows
275
+ * that Three-only operation away.
276
+ */
277
+ export type ComponentContextBase = Pick<
278
+ GameContext,
279
+ 'systems' | 'debug' | 'random' | 'game' | 'roots' | 'playtest' | 'registerSystemAdapter'
280
+ > & {
281
+ readonly input: InputManager | null;
282
+ };
283
+
262
284
  /** Return value from a game setup function. */
263
285
  export interface GameCleanup {
264
286
  dispose: () => void;
@@ -79,10 +79,9 @@ export function applyRendererSettings(
79
79
  * reload-only `antialias` option at construction and the live shadow/resolution
80
80
  * settings (via {@link applyRendererSettings}). Absent → registry defaults.
81
81
  *
82
- * `opts` is additive (T6.1 slice 1, D5 §1/§4): omitted
83
- * entirely, construction is byte-identical to before (no `alpha`/
84
- * `preserveDrawingBuffer` keys at all) — the legacy single-canvas host never
85
- * passes it. The roots path passes `alpha:true` for every stacked canvas
82
+ * When `opts` is omitted, construction leaves `alpha` and
83
+ * `preserveDrawingBuffer` at their renderer defaults. The host passes
84
+ * `alpha:true` for every stacked canvas
86
85
  * above the bottom one (so its clear-alpha-0 shows the layer below through
87
86
  * it) and `preserveDrawingBuffer:true` for every stacked canvas (the
88
87
  * recorded capture-tier cost, paid once here rather than re-derived later).
@@ -15,6 +15,7 @@
15
15
  */
16
16
 
17
17
  import { createContext, useContext } from 'react';
18
+ import { useOptionalGame } from '../react/world-state';
18
19
  import type { R3FGameContext } from './world-context';
19
20
 
20
21
  export interface EngineBridgeValue {
@@ -39,7 +40,7 @@ export const EngineBridge = createContext<EngineBridgeValue | null>(null);
39
40
  * function Player({ speed = 4.5 }) {
40
41
  * const { input } = useGameContext();
41
42
  * const ref = useRef<THREE.Group>(null);
42
- * useFrame((_, dt) => { if (input.isPressed('move_forward')) … });
43
+ * useFrame((_, dt) => { if (input?.isPressed('move_forward')) … });
43
44
  * return <group ref={ref}><PlayerCharacter /></group>;
44
45
  * }
45
46
  * ```
@@ -50,11 +51,35 @@ export const EngineBridge = createContext<EngineBridgeValue | null>(null);
50
51
  */
51
52
  export function useGameContext(): R3FGameContext {
52
53
  const bridge = useContext(EngineBridge);
54
+ // Read unconditionally (rules of hooks) — used only to shape the throw below.
55
+ const hostedByGame = useOptionalGame() !== null;
53
56
  if (!bridge?.ctx) {
57
+ // NAME THE REAL CAUSE. This throw used to say "check that the component is
58
+ // rendered inside your world entry" — advice a scaffolded game has already
59
+ // followed, because the shape `vgai create` produces (`export default
60
+ // function World()`) mounts as a COMPONENT ROOT and receives no vgai
61
+ // runtime context at all: `r3f-root-factory.tsx` passes
62
+ // `engineRuntime: false` deliberately, so this hook can never resolve
63
+ // there however the component is nested. A world whose first render calls
64
+ // it dies with "fiber crashed before its first commit" and a fix
65
+ // instruction that cannot work. The `Game` in scope tells the two cases
66
+ // apart: present means a vgai-mounted world, absent means no root at all.
54
67
  throw new Error(
55
- 'useGameContext(): no engine root context. This hook only works inside an R3F ' +
56
- 'root mounted by createR3FAdapter (@engine/world3d-react) check that the ' +
57
- 'component is rendered inside your world entry, not in a standalone <Canvas>.',
68
+ hostedByGame
69
+ ? 'useGameContext(): this world mounts as a COMPONENT ROOT (the default-exported ' +
70
+ 'shape `vgai create` scaffolds) and receives no vgai runtime context by design, ' +
71
+ 'so nesting cannot fix it. Game-scoped services are on the Game handle instead: ' +
72
+ '`useGame().input` (from @vgai/engine/react/world-state) is the game-owned ' +
73
+ "InputManager the host already loaded this project's input map into and the same " +
74
+ 'one `game.input.hold/tap` drives; `useDebugProvider`/`useDebugCommand`/' +
75
+ '`useWorldState` are the state/command doors. If this world genuinely needs the ' +
76
+ 'imperative runtime (`ctx.systems`, adapter options like `inputMapPath`), export ' +
77
+ '`adapter = createR3FAdapter({ id, content: <World/> })` from the entry instead of ' +
78
+ 'default-exporting the component.'
79
+ : 'useGameContext(): no engine root context and no Game in scope — this component is ' +
80
+ 'rendering outside any mounted vgai root (a standalone <Canvas>, a story, or a ' +
81
+ "bare test render). Mount it through your project's three root, or read services " +
82
+ 'the component root way (native hooks and project-owned state).',
58
83
  );
59
84
  }
60
85
  return bridge.ctx;