@vgai/engine 0.2.0 → 0.4.0-canary.20260715.0

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 (123) hide show
  1. package/README.md +3 -1
  2. package/package.json +24 -4
  3. package/schemas/engine-api.json +124 -0
  4. package/schemas/engine-api.md +53 -0
  5. package/schemas/engine-capabilities.json +124 -0
  6. package/schemas/inputmap.schema.json +314 -0
  7. package/schemas/mat.schema.json +286 -0
  8. package/schemas/prefab.schema.json +10148 -0
  9. package/schemas/scn2d.schema.json +475 -0
  10. package/schemas/vgai-game.schema.json +383 -0
  11. package/schemas/vscn.schema.json +11007 -0
  12. package/src/adapter/{world-kind.ts → adapter-surface.ts} +6 -6
  13. package/src/adapter/authoring.ts +77 -0
  14. package/src/adapter/first-party-systems.ts +23 -34
  15. package/src/adapter/game-adapter.ts +8 -8
  16. package/src/adapter/host-context.ts +2 -4
  17. package/src/adapter/index.ts +4 -4
  18. package/src/adapter/system-adapter.ts +88 -22
  19. package/src/adapter/vgai-scene-game-adapter.ts +244 -194
  20. package/src/animation/anim-graph-types.ts +12 -43
  21. package/src/animation/animation-clock.ts +479 -0
  22. package/src/animation/camera-ownership.ts +467 -0
  23. package/src/animation/cinematic-cues.ts +451 -0
  24. package/src/animation/clip-map.ts +41 -0
  25. package/src/animation/gsap-registration.ts +184 -0
  26. package/src/animation/theatre-clock-binding.ts +111 -0
  27. package/src/animation/theatre-director.ts +347 -0
  28. package/src/animation/theatre-object-binding.ts +661 -0
  29. package/src/animation/xstate-animation-binding.ts +436 -0
  30. package/src/animation/xstate-animation-meta.ts +319 -0
  31. package/src/audio/index.ts +39 -7
  32. package/src/audio/tone-clock-binding.ts +98 -0
  33. package/src/audio/tone-context.ts +129 -0
  34. package/src/audio/tone-offline-render.ts +167 -0
  35. package/src/audio/wav-encode.ts +119 -0
  36. package/src/character/cloth-sim.ts +533 -0
  37. package/src/character/spring-chain.ts +307 -0
  38. package/src/core/game-loop.ts +57 -2
  39. package/src/core/seeded-random.ts +161 -0
  40. package/src/core/system-runner.ts +20 -3
  41. package/src/core/types.ts +50 -0
  42. package/src/data/data-asset.ts +167 -0
  43. package/src/data/data-check-core.ts +242 -0
  44. package/src/data/data-ref.ts +145 -0
  45. package/src/data/vite-plugin-data.ts +290 -0
  46. package/src/dev/performance-profiler.ts +213 -0
  47. package/src/dev/webgl-gpu-timer.ts +53 -0
  48. package/src/ecs/component-manager.ts +45 -12
  49. package/src/ecs/game-component.ts +95 -11
  50. package/src/humanoid/bake.operation.ts +326 -0
  51. package/src/humanoid/body.ts +663 -0
  52. package/src/humanoid/clips.ts +149 -0
  53. package/src/humanoid/compose.ts +209 -0
  54. package/src/humanoid/generate.ts +189 -0
  55. package/src/humanoid/index.ts +36 -0
  56. package/src/humanoid/schema.ts +108 -0
  57. package/src/humanoid/skeleton.ts +345 -0
  58. package/src/index.ts +48 -0
  59. package/src/input/input-manager.ts +1886 -33
  60. package/src/input/input-types.ts +158 -3
  61. package/src/input/prompt-labels.ts +122 -0
  62. package/src/input/rebind-controller.ts +105 -0
  63. package/src/input/schema.ts +206 -52
  64. package/src/manifest/index.ts +5 -5
  65. package/src/manifest/load.ts +125 -72
  66. package/src/manifest/schema.ts +362 -255
  67. package/src/react/game-state.tsx +135 -32
  68. package/src/react/root-adapter.tsx +49 -0
  69. package/src/react/unmanaged-root-detector.ts +66 -0
  70. package/src/react/use-data.ts +124 -0
  71. package/src/react/use-selection.tsx +135 -0
  72. package/src/runtime/create-runtime.ts +112 -273
  73. package/src/runtime/debug-bridge.ts +483 -0
  74. package/src/runtime/debug-registry.ts +856 -0
  75. package/src/runtime/game.ts +342 -93
  76. package/src/runtime/gameplay-rng-trap.ts +134 -0
  77. package/src/runtime/input-router.ts +7 -7
  78. package/src/runtime/mount-game.ts +40 -38
  79. package/src/runtime/mount-manifest.ts +169 -37
  80. package/src/runtime/render-audio-control.ts +168 -0
  81. package/src/runtime/render-control.ts +522 -0
  82. package/src/runtime/render-seed.ts +79 -0
  83. package/src/runtime/state-bridge.ts +24 -10
  84. package/src/runtime/types.ts +110 -33
  85. package/src/scene/asset-loaders.ts +10 -36
  86. package/src/scene/asset-paths.ts +0 -2
  87. package/src/scene/asset-ref-check.ts +248 -0
  88. package/src/scene/asset-registry.ts +22 -0
  89. package/src/scene/component-registry.ts +14 -3
  90. package/src/scene/defaults.ts +1 -0
  91. package/src/scene/light-camera-factory.ts +11 -3
  92. package/src/scene/parse.ts +133 -0
  93. package/src/scene/scene-apply.ts +55 -4
  94. package/src/scene/scene-loader.ts +91 -123
  95. package/src/scene/scene-types.ts +0 -1
  96. package/src/scene/schema/animation.ts +30 -79
  97. package/src/scene/schema/entity.ts +20 -0
  98. package/src/scene/schema/index.ts +2 -46
  99. package/src/scene/schema/light.ts +16 -1
  100. package/src/scene/schema/material.ts +96 -91
  101. package/src/scene/schema/scene-file.ts +1 -7
  102. package/src/scene/user-data.ts +22 -10
  103. package/src/setup/setup-renderer.ts +10 -3
  104. package/src/tools/define-tool.ts +191 -0
  105. package/src/world2d/authoring-2d.ts +17 -1
  106. package/src/world2d/collision-2d.ts +1 -1
  107. package/src/world2d/pixi-game-adapter.ts +19 -17
  108. package/src/world2d/scene2d-loader.ts +1 -0
  109. package/src/world2d/types.ts +8 -2
  110. package/src/animation/anim-graph.ts +0 -406
  111. package/src/animation/anim-system.ts +0 -28
  112. package/src/animation/property-track.ts +0 -178
  113. package/src/animation/schema.ts +0 -204
  114. package/src/audio/ambient.ts +0 -300
  115. package/src/audio/impacts.ts +0 -212
  116. package/src/audio/movement.ts +0 -140
  117. package/src/audio/musical.ts +0 -200
  118. package/src/audio/ui-sounds.ts +0 -171
  119. package/src/audio/vehicle.ts +0 -235
  120. package/src/audio/weapons.ts +0 -152
  121. package/src/runtime/scene-ui-bridge.ts +0 -86
  122. package/src/runtime/scene-ui-data.ts +0 -119
  123. package/src/scene/schema/ui.ts +0 -602
