@vgai/engine 0.5.2 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/README.md +18 -11
  2. package/package.json +10 -6
  3. package/schemas/engine-api.json +1 -68
  4. package/schemas/engine-api.md +1 -32
  5. package/schemas/engine-capabilities.json +30 -42
  6. package/schemas/{vgai-game.schema.json → vgai-project.schema.json} +52 -34
  7. package/src/adapter/adapter-surface.ts +5 -5
  8. package/src/adapter/authoring.ts +168 -226
  9. package/src/adapter/colyseus-networking-adapter.ts +44 -5
  10. package/src/adapter/first-party-systems.ts +156 -42
  11. package/src/adapter/host-context.ts +177 -46
  12. package/src/adapter/index.ts +47 -51
  13. package/src/adapter/ingest/game-contract.ts +2 -2
  14. package/src/adapter/ingest/scene-capture.ts +18 -19
  15. package/src/adapter/ingest/structural-ids.ts +127 -0
  16. package/src/adapter/ingest/upstream-pin.ts +9 -12
  17. package/src/adapter/loop-gate-report.ts +11 -11
  18. package/src/adapter/rapier-physics-adapter.ts +27 -9
  19. package/src/adapter/root-adapter.ts +217 -0
  20. package/src/adapter/{vgai-scene-game-adapter.ts → setup-three-root-adapter.ts} +173 -351
  21. package/src/adapter/system-adapter.ts +80 -63
  22. package/src/ai/navigation.ts +1 -1
  23. package/src/animation/animation-clock.ts +1 -1
  24. package/src/animation/camera-ownership.ts +1 -2
  25. package/src/animation/cubic-spline-interpolant.ts +132 -0
  26. package/src/animation/theatre-clock-binding.ts +2 -2
  27. package/src/animation/theatre-object-binding.ts +4 -4
  28. package/src/animation/xstate-animation-binding.ts +75 -5
  29. package/src/{scene/schema → asset-formats}/camera.ts +2 -5
  30. package/src/{scene/schema → asset-formats}/collider.ts +2 -5
  31. package/src/asset-formats/index.ts +54 -0
  32. package/src/{scene/schema → asset-formats}/instances.ts +10 -6
  33. package/src/{scene/schema → asset-formats}/light.ts +3 -6
  34. package/src/{scene/schema → asset-formats}/material.ts +4 -7
  35. package/src/{scene/schema → asset-formats}/mesh.ts +4 -7
  36. package/src/asset-formats/parse.ts +39 -0
  37. package/src/{scene/schema → asset-formats}/particles.ts +3 -6
  38. package/src/{scene/schema/environment.ts → asset-formats/render-env.ts} +23 -86
  39. package/src/{scene/schema → asset-formats}/tuples.ts +1 -1
  40. package/src/{scene/asset-loaders.ts → asset-loaders.ts} +8 -9
  41. package/src/asset-parse-error.ts +33 -0
  42. package/src/{scene/asset-registry.ts → asset-registry.ts} +1 -1
  43. package/src/assets.ts +1 -1
  44. package/src/audio/wav-encode.ts +9 -9
  45. package/src/canvas-react/engine-bridge.ts +59 -0
  46. package/src/canvas-react/index.ts +50 -0
  47. package/src/canvas-react/pixi-primitive.tsx +202 -0
  48. package/src/canvas-react/pixi-react-adapter.tsx +290 -0
  49. package/src/canvas-react/pixi-react-root-factory.tsx +88 -0
  50. package/src/canvas-react/world-context.ts +328 -0
  51. package/src/core/frame-pacing.ts +100 -0
  52. package/src/core/game-loop.ts +50 -28
  53. package/src/core/seeded-random.ts +7 -7
  54. package/src/core/sim-clock.ts +388 -0
  55. package/src/core/system-runner.ts +17 -63
  56. package/src/core/types.ts +34 -15
  57. package/src/data/data-asset.ts +3 -3
  58. package/src/data/data-check-core.ts +6 -7
  59. package/src/data/data-ref.ts +11 -11
  60. package/src/data/vite-plugin-data.ts +10 -10
  61. package/src/{scene/defaults.ts → defaults.ts} +18 -40
  62. package/src/dev/render-debug-adapter.ts +1 -1
  63. package/src/dev/webgl-frame-capture.ts +1 -1
  64. package/src/ecs/scene-index.ts +439 -0
  65. package/src/ecs/scene-query.ts +43 -0
  66. package/src/{scene → ecs}/user-data.ts +17 -36
  67. package/src/index.ts +7 -9
  68. package/src/input/input-manager.ts +29 -32
  69. package/src/input/input-types.ts +2 -2
  70. package/src/input/schema.ts +5 -5
  71. package/src/loader.ts +57 -0
  72. package/src/manifest/editor-port.ts +69 -0
  73. package/src/manifest/filename.ts +49 -0
  74. package/src/manifest/index.ts +8 -2
  75. package/src/manifest/load-file.ts +11 -0
  76. package/src/manifest/load.ts +65 -77
  77. package/src/manifest/locate.ts +55 -0
  78. package/src/manifest/schema.ts +400 -233
  79. package/src/{scene → physics}/collider-dimensions.ts +3 -3
  80. package/src/physics/physics-registry.ts +1 -1
  81. package/src/{world2d/authoring-2d.ts → pixi/authoring.ts} +24 -11
  82. package/src/pixi/index.ts +43 -0
  83. package/src/{world2d/ingest-iframe-2d.ts → pixi/ingest-iframe.ts} +9 -9
  84. package/src/{world2d/ingest2d.ts → pixi/ingest.ts} +28 -28
  85. package/src/{world2d/physics2d-registry.ts → pixi/physics-registry.ts} +1 -1
  86. package/src/{world2d/scene-capture-2d.ts → pixi/scene-capture.ts} +3 -3
  87. package/src/{world2d/system-adapters-2d.ts → pixi/system-adapters.ts} +2 -2
  88. package/src/react/unmanaged-root-detector.ts +26 -1
  89. package/src/react/use-data.ts +5 -5
  90. package/src/react/use-selection.tsx +15 -42
  91. package/src/react/{game-state.tsx → world-state.tsx} +44 -46
  92. package/src/render/auto-batcher.ts +1 -2
  93. package/src/{scene → render}/instance-mesh.ts +1 -1
  94. package/src/{scene → render}/light-camera-factory.ts +14 -13
  95. package/src/render/lod.ts +17 -0
  96. package/src/{scene → render}/material-factory.ts +8 -7
  97. package/src/{scene → render}/particles-factory.ts +62 -12
  98. package/src/render/render-batch-system.ts +14 -41
  99. package/src/render/render-features.ts +1 -1
  100. package/src/render/render-settings.ts +1 -2
  101. package/src/render/spark-renderer-lifecycle.ts +1 -1
  102. package/src/runtime/create-runtime.ts +352 -463
  103. package/src/runtime/debug-bridge.ts +148 -93
  104. package/src/runtime/debug-registry.ts +79 -54
  105. package/src/runtime/dev-layers.ts +40 -0
  106. package/src/runtime/frame-selector-cache.ts +4 -4
  107. package/src/runtime/game.ts +618 -369
  108. package/src/runtime/gameplay-rng-trap.ts +6 -7
  109. package/src/runtime/input-router.ts +11 -11
  110. package/src/runtime/mount-game.ts +54 -55
  111. package/src/runtime/mount-manifest.ts +154 -150
  112. package/src/runtime/presentation.ts +141 -0
  113. package/src/runtime/render-audio-control.ts +64 -53
  114. package/src/runtime/render-control.ts +45 -51
  115. package/src/runtime/render-seed.ts +3 -4
  116. package/src/runtime/state-bridge.ts +17 -18
  117. package/src/runtime/types.ts +94 -61
  118. package/src/setup/setup-renderer.ts +11 -6
  119. package/src/world3d-react/engine-bridge.ts +46 -33
  120. package/src/world3d-react/index.ts +31 -26
  121. package/src/world3d-react/r3f-adapter.tsx +211 -96
  122. package/src/world3d-react/r3f-root-factory.tsx +91 -0
  123. package/src/world3d-react/renderer-config.ts +137 -0
  124. package/src/world3d-react/world-context.ts +141 -111
  125. package/schemas/entity2d.schema.json +0 -468
  126. package/schemas/prefab.schema.json +0 -9992
  127. package/schemas/scn2d.schema.json +0 -494
  128. package/schemas/vscn.schema.json +0 -10851
  129. package/src/adapter/game-adapter.ts +0 -164
  130. package/src/adapter/ingest/overlay-applier.ts +0 -207
  131. package/src/adapter/ingest/overlay-apply.ts +0 -168
  132. package/src/adapter/ingest/overlay-file.ts +0 -126
  133. package/src/adapter/ingest/overlay-report.ts +0 -176
  134. package/src/animation/gsap-registration.ts +0 -184
  135. package/src/audio/audio-introspection.ts +0 -290
  136. package/src/audio/index.ts +0 -39
  137. package/src/audio/tone-clock-binding.ts +0 -98
  138. package/src/audio/tone-context.ts +0 -175
  139. package/src/audio/tone-offline-render.ts +0 -167
  140. package/src/ecs/component-manager.ts +0 -814
  141. package/src/ecs/game-component.ts +0 -260
  142. package/src/ecs/hmr-swap-report.ts +0 -65
  143. package/src/physics/trigger-dispatch.ts +0 -97
  144. package/src/react/root-adapter.tsx +0 -49
  145. package/src/scene/asset-paths.ts +0 -121
  146. package/src/scene/asset-ref-check.ts +0 -248
  147. package/src/scene/component-registry.ts +0 -51
  148. package/src/scene/parse.ts +0 -204
  149. package/src/scene/scene-apply.ts +0 -407
  150. package/src/scene/scene-diff-schema.ts +0 -115
  151. package/src/scene/scene-diff-types.ts +0 -29
  152. package/src/scene/scene-loader.ts +0 -1526
  153. package/src/scene/scene-query.ts +0 -63
  154. package/src/scene/scene-types.ts +0 -33
  155. package/src/scene/scene-version.ts +0 -40
  156. package/src/scene/schema/animation.ts +0 -46
  157. package/src/scene/schema/audio.ts +0 -25
  158. package/src/scene/schema/entity-ref.ts +0 -78
  159. package/src/scene/schema/entity.ts +0 -189
  160. package/src/scene/schema/index.ts +0 -51
  161. package/src/scene/schema/joint.ts +0 -26
  162. package/src/scene/schema/physics.ts +0 -49
  163. package/src/scene/schema/scene-file.ts +0 -292
  164. package/src/scene/schema/shadow.ts +0 -24
  165. package/src/scene/schema/spline.ts +0 -21
  166. package/src/world2d/asset-paths2d.ts +0 -44
  167. package/src/world2d/capture-to-scene2d.ts +0 -52
  168. package/src/world2d/collision-2d.ts +0 -99
  169. package/src/world2d/entity2d-asset.ts +0 -22
  170. package/src/world2d/index.ts +0 -91
  171. package/src/world2d/physics2d-transform.ts +0 -173
  172. package/src/world2d/physics2d-units.ts +0 -10
  173. package/src/world2d/pixi-game-adapter.ts +0 -439
  174. package/src/world2d/pixi-surface.ts +0 -78
  175. package/src/world2d/scene2d-identity.ts +0 -49
  176. package/src/world2d/scene2d-loader.ts +0 -433
  177. package/src/world2d/schema/entity2d.ts +0 -163
  178. package/src/world2d/schema/physics2d.ts +0 -64
  179. package/src/world2d/schema/sprite.ts +0 -99
  180. package/src/world2d/schema/tilemap.ts +0 -39
  181. package/src/world2d/schema/tuples2d.ts +0 -25
  182. package/src/world2d/transform-writer-2d.ts +0 -42
  183. package/src/world2d/types.ts +0 -74
  184. package/src/world3d-react/behavior.tsx +0 -146
  185. /package/src/{scene → render}/mesh-shadow.ts +0 -0
