@vgai/engine 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/README.md +18 -11
  2. package/package.json +10 -6
  3. package/schemas/engine-api.json +1 -68
  4. package/schemas/engine-api.md +1 -32
  5. package/schemas/engine-capabilities.json +30 -42
  6. package/schemas/{vgai-game.schema.json → vgai-project.schema.json} +52 -34
  7. package/src/adapter/adapter-surface.ts +5 -5
  8. package/src/adapter/authoring.ts +168 -226
  9. package/src/adapter/colyseus-networking-adapter.ts +44 -5
  10. package/src/adapter/first-party-systems.ts +156 -42
  11. package/src/adapter/host-context.ts +177 -46
  12. package/src/adapter/index.ts +47 -51
  13. package/src/adapter/ingest/game-contract.ts +2 -2
  14. package/src/adapter/ingest/scene-capture.ts +18 -19
  15. package/src/adapter/ingest/structural-ids.ts +127 -0
  16. package/src/adapter/ingest/upstream-pin.ts +9 -12
  17. package/src/adapter/loop-gate-report.ts +11 -11
  18. package/src/adapter/rapier-physics-adapter.ts +27 -9
  19. package/src/adapter/root-adapter.ts +217 -0
  20. package/src/adapter/{vgai-scene-game-adapter.ts → setup-three-root-adapter.ts} +173 -351
  21. package/src/adapter/system-adapter.ts +80 -63
  22. package/src/ai/navigation.ts +1 -1
  23. package/src/animation/animation-clock.ts +1 -1
  24. package/src/animation/camera-ownership.ts +1 -2
  25. package/src/animation/cubic-spline-interpolant.ts +132 -0
  26. package/src/animation/theatre-clock-binding.ts +2 -2
  27. package/src/animation/theatre-object-binding.ts +4 -4
  28. package/src/animation/xstate-animation-binding.ts +75 -5
  29. package/src/{scene/schema → asset-formats}/camera.ts +2 -5
  30. package/src/{scene/schema → asset-formats}/collider.ts +2 -5
  31. package/src/asset-formats/index.ts +54 -0
  32. package/src/{scene/schema → asset-formats}/instances.ts +10 -6
  33. package/src/{scene/schema → asset-formats}/light.ts +3 -6
  34. package/src/{scene/schema → asset-formats}/material.ts +4 -7
  35. package/src/{scene/schema → asset-formats}/mesh.ts +4 -7
  36. package/src/asset-formats/parse.ts +39 -0
  37. package/src/{scene/schema → asset-formats}/particles.ts +3 -6
  38. package/src/{scene/schema/environment.ts → asset-formats/render-env.ts} +23 -86
  39. package/src/{scene/schema → asset-formats}/tuples.ts +1 -1
  40. package/src/{scene/asset-loaders.ts → asset-loaders.ts} +8 -9
  41. package/src/asset-parse-error.ts +33 -0
  42. package/src/{scene/asset-registry.ts → asset-registry.ts} +1 -1
  43. package/src/assets.ts +1 -1
  44. package/src/audio/wav-encode.ts +9 -9
  45. package/src/canvas-react/engine-bridge.ts +59 -0
  46. package/src/canvas-react/index.ts +50 -0
  47. package/src/canvas-react/pixi-primitive.tsx +202 -0
  48. package/src/canvas-react/pixi-react-adapter.tsx +290 -0
  49. package/src/canvas-react/pixi-react-root-factory.tsx +88 -0
  50. package/src/canvas-react/world-context.ts +328 -0
  51. package/src/core/frame-pacing.ts +100 -0
  52. package/src/core/game-loop.ts +50 -28
  53. package/src/core/seeded-random.ts +7 -7
  54. package/src/core/sim-clock.ts +388 -0
  55. package/src/core/system-runner.ts +17 -63
  56. package/src/core/types.ts +34 -15
  57. package/src/data/data-asset.ts +3 -3
  58. package/src/data/data-check-core.ts +6 -7
  59. package/src/data/data-ref.ts +11 -11
  60. package/src/data/vite-plugin-data.ts +10 -10
  61. package/src/{scene/defaults.ts → defaults.ts} +18 -40
  62. package/src/dev/render-debug-adapter.ts +1 -1
  63. package/src/dev/webgl-frame-capture.ts +1 -1
  64. package/src/ecs/scene-index.ts +439 -0
  65. package/src/ecs/scene-query.ts +43 -0
  66. package/src/{scene → ecs}/user-data.ts +17 -36
  67. package/src/index.ts +7 -9
  68. package/src/input/input-manager.ts +29 -32
  69. package/src/input/input-types.ts +2 -2
  70. package/src/input/schema.ts +5 -5
  71. package/src/loader.ts +57 -0
  72. package/src/manifest/editor-port.ts +69 -0
  73. package/src/manifest/filename.ts +49 -0
  74. package/src/manifest/index.ts +8 -2
  75. package/src/manifest/load-file.ts +11 -0
  76. package/src/manifest/load.ts +65 -77
  77. package/src/manifest/locate.ts +55 -0
  78. package/src/manifest/schema.ts +400 -233
  79. package/src/{scene → physics}/collider-dimensions.ts +3 -3
  80. package/src/physics/physics-registry.ts +1 -1
  81. package/src/{world2d/authoring-2d.ts → pixi/authoring.ts} +24 -11
  82. package/src/pixi/index.ts +43 -0
  83. package/src/{world2d/ingest-iframe-2d.ts → pixi/ingest-iframe.ts} +9 -9
  84. package/src/{world2d/ingest2d.ts → pixi/ingest.ts} +28 -28
  85. package/src/{world2d/physics2d-registry.ts → pixi/physics-registry.ts} +1 -1
  86. package/src/{world2d/scene-capture-2d.ts → pixi/scene-capture.ts} +3 -3
  87. package/src/{world2d/system-adapters-2d.ts → pixi/system-adapters.ts} +2 -2
  88. package/src/react/unmanaged-root-detector.ts +26 -1
  89. package/src/react/use-data.ts +5 -5
  90. package/src/react/use-selection.tsx +15 -42
  91. package/src/react/{game-state.tsx → world-state.tsx} +44 -46
  92. package/src/render/auto-batcher.ts +1 -2
  93. package/src/{scene → render}/instance-mesh.ts +1 -1
  94. package/src/{scene → render}/light-camera-factory.ts +14 -13
  95. package/src/render/lod.ts +17 -0
  96. package/src/{scene → render}/material-factory.ts +8 -7
  97. package/src/{scene → render}/particles-factory.ts +62 -12
  98. package/src/render/render-batch-system.ts +14 -41
  99. package/src/render/render-features.ts +1 -1
  100. package/src/render/render-settings.ts +1 -2
  101. package/src/render/spark-renderer-lifecycle.ts +1 -1
  102. package/src/runtime/create-runtime.ts +352 -463
  103. package/src/runtime/debug-bridge.ts +148 -93
  104. package/src/runtime/debug-registry.ts +79 -54
  105. package/src/runtime/dev-layers.ts +40 -0
  106. package/src/runtime/frame-selector-cache.ts +4 -4
  107. package/src/runtime/game.ts +618 -369
  108. package/src/runtime/gameplay-rng-trap.ts +6 -7
  109. package/src/runtime/input-router.ts +11 -11
  110. package/src/runtime/mount-game.ts +54 -55
  111. package/src/runtime/mount-manifest.ts +154 -150
  112. package/src/runtime/presentation.ts +141 -0
  113. package/src/runtime/render-audio-control.ts +64 -53
  114. package/src/runtime/render-control.ts +45 -51
  115. package/src/runtime/render-seed.ts +3 -4
  116. package/src/runtime/state-bridge.ts +17 -18
  117. package/src/runtime/types.ts +94 -61
  118. package/src/setup/setup-renderer.ts +11 -6
  119. package/src/world3d-react/engine-bridge.ts +46 -33
  120. package/src/world3d-react/index.ts +31 -26
  121. package/src/world3d-react/r3f-adapter.tsx +211 -96
  122. package/src/world3d-react/r3f-root-factory.tsx +91 -0
  123. package/src/world3d-react/renderer-config.ts +137 -0
  124. package/src/world3d-react/world-context.ts +141 -111
  125. package/schemas/entity2d.schema.json +0 -468
  126. package/schemas/prefab.schema.json +0 -9992
  127. package/schemas/scn2d.schema.json +0 -494
  128. package/schemas/vscn.schema.json +0 -10851
  129. package/src/adapter/game-adapter.ts +0 -164
  130. package/src/adapter/ingest/overlay-applier.ts +0 -207
  131. package/src/adapter/ingest/overlay-apply.ts +0 -168
  132. package/src/adapter/ingest/overlay-file.ts +0 -126
  133. package/src/adapter/ingest/overlay-report.ts +0 -176
  134. package/src/animation/gsap-registration.ts +0 -184
  135. package/src/audio/audio-introspection.ts +0 -290
  136. package/src/audio/index.ts +0 -39
  137. package/src/audio/tone-clock-binding.ts +0 -98
  138. package/src/audio/tone-context.ts +0 -175
  139. package/src/audio/tone-offline-render.ts +0 -167
  140. package/src/ecs/component-manager.ts +0 -814
  141. package/src/ecs/game-component.ts +0 -260
  142. package/src/ecs/hmr-swap-report.ts +0 -65
  143. package/src/physics/trigger-dispatch.ts +0 -97
  144. package/src/react/root-adapter.tsx +0 -49
  145. package/src/scene/asset-paths.ts +0 -121
  146. package/src/scene/asset-ref-check.ts +0 -248
  147. package/src/scene/component-registry.ts +0 -51
  148. package/src/scene/parse.ts +0 -204
  149. package/src/scene/scene-apply.ts +0 -407
  150. package/src/scene/scene-diff-schema.ts +0 -115
  151. package/src/scene/scene-diff-types.ts +0 -29
  152. package/src/scene/scene-loader.ts +0 -1526
  153. package/src/scene/scene-query.ts +0 -63
  154. package/src/scene/scene-types.ts +0 -33
  155. package/src/scene/scene-version.ts +0 -40
  156. package/src/scene/schema/animation.ts +0 -46
  157. package/src/scene/schema/audio.ts +0 -25
  158. package/src/scene/schema/entity-ref.ts +0 -78
  159. package/src/scene/schema/entity.ts +0 -189
  160. package/src/scene/schema/index.ts +0 -51
  161. package/src/scene/schema/joint.ts +0 -26
  162. package/src/scene/schema/physics.ts +0 -49
  163. package/src/scene/schema/scene-file.ts +0 -292
  164. package/src/scene/schema/shadow.ts +0 -24
  165. package/src/scene/schema/spline.ts +0 -21
  166. package/src/world2d/asset-paths2d.ts +0 -44
  167. package/src/world2d/capture-to-scene2d.ts +0 -52
  168. package/src/world2d/collision-2d.ts +0 -99
  169. package/src/world2d/entity2d-asset.ts +0 -22
  170. package/src/world2d/index.ts +0 -91
  171. package/src/world2d/physics2d-transform.ts +0 -173
  172. package/src/world2d/physics2d-units.ts +0 -10
  173. package/src/world2d/pixi-game-adapter.ts +0 -439
  174. package/src/world2d/pixi-surface.ts +0 -78
  175. package/src/world2d/scene2d-identity.ts +0 -49
  176. package/src/world2d/scene2d-loader.ts +0 -433
  177. package/src/world2d/schema/entity2d.ts +0 -163
  178. package/src/world2d/schema/physics2d.ts +0 -64
  179. package/src/world2d/schema/sprite.ts +0 -99
  180. package/src/world2d/schema/tilemap.ts +0 -39
  181. package/src/world2d/schema/tuples2d.ts +0 -25
  182. package/src/world2d/transform-writer-2d.ts +0 -42
  183. package/src/world2d/types.ts +0 -74
  184. package/src/world3d-react/behavior.tsx +0 -146
  185. /package/src/{scene → render}/mesh-shadow.ts +0 -0