@@ -14,12 +14,12 @@
14
14
  // only need `loadGameManifest` never pull in `node:fs`.
15
15
 
16
16
  import {
17
+ type AdapterRoot,
17
18
  type GameManifest,
18
19
  GameManifestSchema,
19
20
  type IngestStrategy,
21
+ type RootAdapter,
20
22
  type Tier,
21
- type WorldAdapter,
22
- type WorldEntry,
23
23
  } from './schema';
24
24
 
25
25
  // ---------------------------------------------------------------------------
@@ -29,10 +29,16 @@ import {
29
29
  /** D6's resolved adapter identities. */
30
30
  export type ResolvedAdapter =
31
31
  | {
32
- readonly type: 'default';
33
- readonly identity: 'default-three' | 'default-pixi' | 'default-react';
32
+ readonly type: 'builtin';
33
+ readonly identity: 'threejs' | 'pixijs' | 'react';
34
+ readonly surface: 'threejs' | 'pixijs' | 'react';
35
+ }
36
+ | {
37
+ readonly type: 'module';
38
+ readonly identity: 'module';
39
+ readonly module: string;
40
+ readonly surface: 'threejs' | 'pixijs' | 'react';
34
41
  }
35
- | { readonly type: 'module'; readonly identity: 'module'; readonly module: string }
36
42
  | {
37
43
  readonly type: 'ingest';
38
44
  // 'ingest-react' (Track N, N1 — docs/REACT-INGEST-LANDING-DESIGN.md
@@ -48,6 +54,7 @@ export type ResolvedAdapter =
48
54
  // strategies either; `resolveIngestDescriptor`/`resolveIngest2DDescriptor`
49
55
  // are where "not yet buildable" strategies throw.
50
56
  readonly identity: 'ingest-three' | 'ingest-pixi' | 'ingest-react';
57
+ readonly surface: 'threejs' | 'pixijs' | 'react';
51
58
  readonly strategy: IngestStrategy;
52
59
  readonly entryHtml: string | undefined;
53
60
  readonly assets: Record<string, string> | undefined;
@@ -62,9 +69,9 @@ export type ResolvedAdapter =
62
69
  readonly bodyHtml: string | undefined;
63
70
  };
64
71
 
65
- export interface ResolvedWorldEntry {
72
+ export interface ResolvedAdapterRoot {
66
73
  readonly id: string;
67
- readonly kind: WorldEntry['kind'];
74
+ readonly surface: ResolvedAdapter['surface'];
68
75
  readonly description: string | undefined;
69
76
  readonly adapter: ResolvedAdapter;
70
77
  readonly scene: string | undefined;
@@ -77,14 +84,41 @@ export interface ResolvedWorldEntry {
77
84
  }
78
85
 
79
86
  export interface ResolvedGameManifest {
80
- readonly manifestVersion: 1;
87
+ readonly manifestVersion: 2;
81
88
  readonly name: string;
89
+ readonly appId: string | undefined;
90
+ readonly editorPort: number | undefined;
82
91
  readonly version: string;
83
92
  readonly engine: { readonly version: string };
84
93
  /** Sorted by zOrder; ties broken by original array order (§3). */
85
- readonly worlds: readonly ResolvedWorldEntry[];
94
+ readonly roots: readonly ResolvedAdapterRoot[];
86
95
  readonly server: { readonly room: string; readonly module: string } | undefined;
87
96
  readonly resolution: { readonly width: number; readonly height: number } | undefined;
97
+ readonly authoring:
98
+ | {
99
+ readonly hierarchy?:
100
+ | {
101
+ readonly rootLabels?: Readonly<Record<string, string>> | undefined;
102
+ readonly groups?:
103
+ | readonly {
104
+ readonly id: string;
105
+ readonly label: string;
106
+ readonly roots: readonly string[];
107
+ }[]
108
+ | undefined;
109
+ }
110
+ | undefined;
111
+ }
112
+ | undefined;
113
+ /** D18 — read by the debug-bridge installer to gate `?vgai-debug=1` in production builds. */
114
+ readonly debug: { readonly allowInProduction: boolean } | undefined;
115
+ /** D15 — read by `mount-manifest.ts`'s boot-time seeding reader (gates
116
+ * whether `ctx.random` is seeded from `defaultSeed`/`?vgai-seed=`), the
117
+ * gameplay-rng-ban burn-down scan, and the dev-mode Math.random phase
118
+ * trap. See `docs/D15-DETERMINISM-DESIGN.md`. */
119
+ readonly determinism:
120
+ | { readonly seededRandom: boolean; readonly defaultSeed?: number | undefined }
121
+ | undefined;
88
122
  }
89
123
 
90
124
  // ---------------------------------------------------------------------------
@@ -94,12 +128,12 @@ export interface ResolvedGameManifest {
94
128
 
95
129
  /**
96
130
  * Ceiling (maximum legal declared `Tier`) per adapter type and delivery
97
- * context. `default` worlds are always first-party in both contexts (D6/§2 —
98
- * a first-party world is fully vgai-native everywhere). `ingest` worlds are
131
+ * context. `default` roots are always first-party in both contexts (D6/§2 —
132
+ * a first-party root is fully vgai-native everywhere). `ingest` roots are
99
133
  * ceilinged by their own capture `strategy`: locally the local CLI's Vite
100
134
  * pipeline gives bundler dedupe, so every strategy reaches its own natural
101
135
  * rung; hosted has no bundler (esbuild-wasm + externals only), and a
102
- * `deduped` world is DEFINED by being source built through our pipeline — no
136
+ * `deduped` root is DEFINED by being source built through our pipeline — no
103
137
  * prebuilt bundle exists to fall back to an iframe rung, and nothing
104
138
  * implements a hosted build for it, so hosted is `unsupported` (honesty over
105
139
  * optimism; if the hosted esbuild-wasm path ever proves capable, raising
@@ -109,11 +143,11 @@ export interface ResolvedGameManifest {
109
143
  * adapter's reach is not derivable", §4) — modeled here as `first-party`
110
144
  * (the top rank), i.e. no ceiling actually constrains a module adapter's
111
145
  * declared tier; the loader instead requires the tier to be declared
112
- * explicitly for module worlds (see `resolveCapabilities`), since there is no
146
+ * explicitly for module roots (see `resolveCapabilities`), since there is no
113
147
  * default to derive.
114
148
  */
115
149
  export const CAPABILITY_CEILINGS = {
116
- default: { local: 'first-party', hosted: 'first-party' },
150
+ builtin: { local: 'first-party', hosted: 'first-party' },
117
151
  ingest: {
118
152
  shared: { local: 'shared', hosted: 'shared' },
119
153
  deduped: { local: 'deduped', hosted: 'unsupported' },
@@ -146,14 +180,14 @@ const SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/;
146
180
  // ---------------------------------------------------------------------------
147
181
 
148
182
  function isModuleAdapter(
149
- adapter: WorldAdapter,
150
- ): adapter is Extract<WorldAdapter, { module: string }> {
183
+ adapter: RootAdapter,
184
+ ): adapter is Extract<RootAdapter, { module: string }> {
151
185
  return typeof adapter === 'object' && 'module' in adapter;
152
186
  }
153
187
 
154
188
  function isIngestAdapter(
155
- adapter: WorldAdapter,
156
- ): adapter is Extract<WorldAdapter, { ingest: { strategy: IngestStrategy } }> {
189
+ adapter: RootAdapter,
190
+ ): adapter is Extract<RootAdapter, { ingest: { strategy: IngestStrategy } }> {
157
191
  return typeof adapter === 'object' && 'ingest' in adapter;
158
192
  }
159
193
 
@@ -161,9 +195,9 @@ function isIngestAdapter(
161
195
  // Cross-field checks (§3 — beyond what Zod alone enforces)
162
196
  // ---------------------------------------------------------------------------
163
197
  //
164
- // D-V6 (docs/WAVE5-MULTIWORLD-INGEST-DESIGN.md): world-id uniqueness used to
198
+ // D-V6 (docs/WAVE5-MULTIWORLD-INGEST-DESIGN.md): root-id uniqueness used to
165
199
  // be a cross-field check HERE (`checkUniqueWorldIds`, removed) — it moved
166
- // INTO `GameManifestSchema`'s `worlds` `superRefine` (schema.ts) instead, so
200
+ // INTO `GameManifestSchema`'s `roots` `superRefine` (schema.ts) instead, so
167
201
  // the generated JSON Schema and any other schema consumer see the same
168
202
  // constraint the loader always enforced. `parseManifest` below catches that
169
203
  // one recognizable schema-level issue and rethrows it as a clean, single
@@ -172,17 +206,22 @@ function isIngestAdapter(
172
206
  // callers/tests that pattern-match the message text are unaffected by the
173
207
  // enforcement moving one layer down.
174
208
 
175
- function checkAdapterSceneEntryRules(world: WorldEntry): void {
176
- const { adapter } = world;
177
- if (adapter === 'default' && !world.scene && !world.entry) {
209
+ function checkAdapterSceneEntryRules(root: AdapterRoot): void {
210
+ const { adapter } = root;
211
+ if (typeof adapter === 'string' && !root.scene && !root.entry) {
178
212
  throw new Error(
179
- `Game manifest: world "${world.id}" uses the 'default' adapter but declares neither ` +
180
- '`scene` nor `entry` — a default-adapter world requires one of the two (§3).',
213
+ `Game manifest: root "${root.id}" uses the '${adapter}' adapter but declares neither ` +
214
+ '`scene` nor `entry` — a built-in adapter root requires an authored document or entry.',
181
215
  );
182
216
  }
183
- if (isIngestAdapter(adapter) && world.scene !== undefined) {
217
+ if (adapter === 'react' && root.scene !== undefined) {
184
218
  throw new Error(
185
- `Game manifest: world "${world.id}" uses an { ingest } adapter but declares \`scene\` — ` +
219
+ `Game manifest: React root "${root.id}" declares \`scene\` — React roots use JSX entry modules.`,
220
+ );
221
+ }
222
+ if (isIngestAdapter(adapter) && root.scene !== undefined) {
223
+ throw new Error(
224
+ `Game manifest: root "${root.id}" uses an { ingest } adapter but declares \`scene\` — ` +
186
225
  'ingest adapters forbid `scene` (adapt-never-migrate: ingested games keep their own ' +
187
226
  "formats; overlay persistence is T3.2's) (§3).",
188
227
  );
@@ -190,13 +229,13 @@ function checkAdapterSceneEntryRules(world: WorldEntry): void {
190
229
  }
191
230
 
192
231
  // Historical note (Track N, N1): this file used to reject EVERY `kind:
193
- // 'react'` + `{ ingest }` world outright here (`checkReactAdapterRestriction`,
232
+ // 'react'` + `{ ingest }` root outright here (`checkReactAdapterRestriction`,
194
233
  // citing "no react ingest program is un-parked"). Track N un-parks exactly
195
234
  // that program (docs/REACT-INGEST-LANDING-DESIGN.md) — react+ingest is now a
196
235
  // structurally legal combination, resolved to the `'ingest-react'` identity
197
- // by `resolveAdapter` below like any other ingest world. There is nothing
236
+ // by `resolveAdapter` below like any other ingest root. There is nothing
198
237
  // left to reject at THIS layer: `checkAdapterSceneEntryRules` above already
199
- // forbids `scene` on any `{ ingest }`-adapter world (kind-agnostic), and the
238
+ // forbids `scene` on any `{ ingest }`-adapter root (kind-agnostic), and the
200
239
  // v1 "deduped strategy only" restriction is an editor-resolver concern
201
240
  // (D-N3), not a manifest-shape concern — see the `ResolvedAdapter` type's
202
241
  // comment above.
@@ -205,28 +244,36 @@ function checkAdapterSceneEntryRules(world: WorldEntry): void {
205
244
  // Resolution (§5)
206
245
  // ---------------------------------------------------------------------------
207
246
 
208
- function resolveAdapter(world: WorldEntry): ResolvedAdapter {
209
- const { adapter, kind } = world;
247
+ function resolveAdapter(root: AdapterRoot): ResolvedAdapter {
248
+ const { adapter } = root;
210
249
 
211
- if (adapter === 'default') {
212
- const identity =
213
- kind === 'threejs' ? 'default-three' : kind === 'pixijs' ? 'default-pixi' : 'default-react';
214
- return { type: 'default', identity };
250
+ if (typeof adapter === 'string') {
251
+ return { type: 'builtin', identity: adapter, surface: adapter };
215
252
  }
216
253
 
217
254
  if (isModuleAdapter(adapter)) {
218
- return { type: 'module', identity: 'module', module: adapter.module };
255
+ return {
256
+ type: 'module',
257
+ identity: 'module',
258
+ module: adapter.module,
259
+ surface: adapter.surface,
260
+ };
219
261
  }
220
262
 
221
263
  // isIngestAdapter (Track N, N1: 'ingest-react' joins 'ingest-three'/
222
264
  // 'ingest-pixi' — see the `ResolvedAdapter` type's comment above for why
223
265
  // this stays strategy-agnostic here).
224
266
  const identity =
225
- kind === 'threejs' ? 'ingest-three' : kind === 'pixijs' ? 'ingest-pixi' : 'ingest-react';
267
+ adapter.surface === 'threejs'
268
+ ? 'ingest-three'
269
+ : adapter.surface === 'pixijs'
270
+ ? 'ingest-pixi'
271
+ : 'ingest-react';
226
272
  const ingest = adapter.ingest;
227
273
  return {
228
274
  type: 'ingest',
229
275
  identity,
276
+ surface: adapter.surface,
230
277
  strategy: ingest.strategy,
231
278
  entryHtml: ingest.entryHtml,
232
279
  assets: ingest.assets,
@@ -241,23 +288,23 @@ function resolveAdapter(world: WorldEntry): ResolvedAdapter {
241
288
  };
242
289
  }
243
290
 
244
- function ceilingFor(adapter: WorldAdapter, context: 'local' | 'hosted'): Tier {
245
- if (adapter === 'default') return CAPABILITY_CEILINGS.default[context];
291
+ function ceilingFor(adapter: RootAdapter, context: 'local' | 'hosted'): Tier {
292
+ if (typeof adapter === 'string') return CAPABILITY_CEILINGS.builtin[context];
246
293
  if (isModuleAdapter(adapter)) return CAPABILITY_CEILINGS.module[context];
247
294
  return CAPABILITY_CEILINGS.ingest[adapter.ingest.strategy][context];
248
295
  }
249
296
 
250
- function resolveCapabilities(world: WorldEntry): { local: Tier; hosted: Tier } {
297
+ function resolveCapabilities(root: AdapterRoot): { local: Tier; hosted: Tier } {
251
298
  const contexts = ['local', 'hosted'] as const;
252
299
  const result = {} as { local: Tier; hosted: Tier };
253
300
 
254
301
  for (const context of contexts) {
255
- const declared = world.capabilities?.[context];
302
+ const declared = root.capabilities?.[context];
256
303
  if (declared !== undefined) {
257
- const ceiling = ceilingFor(world.adapter, context);
304
+ const ceiling = ceilingFor(root.adapter, context);
258
305
  if (TIER_RANK[declared] > TIER_RANK[ceiling]) {
259
306
  throw new Error(
260
- `Game manifest: world "${world.id}": declared capabilities.${context} tier "${declared}" ` +
307
+ `Game manifest: root "${root.id}": declared capabilities.${context} tier "${declared}" ` +
261
308
  `exceeds the "${ceiling}" ceiling for this adapter in the ${context} context (§4). ` +
262
309
  'Under-claiming (including `unsupported`) is legal; over-claiming is not.',
263
310
  );
@@ -267,15 +314,15 @@ function resolveCapabilities(world: WorldEntry): { local: Tier; hosted: Tier } {
267
314
  }
268
315
 
269
316
  // Omitted -> derive.
270
- if (world.adapter === 'default') {
317
+ if (typeof root.adapter === 'string') {
271
318
  result[context] = 'first-party';
272
- } else if (isModuleAdapter(world.adapter)) {
319
+ } else if (isModuleAdapter(root.adapter)) {
273
320
  throw new Error(
274
- `Game manifest: world "${world.id}": { module } adapter requires an explicit ` +
321
+ `Game manifest: root "${root.id}": { module } adapter requires an explicit ` +
275
322
  `capabilities.${context} declaration — a custom adapter's reach is not derivable (§4).`,
276
323
  );
277
324
  } else {
278
- result[context] = CAPABILITY_CEILINGS.ingest[world.adapter.ingest.strategy][context];
325
+ result[context] = CAPABILITY_CEILINGS.ingest[root.adapter.ingest.strategy][context];
279
326
  }
280
327
  }
281
328
 
@@ -292,10 +339,10 @@ function checkEngineVersionPin(version: string): void {
292
339
  }
293
340
 
294
341
  /**
295
- * Resolve one already-Zod-validated world entry into its `ResolvedWorldEntry`
342
+ * Resolve one already-Zod-validated root entry into its `ResolvedAdapterRoot`
296
343
  * (adapter identity, capabilities, and a pass-through of every other field).
297
- * Every `WorldEntry` field participates here or in a check above:
298
- * `id`/`kind` drive adapter resolution + uniqueness; `adapter` (incl. its
344
+ * Every `AdapterRoot` field participates here or in a check above:
345
+ * `id` drives uniqueness; `adapter` (incl. its surface and
299
346
  * `module`/`ingest.*` sub-fields) drives identity + capability-ceiling
300
347
  * resolution; `scene`/`entry` drive the scene/entry cross-field rule and are
301
348
  * carried through; `zOrder` drives the final sort; `pausable`/`loop` are
@@ -303,22 +350,23 @@ function checkEngineVersionPin(version: string): void {
303
350
  * `capabilities.{local,hosted}` drive tier resolution; `description` (T3.3
304
351
  * slice 3) is carried through as-is — its consumer today is
305
352
  * `resolveIngestDescriptor` (packages/editor/src/adapter-resolver.ts), which
306
- * threads an ingest-three world's description into `IngestGame.description`.
353
+ * threads an ingest-three root's description into `IngestGame.description`.
307
354
  */
308
- function resolveWorld(world: WorldEntry): ResolvedWorldEntry {
309
- checkAdapterSceneEntryRules(world);
355
+ function resolveRoot(root: AdapterRoot): ResolvedAdapterRoot {
356
+ checkAdapterSceneEntryRules(root);
357
+ const adapter = resolveAdapter(root);
310
358
 
311
359
  return {
312
- id: world.id,
313
- kind: world.kind,
314
- description: world.description,
315
- adapter: resolveAdapter(world),
316
- scene: world.scene,
317
- entry: world.entry,
318
- zOrder: world.zOrder,
319
- pausable: world.pausable,
320
- loop: world.loop,
321
- capabilities: resolveCapabilities(world),
360
+ id: root.id,
361
+ surface: adapter.surface,
362
+ description: root.description,
363
+ adapter,
364
+ scene: root.scene,
365
+ entry: root.entry,
366
+ zOrder: root.zOrder,
367
+ pausable: root.pausable,
368
+ loop: root.loop,
369
+ capabilities: resolveCapabilities(root),
322
370
  };
323
371
  }
324
372
 
@@ -329,20 +377,20 @@ function resolveWorld(world: WorldEntry): ResolvedWorldEntry {
329
377
  /**
330
378
  * Parse + validate + resolve a raw manifest value into a `ResolvedGameManifest`.
331
379
  * Pure data-in/data-out (no `fs`) — see `load-file.ts` for the Node path-based
332
- * wrapper. Throws a descriptive `Error` (naming the offending world/field) on
380
+ * wrapper. Throws a descriptive `Error` (naming the offending root/field) on
333
381
  * any Zod validation failure or cross-field rule violation.
334
382
  */
335
383
  export function loadGameManifest(raw: unknown): ResolvedGameManifest {
336
384
  const parsed = GameManifestSchema.safeParse(raw);
337
385
  if (!parsed.success) {
338
- // D-V6: the `worlds` `superRefine`'s duplicate-id issue is the one
386
+ // D-V6: the `roots` `superRefine`'s duplicate-id issue is the one
339
387
  // schema-level violation this loader still surfaces as a clean, single
340
388
  // `Error` (matching `checkUniqueWorldIds`'s pre-D-V6 message shape) —
341
389
  // every other schema violation keeps propagating as the raw `ZodError`
342
390
  // it always has (unchanged behavior, e.g. the iframe-reachable
343
391
  // field-exclusivity / manifestVersion checks).
344
392
  const dupIssue = parsed.error.issues.find((issue) =>
345
- issue.message.startsWith('Game manifest: duplicate world id'),
393
+ issue.message.startsWith('Game manifest: duplicate root id'),
346
394
  );
347
395
  if (dupIssue) throw new Error(dupIssue.message);
348
396
  throw parsed.error;
@@ -351,16 +399,16 @@ export function loadGameManifest(raw: unknown): ResolvedGameManifest {
351
399
 
352
400
  checkEngineVersionPin(manifest.engine.version);
353
401
 
354
- const resolvedWithIndex = manifest.worlds.map((world, index) => ({
355
- world: resolveWorld(world),
402
+ const resolvedWithIndex = manifest.roots.map((root, index) => ({
403
+ root: resolveRoot(root),
356
404
  index,
357
405
  }));
358
406
 
359
407
  resolvedWithIndex.sort(
360
- (a, b) => a.world.zOrder - b.world.zOrder || a.index - b.index, // ties -> array order (§3)
408
+ (a, b) => a.root.zOrder - b.root.zOrder || a.index - b.index, // ties -> array order (§3)
361
409
  );
362
410
 
363
- const worlds = resolvedWithIndex.map(({ world }) => world);
411
+ const roots = resolvedWithIndex.map(({ root }) => root);
364
412
 
365
413
  const server = manifest.server
366
414
  ? { room: manifest.server.room, module: manifest.server.module ?? DEFAULT_SERVER_MODULE }
@@ -369,10 +417,15 @@ export function loadGameManifest(raw: unknown): ResolvedGameManifest {
369
417
  return {
370
418
  manifestVersion: manifest.manifestVersion,
371
419
  name: manifest.name,
420
+ appId: manifest.appId,
421
+ editorPort: manifest.editorPort,
372
422
  version: manifest.version,
373
423
  engine: { version: manifest.engine.version },
374
- worlds,
424
+ roots,
375
425
  server,
376
426
  resolution: manifest.resolution,
427
+ authoring: manifest.authoring,
428
+ debug: manifest.debug,
429
+ determinism: manifest.determinism,
377
430
  };
378
431
  }