@@ -0,0 +1,137 @@
1
+ /**
2
+ * world3d-react/renderer-config.ts — the seam a WORLD uses to state how its own frame is rendered.
3
+ *
4
+ * The host owns the `WebGLRenderer` (`ThreeHostContext.renderer`) and configures it with this
5
+ * engine's defaults: ACES tone mapping, sRGB output, PCF-soft shadows. Those defaults are right for
6
+ * a world authored against them and WRONG for a world that was authored against a different
7
+ * engine's pipeline — an imported Godot 3 GLES2 game does gamma-space lighting with no tonemapper
8
+ * at all, so ACES quietly desaturates and darkens every colour its author picked.
9
+ *
10
+ * So a world may DECLARE the pipeline it was authored for, and `createR3FAdapter` applies it to the
11
+ * host's renderer for the life of the mount, restoring what it found on dispose. Three properties
12
+ * of that shape are load-bearing:
13
+ *
14
+ * - **It is per-renderer, never engine-wide.** Every field here is a `WebGLRenderer` instance
15
+ * property, and a play root gets its own renderer (`create-runtime.ts`). Nothing here reaches a
16
+ * module-level three global, so one world's declaration cannot change how the editor's own
17
+ * viewport, another root, or a thumbnail bake renders.
18
+ * - **Absent means "leave the host's value alone".** Every field is optional and an omitted one
19
+ * is never written, so declaring a tone mapping does not silently reset the clear colour.
20
+ * - **It is restored on dispose.** The renderer outlives the mount, so a world that did not put
21
+ * back what it found would leak its pipeline into whatever mounts next.
22
+ *
23
+ * This is deliberately NOT a general render-settings system. It carries what a world can honestly
24
+ * state about its own colour pipeline and nothing else; a property the host fixes at CONSTRUCTION
25
+ * (the WebGL context's `antialias` attribute, and therefore the MSAA sample count) cannot be
26
+ * declared here, because there would be no honest moment to apply it.
27
+ */
28
+
29
+ import type * as THREE from 'three';
30
+
31
+ /** The tone-mapping operators three exposes, named as data rather than as three's numeric enum. */
32
+ export type WorldToneMapping =
33
+ | 'none'
34
+ | 'linear'
35
+ | 'reinhard'
36
+ | 'cineon'
37
+ | 'aces'
38
+ | 'agx'
39
+ | 'neutral';
40
+
41
+ /**
42
+ * The output transfer function the frame is written with.
43
+ *
44
+ * - `srgb` — three's own default and this engine's: linear lighting, sRGB encode on output.
45
+ * - `srgb-linear` — NO output transform. This is what a gamma-space renderer needs: the shading
46
+ * result is already in display space and encoding it a second time washes the frame out.
47
+ */
48
+ export type WorldOutputColorSpace = 'srgb' | 'srgb-linear';
49
+
50
+ /** What a world may declare about the renderer that draws it. Every field is optional; see header. */
51
+ export interface WorldRendererConfig {
52
+ readonly toneMapping?: WorldToneMapping | undefined;
53
+ readonly toneMappingExposure?: number | undefined;
54
+ readonly outputColorSpace?: WorldOutputColorSpace | undefined;
55
+ /**
56
+ * The colour the frame is cleared to, as a CSS hex string. The renderer's existing clear ALPHA
57
+ * is preserved: a stacked canvas is transparent on purpose (`create-runtime.ts` gives every
58
+ * non-bottom root `alpha: true`), and forcing it opaque here would hide every layer below.
59
+ */
60
+ readonly clearColor?: string | undefined;
61
+ }
62
+
63
+ /**
64
+ * Apply `config` to `renderer`, returning the restore function that puts back what was there.
65
+ *
66
+ * `three` is passed in rather than imported for values so the enum constants come from the HOST's
67
+ * three instance — the same identity rule `r3f-adapter.tsx` follows for the scene and camera.
68
+ */
69
+ export function applyWorldRendererConfig(
70
+ three: typeof THREE,
71
+ renderer: THREE.WebGLRenderer,
72
+ config: WorldRendererConfig,
73
+ ): () => void {
74
+ // A host that mounts a world WITHOUT rasterizing it hands the adapter a duck-typed renderer —
75
+ // the editor's design session (`createDesignTimeRenderer`: four members, deliberately never
76
+ // widened) and jsdom test harnesses both do. Such a surface has no colour pipeline to configure:
77
+ // the frame the user sees is drawn by a DIFFERENT renderer (the editor's own), so applying the
78
+ // world's config there is meaningless — and calling `getClearColor` on it is a TypeError that
79
+ // unmounts the whole world at edit time (measured: every Godot port's edit viewport blanked with
80
+ // '"world" failed to mount — renderer.getClearColor is not a function'). Detect the real
81
+ // `WebGLRenderer` surface by the one method this function must call, and no-op otherwise.
82
+ if (typeof renderer.getClearColor !== 'function') {
83
+ return () => {};
84
+ }
85
+ const toneMappings: Record<WorldToneMapping, THREE.ToneMapping> = {
86
+ none: three.NoToneMapping,
87
+ linear: three.LinearToneMapping,
88
+ reinhard: three.ReinhardToneMapping,
89
+ cineon: three.CineonToneMapping,
90
+ aces: three.ACESFilmicToneMapping,
91
+ agx: three.AgXToneMapping,
92
+ neutral: three.NeutralToneMapping,
93
+ };
94
+ const colorSpaces: Record<WorldOutputColorSpace, THREE.ColorSpace> = {
95
+ srgb: three.SRGBColorSpace,
96
+ 'srgb-linear': three.LinearSRGBColorSpace,
97
+ };
98
+
99
+ const restores: (() => void)[] = [];
100
+
101
+ if (config.toneMapping !== undefined) {
102
+ const previous = renderer.toneMapping;
103
+ renderer.toneMapping = toneMappings[config.toneMapping];
104
+ restores.push(() => {
105
+ renderer.toneMapping = previous;
106
+ });
107
+ }
108
+ if (config.toneMappingExposure !== undefined) {
109
+ const previous = renderer.toneMappingExposure;
110
+ renderer.toneMappingExposure = config.toneMappingExposure;
111
+ restores.push(() => {
112
+ renderer.toneMappingExposure = previous;
113
+ });
114
+ }
115
+ if (config.outputColorSpace !== undefined) {
116
+ const previous = renderer.outputColorSpace;
117
+ renderer.outputColorSpace = colorSpaces[config.outputColorSpace];
118
+ restores.push(() => {
119
+ renderer.outputColorSpace = previous;
120
+ });
121
+ }
122
+ if (config.clearColor !== undefined) {
123
+ const previousColor = new three.Color();
124
+ renderer.getClearColor(previousColor);
125
+ // Alpha is READ BACK and re-passed, never assumed: see `clearColor`'s doc above.
126
+ const alpha = renderer.getClearAlpha();
127
+ renderer.setClearColor(new three.Color(config.clearColor), alpha);
128
+ restores.push(() => {
129
+ renderer.setClearColor(previousColor, alpha);
130
+ });
131
+ }
132
+
133
+ return () => {
134
+ // Reverse order, so a field written twice (it cannot be, today) unwinds correctly.
135
+ for (let i = restores.length - 1; i >= 0; i--) restores[i]?.();
136
+ };
137
+ }
@@ -1,18 +1,18 @@
1
1
  /**
2
- * `createR3FWorldContext` — the ENGINE runtime an R3F world owns, split out of
2
+ * `createR3FRootContext` — the ENGINE runtime an R3F world owns, split out of
3
3
  * `./r3f-adapter.tsx` so it stays react-free and headlessly unit-testable
4
4
  * (`test/world3d-react-context-parity.test.tsx`).
5
5
  *
6
6
  * Context parity with the classic first-party adapter
7
- * (`../adapter/vgai-scene-game-adapter.ts`) — closed here after a dogfood
7
+ * (`../adapter/setup-three-root-adapter.ts`) — closed here after a dogfood
8
8
  * build had to shim around three silent gaps:
9
9
  *
10
10
  * 1. **`ctx.debug` / `ctx.random` / `ctx.game` / `ctx.roots` are wired**
11
11
  * exactly the way the classic adapter wires them: the ONE game-scoped
12
12
  * debug registry (`getDebugRegistry(host.game)`) provides
13
- * `ctx.debug = registry.forWorld(id)`, so a Behavior-attached component's
14
- * `init(ctx)` can `ctx.debug.registerStateProvider(...)` and be visible
15
- * to `vgai e2e`/the editor's debug panels — previously a silent no-op.
13
+ * `ctx.debug = registry.forRoot(id)`, so a component's
14
+ * effect can `ctx.debug.registerStateProvider(...)` and be visible
15
+ * to `vgai eval`/the editor's debug panels — previously a silent no-op.
16
16
  * 2. **`ctx.input` is the game-owned `InputManager`** (`host.game.input` —
17
17
  * the same instance the classic adapter uses when hosted), and this
18
18
  * module registers the SAME per-world debug-registry seams the classic
@@ -25,40 +25,89 @@
25
25
  * LOUDLY (console.error naming the path and what breaks) without
26
26
  * failing the mount — an R3F tree with no actions (e.g. a pure
27
27
  * OrbitControls demo, `examples/r3f-first-party`) is legal.
28
- * 3. **Absent subsystems fail loudly at the ctx surface.** An R3F world
29
- * builds no Rapier world, composer, audio, particles, or debug-draw
30
- * reading those `ctx` fields used to yield `undefined` (then fail
31
- * somewhere downstream, or silently no-op). They are now throwing
32
- * getters naming the limitation and the sanctioned alternative.
33
- *
34
- * The `ComponentManager` built here is also exposed on the mounted world
35
- * (`MountedWorldBase.components`) so `Game.queryByComponent` aggregates R3F
36
- * Behavior components exactly like scene-authored ones — see
37
- * `runtime/game.ts`.
38
- */
28
+ * 3. **Absent subsystems are absent from the TYPE.** An R3F world builds no
29
+ * Rapier world, composer, audio, particles, or debug-draw, and
30
+ * {@link R3FGameContext} the type `useGameContext()` hands back — simply
31
+ * does not have those fields. Reaching for one is a compile error at the
32
+ * call site, with the sanctioned alternative in that type's doc comment.
33
+ * */
39
34
 