@@ -1,8 +1,7 @@
1
- // D-Z2 (docs/WAVE3-ADAPTER-PLUMBING-DESIGN.md) the standalone manifest-mount
2
- // helper: an ENGINE-side generalization of `examples/tri-world/src/main.ts`'s
3
- // hand-built `WorldMountSpec[]`.
1
+ // The standalone manifest-mount helper: an ENGINE-side generalization of
2
+ // `examples/tri-world/src/main.ts`'s hand-built `RootMountSpec[]`.
4
3
  //
5
- // `resolveAllWorlds`/`resolveRootAdapter` (`packages/editor/src/
4
+ // `resolveAllRoots`/`resolveRootAdapter` (`packages/editor/src/
6
5
  // adapter-resolver.ts`) already do this translation for the EDITOR, but they
7
6
  // are Vite-coupled by construction (`/@fs/` dynamic imports of project files
8
7
  // through the dev server, §0.1/§0.5 of the design doc) — unusable from a
@@ -14,106 +13,92 @@
14
13
  // dev server, no editor import.
15
14
  //
16
15
  // Engine-core react/pixi-free discipline (§0.6, mirrored from
17
- // `create-runtime.ts`'s own documented rule for its `Pixi2DGameAdapter`/
18
- // `ReactRootAdapter` type-only imports): this file never value-imports
19
- // `pixi.js`, `world2d/pixi-game-adapter.ts`, `react`, or `react-dom` — a
20
- // pixijs or react world's adapter is ALWAYS supplied already-constructed via
21
- // `entries[id].adapter` (the caller's own module graph built it, exactly like
22
- // `tri-world`'s original hand-mount built `new PixiSceneGameAdapter(...)` and
23
- // a hand-rolled `ReactRootAdapter` itself). Threejs is different: `three`/
24
- // `VgaiSceneGameAdapter` are already unconditional dependencies of every
16
+ // `create-runtime.ts`'s own documented rule for its `ReactRootAdapter`
17
+ // type-only import): this file never value-imports
18
+ // `pixi.js`, `react`, or `react-dom` — a
19
+ // canvas or react world's adapter is ALWAYS supplied already-constructed via
20
+ // `entries[id].adapter` (the caller's own module graph built it). Threejs is
21
+ // different: `three`/
22
+ // `SetupThreeRootAdapter` are already unconditional dependencies of every
25
23
  // caller of `createGameRuntime`'s legacy path (this same file's sibling,
