@vgai/engine 0.5.2 → 0.5.3

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
@@ -1,12 +1,20 @@
1
1
  /**
2
- * VgaiSceneGameAdapter — the FIRST-PARTY implementer of {@link GameAdapter}.
2
+ * SetupThreeRootAdapter — the first-party implementer of {@link RootAdapter}
3
+ * that mounts ONE three root from an imperative `setup(ctx)` function.
3
4
  *
4
- * This is where `.vscn` + `GameComponent` + Rapier live now: the host no longer
5
- * knows about any of them. `mount(host)` builds the full first-party runtime
6
- * (the body that used to live inline in `create-runtime.ts`) from a neutral
7
- * {@link HostContext}, runs the game's `setup`/scene load, and returns a
8
- * {@link MountedGame}. First-party content is now just one implementer of the
9
- * same interface an unmodified external game implements (Phase B).
5
+ * This is where Rapier lives now: the host no longer knows about it.
6
+ * `mount(host)` builds the full first-party runtime (the body
7
+ * that used to live inline in `create-runtime.ts`) from a neutral
8
+ * {@link ThreeHostContext}, runs the game's `setup`, and returns a
9
+ * {@link MountedThreeRoot}. First-party content is now just one implementer of
10
+ * the same interface an unmodified external game implements (Phase B).
11
+ *
12
+ * It was `VgaiSceneGameAdapter` until WO-9 tasks #6/#9. Both halves of that
13
+ * name had stopped being true: it loads no scene (the `.vscn.json` branches
14
+ * went with the format in WO-8 — `setup` is the only mount path left), and what
15
+ * it mounts is one three ROOT, not a game (a game is the manifest's whole set
16
+ * of roots). The R3F/TSX world root is a SEPARATE adapter; this one is
17
+ * specifically the `fromSetup` path.
10
18
  *
11
19
  * `GameSetupFn` / `GameContext` are imported ONLY here (and in the example
12
20
  * `fromSetup` wrappers) — never by the host. That confinement is the inversion.
@@ -16,14 +24,15 @@ import RAPIER from '@dimforge/rapier3d-compat';
16
24
  import type { SparkRenderer } from '@sparkjsdev/spark';
17
25
  import type { EffectComposer } from 'postprocessing';
18
26
  import * as THREE from 'three';
27
+ import { clearAssetCaches } from '../asset-loaders';
19
28
  import {
20
29
  createSeededRandom,
21
30
  DEFAULT_SEEDED_RANDOM_SEED,
22
31
  getSeededRandom,
23
32
  } from '../core/seeded-random';
33
+ import { createSimClock, getSimClock, type SimClockInternal } from '../core/sim-clock';
24
34
  import { createSystemRunner } from '../core/system-runner';
25
35
  import { createDebugDraw } from '../dev/debug-draw';
26
- import { log } from '../dev/logger';
27
36
  import {
28
37
  createRenderDebugAdapter,
29
38
  frameCaptureContextFor,
@@ -32,15 +41,14 @@ import {
32
41
  import { collectRenderMemory } from '../dev/render-memory';
33
42
  import { createWebGLFrameCapture } from '../dev/webgl-frame-capture';
34
43
  import { createWebGLGpuTimer } from '../dev/webgl-gpu-timer';
35
- import { createComponentManager } from '../ecs/component-manager';
44
+ import { createSceneIndex } from '../ecs/scene-index';
45
+ import { getUserData, hasUserData, setUserData } from '../ecs/user-data';
36
46
  import { InputManager } from '../input/input-manager';
37
47
  import { setAssetPrefix } from '../loader';
38
48
  import { createCollisionSystem } from '../physics/collision-system';
39
49
  import { createPhysicsRegistry } from '../physics/physics-registry';
40
50
  import { createTransformWriter } from '../physics/transform-writer';
41
- import { createTriggerDispatch } from '../physics/trigger-dispatch';
42
- import { RenderBatchSystem } from '../render/render-batch-system';
43
- import { type RenderScope, resolveRenderSettings } from '../render/render-settings';
51
+ import { updateSceneLODs } from '../render/lod';
44
52
  import {
45
53
  disposeSparkRendererWhenIdle,
46
54
  SPARK_DISCOVERY_INTERVAL_MS,
@@ -53,76 +61,42 @@ import {
53
61
  type DebugRegistry,
54
62
  getDebugRegistry,
55
63
  } from '../runtime/debug-registry';
56
- import type { WorldFrameHooks } from '../runtime/game';
64
+ import { disposeDebrisSubtree, type RootFrameHooks } from '../runtime/game';
57
65
  import type { EditorPreview, GameCleanup, GameContext, GameSetupFn } from '../runtime/types';
58
- import type { ComponentRegistry } from '../scene/component-registry';
59
- import {
60
- clearAssetCaches,
61
- loadScene,
62
- loadSceneFromData,
63
- type SceneInstance,
64
- updateSceneLODs,
65
- } from '../scene/scene-loader';
66
- import type { SceneFile } from '../scene/scene-types';
67
- import { hasUserData, setUserData } from '../scene/user-data';
68
66
  import { type AudioContext as GameAudio, setupAudio } from '../setup/setup-audio';
69
67
  import { setupParticles } from '../setup/setup-particles';
70
68
  import { setupPhysics, updatePhysicsDebug } from '../setup/setup-physics';
71
- import { applySceneRenderPipeline, createSceneView } from '../setup/setup-renderer';
72
- import {
73
- createAudioSystemAdapter,
74
- createInputManagerAdapter,
75
- createNavigationAdapter,
76
- createVgaiAssetAdapter,
77
- } from './first-party-systems';
78
- import type { GameAdapter, MountedGame } from './game-adapter';
79
- import type { HostContext } from './host-context';
69
+ import { createSceneView } from '../setup/setup-renderer';
70
+ import { createAudioSystemAdapter, releaseAudioMeters } from './first-party-systems';
71
+ import type { ThreeHostContext } from './host-context';
80
72
  import { createRapierPhysicsAdapter } from './rapier-physics-adapter';
73
+ import type { MountedThreeRoot, RootAdapter } from './root-adapter';
81
74
  import type { SystemAdapters } from './system-adapter';
82
75
 
83
- /** How a {@link VgaiSceneGameAdapter} builds its game (the former RuntimeConfig). */
84
- export interface VgaiSceneConfig {
76
+ /** How a {@link SetupThreeRootAdapter} builds its game (the former RuntimeConfig). */
77
+ export interface SetupThreeRootConfig {
85
78
  /** Game setup function — registers systems and loads scenes. */
86
79
  setup?: GameSetupFn | undefined;
87
80
  /** Editor context — passed to setup() when launched from the editor. */
88
81
  editorPreview?: EditorPreview | undefined;
89
- /** Path to a .vscn.json scene file. Loaded if no setup is provided. */
90
- scenePath?: string | undefined;
91
- /** In-memory scene data. Loaded if no setup and no scenePath is provided. */
92
- sceneData?: SceneFile | undefined;
93
- /**
94
- * Component name -> GameComponent class map, threaded into the scene
95
- * loader's `SceneLoadContext.componentRegistry` for both the `scenePath`
96
- * and `sceneData` branches (never for `setup` — a custom setup already
97
- * owns its own registry, e.g. `template/src/scripts/main.ts`'s generic
98
- * fallback). Without this, a scene's `components:` entries throw
99
- * ("Component ... not found in registry") or, pre-T7.2-closure, were
100
- * simply unreachable through these two config options at all (the T7.2
101
- * finding `docs/CLI-ON-FOLDER-DESIGN.md` §1D calls out — see
102
- * `packages/editor/src/adapter-resolver.ts`, the only production caller
103
- * that sets this today).
104
- */
105
- componentRegistry?: ComponentRegistry | undefined;
106
82
  /** Input map JSON path. Defaults to 'inputmaps/default.inputmap.json'. */
107
83
  inputMapPath?: string | undefined;
108
84
  /** URL prefix for relative asset paths. Defaults to '/'. */
109
85
  assetPrefix?: string | undefined;
110
- /** Stable adapter id (for registry/conformance). Defaults to 'vgai-scene'. */
86
+ /** Stable adapter id (for registry/conformance). Defaults to 'setup-three'. */
111
87
  id?: string | undefined;
112
88
  }
113
89
 
114
90
  /**
115
- * The first-party `MountedGame`, with concrete extras the editor host uses for
91
+ * The first-party `MountedThreeRoot`, with concrete extras the editor host uses for
116
92
  * first-party features (HMR, physics sync) that are NOT part of the neutral
117
- * interface. The generic host only touches the `MountedGame` surface.
93
+ * interface. The generic host only touches the `MountedThreeRoot` surface.
118
94
  */
119
- export interface VgaiMountedGame extends MountedGame {
95
+ export interface MountedSetupThreeRoot extends MountedThreeRoot {
120
96
  /**
121
97
  * First-party brand (checklist item 1 / T7.3 lookahead): a plain `'ctx' in
122
- * mounted` structural check misfires once world2d's `MountedGame2D`
123
- * (`world2d/pixi-game-adapter.ts`) also has a `ctx` key its `ctx` is a
124
- * `World2DContext`, not a `GameContext`, and has none of
125
- * `queryByComponent`/`physics`/`collisions`/`camera`. This literal-`true`
98
+ * mounted` structural check misfires against any foreign mount that happens
99
+ * to carry an unrelated `ctx` key. This literal-`true`
126
100
  * property is what `isFirstPartyMounted` (`runtime/game.ts`) actually
127
101
  * checks; `game.ts` reads it via a structural `{ firstParty?: unknown }`
128
102
  * probe so it never needs a value import of this module (the existing
@@ -136,16 +110,16 @@ export interface VgaiMountedGame extends MountedGame {
136
110
  /** Warm-restart: dispose the current game, re-run a new setup on the same ctx. */
137
111
  hotReload(newSetup: GameSetupFn, editorPreview?: EditorPreview): Promise<void>;
138
112
  /**
139
- * Phase-partitioned frame entry point (T7.1 slice 2,
140
- * `docs/GAME-ROOT-DESIGN.md` §4) wired onto this world's `WorldInstance`
141
- * by `registerThreeWorld` (`create-runtime.ts`). `Game.runFrame` is
142
- * the CANONICAL driver going forward: it calls `frame.runPhase` once per
143
- * (phase, substep) for every world, then `frame.endFrame` once per
144
- * substep after all roots finish all phases. `update` (below) remains
145
- * the legacy/direct entry point to the exact same `SystemRunner` — direct
146
- * callers (editor HMR, adapter conformance tests) keep using it unchanged.
113
+ * Phase-partitioned frame entry point (T7.1 slice 2) — wired onto this
114
+ * world's `RootInstance` by `registerThreeRoot` (`create-runtime.ts`).
115
+ * `Game.runFrame` is the CANONICAL driver going forward: it calls
116
+ * `frame.runPhase` once per (phase, substep) for every world, then
117
+ * `frame.endFrame` once per substep after all roots finish all phases.
118
+ * `update` (below) remains the legacy/direct entry point to the exact same
119
+ * `SystemRunner` direct callers (editor HMR, adapter conformance tests)
120
+ * keep using it unchanged.
147
121
  */
148
- readonly frame: WorldFrameHooks;
122
+ readonly frame: RootFrameHooks;
149
123
  }
150
124
 
151
125
  /** Minimal stand-in for the GPU composer when mounting headlessly. */
@@ -162,28 +136,25 @@ function headlessComposer(renderer: THREE.WebGLRenderer): EffectComposer {
162
136
  }
163
137
 
164
138
  /**
165
- * Wrap a first-party `setup` function as a {@link GameAdapter}. This is how every
139
+ * Wrap a first-party `setup` function as a {@link RootAdapter}. This is how every
166
140
  * first-party example becomes an implementer of the same interface an external
167
141
  * game implements — with no change to the setup's body.
168
142
  */
169
- export function fromSetup(id: string, setup: GameSetupFn): GameAdapter {
170
- return new VgaiSceneGameAdapter({ id, setup });
143
+ export function fromSetup(id: string, setup: GameSetupFn): RootAdapter {
144
+ return new SetupThreeRootAdapter({ id, setup });
171
145
  }
172
146
 
173
- export class VgaiSceneGameAdapter implements GameAdapter {
147
+ export class SetupThreeRootAdapter implements RootAdapter {
174
148
  readonly id: string;
175
- constructor(private readonly config: VgaiSceneConfig = {}) {
176
- this.id = config.id ?? 'vgai-scene';
149
+ constructor(private readonly config: SetupThreeRootConfig = {}) {
150
+ this.id = config.id ?? 'setup-three';
177
151
  }
178
152
 
179
153
  // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one cohesive first-party bootstrap (moved verbatim from create-runtime); splitting it would obscure the ordering contract
180
- async mount(host: HostContext): Promise<VgaiMountedGame> {
154
+ async mount(host: ThreeHostContext): Promise<MountedSetupThreeRoot> {
181
155
  const {
182
156
  setup,
183
157
  editorPreview,
184
- scenePath,
185
- sceneData,
186
- componentRegistry,
187
158
  inputMapPath = '/inputmaps/default.inputmap.json',
188
159
  assetPrefix = '/',
189
160
  } = this.config;
@@ -268,7 +239,7 @@ export class VgaiSceneGameAdapter implements GameAdapter {
268
239
  let viewportShadingMode: ViewportShadingMode = 'solid';
269
240
 
270
241
  // --- System runner (engine-level systems) ---
271
- const systems = createSystemRunner(host.game?.profiler.systemObserver, 'threejs');
242
+ const systems = createSystemRunner(host.game?.profiler.systemObserver, 'three');
272
243
  // D15/T-D15.3/.5: pass the LIVE shared game tick (not a locally-counted
273
244
  // one) into every `poll()` call — `debugRegistry` is declared further
274
245
  // down in this same `mount()` (it needs `ctx`/`systemAdapters` to exist
@@ -384,6 +355,22 @@ export class VgaiSceneGameAdapter implements GameAdapter {
384
355
  // late registrations are visible to its panels at read time.
385
356
  const systemAdapters: SystemAdapters = {};
386
357
 
358
+ // P3 — `ctx.clock`. Same "ONE per Game root, shared across every world"
359
+ // reach-in as `ctx.debug`/`ctx.random` below: `getSimClock(host.game)`
360
+ // returns the clock `createGame` filed, and `runFrameImpl` is what flushes
361
+ // it. A bare mount with no Game shell (headless harness, foreign host) gets
362
+ // a private, mount-local clock so the field is always a real value — but
363
+ // nothing flushes that one, because a mount with no Game has no fixed loop
364
+ // to bind timers to and inventing a wall-clock frontier here is exactly the
365
+ // hazard `core/sim-clock.ts` exists to avoid. Its `disposeAfter` is
366
+ // therefore unreachable; the disposer is still the real one.
367
+ const gameClock: SimClockInternal | null = host.game ? getSimClock(host.game) : null;
368
+ const clock: SimClockInternal =
369
+ gameClock ?? createSimClock({ dispose: (obj) => disposeDebrisSubtree(obj, [ctx]) });
370
+ /** Only a mount-local fallback is THIS mount's to dispose — see the
371
+ * `clock.dispose` teardown step below for why the game-scoped one is not. */
372
+ const ownsClock = gameClock === null;
373
+
387
374
  // --- GameContext (the first-party runtime surface) ---
388
375
  const ctx: GameContext = {
389
376
  scene,
@@ -399,30 +386,33 @@ export class VgaiSceneGameAdapter implements GameAdapter {
399
386
  debugDraw,
400
387
  assets,
401
388
  systems,
402
- components: null!,
389
+ clock,
390
+ // The live scene index for THIS root (P2 `observe`). Created before
391
+ // setup() runs so a game's own objects are indexed as they are added —
392
+ // it subscribes to three's `childadded`/`childremoved` on the scene, so
393
+ // everything the game builds arrives through it. Disposed in
394
+ // disposeGame() below (a warm restart keeps it: same `scene`, same ctx).
395
+ sceneIndex: createSceneIndex(scene),
403
396
  registerSystemAdapter: (kind, adapter) => {
404
397
  systemAdapters[kind] = adapter;
398
+ host.game?.notifySystemAdaptersChanged();
405
399
  },
406
400
  };
407
401
  // Game root (T7.1 slice 1): only present once the host has constructed a
408
402
  // Game shell (createGameRuntime does this before mount; headless/foreign
409
403
  // hosts may omit it — ctx.game/ctx.roots simply stay undefined, which is
410
404
  // the documented zero-break behavior). `ctx.roots` is assigned the SAME
411
- // live array `host.game` mutates via `registerWorld` (not a copy) — the
405
+ // live array `host.game` mutates via `registerRoot` (not a copy) — the
412
406
  // default world is registered AFTER mount returns, and this reference
413
407
  // must observe that later push.
414
408
  if (host.game) {
415
409
  ctx.game = host.game;
416
410
  ctx.roots = host.game.roots;
411
+ ctx.playtest = host.game.playtest ?? null;
417
412
  }
418
- ctx.components = createComponentManager(ctx, physicsRegistry);
419
- collisions.onCollision(
420
- createTriggerDispatch(physicsRegistry, ctx.components, physics.rapierWorld, ctx),
421
- );
422
-
423
- // Everything registered above (engine systems + the ComponentManager's
424
- // per-phase tick, just wired in) is "engine" and must survive a warm
425
- // restart. Everything a game (setup()/scenePath/sceneData, below) adds
413
+ // Everything registered above (the engine systems) is "engine" and must
414
+ // survive a warm
415
+ // restart. Everything a game's setup() (below) adds
426
416
  // from here on is "game" content that `hotReload` bulk-removes via
427
417
  // `systems.removeAllNonEngine()` on every restart (T1.7).
428
418
  systems.markEngineBoundary();
@@ -433,26 +423,44 @@ export class VgaiSceneGameAdapter implements GameAdapter {
433
423
  // always wins over the engine's first-party entry (see
434
424
  // registerSystemAdapter's docstring in runtime/types.ts). `navigation` is
435
425
  // ALSO first-party now (R8 item 7): `createNavigationAdapter` wraps the
436
- // real `NavMeshManager` a scenePath/sceneData scene load may produce (see
437
- // `applySceneNavigation` below) — it is wired once that scene instance
438
426
  // resolves, not here (no NavMeshManager exists yet at this point in the
439
427
  // mount). `networking` remains the one game-owned capability with no
440
428
  // first-party implementer; it simply starts absent here.
441
- systemAdapters.physics = createRapierPhysicsAdapter(physicsRegistry, physics);
442
- if (ownsInput) systemAdapters.input = createInputManagerAdapter(input);
443
- systemAdapters.assets = createVgaiAssetAdapter();
429
+ // P-4: the physics seam is keyed by node id, so the first-party mount
430
+ // supplies the first-party `id → Object3D` map. `userData.entityId` is
431
+ // THIS stack's identity convention (the scene loader stamps it, and the
432
+ // editor's `objectMap`/`hierarchy.object3D` agree on it), which is exactly
433
+ // why the lookup belongs here and not inside `rapier-physics-adapter.ts`.
434
+ // Memoized because the gizmo/inspector ask repeatedly for one selection;
435
+ // a miss (or an object since detached) re-walks once and re-caches.
436
+ const nodeObjectCache = new Map<string, THREE.Object3D>();
437
+ const resolveNodeObject = (nodeId: string): THREE.Object3D | null => {
438
+ const cached = nodeObjectCache.get(nodeId);
439
+ if (cached?.parent) return cached;
440
+ let found: THREE.Object3D | null = null;
441
+ scene.traverse((o) => {
442
+ if (!found && getUserData(o, 'entityId') === nodeId) found = o;
443
+ });
444
+ if (found) nodeObjectCache.set(nodeId, found);
445
+ else nodeObjectCache.delete(nodeId);
446
+ return found;
447
+ };
448
+ systemAdapters.physics = createRapierPhysicsAdapter(
449
+ physicsRegistry,
450
+ physics,
451
+ resolveNodeObject,
452
+ );
444
453
  // D10/T7.6: the audio seam `Game.play.pause()` silences on pause. Works
445
454
  // against the headless stand-in too (`headlessAudio()`'s plain
446
455
  // `masterGain.gain` object) — this world's `pause()` mutes it harmlessly.
447
456
  systemAdapters.audio = createAudioSystemAdapter(audio);
448
- // Debug/synthetic-player seam (T1.1, docs/SYNTHETIC-PLAYER-SPEC.md §3.1):
449
- // ONE registry per Game root, shared across every world that mounts onto
450
- // it `getDebugRegistry(host.game)` reaches the SAME accumulator
451
- // `createGame` filed, so a second/third world's `ctx.debug` feeds it too
452
- // (see debug-registry.ts's module doc for the provenance/id note). A
453
- // mount with no Game shell at all (bare test harnesses predating T7.1)
454
- // gets a private, mount-local registry instead — same absence precedent
455
- // as `ctx.game`/`ctx.roots` above.
457
+ // Debug/synthetic-player seam (T1.1): ONE registry per Game root, shared
458
+ // across every world that mounts onto it — `getDebugRegistry(host.game)`
459
+ // reaches the SAME accumulator `createGame` filed, so a second/third
460
+ // world's `ctx.debug` feeds it too (see debug-registry.ts's module doc
461
+ // for the provenance/id note). A mount with no Game shell at all (bare
462
+ // test harnesses predating T7.1) gets a private, mount-local registry
463
+ // instead — same absence precedent as `ctx.game`/`ctx.roots` above.
456
464
  const debugRegistry: DebugRegistry =
457
465
  (host.game ? getDebugRegistry(host.game) : null) ??
458
466
  createDebugRegistry({ getTick: () => 0, getSimT: () => 0 });
@@ -462,7 +470,7 @@ export class VgaiSceneGameAdapter implements GameAdapter {
462
470
  // below so a warm restart / stop never mistakes it for game content. Only
463
471
  // present when a real WebGL2 context backed the mount (renderDebugWiring).
464
472
  if (renderDebugWiring) systemAdapters.renderDebug = renderDebugWiring.adapter;
465
- ctx.debug = debugRegistry.forWorld(this.id);
473
+ ctx.debug = debugRegistry.forRoot(this.id);
466
474
  // T1.2: the InputManager is a first-party per-world handle the registry
467
475
  // has no other way to reach — wired as a lazy supplier (not a direct
468
476
  // read here), scoped to THIS world's id, so the built-in `input.actions`
@@ -555,7 +563,6 @@ export class VgaiSceneGameAdapter implements GameAdapter {
555
563
  c.traverse((n) => setUserData(n, 'engineInternal', true));
556
564
  }
557
565
  let currentCleanup: GameCleanup | null = null;
558
- let batchSystem: RenderBatchSystem | null = null;
559
566
  let sparkRenderer: SparkRenderer | null = null;
560
567
  let sparkRendererLoading = false;
561
568
  let sparkRendererFailed = false;
@@ -564,109 +571,21 @@ export class VgaiSceneGameAdapter implements GameAdapter {
564
571
  // Start at the threshold so the first rendered frame discovers eager splats.
565
572
  let sparkDiscoveryElapsedMs = SPARK_DISCOVERY_INTERVAL_MS;
566
573
 
567
- // Apply scene-level render settings + transparent auto-batching after a scene
568
- // loads (GPU host only). Gated on the resolved `environment.rendering` cascade
569
- // (defaults preserve prior behavior when a scene omits `rendering`).
570
- const applySceneRendering = async (inst: SceneInstance): Promise<void> => {
571
- if (headless) return;
572
- const scope = inst.environment?.rendering as RenderScope | undefined;
573
- const settings = resolveRenderSettings(scope);
574
- // Always apply the whole authored scene pipeline. Tone mapping is an
575
- // authored setting even when the scene has no post-processing effects.
576
- applySceneRenderPipeline(composer, renderer, scene, camera, inst.environment);
577
- // transparent batching: collapse static/mover meshes sharing a signature
578
- if (settings['autoBatch']) {
579
- batchSystem = new RenderBatchSystem(scene, settings);
580
- batchSystem.build();
581
- ctx.systems.add('preRender', () => batchSystem?.update());
582
- if (import.meta.env?.DEV)
583
- (window as unknown as { __vgaiBatch?: unknown }).__vgaiBatch = batchSystem;
584
- }
585
- if (inst.splatCount > 0 && !sparkRenderer) {
586
- const { SparkRenderer } = await import('@sparkjsdev/spark');
587
- sparkRenderer = new SparkRenderer({ renderer, enableLod: false });
588
- sparkRenderer.traverse((node) => setUserData(node, 'engineInternal', true));
589
- }
590
- };
591
-
592
- // Wire the (first-party) navigation system adapter to whichever
593
- // NavMeshManager the CURRENT scene instance carries — reassigned on every
594
- // scene load, and explicitly cleared (not just left stale) when the new
595
- // scene has none, so `mounted.systems.navigation` never lingers pointing
596
- // at a manager a later teardown step disposes (R8 item 7).
597
- const applySceneNavigation = (inst: SceneInstance): void => {
598
- if (inst.navMesh) {
599
- systemAdapters.navigation = createNavigationAdapter(inst.navMesh);
600
- } else {
601
- delete systemAdapters.navigation;
602
- }
603
- };
604
-
605
- const loadFromData = async (data: SceneFile): Promise<SceneInstance> => {
606
- const inst = await loadSceneFromData(data, {
607
- scene,
608
- rapierWorld: ctx.rapierWorld,
609
- rapier: ctx.rapier,
610
- physics: ctx.physics,
611
- audioListener: ctx.audio.listener,
612
- ...(componentRegistry ? { componentRegistry } : {}),
613
- componentManager: ctx.components,
614
- particleRenderer: ctx.particles.batchedRenderer,
615
- ...(headless ? {} : { renderer }),
616
- });
617
- await applySceneRendering(inst);
618
- applySceneNavigation(inst);
619
- ctx.systems.add('postPhysics', (dt) => inst.update(dt));
620
- return inst;
621
- };
622
-
623
- if (setup) {
624
- if (headless) throw new Error('VgaiSceneGameAdapter: setup-based games require a GPU host');
625
- currentCleanup = await setup(ctx, editorPreview);
626
- } else if (scenePath) {
627
- // The editor viewport is only the fallback/initial play viewpoint. Apply
628
- // it BEFORE capturing the load baseline so authored scene cameras and
629
- // camera GameComponents can override it during/after scene load.
630
- applyEditorCamera(editorPreview, camera);
631
- // #139: captured immediately BEFORE load so adoptSceneCamera can tell
632
- // whether a component (e.g. SceneCamera.init) framed the camera.
633
- const preLoadPose = captureCameraPose(camera);
634
- const inst = await loadScene(scenePath, {
635
- scene,
636
- rapierWorld: ctx.rapierWorld,
637
- rapier: ctx.rapier,
638
- physics: ctx.physics,
639
- audioListener: ctx.audio.listener,
640
- ...(componentRegistry ? { componentRegistry } : {}),
641
- componentManager: ctx.components,
642
- particleRenderer: ctx.particles.batchedRenderer,
643
- ...(headless ? {} : { renderer }),
644
- });
645
- adoptSceneCamera(inst, camera, preLoadPose);
646
- await applySceneRendering(inst);
647
- applySceneNavigation(inst);
648
- ctx.systems.add('postPhysics', (dt) => inst.update(dt));
649
- // A pre-baked navmesh (sibling .navmesh file — see scene-loader.ts) holds
650
- // live recast-navigation WASM handles that leak unless destroyed
651
- // explicitly; dispose it alongside the scene instance on teardown.
652
- currentCleanup = {
653
- dispose: () => {
654
- inst.navMesh?.dispose(scene);
655
- inst.dispose();
656
- },
657
- };
658
- } else if (sceneData) {
659
- applyEditorCamera(editorPreview, camera);
660
- const preLoadPose = captureCameraPose(camera);
661
- const inst = await loadFromData(sceneData);
662
- adoptSceneCamera(inst, camera, preLoadPose);
663
- currentCleanup = {
664
- dispose: () => {
665
- inst.navMesh?.dispose(scene);
666
- inst.dispose();
667
- },
668
- };
574
+ if (!setup) {
575
+ throw new Error(
576
+ 'SetupThreeRootAdapter: no `setup` supplied. The `.vscn.json` scene branches ' +
577
+ '(`scenePath` / `sceneData`) were removed with the format; a first-party three root ' +
578
+ 'is either an imperative ' +
579
+ '`setup(ctx)` (wrap it with `fromSetup(id, setup)`) or a TSX/R3F world root ' +
580
+ '(`entry` pointing at a .tsx module — mounted by the R3F adapter, not this one).',
581
+ );
669
582
  }
583
+ // Headless is legal here: `createSceneView` was skipped above, the composer
584
+ // is a no-op stand-in, and `setup` gets a real `THREE.Scene`/camera. It used
585
+ // to throw because headless existed only for the `.vscn` `sceneData` branch;
586
+ // with the format gone (WO-8) `setup` is the only mount path, and headless
587
+ // unit mounts are what exercise the Game/roots/frame machinery without WebGL.
588
+ currentCleanup = await setup(ctx, editorPreview);
670
589
 
671
590
  // Warm the GPU program cache before the first visible frame. A mount gets
672
591
  // a fresh WebGL context, so without this every scene material compiles
@@ -697,40 +616,16 @@ export class VgaiSceneGameAdapter implements GameAdapter {
697
616
  // adapter.
698
617
  let disposed = false;
699
618
 
700
- // W3c: attach the read-only Audio-debugger introspection capabilities
701
- // (graph/transport/meters/events) onto the SAME first-party audio adapter
702
- // seeded above. Dynamically imported so `tone` stays out of the
703
- // mount-critical chunk (the Colyseus dynamic-import idiom); the editor
704
- // panel polls, so a few-ms-late attachment is invisible. If the game
705
- // replaced `systems.audio` with its OWN adapter during setup(), the
706
- // first-party augmentation is skipped — the game's adapter is the truth
707
- // the editor sees.
708
- let disposeAudioIntrospection: (() => void) | null = null;
709
- const firstPartyAudioAdapter = systemAdapters.audio;
710
- if (firstPartyAudioAdapter) {
711
- void import('../audio/audio-introspection').then(
712
- (m) => {
713
- if (disposed || systemAdapters.audio !== firstPartyAudioAdapter) return;
714
- disposeAudioIntrospection = m.attachAudioIntrospection(firstPartyAudioAdapter, audio);
715
- },
716
- (err) => {
717
- log.audio.warn('VgaiSceneGameAdapter: audio introspection unavailable', {
718
- error: String(err),
719
- });
720
- },
721
- );
722
- }
723
-
724
- // Run one teardown step in isolation: a throwing step (e.g. a
725
- // GameComponent's dispose() misbehaving, or a WASM free() panicking) must
726
- // not abort the remaining steps — otherwise a single bad component leaks
619
+ // Run one teardown step in isolation: a throwing step (e.g. a game's own
620
+ // cleanup misbehaving, or a WASM free() panicking) must
621
+ // not abort the remaining steps otherwise a single bad step leaks
727
622
  // the Rapier world and skips input.dispose() with no way to retry (a
728
623
  // second call is a no-op once `disposed` is set). Logs and continues.
729
624
  const safeStep = (label: string, fn: () => void): void => {
730
625
  try {
731
626
  fn();
732
627
  } catch (err) {
733
- console.error(`VgaiSceneGameAdapter: teardown step "${label}" threw (continuing)`, err);
628
+ console.error(`SetupThreeRootAdapter: teardown step "${label}" threw (continuing)`, err);
734
629
  }
735
630
  };
736
631
 
@@ -741,10 +636,6 @@ export class VgaiSceneGameAdapter implements GameAdapter {
741
636
  if (disposed) return;
742
637
  disposed = true;
743
638
 
744
- safeStep('batchSystem.teardown', () => {
745
- batchSystem?.teardown(); // re-attach detached sources before scene teardown
746
- batchSystem = null;
747
- });
748
639
  safeStep('currentCleanup.dispose', () => {
749
640
  currentCleanup?.dispose();
750
641
  currentCleanup = null;
@@ -769,12 +660,29 @@ export class VgaiSceneGameAdapter implements GameAdapter {
769
660
  );
770
661
  }
771
662
 
772
- // Components BEFORE the physics world is freed/bodies are removed: a
773
- // GameComponent's dispose() may read `this.rigidBody`/`this.collider`
774
- // (see game-component.ts) freeing the world first would make those
775
- // dangling WASM references. Isolated: a throwing component dispose must
776
- // not prevent the world/eventQueue frees or input.dispose() below.
777
- safeStep('components.clear', () => ctx.components.clear());
663
+ // P3 the MOUNT-LOCAL clock only, and only when this mount created one
664
+ // (a bare mount with no Game shell). The game-scoped clock is disposed by
665
+ // `GameInternal.dispose()`, which `create-runtime.ts` runs after every
666
+ // root has torn down because disposing ONE world's mount is a supported
667
+ // way to end a sub-session while the Game keeps running, and destroying a
668
+ // shared clock there froze `now()` and killed the sibling worlds' timers.
669
+ // The asymmetry is the tell: `seededRandom` and `debugRegistry` are
670
+ // game-scoped too, and this teardown destroys neither (it strips only its
671
+ // OWN slice of the registry, a few steps below).
672
+ //
673
+ // Sim timers still go BEFORE the Rapier world they would touch: a pending
674
+ // `disposeAfter`/`after` fires into a live Rapier world, and once that is
675
+ // gone it would operate on freed WASM handles.
676
+ // Disposing cancels every timer and rejects every pending `delay` with an
677
+ // `AbortError` (which awaiting game code is documented to tolerate,
678
+ // exactly like an aborted `fetch`).
679
+ if (ownsClock) safeStep('clock.dispose', () => clock.dispose());
680
+
681
+ // Before the scene is emptied below: dropping the listeners
682
+ // first means the teardown's ~N `scene.remove()` calls do no index
683
+ // bookkeeping, and — the failure mode this exists to prevent — no
684
+ // `childadded` listener is left behind on a disposed scene.
685
+ safeStep('sceneIndex.dispose', () => ctx.sceneIndex.dispose());
778
686
 
779
687
  safeStep('remove rigid bodies', () => {
780
688
  const handles: number[] = [];
@@ -833,12 +741,18 @@ export class VgaiSceneGameAdapter implements GameAdapter {
833
741
  host.game ? debugRegistry.strip(this.id) : debugRegistry.strip(),
834
742
  );
835
743
 
836
- // W3c: detach introspection BEFORE the audio teardown below releases
837
- // Tone transport listeners (the transport singleton outlives this
838
- // world), the context statechange hook, and any meter taps still alive.
839
- safeStep('audio introspection dispose', () => {
840
- disposeAudioIntrospection?.();
841
- disposeAudioIntrospection = null;
744
+ // Release analyser taps BEFORE the audio teardown below. A consumer
745
+ // (the editor's meter poll) should dispose its own handle, but Stop must
746
+ // never leak taps regardless.
747
+ //
748
+ // This used to ride on the Tone introspection module's disposer, which
749
+ // meant a game that never touched Tone leaked them — the module was
750
+ // never loaded, so nothing ever called this. `acquireMeters` belongs to
751
+ // the base adapter (535d248dc), so its release does too, and the world
752
+ // that created the adapter is what disposes it.
753
+ safeStep('audio meters release', () => {
754
+ const audioAdapter = systemAdapters.audio;
755
+ if (audioAdapter) releaseAudioMeters(audioAdapter);
842
756
  });
843
757
 
844
758
  if (!headless) {
@@ -862,35 +776,27 @@ export class VgaiSceneGameAdapter implements GameAdapter {
862
776
  newEditorPreview?: EditorPreview,
863
777
  ): Promise<void> => {
864
778
  if (disposed) {
865
- throw new Error('VgaiSceneGameAdapter: cannot hotReload after dispose()/stop()');
779
+ throw new Error('SetupThreeRootAdapter: cannot hotReload after dispose()/stop()');
866
780
  }
867
781
 
868
782
  // Full parity with disposeGame for the parts a warm restart rebuilds:
869
- // batch-render teardown, game cleanup, component batch teardown (in that
783
+ // game cleanup, component batch teardown (in that
870
784
  // ordering, BEFORE bodies are pulled from the world — see disposeGame),
871
785
  // scene-UI dispose (React root unmount), and bulk system removal. Each
872
786
  // step isolated the same way as disposeGame — one throwing step (e.g. a
873
787
  // component dispose) must not abort the rest of the warm restart.
874
- safeStep('batchSystem.teardown', () => {
875
- batchSystem?.teardown(); // re-attach detached sources before scene teardown
876
- batchSystem = null;
877
- });
878
788
  safeStep('currentCleanup.dispose', () => {
879
789
  currentCleanup?.dispose();
880
790
  currentCleanup = null;
881
791
  });
882
- // Same reasoning as disposeGame: the outgoing scene instance's
883
- // NavMeshManager was just disposed above, and `hotReload`'s `newSetup`
884
- // (below) never auto-wires a replacement (that only happens for THIS
885
- // adapter's own scenePath/sceneData mount, once, at initial mount) — so
886
- // clear the stale wrapper here. A `newSetup` that wants navigation may
792
+ // Clear any navigation adapter a previous `setup` registered: `hotReload`'s
793
+ // `newSetup` (below) never auto-wires a replacement, so leaving the old
794
+ // wrapper would point at a disposed manager. A `newSetup` that wants navigation may
887
795
  // register its own via `ctx.registerSystemAdapter('navigation', ...)`.
888
796
  safeStep('clear navigation adapter', () => {
889
797
  delete systemAdapters.navigation;
890
798
  });
891
799
 
892
- safeStep('components.clear', () => ctx.components.clear());
893
-
894
800
  safeStep('remove rigid bodies', () => {
895
801
  const handles: number[] = [];
896
802
  physics.rapierWorld.forEachRigidBody((b) => handles.push(b.handle));
@@ -954,7 +860,7 @@ export class VgaiSceneGameAdapter implements GameAdapter {
954
860
  };
955
861
 
956
862
  return {
957
- kind: 'threejs',
863
+ kind: 'three',
958
864
  scene,
959
865
  camera,
960
866
  firstParty: true,
@@ -986,92 +892,8 @@ export class VgaiSceneGameAdapter implements GameAdapter {
986
892
  }
987
893
  }
988
894
 
989
- // --- Camera precedence helpers (moved from create-runtime, unchanged) ---
990
-
991
- function applyEditorCamera(
992
- editor: EditorPreview | undefined,
993
- runtimeCamera: THREE.PerspectiveCamera,
994
- ): boolean {
995
- if (!editor?.viewportCamera) return false;
996
- runtimeCamera.position.fromArray(editor.viewportCamera.position);
997
- runtimeCamera.quaternion.fromArray(editor.viewportCamera.quaternion);
998
- return true;
999
- }
1000
-
1001
- /** The pose `adoptSceneCamera` compares against to detect that a component
1002
- * already framed the runtime camera during scene load (#139). Capture with
1003
- * `captureCameraPose(camera)` immediately BEFORE `loadScene`. */
1004
- export interface CameraPose {
1005
- position: THREE.Vector3;
1006
- quaternion: THREE.Quaternion;
1007
- }
1008
-
1009
- export function captureCameraPose(camera: THREE.PerspectiveCamera): CameraPose {
1010
- return { position: camera.position.clone(), quaternion: camera.quaternion.clone() };
1011
- }
1012
-
1013
- export function adoptSceneCamera(
1014
- sceneInstance: SceneInstance,
1015
- runtimeCamera: THREE.PerspectiveCamera,
1016
- preLoadPose?: CameraPose,
1017
- ): boolean {
1018
- const sceneCam = sceneInstance.cameras[0];
1019
- if (!sceneCam) return false;
1020
- // #133 (blind run #4): `camera.type: 'orthographic'` builds a real
1021
- // OrthographicCamera in the scene graph (`createCamera`), but the RUNTIME
1022
- // renders through a hardcoded PerspectiveCamera — the authored projection
1023
- // silently never reached the screen. Until ortho support is ranked
1024
- // (implement-vs-reject, owner call on #133), say so loudly instead of
1025
- // letting a coplanar game render in unexplained perspective.
1026
- if ((sceneCam as THREE.Camera & { isOrthographicCamera?: boolean }).isOrthographicCamera) {
1027
- console.warn(
1028
- "adoptSceneCamera (#133): this scene's camera is ORTHOGRAPHIC, but the runtime camera is " +
1029
- 'perspective-only — the orthographic projection is NOT applied (only the pose is). ' +
1030
- 'Use type: "perspective" (a long-lens fake: far position + small fov) until #133 lands.',
1031
- );
1032
- }
1033
- // #139 (blind run #5): if a GameComponent already positioned/oriented the
1034
- // runtime camera during scene load (`SceneCamera.init`'s
1035
- // `ctx.camera.position.set` + `lookAt` is the blessed shape), the camera
1036
- // ENTITY's transform must not stomp it — an unauthored `transform.rotation`
1037
- // is identity ("stare down −Z"), which silently blanked an entire correct
1038
- // game while every state-based probe spec passed. Components win the pose;
1039
- // the entity still contributes the LENS (fov/near/far below), which
1040
- // `SceneCamera` never sets.
1041
- const componentFramedCamera =
1042
- preLoadPose !== undefined &&
1043
- (!runtimeCamera.position.equals(preLoadPose.position) ||
1044
- !runtimeCamera.quaternion.equals(preLoadPose.quaternion));
1045
- const parent = sceneCam.parent;
1046
- if (parent && !componentFramedCamera) {
1047
- parent.updateMatrixWorld(true);
1048
- runtimeCamera.position.copy(parent.getWorldPosition(new THREE.Vector3()));
1049
- const worldQuat = parent.getWorldQuaternion(new THREE.Quaternion());
1050
- runtimeCamera.quaternion.copy(worldQuat);
1051
- // Identity orientation on an adopted scene camera is almost never intent
1052
- // (it means "look at the horizon down −Z", not "look at my scene") — the
1053
- // exact silent-blank-viewport footgun above. Adopt it anyway (authored
1054
- // data is truth) but say so loudly, with the two blessed remedies.
1055
- if (Math.abs(1 - Math.abs(worldQuat.w)) < 1e-6) {
1056
- console.warn(
1057
- 'adoptSceneCamera (#139): the scene camera entity has no rotation — the runtime camera ' +
1058
- 'will stare horizontally down −Z and may show nothing. Either attach the SceneCamera ' +
1059
- 'component (position + lookAt, no quaternion math) or author transform.rotation on ' +
1060
- 'the camera entity.',
1061
- );
1062
- }
1063
- }
1064
- if (sceneCam instanceof THREE.PerspectiveCamera) {
1065
- runtimeCamera.fov = sceneCam.fov;
1066
- runtimeCamera.near = sceneCam.near;
1067
- runtimeCamera.far = sceneCam.far;
1068
- runtimeCamera.updateProjectionMatrix();
1069
- }
1070
- return true;
1071
- }
1072
-
1073
895
  /** Headless input stand-in — `InputManager` attaches `window` listeners in its
1074
- * constructor, which Node lacks. The scene-data path never reads input. */
896
+ * constructor, which Node lacks. A headless mount never reads input. */
1075
897
  function headlessInput(): InputManager {
1076
898
  return {
1077
899
  poll: () => {},
@@ -1083,8 +905,8 @@ function headlessInput(): InputManager {
1083
905
  } as unknown as InputManager;
1084
906
  }
1085
907
 
1086
- /** Headless audio stand-in (no Web Audio). Only the fields the loader/cleanup
1087
- * touch are present; setup-based games never mount headlessly. */
908
+ /** Headless audio stand-in (no Web Audio). Only the fields the mount/cleanup
909
+ * touch are present. */
1088
910
  function headlessAudio(): GameAudio {
1089
911
  return {
1090
912
  listener: undefined as unknown as THREE.AudioListener,