@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
@@ -0,0 +1,134 @@
1
+ /**
2
+ * D15 — the dev-mode `Math.random` phase trap (`docs/D15-DETERMINISM-DESIGN.md`
3
+ * §2.a enforcer 3, T-D15.3). A project that declares
4
+ * `determinism.seededRandom` is claiming ALL gameplay RNG flows through
5
+ * `ctx.random` — the burn-down scan (`test/gameplay-rng-ban.test.ts`) catches
6
+ * raw `Math.random()`/`Date.now()`/`performance.now()` call sites it can
7
+ * SEE in source; this trap catches what the scan structurally can't: a
8
+ * TRANSITIVE draw made by a third-party library the game calls into during a
9
+ * gameplay tick.
10
+ *
11
+ * Scope is exactly one fixed-step frame (`GameInternal.runFrame`'s whole
12
+ * body — see `game.ts`'s `runFrameImpl`, which brackets its entire call with
13
+ * `profiler.beginFrame()`/`endFrame()`; this trap's `enable()`/`disable()`
14
+ * are called at those exact two points) — code that runs OUTSIDE a frame
15
+ * (module-load-time library init, an unrelated timer callback) never trips
16
+ * it. Warn-once PER CALL SITE (not once ever, not once per call) — a
17
+ * third-party library that draws from several different internal call sites
18
+ * gets one warning per site, so nothing is silently swallowed after the
19
+ * first hit, but a hot per-frame draw from the SAME site doesn't spam.
20
+ * Never throws: a cosmetic third-party draw is legal, just outside the
21
+ * contract (the design doc is explicit about this — "warn, never throw").
22
+ */
23
+
24
+ export interface GameplayRngTrap {
25
+ /** Wrap `Math.random` for the duration of one frame — call at frame start. */
26
+ enable(): void;
27
+ /** Restore whatever `Math.random` was immediately before `enable()` — call
28
+ * at frame end. A no-op if something else already replaced `Math.random`
29
+ * out from under this trap between `enable()` and `disable()` (defensive;
30
+ * should not happen in practice). */
31
+ disable(): void;
32
+ }
33
+
34
+ /** Pull a single representative "call site" line out of a captured stack —
35
+ * the warn-once dedupe key. `stack[0]` is the `Error:` header line,
36
+ * `stack[1]` is this trap's OWN wrapper frame — the caller's frame is the
37
+ * next one down. Falls back to a fixed string when `Error().stack` isn't
38
+ * populated (some non-V8 engines) so the trap still functions, just with
39
+ * coarser (single-bucket) deduping. */
40
+ function callSiteFromStack(stack: string | undefined): string {
41
+ if (!stack) return '<unknown call site — no stack captured>';
42
+ const lines = stack.split('\n');
43
+ return (lines[2] ?? lines[1] ?? lines[0] ?? '<unknown call site>').trim();
44
+ }
45
+
46
+ /**
47
+ * Construct a fresh trap. `warn` defaults to `console.warn`; tests (and any
48
+ * caller that wants to assert on the message) can override it to a plain
49
+ * capturing function instead of spying on the global console.
50
+ */
51
+ export function createGameplayRngTrap(
52
+ warn: (message: string) => void = (message: string) => {
53
+ // biome-ignore lint/suspicious/noConsole: the trap's entire purpose is a structured, greppable dev-mode warning (D15) — this IS the console sink, not incidental debug output.
54
+ console.warn(message);
55
+ },
56
+ ): GameplayRngTrap {
57
+ const warnedSites = new Set<string>();
58
+ let previous: (() => number) | undefined;
59
+
60
+ function trapped(): number {
61
+ const site = callSiteFromStack(new Error().stack);
62
+ if (!warnedSites.has(site)) {
63
+ warnedSites.add(site);
64
+ warn(
65
+ // Deliberately NOT the literal substring "Math.random(" (the phrasing
66
+ // below reads fine without it) — this engine repo's OWN gameplay-rng-
67
+ // ban scan (`test/gameplay-rng-ban.test.ts`) greps engine src/ for
68
+ // that exact pattern, and a plain-text mention of the call inside
69
+ // this warning STRING would otherwise false-positive against itself.
70
+ '[determinism] the global Math.random generator was drawn from during a gameplay frame ' +
71
+ 'while this project declares determinism.seededRandom — use ctx.random() / ' +
72
+ `ctx.random.stream(name) instead (docs/D15-DETERMINISM-DESIGN.md). Call site:\n${site}`,
73
+ );
74
+ }
75
+ // `previous` is always set by the time `trapped` can run — `enable()`
76
+ // assigns it before installing `trapped` as `Math.random`, and nothing
77
+ // else can invoke this function reference.
78
+ return previous!();
79
+ }
80
+
81
+ return {
82
+ enable() {
83
+ previous = Math.random;
84
+ Math.random = trapped;
85
+ },
86
+ disable() {
87
+ if (Math.random === trapped && previous) {
88
+ Math.random = previous;
89
+ previous = undefined;
90
+ }
91
+ // else: something else replaced `Math.random` out from under this trap
92
+ // between `enable()` and `disable()` — leave `previous` untouched.
93
+ // `trapped` may still be reachable via a chain built on top of it (e.g.
94
+ // a third party did `const wrapped = Math.random; Math.random = () =>
95
+ // { ...; return wrapped(); }` while `wrapped` was `trapped`) — nulling
96
+ // `previous` here would make that still-chained call to `trapped()`
97
+ // throw (`previous!()` on undefined) instead of quietly forwarding to
98
+ // the real original generator.
99
+ },
100
+ };
101
+ }
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Game-scoped registry — same WeakMap-on-owner-object pattern as
105
+ // `core/seeded-random.ts`'s `registerSeededRandom`/`getSeededRandom` (and
106
+ // `debug-registry.ts`'s `registerDebugRegistry`/`getDebugRegistry`): the
107
+ // trap's enable/disable calls live inside `game.ts`'s `runFrameImpl`, gated
108
+ // on a boolean only the manifest-aware boot path (`mount-manifest.ts`) knows
109
+ // how to set — `createGame` constructs the trap unconditionally (cheap, does
110
+ // nothing while disabled) and registers a control surface for that boot path
111
+ // to flip once it has resolved `manifest.determinism?.seededRandom`.
112
+ // ---------------------------------------------------------------------------
113
+
114
+ export interface GameplayRngTrapControl {
115
+ setEnabled(enabled: boolean): void;
116
+ readonly enabled: boolean;
117
+ }
118
+
119
+ const controlByOwner = new WeakMap<object, GameplayRngTrapControl>();
120
+
121
+ /** Called once by `createGame`, alongside `registerSeededRandom`/
122
+ * `registerDebugRegistry`. */
123
+ export function registerGameplayRngTrapControl(
124
+ owner: object,
125
+ control: GameplayRngTrapControl,
126
+ ): void {
127
+ controlByOwner.set(owner, control);
128
+ }
129
+
130
+ /** `null` for an owner built without one — same absence precedent as
131
+ * `getSeededRandom`/`getDebugRegistry`. */
132
+ export function getGameplayRngTrapControl(owner: object): GameplayRngTrapControl | null {
133
+ return controlByOwner.get(owner) ?? null;
134
+ }
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * The delegating input router (COMPOSITION-DESIGN.md D5 §2a) —
3
- * `createGameRuntime`'s worlds path (T6.1 slice 1,
3
+ * `createGameRuntime`'s roots path (T6.1 slice 1,
4
4
  * docs/MULTI-WORLD-DESIGN.md §1.C) uses this to route pointer input across