26
- // immediately below in this directory), so building a `VgaiSceneGameAdapter`
27
- // here for the common scene-driven/`setup`-driven cases adds no NEW bundle
28
- // weight — only pixi/react are avoided.
24
+ // immediately below in this directory), so wrapping a `setup` with
25
+ // `fromSetup` here adds no NEW bundle weight only pixi/react are avoided.
29
26
 
30
- import { fromSetup, VgaiSceneGameAdapter } from '../adapter';
31
27
  import { assertNever } from '../adapter/adapter-surface';
32
- import type { GameAdapter } from '../adapter/game-adapter';
28
+ import type { RootAdapter } from '../adapter/root-adapter';
29
+ // Value-imported from the implementer's OWN path, not the type-only
30
+ // `../adapter` barrel (P-6).
31
+ import { fromSetup } from '../adapter/setup-three-root-adapter';
33
32
  import {
34
33
  loadGameManifest,
35
34
  type ResolvedAdapterRoot,
36
35
  type ResolvedGameManifest,
37
36
  } from '../manifest/load';
38
- import type { ComponentRegistry } from '../scene/component-registry';
39
- import {
40
- createGameRuntime,
41
- type GameSession,
42
- type Pixi2DGameAdapter,
43
- type ReactRootAdapter,
44
- type WorldMountSpec,
45
- } from './create-runtime';
37
+ import { createGameRuntime, type GameSession, type RootMountSpec } from './create-runtime';
46
38
  import { type DebugBridgeWindowTarget, maybeInstallDebugBridge } from './debug-bridge';
47
39
  import { getDebugRegistry } from './debug-registry';
40
+ import { devLayersEnabled } from './dev-layers';
48
41
  import { getGameplayRngTrapControl } from './gameplay-rng-trap';
49
42
  import { RENDER_SEED_QUERY_PARAM } from './render-seed';