40
35
  import type * as THREE from 'three';
41
- import type { HostContext } from '../adapter';
36
+ import type { ThreeHostContext } from '../adapter';
42
37
  import type { SystemAdapters } from '../adapter/system-adapter';
43
38
  import {
44
39
  createSeededRandom,
45
40
  DEFAULT_SEEDED_RANDOM_SEED,
46
41
  getSeededRandom,
47
42
  } from '../core/seeded-random';
43
+ import { createSimClock, getSimClock, type SimClockInternal } from '../core/sim-clock';
48
44
  import { createSystemRunner } from '../core/system-runner';
49
- import { type ComponentManager, createComponentManager } from '../ecs/component-manager';
45
+ import { createSceneIndex, type SceneIndex } from '../ecs/scene-index';
50
46
  import {
51
47
  createDebugRegistry,
52
48
  type DebugRegistry,
53
49
  getDebugRegistry,
54
50
  } from '../runtime/debug-registry';
51
+ import { disposeDebrisSubtree } from '../runtime/game';
55
52
  import type { GameContext } from '../runtime/types';
56
53
 
57
54
  /** The classic adapter's default input-map path — the conventional location
58
55
  * every scaffolded project ships (`public/inputmaps/default.inputmap.json`). */
59
56
  export const DEFAULT_INPUT_MAP_PATH = '/inputmaps/default.inputmap.json';