5
5
  * N stacked world canvases with exactly ONE root listener.
6
6
  *
7
7
  * Split into two layers so the decision logic is unit-testable with plain
8
- * fake worlds — no real DOM/canvas needed:
8
+ * fake roots — no real DOM/canvas needed:
9
9
  *
10
10
  * - `stackOrder` / `resolveClaimingWorld`: pure functions over
11
11
  * `{id, zOrder, hitTest?}` — "who claims this point".
@@ -26,21 +26,21 @@ export interface ClaimEntry {
26
26
  /**
27
27
  * Optional claim predicate over a point RELATIVE TO THE CONTAINER. Absent
28
28
  * means: "this world claims only if it is the bottom (lowest zOrder)
29
- * world" — D5 §2a's stated default ("transparent upper worlds do not
29
+ * world" — D5 §2a's stated default ("transparent upper roots do not
30
30
  * claim, the bottom world claims everything").
31
31
  */
32
32
  readonly hitTest?: ((x: number, y: number) => boolean) | undefined;
33
33
  }
34
34
 
35
35
  /** The DOM-wiring inputs — one real canvas per world. */
36
- export interface RouterWorldEntry extends ClaimEntry {
36
+ export interface RouterAdapterRoot extends ClaimEntry {
37
37
  readonly canvas: HTMLCanvasElement;
38
38
  }
39
39
 
40
40
  /**
41
41
  * Bottom-to-top paint order: ascending `zOrder`, ties -> original array
42
42
  * order — the SAME rule `manifest/load.ts`'s `loadGameManifest` sorts
43
- * worlds by, so a world's DOM stacking position always matches its
43
+ * roots by, so a world's DOM stacking position always matches its
44
44
  * manifest-resolved zOrder. The last element is the topmost (visually on
45
45
  * top / highest z-index).
46
46
  */
@@ -83,7 +83,7 @@ export function resolveClaimingWorld(
83
83
  * DOM-hit-tests it — the router's JS-level `hitTest` is what still lets it
84
84
  * claim a point via `createInputRouter`'s forwarding).
85
85
  */
86
- export function applyPointerEventsStacking(entries: readonly RouterWorldEntry[]): void {
86
+ export function applyPointerEventsStacking(entries: readonly RouterAdapterRoot[]): void {
87
87
  const order = stackOrder(entries);
88
88
  order.forEach((entry, i) => {
89
89
  entry.canvas.style.pointerEvents = i === 0 ? 'auto' : 'none';
@@ -179,7 +179,7 @@ export interface InputRouterHandle {
179
179
  */
180
180
  export function createInputRouter(
181
181
  container: HTMLElement,
182
- entries: readonly RouterWorldEntry[],
182
+ entries: readonly RouterAdapterRoot[],
183
183
  ): InputRouterHandle {
184
184
  applyPointerEventsStacking(entries);
185
185
  const byId = new Map(entries.map((e) => [e.id, e] as const));
@@ -4,22 +4,22 @@
4
4
  // duplicates its per-kind dispatch/validation) to add the one thing that
5
5
  // module deliberately does NOT have: a KIND REGISTRY, so a project doesn't
6
6
  // have to hand-build an `entries` map inline in its own `main.ts` every time
7
- // — it registers a `WorldKindFactory` per kind it uses (once, at module load)
7
+ // — it registers a `AdapterSurfaceFactory` per kind it uses (once, at module load)
8
8
  // and then mounts with a single `mountGameFromManifest(manifest, host)` call.
9
9
  //
10
10
  // Engine-core react/pixi-free discipline (mirrors `mount-manifest.ts`'s own
11
11
  // header comment, and the HARD INVARIANT this file was built under): this
12
- // registry is pure mechanism — it holds whatever `WorldKindFactory` functions
12
+ // registry is pure mechanism — it holds whatever `AdapterSurfaceFactory` functions
13
13
  // callers register, but it registers NONE itself. In particular there is no
14
14
  // built-in 'pixijs'/'react' registration here (that would require this file
15
15
  // to value-import `pixi.js`/`react`/`react-dom`, exactly what `mount-manifest
16
16
  // .ts` forbids); a project that has a pixijs or react world MUST register a
17
- // factory for that kind itself — see `defaultThreeKindFactory` below for why
17
+ // factory for that kind itself — see `defaultThreeAdapterFactory` below for why
18
18
  // even the 'threejs' convenience export is a plain function the CALLER
19
19
  // registers, not something this module wires up automatically.
20
20
 
21
21
  import type { GameAdapter } from '../adapter/game-adapter';
22
- import type { ResolvedWorldEntry } from '../manifest/load';
22
+ import type { ResolvedAdapterRoot } from '../manifest/load';
23
23
  import type { ComponentRegistry } from '../scene/component-registry';
24
24
  import type { GameSession } from './create-runtime';
25
25
  import { type MountEntry, mountManifestWorlds, resolveManifest } from './mount-manifest';
@@ -42,7 +42,7 @@ export interface ManifestHost {
42
42
  /**
43
43
  * Resolve a manifest-declared `entry` (a project-relative module path,
44
44
  * e.g. `"src/scripts/main.ts"`) to the ALREADY-IMPORTED module namespace a
45
- * registered {@link WorldKindFactory} needs to build a `MountEntry` from.
45
+ * registered {@link AdapterSurfaceFactory} needs to build a `MountEntry` from.
46
46
  *
47
47
  * Deliberately NOT "import this arbitrary string path" — a static bundler
48
48
  * (Vite/rollup building a `dist/`) cannot resolve a runtime-computed import
@@ -55,7 +55,7 @@ export interface ManifestHost {
55
55
  */
56
56
  loadEntryModule?(path: string): Promise<unknown>;
57
57
  /** Resolve asset/scene URLs relative to the project — forwarded to a
58
- * registered {@link WorldKindFactory} via {@link WorldKindFactoryContext},
58
+ * registered {@link AdapterSurfaceFactory} via {@link AdapterSurfaceFactoryContext},
59
59
  * never called by this module itself (a factory only needs it for a
60
60
  * non-default resolution scheme, e.g. a hosted-editor iframe). */
61
61
  resolveUrl?(path: string): string;
@@ -66,8 +66,8 @@ export interface ManifestHost {
66
66
  readonly headless?: boolean | undefined;
67
67
  }
68
68
 
69
- /** What a registered {@link WorldKindFactory} is handed for one world. */
70
- export interface WorldKindFactoryContext {
69
+ /** What a registered {@link AdapterSurfaceFactory} is handed for one world. */
70
+ export interface AdapterSurfaceFactoryContext {
71
71
  readonly host: ManifestHost;
72
72
  /**
73
73
  * The result of `host.loadEntryModule(world.entry)`, if the world declares
@@ -85,19 +85,19 @@ export interface WorldKindFactoryContext {
85
85
  * `examples/tri-world/src/main.ts`'s pre-E4 shape). May be async (loading an
86
86
  * asset, fetching a scene file, etc.).
87
87
  */
88
- export type WorldKindFactory = (
89
- world: ResolvedWorldEntry,
90
- ctx: WorldKindFactoryContext,
88
+ export type AdapterSurfaceFactory = (
89
+ world: ResolvedAdapterRoot,
90
+ ctx: AdapterSurfaceFactoryContext,
91
91
  ) => MountEntry | Promise<MountEntry>;
92
92
 
93
93
  // ---------------------------------------------------------------------------
94
94
  // The registry
95
95
  // ---------------------------------------------------------------------------
96
96
 
97
- const registry = new Map<string, WorldKindFactory>();
97
+ const registry = new Map<string, AdapterSurfaceFactory>();
98
98
 
99
99
  /**
100
- * Register a `WorldKindFactory` for a manifest world `kind` (`'threejs'` /
100
+ * Register a `AdapterSurfaceFactory` for a manifest world `kind` (`'threejs'` /
101
101
  * `'pixijs'` / `'react'`, or any future kind the manifest schema grows).
102
102
  * Call once, at module load — mirrors `registerSceneUIRenderer()`'s existing
103
103
  * "call before mounting" convention (`packages/editor/template/src/main.ts`).
@@ -105,14 +105,14 @@ const registry = new Map<string, WorldKindFactory>();
105
105
  * Throws on double-registration of the SAME kind: an accidental duplicate
106
106
  * (two side-effect imports of the same registration module, or a copy-paste)
107
107
  * is far more likely than an intentional runtime swap — a caller that really
108
- * wants to replace a registration must `unregisterWorldKind` first, making
108
+ * wants to replace a registration must `unregisterAdapter` first, making
109
109
  * the intent explicit.
110
110
  */
111
- export function registerWorldKind(kind: string, factory: WorldKindFactory): void {
111
+ export function registerAdapter(kind: string, factory: AdapterSurfaceFactory): void {
112
112
  if (registry.has(kind)) {
113
113
  throw new Error(
114
- `registerWorldKind: a factory is already registered for kind "${kind}" — ` +
115
- 'call unregisterWorldKind(kind) first if you intend to replace it (an accidental ' +
114
+ `registerAdapter: a factory is already registered for kind "${kind}" — ` +
115
+ 'call unregisterAdapter(kind) first if you intend to replace it (an accidental ' +
116
116
  'double-registration, e.g. two side-effect imports of the same registration module, ' +
117
117
  'is far more common than an intentional swap).',
118
118
  );
@@ -121,12 +121,12 @@ export function registerWorldKind(kind: string, factory: WorldKindFactory): void
121
121
  }
122
122
 
123
123
  /** Remove a kind's registration (e.g. before re-registering a replacement). */
124
- export function unregisterWorldKind(kind: string): void {
124
+ export function unregisterAdapter(kind: string): void {
125
125
  registry.delete(kind);
126
126
  }
127
127
 
128
128
  /** Whether a factory is currently registered for `kind`. */
129
- export function isWorldKindRegistered(kind: string): boolean {
129
+ export function isAdapterRegistered(kind: string): boolean {
130
130
  return registry.has(kind);
131
131
  }
132
132
 
@@ -136,17 +136,17 @@ export function isWorldKindRegistered(kind: string): boolean {
136
136
  * call this; it exists so unit tests can start each case from a clean
137
137
  * registry without cross-test leakage.
138
138
  */
139
- export function __clearWorldKindRegistryForTests(): void {
139
+ export function __clearAdapterSurfaceRegistryForTests(): void {
140
140
  registry.clear();
141
141
  }
142
142
 
143
143
  // ---------------------------------------------------------------------------
144
- // `defaultThreeKindFactory` — an OPT-IN convenience, never auto-registered
144
+ // `defaultThreeAdapterFactory` — an OPT-IN convenience, never auto-registered
145
145
  // ---------------------------------------------------------------------------
146
146
 
147
147
  /** The entry-module export shape every first-party entry module follows
148
- * (`packages/editor/template/src/scripts/main.ts`, every `examples/*`
149
- * project's `src/index.ts` — see CLAUDE.md's "Adding an example project"). */
148
+ * (an explicit world entry module, including `examples/*` projects'
149
+ * `src/index.ts` — see CLAUDE.md's "Adding an example project"). */
150
150
  interface ThreeEntryModuleExports {
151
151
  readonly adapter?: GameAdapter;
152
152
  readonly setup?: GameSetupFn;
@@ -154,7 +154,7 @@ interface ThreeEntryModuleExports {
154
154
  }
155
155
 
156
156
  /**
157
- * A ready-to-register {@link WorldKindFactory} for `kind: 'threejs'` worlds
157
+ * A ready-to-register {@link AdapterSurfaceFactory} for `kind: 'threejs'` roots
158
158
  * that follow the first-party entry-module convention (`export const
159
159
  * adapter` and/or `export async function setup`) or are plain scene files
160
160
  * with an optional named `componentRegistry` export. Threejs is safe to
@@ -162,7 +162,7 @@ interface ThreeEntryModuleExports {
162
162
  * `VgaiSceneGameAdapter` are already unconditional engine dependencies —
163
163
  * see `mount-manifest.ts`'s header comment. Still never auto-registered:
164
164
  * the registry itself stays zero-policy (see this file's header comment) —
165
- * a caller opts in with `registerWorldKind('threejs', defaultThreeKindFactory)`.
165
+ * a caller opts in with `registerAdapter('threejs', defaultThreeAdapterFactory)`.
166
166
  *
167
167
  * Resolution mirrors `mount-manifest.ts`'s own `resolveThreeAdapter` branch
168
168
  * order: an entry module's `adapter` export wins outright; else its `setup`
@@ -171,15 +171,15 @@ interface ThreeEntryModuleExports {
171
171
  * `entry` at all) a scene-driven world, forwarding `componentRegistry` if
172
172
  * the entry module supplied one.
173
173
  */
174
- export const defaultThreeKindFactory: WorldKindFactory = (world, ctx) => {
174
+ export const defaultThreeAdapterFactory: AdapterSurfaceFactory = (world, ctx) => {
175
175
  const mod = ctx.entryModule as ThreeEntryModuleExports | undefined;
176
176
  if (mod?.adapter) return { kind: 'threejs', adapter: mod.adapter };
177
177
  if (mod?.setup) return { kind: 'threejs', setup: mod.setup };
178
178
  if (world.entry !== undefined) {
179
179
  throw new Error(
180
- `defaultThreeKindFactory: entry module "${world.entry}" for world "${world.id}" exports ` +
180
+ `defaultThreeAdapterFactory: entry module "${world.entry}" for world "${world.id}" exports ` +
181
181
  'neither `adapter` nor `setup` — every first-party entry module exports at least one ' +
182
- '(see packages/editor/template/src/scripts/main.ts).',
182
+ '(export one from the declared entry module).',
183
183
  );
184
184
  }
185
185
  return { kind: 'threejs', componentRegistry: mod?.componentRegistry };
@@ -203,13 +203,15 @@ export interface MountGameOptions {
203
203
  * nothing beyond the manifest itself). Every other shape (pixijs, react, an
204
204
  * entry-declaring or module/ingest-adapter threejs world) needs SOMETHING
205
205
  * in `entries[id]` to succeed. */
206
- function worldIsSelfSufficient(world: ResolvedWorldEntry): boolean {
207
- return world.kind === 'threejs' && world.adapter.type === 'default' && world.entry === undefined;
206
+ function worldIsSelfSufficient(world: ResolvedAdapterRoot): boolean {
207
+ return (
208
+ world.surface === 'threejs' && world.adapter.type === 'builtin' && world.entry === undefined
209
+ );
208
210
  }
209
211
 
210
212
  /**
211
213
  * Mount every world declared by a `vgai.game.json` manifest onto `host
212
- * .container`, resolving each world's kind through the `registerWorldKind`
214
+ * .container`, resolving each world's kind through the `registerAdapter`
213
215
  * registry — falling back to an explicit `opts.entries[id]` where supplied,
214
216
  * exactly like `mountManifestWorlds` (so a caller can migrate one world at a
215
217
  * time, or never touch the registry if it prefers hand-building entries).
@@ -224,7 +226,7 @@ function worldIsSelfSufficient(world: ResolvedWorldEntry): boolean {
224
226
  * into E4): a world whose kind has no registered factory AND no explicit
225
227
  * entry, and which cannot self-mount from the manifest alone, throws a named
226
228
  * `Error` identifying the world id + kind and BOTH ways to fix it
227
- * (`registerWorldKind` or `opts.entries`) — never a silent skip.
229
+ * (`registerAdapter` or `opts.entries`) — never a silent skip.
228
230
  */
229
231
  export async function mountGameFromManifest(
230
232
  manifestInput: unknown,
@@ -234,10 +236,10 @@ export async function mountGameFromManifest(
234
236
  const manifest = resolveManifest(manifestInput);
235
237
  const entries: Record<string, MountEntry> = { ...opts.entries };
236
238
 
237
- for (const world of manifest.worlds) {
239
+ for (const world of manifest.roots) {
238
240
  if (entries[world.id] !== undefined) continue; // explicit entry always wins
239
241
 
240
- const factory = registry.get(world.kind);
242
+ const factory = registry.get(world.adapter.identity);
241
243
  if (factory) {
242
244
  const entryModule =
243
245
  world.entry !== undefined && host.loadEntryModule
@@ -250,10 +252,10 @@ export async function mountGameFromManifest(
250
252
  if (worldIsSelfSufficient(world)) continue; // mountManifestWorlds needs nothing further
251
253
 
252
254
  throw new Error(
253
- `mountGameFromManifest: world "${world.id}" (kind "${world.kind}") has no registered ` +
254
- `world-kind factory and no explicit entries["${world.id}"] — call ` +
255
- `registerWorldKind("${world.kind}", factory) before mounting (see ` +
256
- '`defaultThreeKindFactory` for a worked threejs example), or pass ' +
255
+ `mountGameFromManifest: root "${world.id}" (adapter "${world.adapter.identity}") has no registered ` +
256
+ `adapter factory and no explicit entries["${world.id}"] — call ` +
257
+ `registerAdapter("${world.adapter.identity}", factory) before mounting (see ` +
258
+ '`defaultThreeAdapterFactory` for a worked threejs example), or pass ' +
257
259
  `opts.entries["${world.id}"] directly (see mount-manifest.ts's \`MountEntry\`).`,
258
260
  );
259
261
  }