50
- import type { GameSetupFn } from './types';
43
+ import type { GameSetupFn, PlaytestContext } from './types';
51
44
 
52
45
  // ---------------------------------------------------------------------------
53
46
  // The `entries` contract
54
47
  // ---------------------------------------------------------------------------
55
48
 
56
49
  /**
57
- * A caller-supplied entry for a `kind: 'threejs'` world — three ways to
58
- * satisfy it, matching exactly what `tri-world`'s manifest actually declares
59
- * (a scene-driven world with a project component registry) plus the two
60
- * other shapes `resolveDefaultThreeAdapter` supports (an `entry`-module
61
- * `setup`, or full caller control via a pre-built adapter):
50
+ * A caller-supplied entry for a `kind: 'three'` world — two ways to satisfy
51
+ * it, matching the two shapes `resolveDefaultThreeAdapter` supports (an
52
+ * `entry`-module `setup`, or full caller control via a pre-built adapter):
62
53
  *
63
- * - `{ adapter }` — a fully-constructed {@link GameAdapter} (first-party or
64
- * not). Always wins if present, regardless of the world's `scene`/`entry`
65
- * fields — full caller control, including for a `{ module }`-adapter world
66
- * (mountManifestWorlds cannot import an arbitrary module path itself).
54
+ * - `{ adapter }` — a fully-constructed {@link RootAdapter} (first-party or
55
+ * not). Always wins if present, regardless of the world's `entry` field —
56
+ * full caller control, including for a `{ module }`-adapter world
57
+ * (mountManifestRoots cannot import an arbitrary module path itself).
67
58
  * - `{ setup }` — for an `entry`-declaring world: the project's own
68
59
  * `entry` module's exported `setup` function (the same `GameSetupFn`
69
60
  * shape `loadProjectScripts` extracts editor-side). Wrapped in a
70
- * `VgaiSceneGameAdapter` here, mirroring `resolveDefaultThreeAdapter`'s
61
+ * `SetupThreeRootAdapter` here, mirroring `resolveDefaultThreeAdapter`'s
71
62
  * entry branch exactly.
72
- * - `{ componentRegistry }` (or nothing at all) — for a `scene`-declaring
73
- * world: mirrors `resolveDefaultThreeAdapter`'s scene branch, fetching
74
- * `world.scene` itself via `VgaiSceneGameAdapter`'s own `scenePath`
75
- * support. `componentRegistry` is optional — omit it if the scene
76
- * authors no named `components:` entries.
63
+ *
64
+ * The former third shape, `{ componentRegistry }` for a `scene`-declaring
65
+ * world, went with the `.vscn.json` format in WO-8.
77
66
  */
78
67
  export interface ThreeMountEntry {
79
- readonly kind: 'threejs';
80
- readonly adapter?: GameAdapter | undefined;
68
+ readonly kind: 'three';
69
+ readonly adapter?: RootAdapter | undefined;
81
70
  readonly setup?: GameSetupFn | undefined;
82
- readonly componentRegistry?: ComponentRegistry | undefined;
83
71
  }
84
72
 
85
73
  /**
86
- * A caller-supplied entry for a `kind: 'pixijs'` world — ALWAYS a
87
- * fully-constructed {@link Pixi2DGameAdapter} (e.g. `new
88
- * PixiSceneGameAdapter({ id, sceneData })`, built exactly like `tri-world`'s
89
- * original hand-mount built it — fetch+parse the `.scn2d.json` yourself,
90
- * mirroring `resolveDefaultPixiAdapter`'s `fetchScene2DFile` helper, if the
91
- * world declares `scene`). There is no "mountManifestWorlds builds it for
74
+ * A caller-supplied entry for a `kind: 'canvas'` world — ALWAYS a
75
+ * fully-constructed `RootAdapter<'canvas'>`, built by the caller's own module
76
+ * graph. There is no "mountManifestRoots builds it for
92
77
  * you" branch here — doing so would require this file to value-import
93
78
  * `pixi.js`, which it deliberately never does (see this file's header
94
79
  * comment).
95
80
  */
96
81
  export interface PixiMountEntry {
97
- readonly kind: 'pixijs';
98
- readonly adapter: Pixi2DGameAdapter;
82
+ readonly kind: 'canvas';
83
+ readonly adapter: RootAdapter<'canvas'>;
99
84
  }
100
85
 
101
86
  /**
102
- * A caller-supplied entry for a `kind: 'react'` world — ALWAYS a
103
- * fully-constructed {@link ReactRootAdapter} (the caller's own module graph
104
- * calls `createRoot(host.container).render(<GameProvider game={host.game}>
105
- * <Entry/></GameProvider>)` itself, exactly like `tri-world`'s original
87
+ * A caller-supplied entry for a `kind: 'dom'` world — ALWAYS a
88
+ * fully-constructed `RootAdapter<'dom'>` (the caller's own module graph
89
+ * calls `createRoot(host.container).render(<WorldProvider game={host.game}>
90
+ * <Entry/></WorldProvider>)` itself, exactly like `tri-world`'s original
106
91
  * hand-mount did). A standalone build has ONE module graph, so there is no
107
- * context-identity hazard the editor's per-project `GameProvider`
108
- * indirection exists to solve (`docs/REACT-STATE-BRIDGE.md` §2) the
109
- * caller's own `<GameProvider>` import is already the single canonical
110
- * instance its `Entry` component's own hooks resolve against. This file
111
- * never value-imports `react`/`react-dom` itself (see this file's header
112
- * comment) — building the adapter is entirely the caller's job.
92
+ * context-identity hazard the editor's per-project `WorldProvider`
93
+ * indirection exists to solve the caller's own `<WorldProvider>` import
94
+ * is already the single canonical instance its `Entry` component's own
95
+ * hooks resolve against. This file never value-imports `react`/`react-dom`
96
+ * itself (see this file's header comment) — building the adapter is
97
+ * entirely the caller's job.
113
98
  */