60
57
 
61
- export interface R3FWorldContextOptions {
58
+ /**
59
+ * The engine context an R3F world genuinely provides — what
60
+ * `useGameContext()` hands back.
61
+ *
62
+ * An EXPLICIT `Pick`, never an `Omit`: a field added to `GameContext` later
63
+ * must be deliberately admitted here, so this surface can never silently widen
64
+ * into something an R3F mount does not build.
65
+ *
66
+ * **What an R3F world does NOT have, and what to reach for instead.** These
67
+ * are not oversights — the mount builds none of them, so they are absent from
68
+ * this type and naming one is a compile error rather than a crash mid-render:
69
+ *
70
+ * - **Physics** (`rapierWorld`, `rapier`, `physics`, `collisions`) — an R3F
71
+ * world does not build the first-party Rapier runtime. Use a physics
72
+ * solution inside the fiber tree (e.g. `@react-three/rapier`), or an
73
+ * imperative three root whose `setup(ctx)` receives the first-party Rapier
74
+ * runtime.
75
+ * - **`composer`** — R3F renders through react-three-fiber against the host
76
+ * renderer; there is no postprocessing `EffectComposer` here. Use
77
+ * fiber-native postprocessing inside the tree.
78
+ * - **`audio`** — no first-party audio context is built. Own a Web Audio graph
79
+ * inside the tree (or use drei's audio helpers) and register it as this
80
+ * world's `SystemAdapters.audio` via
81
+ * `ctx.registerSystemAdapter('audio', …)`, so host mute/pause can still
82
+ * silence it — `examples/third-person/src/lib/audio/sfx.ts` is the worked
83
+ * reference.
84
+ * - **`particles`** — no three.quarks particle runtime. Drive particles from
85
+ * the fiber tree.
86
+ * - **`debugDraw`** — no debug-draw helper. Add helper objects to the fiber
87
+ * tree directly.
88
+ *
89
+ * For the full classic runtime, use an imperative three root instead: a module
90
+ * exporting `setup(ctx): Promise<GameCleanup>`, which `SetupThreeRootAdapter`
91
+ * mounts with the complete {@link GameContext}.
92
+ */
93
+ export type R3FGameContext = Pick<
94
+ GameContext,
95
+ | 'scene'
96
+ | 'camera'
97
+ | 'sceneIndex'
98
+ | 'input'
99
+ | 'assets'
100
+ | 'systems'
101
+ | 'clock'
102
+ | 'debug'
103
+ | 'random'
104
+ | 'game'
105
+ | 'roots'
106
+ | 'playtest'
107
+ | 'registerSystemAdapter'
108
+ >;
109
+
110
+ export interface R3FRootContextOptions {
62
111
  /** World id — provenance for every debug-registry registration (matches the
63
112
  * manifest root id by the same convention the classic adapter follows). */
64
113
  readonly id: string;
@@ -72,18 +121,14 @@ export interface R3FWorldContextOptions {
72
121
  readonly inputMapPath?: string | null | undefined;
73
122
  }
74
123
 
75
- /** What {@link createR3FWorldContext} returns — the engine runtime one R3F
124
+ /** What {@link createR3FRootContext} returns — the engine runtime one R3F
76
125
  * world owns, plus the wiring hooks `createR3FAdapter`'s `mount()` drives. */
77
- export interface R3FWorldRuntime {
78
- /** The (partial, loud-on-absence) `GameContext` handed to every attached
79
- * GameComponent. See this module's header for exactly what is present. */
80
- readonly ctx: GameContext;
126
+ export interface R3FRootRuntime {
127
+ /** The context this world's components read through `useGameContext()` —
128
+ * see {@link R3FGameContext} for exactly what an R3F world provides. */
129
+ readonly ctx: R3FGameContext;
81
130
  /** The world-local phase runner `mount().update(dt)` ticks. */
82
131
  readonly systems: ReturnType<typeof createSystemRunner>;
83
- /** The engine's REAL ComponentManager for this world — `<Behavior>`
84
- * attaches through it; expose it as `mounted.components` so
85
- * `Game.queryByComponent` spans this world. */
86
- readonly components: ComponentManager;
87
132
  /** The mounted world's `SystemAdapters` bag (`mounted.systems`) —
88
133
  * `ctx.registerSystemAdapter` writes into it; `debug` is pre-seeded. */
89
134
  readonly systemAdapters: SystemAdapters;
@@ -101,68 +146,48 @@ export interface R3FWorldRuntime {
101
146
  dispose(): void;
102
147
  }
103
148
 
104
- /** Install a throwing getter for a `GameContext` field this world genuinely
105
- * does not build — a component reaching for it fails LOUDLY at its own call
106
- * site, naming the limitation, instead of reading `undefined` and breaking
107
- * somewhere downstream (or silently no-op-ing). Non-enumerable so spreads/
108
- * serialization of the ctx never trip it. */
109
- function defineAbsentCtxField(
110
- target: Record<string, unknown>,
111
- worldId: string,
112
- key: string,
113
- why: string,
114
- ): void {
115
- Object.defineProperty(target, key, {
116
- configurable: true,
117
- enumerable: false,
118
- get(): never {
119
- throw new Error(
120
- `R3F world "${worldId}": ctx.${key} is not available — ${why} ` +
121
- '(An R3F world ctx provides: scene, camera, input, assets, systems, components, ' +
122
- 'debug, random, game, roots, registerSystemAdapter. For the full classic runtime, ' +
123
- 'use a .vscn scene root instead.)',
124
- );
125
- },
126
- });
127
- }
128
-
129
149
  /**
130
- * Build the engine runtime for one R3F world from a `HostContext` — the same
131
- * wiring, at the same layer, as the classic `VgaiSceneGameAdapter.mount()`
150
+ * Build the engine runtime for one R3F world from a `ThreeHostContext` — the same
151
+ * wiring, at the same layer, as the classic `SetupThreeRootAdapter.mount()`
132
152
  * performs for the subsystems an R3F world shares with it (debug, input,
133
- * random, components); loud throwing getters for the ones it doesn't.
153
+ * random). The ones it does not build are not on
154
+ * {@link R3FGameContext} at all.
134
155
  */
135
- export function createR3FWorldContext(
136
- host: HostContext,
137
- options: R3FWorldContextOptions,
138
- ): R3FWorldRuntime {
156
+ export function createR3FRootContext(
157
+ host: ThreeHostContext,
158
+ options: R3FRootContextOptions,
159
+ ): R3FRootRuntime {
139
160
  const { id, inputMapPath = DEFAULT_INPUT_MAP_PATH } = options;
140
161
 
141
162
  // Same observer threading as the classic adapter — profiler sees this
142
163
  // world's phases under the shared game profiler when hosted.
143
- const systems = createSystemRunner(host.game?.profiler.systemObserver, 'threejs');
164
+ const systems = createSystemRunner(host.game?.profiler.systemObserver, 'three');
144
165
 
145
166
  // The mounted world's adapter surface (`mounted.systems`) — declared before
146
167
  // ctx so `ctx.registerSystemAdapter` can close over it (classic parity: a
147
168
  // game-owned capability like networking registers here from component code).
148
169
  const systemAdapters: SystemAdapters = {};
149
170
 
171
+ // The world's live scene index — built in `setSceneCamera` (below), because
172
+ // fiber resolves the scene after this function returns. Same lifetime as
173
+ // `ctx.scene`, which is backfilled at the same moment.
174
+ let sceneIndex: SceneIndex | null = null;
175
+
150
176
  const ctxRaw: Record<string, unknown> = {
151
177
  systems,
152
178
  assets: host.assets,
153
179
  registerSystemAdapter: (kind: keyof SystemAdapters, adapter: SystemAdapters[typeof kind]) => {
154
180
  // biome-ignore lint/suspicious/noExplicitAny: same per-key record write the classic adapter performs; correct by construction
155
181
  (systemAdapters as any)[kind] = adapter;
182
+ host.game?.notifySystemAdaptersChanged();
156
183
  },
157
184
  };
158
- const ctx = ctxRaw as unknown as GameContext;
185
+ const ctx = ctxRaw as unknown as R3FGameContext;
159
186
  if (host.game) {
160
187
  ctx.game = host.game;
161
188
  ctx.roots = host.game.roots;
189
+ ctx.playtest = host.game.playtest ?? null;
162
190
  }
163
- const components = createComponentManager(ctx);
164
- ctxRaw['components'] = components;
165
-
166
191
  // --- ctx.debug (gap 1) — the ONE registry per Game root, shared across
167
192
  // every world mounted onto it; a bare mount with no Game shell gets a
168
193
  // private, mount-local registry instead (same absence precedent as the
@@ -171,11 +196,24 @@ export function createR3FWorldContext(
171
196
  (host.game ? getDebugRegistry(host.game) : null) ??
172
197
  createDebugRegistry({ getTick: () => 0, getSimT: () => 0 });
173
198
  systemAdapters.debug = debugRegistry.adapter;
174
- ctx.debug = debugRegistry.forWorld(id);
199
+ ctx.debug = debugRegistry.forRoot(id);
175
200
  ctx.random =
176
201
  (host.game ? getSeededRandom(host.game) : null) ??
177
202
  createSeededRandom(DEFAULT_SEEDED_RANDOM_SEED);
178
203
 
204
+ // --- ctx.clock (P3) — every world has a sim clock, R3F included. Same
205
+ // game-scoped reach-in as `ctx.random` above; a bare mount with no Game
206
+ // shell gets a private, mount-local clock that nothing flushes (there is no
207
+ // fixed loop to bind timers to). The mount-local fallback uses the SAME
208
+ // `disposeDebrisSubtree` the hosted path uses — it drops
209
+ // the subtree and disposes geometry/materials, with no Rapier owners
210
+ // because this ctx carries no `physics`/`rapierWorld` (the
211
+ // honest "this world owns no bodies").
212
+ const gameClock: SimClockInternal | null = host.game ? getSimClock(host.game) : null;
213
+ const clock: SimClockInternal =
214
+ gameClock ?? createSimClock({ dispose: (obj) => disposeDebrisSubtree(obj, []) });
215
+ ctx.clock = clock;
216
+
179
217
  // --- ctx.input + the debug-registry input seams (gap 3) — the game-owned
180
218
  // InputManager, polled by the game-scoped runner each frame; this world
181
219
  // registers the SAME per-world seams the classic adapter registers so
@@ -228,63 +266,55 @@ export function createR3FWorldContext(
228
266
  });
229
267
  }
230
268
  } else {
231
- defineAbsentCtxField(
232
- ctxRaw,
233
- id,
234
- 'input',
235
- 'input is game-owned and this mount has no Game host (host.game is absent — bare ' +
236
- 'harness/foreign host); mount through the vgai host to get the shared InputManager.',
237
- );
269
+ // `input` IS a field of every R3F world's ctx — it is game-owned, and this
270
+ // particular mount has no Game to own it (a bare harness or a foreign
271
+ // host). A throwing getter names that at the read, instead of handing back
272
+ // `undefined` that breaks deep inside a frame callback. Non-enumerable so
273
+ // spreads/serialization of the ctx never trip it.
274
+ Object.defineProperty(ctxRaw, 'input', {
275
+ configurable: true,
276
+ enumerable: false,
277
+ get(): never {
278
+ throw new Error(
279
+ `R3F world "${id}": ctx.input is game-owned and this mount has no Game host ` +
280
+ '(host.game is absent — bare harness/foreign host); mount through the vgai host ' +
281
+ 'to get the shared InputManager.',
282
+ );
283
+ },
284
+ });
238
285
  }
239
286
 
240
- // --- Loud absence for the classic-only subsystems (gap 1's honesty rule:
241
- // a thrown error naming the limitation beats a silent undefined). ---
242
- const rapierWhy =
243
- 'R3F worlds do not build the first-party Rapier physics runtime; use a physics ' +
244
- 'solution inside the fiber tree (e.g. @react-three/rapier) or a classic .vscn world root.';
245
- defineAbsentCtxField(ctxRaw, id, 'rapierWorld', rapierWhy);
246
- defineAbsentCtxField(ctxRaw, id, 'rapier', rapierWhy);
247
- defineAbsentCtxField(ctxRaw, id, 'physics', rapierWhy);
248
- defineAbsentCtxField(ctxRaw, id, 'collisions', rapierWhy);
249
- defineAbsentCtxField(
250
- ctxRaw,
251
- id,
252
- 'composer',
253
- 'R3F worlds render through react-three-fiber against the host renderer; there is no ' +
254
- 'postprocessing EffectComposer here (use fiber-native postprocessing inside the tree).',
255
- );
256
- defineAbsentCtxField(
257
- ctxRaw,
258
- id,
259
- 'audio',
260
- 'R3F worlds build no first-party audio context; use Web Audio directly (or drei audio ' +
261
- 'helpers) inside the tree.',
262
- );
263
- defineAbsentCtxField(
264
- ctxRaw,
265
- id,
266
- 'particles',
267
- 'R3F worlds build no three.quarks particle runtime; drive particles from the fiber tree.',
268
- );
269
- defineAbsentCtxField(
270
- ctxRaw,
271
- id,
272
- 'debugDraw',
273
- 'R3F worlds build no debug-draw helper; add helper objects to the fiber tree directly.',
274
- );
275
-
276
287
  return {
277
288
  ctx,
278
289
  systems,
279
- components,
280
290
  systemAdapters,
281
291
  inputMapReady,
282
292
  setSceneCamera(scene, camera): void {
293
+ const sameScene = ctxRaw['scene'] === scene && sceneIndex !== null;
283
294
  ctxRaw['scene'] = scene;
284
295
  ctxRaw['camera'] = camera;
296
+ // The live scene index (P2 `observe`) is a REAL value here, not a
297
+ // throwing getter — every three world has one. It can only be built once
298
+ // fiber has resolved the scene, which is the same moment `ctx.scene`
299
+ // itself becomes readable, so index and scene share one lifetime. A
300
+ // second commit (fiber handing back a different scene) replaces it.
301
+ //
302
+ // Keyed on the SCENE, so a camera-only backfill (a world swapping the
303
+ // default camera after mount) leaves the index — and every `observe`
304
+ // subscription hanging off it — alone.
305
+ if (sameScene) return;
306
+ sceneIndex?.dispose();
307
+ sceneIndex = createSceneIndex(scene);
308
+ ctxRaw['sceneIndex'] = sceneIndex;
285
309
  },
286
310
  dispose(): void {
287
- components.clear();
311
+ // Same ownership rule as the classic adapter's teardown: dispose ONLY a
312
+ // clock this mount created. The game-scoped one belongs to
313
+ // `GameInternal.dispose()` — killing it here would freeze sim time and
314
+ // cancel every SIBLING world's timers.
315
+ if (!gameClock) clock.dispose();
316
+ sceneIndex?.dispose();
317
+ sceneIndex = null;
288
318
  // Scoped strip when a Game hosts this world (sibling roots stay live);
289
319
  // a bare standalone mount owns its private registry and clears it all.
290
320
  if (host.game) debugRegistry.strip(id);