114
99
  export interface ReactMountEntry {
115
- readonly kind: 'react';
116
- readonly adapter: ReactRootAdapter;
100
+ readonly kind: 'dom';
101
+ readonly adapter: RootAdapter<'dom'>;
117
102
  }
118
103
 
119
104
  export type MountEntry = ThreeMountEntry | PixiMountEntry | ReactMountEntry;
@@ -125,7 +110,7 @@ export type MountEntry = ThreeMountEntry | PixiMountEntry | ReactMountEntry;
125
110
  export interface MountManifestOptions {
126
111
  /**
127
112
  * A raw (unparsed) manifest value OR an already-`loadGameManifest`d
128
- * {@link ResolvedGameManifest}. `mountManifestWorlds` NEVER fetches or
113
+ * {@link ResolvedGameManifest}. `mountManifestRoots` NEVER fetches or
129
114
  * fs-reads the manifest itself (stays environment-neutral — Node CLI,
130
115
  * browser, or headless test alike): the caller owns getting the JSON off
131
116
  * disk/network (`loadGameManifestFile` for Node, a plain `fetch()` for a
@@ -134,28 +119,26 @@ export interface MountManifestOptions {
134
119
  */
135
120
  readonly manifest: unknown;
136
121
  /** The host creates one absolutely-positioned surface per world inside
137
- * this element — see `WorldsRuntimeConfig.container`. */
122
+ * this element — see `RootsRuntimeConfig.container`. */
138
123
  readonly container: HTMLElement;
139
124
  /** Caller-supplied, already-imported entries keyed by manifest world id.
140
125
  * Optional overall — a manifest whose every world is scene-driven
141
- * threejs with no custom component registry needs none at all. */
126
+ * three with no custom component registry needs none at all. */
142
127
  readonly entries?: Readonly<Record<string, MountEntry>> | undefined;
143
128
  /** Defaults to the manifest's own `resolution` field if declared, else
144
- * the container's own size (`WorldsRuntimeConfig`'s existing default). */
129
+ * the container's own size (`RootsRuntimeConfig`'s existing default). */
145
130
  readonly width?: number | undefined;
146
131
  readonly height?: number | undefined;
147
132
  /** Forwarded to `createGameRuntime` — Node/headless test harnesses only,
148
- * never a real host. See `WorldsRuntimeConfig.headless`. */
133
+ * never a real host. See `RootsRuntimeConfig.headless`. */
149
134
  readonly headless?: boolean | undefined;
150
135
  /**
151
- * Task 2.1 (`docs/E2E-TESTING-BUILD-PLAN.md`,
152
- * `docs/SYNTHETIC-PLAYER-SPEC.md` §3.4): overrides for the `?vgai-debug=1`
153
- * bridge `mountManifestWorlds` installs at the tail of every mount the
154
- * engine-owned install point so every standalone project gets it with zero
155
- * template edits. Omit both in a real host (defaults to `window.location`/
156
- * `window`); a headless test supplies fakes here instead of touching the
157
- * global object, mirroring `render-control.ts`'s own `location`/`target`
158
- * override precedent.
136
+ * Task 2.1: overrides for the `?vgai-debug=1` bridge `mountManifestRoots`
137
+ * installs at the tail of every mount — the engine-owned install point so
138
+ * every standalone project gets it with zero template edits. Omit both in
139
+ * a real host (defaults to `window.location`/ `window`); a headless test
140
+ * supplies fakes here instead of touching the global object, mirroring
141
+ * `render-control.ts`'s own `location`/`target` override precedent.
159
142
  */
160
143
  readonly debugBridge?:
161
144
  | {
@@ -172,6 +155,17 @@ export interface MountManifestOptions {
172
155
  * `determinism.seededRandom` — see `resolveDeterminismSeed` below.
173
156
  */
174
157
  readonly seed?: number | undefined;
158
+ /** Host identity for a private play run or coordinated Team Test. */
159
+ 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;
175
169
  /** Where to read `?vgai-seed=` from for the boot-time seed reader. Same
176
170
  * override precedent as `debugBridge.url` (defaults to `window.location`
177
171
  * when a real `window` exists; a headless caller with no override gets no
@@ -181,7 +175,7 @@ export interface MountManifestOptions {
181
175
 
182
176
  /**
183
177
  * D15 (T-D15.1) boot-time seed reader — resolves the root seed
184
- * `mountManifestWorlds` threads into `createGameRuntime` (and therefore
178
+ * `mountManifestRoots` threads into `createGameRuntime` (and therefore
185
179
  * `createGame`, BEFORE any world's `mount()`/`setup()` runs). Returns
186
180
  * `undefined` when the manifest doesn't declare `determinism.seededRandom`
187
181
  * at all — `?vgai-seed=` and `defaultSeed` are both ignored in that case
@@ -214,7 +208,7 @@ function hasRealWindow(): boolean {
214
208
  return typeof window !== 'undefined';
215
209
  }
216
210
 
217
- // `mountManifestWorlds` returns the SAME `GameSession` shape
211
+ // `mountManifestRoots` returns the SAME `GameSession` shape
218
212
  // `createGameRuntime` returns (D-Z2: "the same session shape... or a thin
219
213
  // superset") — callers keep exactly one teardown path (`session.stop()`)
220
214
  // whether they mounted through `createGameRuntime` directly or through this
@@ -250,10 +244,9 @@ function looksAlreadyResolved(value: unknown): value is ResolvedGameManifest {
250
244
  }
251
245
 
252
246
  /**
253
- * Exported (E4, docs/unified-world-editor/27-visual-react-editing.md §7) so
254
- * `mount-game.ts`'s `mountGameFromManifest` composer can share this exact
255
- * raw-vs-resolved detection instead of re-implementing it the two modules
256
- * must always agree on what "already resolved" means.
247
+ * Exported (E4) so `mount-game.ts`'s `mountGameFromManifest` composer can
248
+ * share this exact raw-vs-resolved detection instead of re-implementing it
249
+ * the two modules must always agree on what "already resolved" means.
257
250
  */
258
251
  export function resolveManifest(raw: unknown): ResolvedGameManifest {
259
252
  if (looksAlreadyResolved(raw)) return raw;
@@ -268,24 +261,23 @@ export function resolveManifest(raw: unknown): ResolvedGameManifest {
268
261
  function resolveThreeAdapter(
269
262
  world: ResolvedAdapterRoot,
270
263
  entry: ThreeMountEntry | undefined,
271
- ): GameAdapter {
264
+ ): RootAdapter {
272
265
  if (entry?.adapter) return entry.adapter;
273
266
 
274
267
  if (world.adapter.type === 'module') {
275
268
  throw new Error(
276
- `mountManifestWorlds: world "${world.id}" (threejs) declares a { module } adapter ` +
277
- `("${world.adapter.module}") — mountManifestWorlds never imports an arbitrary module ` +
269
+ `mountManifestRoots: world "${world.id}" (three) declares a { module } adapter ` +
270
+ `("${world.adapter.module}") — mountManifestRoots never imports an arbitrary module ` +
278
271
  'path itself (no `/@fs/`, no dev server). Import the module yourself and supply the ' +
279
- `constructed adapter via entries["${world.id}"] = { kind: 'threejs', adapter }.`,
272
+ `constructed adapter via entries["${world.id}"] = { kind: 'three', adapter }.`,
280
273
  );
281
274
  }
282
275
  if (world.adapter.type === 'ingest') {
283
276
  throw new Error(
284
- `mountManifestWorlds: world "${world.id}" (threejs) declares an { ingest } adapter — ` +
277
+ `mountManifestRoots: world "${world.id}" (three) declares an { ingest } adapter — ` +
285
278
  "ingest roots require the editor's dev-server-backed mount machinery (an EditorStore " +
286
279
  "plus iframe/DOM capture, see adapter-resolver.ts's resolveIngestThreeAdapter) and are " +
287
- 'not supported by mountManifestWorlds (docs/WAVE3-ADAPTER-PLUMBING-DESIGN.md D-Z7 no ' +
288
- 'porting aids, no hosted ingest routes).',
280
+ 'not supported by mountManifestRoots (no porting aids, no hosted ingest routes).',
289
281
  );
290
282
  }
291
283
 
@@ -293,40 +285,33 @@ function resolveThreeAdapter(
293
285
  if (world.entry !== undefined) {
294
286
  if (!entry?.setup) {
295
287
  throw new Error(
296
- `mountManifestWorlds: world "${world.id}" (threejs) declares \`entry\` "${world.entry}" ` +
297
- `— supply entries["${world.id}"] = { kind: 'threejs', setup } (the entry module's ` +
298
- "exported `setup` function) or { kind: 'threejs', adapter } for full control. Got " +
288
+ `mountManifestRoots: world "${world.id}" (three) declares \`entry\` "${world.entry}" ` +
289
+ `— supply entries["${world.id}"] = { kind: 'three', setup } (the entry module's ` +
290
+ "exported `setup` function) or { kind: 'three', adapter } for full control. Got " +
299
291
  `${entry === undefined ? 'no entry at all' : 'an entry with neither `setup` nor `adapter`'}.`,
300
292
  );
301
293
  }
302
294
  return fromSetup(world.id, entry.setup);
303
295
  }
304
- if (world.scene !== undefined) {
305
- return new VgaiSceneGameAdapter({
306
- id: world.id,
307
- scenePath: world.scene,
308
- componentRegistry: entry?.componentRegistry,
309
- });
310
- }
311
- // Unreachable in practice: `load.ts`'s `checkAdapterSceneEntryRules`
312
- // requires `scene` or `entry` for every 'default' adapter world at
313
- // manifest-load time — see `resolveDefaultThreeAdapter`'s identical guard.
296
+ // Unreachable in practice: `load.ts`'s `checkAdapterSceneEntryRules` requires
297
+ // `entry` for every 'default' THREE adapter world at manifest-load time (and
298
+ // rejects `scene` outright — the `.vscn.json` format was removed in WO-8).
314
299
  throw new Error(
315
- `mountManifestWorlds: world "${world.id}" is default-three with neither \`scene\` nor ` +
316
- '`entry` — this should have been rejected by manifest validation.',
300
+ `mountManifestRoots: world "${world.id}" is default-three with no \`entry\` this ` +
301
+ 'should have been rejected by manifest validation.',
317
302
  );
318
303
  }
319
304
 
320
305
  function resolvePixiAdapter(
321
306
  world: ResolvedAdapterRoot,
322
307
  entry: PixiMountEntry | undefined,
323
- ): Pixi2DGameAdapter {
308
+ ): RootAdapter<'canvas'> {
324
309
  if (entry?.adapter) return entry.adapter;
325
310
  throw new Error(
326
- `mountManifestWorlds: world "${world.id}" (pixijs) has no entries["${world.id}"] — a pixijs ` +
311
+ `mountManifestRoots: world "${world.id}" (canvas) has no entries["${world.id}"] — a canvas ` +
327
312
  'world always needs a caller-supplied, already-constructed adapter ' +
328
- `(entries["${world.id}"] = { kind: 'pixijs', adapter }, e.g. \`new PixiSceneGameAdapter(...)\` ` +
329
- "built from your own already-imported '@engine/world2d') — mountManifestWorlds never " +
313
+ `(entries["${world.id}"] = { kind: 'canvas', adapter }, constructed from your own ` +
314
+ 'already-imported pixi module graph) — mountManifestRoots never ' +
330
315
  "value-imports pixi.js (mirrors create-runtime.ts's own pixi-free-core discipline).",
331
316
  );
332
317
  }
@@ -334,25 +319,24 @@ function resolvePixiAdapter(
334
319
  function resolveReactAdapter(
335
320
  world: ResolvedAdapterRoot,
336
321
  entry: ReactMountEntry | undefined,
337
- ): ReactRootAdapter {
322
+ ): RootAdapter<'dom'> {
338
323
  if (entry?.adapter) return entry.adapter;
339
324
  throw new Error(
340
- `mountManifestWorlds: world "${world.id}" (react) has no entries["${world.id}"] — a react ` +
341
- 'world always needs a caller-supplied, already-constructed `ReactRootAdapter` ' +
342
- `(entries["${world.id}"] = { kind: 'react', adapter }, mounting via your own already-imported ` +
343
- 'react-dom `createRoot` + your own `<GameProvider>`) — mountManifestWorlds never ' +
344
- "value-imports react/react-dom (mirrors create-runtime.ts's own react-free-core discipline; " +
345
- 'see docs/REACT-STATE-BRIDGE.md §2-3).',
325
+ `mountManifestRoots: world "${world.id}" (react) has no entries["${world.id}"] — a react ` +
326
+ "world always needs a caller-supplied, already-constructed `RootAdapter<'dom'>` " +
327
+ `(entries["${world.id}"] = { kind: 'dom', adapter }, mounting via your own already-imported ` +
328
+ 'react-dom `createRoot` + your own `<WorldProvider>`) — mountManifestRoots never ' +
329
+ "value-imports react/react-dom (mirrors create-runtime.ts's own react-free-core discipline).",
346
330
  );
347
331
  }
348
332
 
349
- function buildWorldMountSpec(
333
+ function buildRootMountSpec(
350
334
  world: ResolvedAdapterRoot,
351
335
  entry: MountEntry | undefined,
352
- ): WorldMountSpec {
336
+ ): RootMountSpec {
353
337
  if (entry !== undefined && entry.kind !== world.surface) {
354
338
  throw new Error(
355
- `mountManifestWorlds: entries["${world.id}"] declares kind "${entry.kind}" but the ` +
339
+ `mountManifestRoots: entries["${world.id}"] declares kind "${entry.kind}" but the ` +
356
340
  `manifest's root "${world.id}" uses surface "${world.surface}" — fix the entries key (or the ` +
357
341
  'manifest) so the two agree.',
358
342
  );
@@ -362,6 +346,7 @@ function buildWorldMountSpec(
362
346
  id: world.id,
363
347
  zOrder: world.zOrder,
364
348
  pausable: world.pausable,
349
+ dev: world.dev,
365
350
  loop: world.loop,
366
351
  };
367
352
 
@@ -369,23 +354,23 @@ function buildWorldMountSpec(
369
354
  // above (or `entry` is `undefined`) — TS can't narrow a `Record` lookup
370
355
  // through that runtime check, so each branch casts to its own entry shape;
371
356
  // the actual safety comes from the mismatch guard, not from the cast.
372
- if (world.surface === 'threejs') {
357
+ if (world.surface === 'three') {
373
358
  const threeEntry = entry as ThreeMountEntry | undefined;
374
- return { ...base, kind: 'threejs', adapter: resolveThreeAdapter(world, threeEntry) };
359
+ return { ...base, kind: 'three', adapter: resolveThreeAdapter(world, threeEntry) };
375
360
  }
376
- if (world.surface === 'pixijs') {
361
+ if (world.surface === 'canvas') {
377
362
  const pixiEntry = entry as PixiMountEntry | undefined;
378
- return { ...base, kind: 'pixijs', adapter: resolvePixiAdapter(world, pixiEntry) };
363
+ return { ...base, kind: 'canvas', adapter: resolvePixiAdapter(world, pixiEntry) };
379
364
  }
380
- if (world.surface === 'react') {
365
+ if (world.surface === 'dom') {
381
366
  const reactEntry = entry as ReactMountEntry | undefined;
382
- return { ...base, kind: 'react', adapter: resolveReactAdapter(world, reactEntry) };
367
+ return { ...base, kind: 'dom', adapter: resolveReactAdapter(world, reactEntry) };
383
368
  }
384
- // Exhaustiveness guard (§7.4-2, same idiom as `resolveAllWorlds`'s own
369
+ // Exhaustiveness guard (§7.4-2, same idiom as `resolveAllRoots`'s own
385
370
  // dispatch loop): `world.kind` is the closed `AdapterRoot['kind']` union
386
- // (`z.enum(['threejs','pixijs','react'])`), so a hypothetical 4th kind
371
+ // (`z.enum(['three','canvas','react'])`), so a hypothetical 4th kind
387
372
  // must fail to compile here, not silently fall through.
388
- return assertNever(world.surface, 'mountManifestWorlds');
373
+ return assertNever(world.surface, 'mountManifestRoots');
389
374
  }
390
375
 
391
376
  // ---------------------------------------------------------------------------
@@ -393,31 +378,49 @@ function buildWorldMountSpec(
393
378
  // ---------------------------------------------------------------------------
394
379
 
395
380
  /**
396
- * Mount every world declared by a `vgai.game.json` manifest onto `container`,
381
+ * Mount every world declared by a `vgai.project.json` manifest onto `container`,
397
382
  * standalone — no editor, no dev server. The generalization of
398
- * `examples/tri-world/src/main.ts`'s hand-built `WorldMountSpec[]` (D-Z2,
399
- * docs/WAVE3-ADAPTER-PLUMBING-DESIGN.md). Reuses the pure `loadGameManifest`
400
- * + `createGameRuntime({ roots })` and mirrors `resolveAllWorlds`'s per-kind
401
- * dispatch but every entry the manifest needs beyond what a `default`-
402
- * adapter scene/entry can express on its own comes from the caller's OWN
403
- * already-imported module graph (`opts.entries`), never a dynamic import
404
- * this file performs itself.
383
+ * `examples/tri-world/src/main.ts`'s hand-built `RootMountSpec[]` (D-Z2). Reuses
384
+ * the pure `loadGameManifest` + `createGameRuntime({ roots })` and mirrors
385
+ * `resolveAllRoots`'s per-kind dispatch — but every entry the manifest needs
386
+ * beyond what a `default`- adapter scene/entry can express on its own comes from
387
+ * the caller's OWN already-imported module graph (`opts.entries`), never a
388
+ * dynamic import this file performs itself.
405
389
  *
406
- * Degrades loudly (docs/WAVE3-ADAPTER-PLUMBING-DESIGN.md D-Z2): a missing
407
- * entry, a kind/shape mismatch, a `{ module }`/`{ ingest }` adapter with no
408
- * caller-supplied adapter, or an empty manifest all throw a named `Error`
409
- * identifying the world id, its kind, and what was expected — never a silent
410
- * skip or a partially-mounted session.
390
+ * Degrades loudly: a missing entry, a kind/shape mismatch, a `{ module }`/`{
391
+ * ingest }` adapter with no caller-supplied adapter, or an empty manifest all
392
+ * throw a named `Error` identifying the world id, its kind, and what was
393
+ * 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.
411
398
  */
412
- export async function mountManifestWorlds(opts: MountManifestOptions): Promise<GameSession> {
399
+ export async function mountManifestRoots(opts: MountManifestOptions): Promise<GameSession> {
413
400
  const manifest = resolveManifest(opts.manifest);
414
401
  if (manifest.roots.length === 0) {
415
- throw new Error('mountManifestWorlds: manifest declares no roots — nothing to mount.');
402
+ throw new Error('mountManifestRoots: manifest declares no roots — nothing to mount.');
416
403
  }
417
404
  const entries = opts.entries ?? {};
418
405
 
419
- const roots: WorldMountSpec[] = manifest.roots.map((world) =>
420
- buildWorldMountSpec(world, entries[world.id]),
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) =>
423
+ buildRootMountSpec(world, entries[world.id]),
421
424
  );
422
425
 
423
426
  // D15 (T-D15.1) — resolved BEFORE `createGameRuntime` (which constructs the
@@ -440,6 +443,7 @@ export async function mountManifestWorlds(opts: MountManifestOptions): Promise<G
440
443
  height: opts.height ?? manifest.resolution?.height,
441
444
  headless: opts.headless,
442
445
  seed: resolvedSeed,
446
+ playtest: opts.playtest,
443
447
  });
444
448
 
445
449
  // D15 (T-D15.3) — the dev-mode Math.random phase trap: only while the
@@ -462,11 +466,11 @@ export async function mountManifestWorlds(opts: MountManifestOptions): Promise<G
462
466
  if (debugRegistry) {
463
467
  // Defect 3 fix: `setRoomDeclared` (the locus-required rule — a project
464
468
  // with a Colyseus room must declare `locus: 'client' | 'server'` on every
465
- // debug command, docs/SYNTHETIC-PLAYER-SPEC.md §3.1) was never actually
466
- // called from a real mount path — this is that wiring. `manifest.server`
467
- // (`ResolvedGameManifest.server`, `manifest/load.ts`) is `{ room, module }
468
- // | undefined`; its mere presence is the "this project declares a room"
469
- // signal, independent of whether/when the game actually joins it.
469
+ // debug command) was never actually called from a real mount path — this
470
+ // is that wiring. `manifest.server` (`ResolvedGameManifest.server`,
471
+ // `manifest/load.ts`) is `{ room, module } | undefined`; its mere presence
472
+ // is the "this project declares a room" signal, independent of
473
+ // whether/when the game actually joins it.
470
474
  if (manifest.server) {
471
475
  debugRegistry.setRoomDeclared(true);
472
476
  }