@vgai/engine 0.4.1 → 0.5.0-canary.20260719.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 (103) hide show
  1. package/README.md +48 -15
  2. package/package.json +11 -25
  3. package/schemas/engine-capabilities.json +10 -10
  4. package/schemas/entity2d.schema.json +468 -0
  5. package/schemas/mat.schema.json +2 -33
  6. package/schemas/prefab.schema.json +16 -172
  7. package/schemas/scn2d.schema.json +42 -23
  8. package/schemas/vgai-game.schema.json +34 -0
  9. package/schemas/vscn.schema.json +16 -172
  10. package/src/adapter/authoring.ts +152 -2
  11. package/src/adapter/colyseus-networking-adapter.ts +35 -1
  12. package/src/adapter/first-party-systems.ts +7 -1
  13. package/src/adapter/game-adapter.ts +13 -0
  14. package/src/adapter/index.ts +25 -0
  15. package/src/adapter/rapier-physics-adapter.ts +55 -2
  16. package/src/adapter/system-adapter.ts +249 -2
  17. package/src/adapter/vgai-scene-game-adapter.ts +149 -25
  18. package/src/ai/navigation.ts +28 -0
  19. package/src/animation/clip-map.ts +1 -8
  20. package/src/animation/theatre-director.ts +50 -0
  21. package/src/animation/xstate-animation-binding.ts +6 -0
  22. package/src/audio/audio-introspection.ts +290 -0
  23. package/src/audio/tone-context.ts +46 -0
  24. package/src/dev/chrome-trace.ts +153 -0
  25. package/src/dev/performance-profiler.ts +93 -6
  26. package/src/dev/render-debug-adapter.ts +199 -0
  27. package/src/dev/render-memory.ts +243 -0
  28. package/src/dev/webgl-frame-capture.ts +424 -0
  29. package/src/ecs/component-manager.ts +43 -10
  30. package/src/ecs/game-component.ts +39 -10
  31. package/src/input/input-manager.ts +24 -19
  32. package/src/input/input-types.ts +1 -1
  33. package/src/loader.ts +7 -0
  34. package/src/manifest/load.ts +14 -0
  35. package/src/manifest/schema.ts +65 -0
  36. package/src/react/game-state.tsx +1 -1
  37. package/src/render/render-batch-system.ts +26 -12
  38. package/src/render/spark-renderer-lifecycle.ts +64 -0
  39. package/src/runtime/create-runtime.ts +33 -5
  40. package/src/runtime/debug-bridge.ts +5 -5
  41. package/src/runtime/game.ts +102 -20
  42. package/src/runtime/mount-manifest.ts +1 -1
  43. package/src/runtime/render-control.ts +121 -0
  44. package/src/runtime/types.ts +1 -1
  45. package/src/scene/asset-loaders.ts +77 -3
  46. package/src/scene/instance-mesh.ts +25 -0
  47. package/src/scene/material-factory.ts +4 -15
  48. package/src/scene/mesh-shadow.ts +18 -0
  49. package/src/scene/particles-factory.ts +59 -0
  50. package/src/scene/scene-loader.ts +41 -16
  51. package/src/scene/schema/instances.ts +1 -2
  52. package/src/scene/schema/material.ts +83 -94
  53. package/src/scene/schema/mesh.ts +76 -90
  54. package/src/scene/schema/scene-file.ts +1 -2
  55. package/src/scene/user-data.ts +30 -14
  56. package/src/setup/setup-renderer.ts +6 -1
  57. package/src/world2d/asset-paths2d.ts +44 -0
  58. package/src/world2d/collision-2d.ts +7 -14
  59. package/src/world2d/entity2d-asset.ts +22 -0
  60. package/src/world2d/index.ts +27 -2
  61. package/src/world2d/physics2d-transform.ts +173 -0
  62. package/src/world2d/physics2d-units.ts +10 -0
  63. package/src/world2d/pixi-game-adapter.ts +148 -36
  64. package/src/world2d/scene2d-identity.ts +49 -0
  65. package/src/world2d/scene2d-loader.ts +243 -119
  66. package/src/world2d/schema/entity2d.ts +51 -33
  67. package/src/world2d/schema/physics2d.ts +14 -3
  68. package/src/world2d/schema/sprite.ts +32 -4
  69. package/src/world2d/schema/tilemap.ts +26 -9
  70. package/src/world2d/transform-writer-2d.ts +29 -11
  71. package/src/world2d/types.ts +21 -8
  72. package/src/world3d-react/behavior.tsx +138 -0
  73. package/src/world3d-react/engine-bridge.ts +48 -0
  74. package/src/world3d-react/index.ts +44 -0
  75. package/src/world3d-react/r3f-adapter.tsx +303 -0
  76. package/src/world3d-react/world-context.ts +294 -0
  77. package/vendor/realism-effects/LICENSE.md +21 -0
  78. package/vendor/realism-effects/UPSTREAM.md +19 -0
  79. package/vendor/realism-effects/dist/index.cjs +3447 -0
  80. package/vendor/realism-effects/dist/index.d.ts +59 -0
  81. package/vendor/realism-effects/dist/index.js +3434 -0
  82. package/vendor/realism-effects/package.json +23 -0
  83. package/src/character/cloth-sim.ts +0 -533
  84. package/src/character/spring-chain.ts +0 -307
  85. package/src/humanoid/body.ts +0 -663
  86. package/src/humanoid/clips.ts +0 -149
  87. package/src/humanoid/compose.ts +0 -209
  88. package/src/humanoid/generate.ts +0 -189
  89. package/src/humanoid/index.ts +0 -36
  90. package/src/humanoid/schema.ts +0 -108
  91. package/src/humanoid/skeleton.ts +0 -345
  92. package/src/react/humanoid-bake.document.tsx +0 -337
  93. package/src/scene/geometries/index.ts +0 -7
  94. package/src/scene/geometries/terrain.ts +0 -42
  95. package/src/scene/geometry-registry.ts +0 -42
  96. package/src/scene/instance-registry.ts +0 -84
  97. package/src/scene/instancers/grid.ts +0 -38
  98. package/src/scene/instancers/index.ts +0 -7
  99. package/src/scene/material-registry.ts +0 -73
  100. package/src/scene/materials/index.ts +0 -7
  101. package/src/scene/materials/water.ts +0 -56
  102. package/src/world2d/components-2d.ts +0 -86
  103. package/tools/humanoid-bake.tool.ts +0 -274
@@ -0,0 +1,303 @@
1
+ /**
2
+ * `createR3FAdapter` — wraps a react-three-fiber tree as a first-party
3
+ * `GameAdapter` so an R3F scene mounts as an ordinary `kind: "threejs"`
4
+ * world: the engine hands `mount()` a `HostContext` (ITS OWN renderer +
5
+ * canvas + gated loop, per `../adapter/host-context.ts`); this returns a
6
+ * `MountedThreeWorld` (`../adapter/game-adapter.ts`) whose `scene`/`camera`
7
+ * are fiber's REAL `THREE.Scene`/`THREE.Camera` instances.
8
+ *
9
+ * Upstreamed from `examples/r3f-first-party/src/r3f-adapter.tsx` (R4,
10
+ * docs/R3F-FOLLOW-THROUGH-SPEC.md — the `@engine/world3d-react` opt-in
11
+ * module R3F-FIRST-PARTY-DESIGN §1.B Phase 2 names, mirroring how `world2d/`
12
+ * is pixi's opt-in home). The mount semantics are byte-for-byte the
13
+ * example's live-proven Phase-1 bridge; the one addition is the optional
14
+ * `components` registry threaded into `EngineBridge` so `<Behavior>` can
15
+ * resolve project component classes without the engine importing project
16
+ * code. External adopters (alien-stories-style) keep the exact same calling
17
+ * contract.
18
+ *
19
+ * three.js identity (docs/ADAPTER-AUTHORING-GUIDE.md §2): this module never
20
+ * imports `three` itself for scene objects — fiber's internal `import * as
21
+ * THREE from 'three'` must resolve to the SAME instance the engine's
22
+ * `HostContext.three` points at, which the importing project guarantees via
23
+ * its Vite `resolve.dedupe: ['three', 'react', 'react-dom']` (see
24
+ * `examples/r3f-first-party/vite.config.ts`).
25
+ */
26
+
27
+ import { advance, createRoot, extend, type RootState } from '@react-three/fiber';
28
+ import { createElement, type ReactNode } from 'react';
29
+ import type { GameAdapter, HostContext, MountedThreeWorld } from '../adapter';
30
+ import { type BehaviorRegistry, EngineBridge, type EngineBridgeValue } from './engine-bridge';
31
+ import { createR3FWorldContext } from './world-context';
32
+
33
+ /** What {@link createR3FAdapter} needs to build one `GameAdapter`. */
34
+ export interface CreateR3FAdapterOptions {
35
+ /** Stable id (telemetry/registry/conformance) — `GameAdapter.id`. */
36
+ readonly id: string;
37
+ /** The R3F scene tree to mount — drei helpers, `useFrame` hooks, etc. all
38
+ * work unchanged (design §1.C: "drei works unchanged (it is fiber-context
39
+ * userland)"). */
40
+ readonly content: ReactNode;
41
+ /** The project's behavior registry — `<Behavior type="…">` resolves against
42
+ * it through `EngineBridge`. Optional: a tree with no `<Behavior>` usage
43
+ * needs none; a `<Behavior>` rendered without it degrades loudly (warn,
44
+ * no attach) rather than crashing. */
45
+ readonly components?: BehaviorRegistry | undefined;
46
+ /** Input map loaded through the game-owned `Game.loadInputMap` (load-once
47
+ * across roots). Defaults to the classic adapter's conventional path,
48
+ * `/inputmaps/default.inputmap.json`; `null` opts out (a world with no
49
+ * actions). A missing/invalid map degrades loudly without failing the
50
+ * mount — see `world-context.ts`. */
51
+ readonly inputMapPath?: string | null | undefined;
52
+ }
53
+
54
+ /**
55
+ * Build a `GameAdapter` that mounts `options.content` through
56
+ * react-three-fiber, gated entirely by the host's own loop and rendering
57
+ * through the host's own `WebGLRenderer` — never a second renderer, never a
58
+ * second `requestAnimationFrame` loop (design §1.C).
59
+ */
60
+ export function createR3FAdapter(options: CreateR3FAdapterOptions): GameAdapter {
61
+ const { id, content, components: registry, inputMapPath } = options;
62
+
63
+ return {
64
+ id,
65
+
66
+ async mount(host: HostContext): Promise<MountedThreeWorld> {
67
+ // Headless honesty (design §1.D; `docs/ADAPTER-AUTHORING-GUIDE.md` §2's
68
+ // "Your `mount()` must work headless" rule): the conformance kit / CI
69
+ // runs `mount()` in Node — no canvas, no WebGL, no fiber reconciler.
70
+ // Guard the ENTIRE fiber mount behind `!host.headless` and return a
71
+ // bare, real scene + camera instead (still `instanceof
72
+ // host.three.Scene` / `.Camera` — the identity rule holds even here,
73
+ // since this uses the host's own `three` instance, not a fresh
74
+ // import). Residual: the hierarchy is empty under headless
75
+ // conformance — recorded as design O2, not hidden; a react-nil-style
76
+ // headless reconciler mount is the known fix, out of scope for v1.
77
+ if (host.headless) {
78
+ const scene = new host.three.Scene();
79
+ const camera = new host.three.PerspectiveCamera();
80
+ return {
81
+ kind: 'threejs',
82
+ scene,
83
+ camera,
84
+ drivesOwnLoop: false,
85
+ dispose(): void {
86
+ /* nothing was ever mounted */
87
+ },
88
+ };
89
+ }
90
+
91
+ // Fiber v9 made the THREE catalogue tree-shakeable: `<Canvas>` calls
92
+ // `extend(THREE)` for you, a bare `createRoot` does NOT — without this,
93
+ // the FIRST three intrinsic in the tree (`<color>`, `<ambientLight>`,
94
+ // …) throws "X is not part of the THREE namespace! Did you forget to
95
+ // extend?" at reconcile time. Extending with `host.three` (not a fresh
96
+ // `import * as THREE`) keeps the catalogue on the host's deduped three
97
+ // instance — the same identity rule the returned scene/camera rely on.
98
+ // `extend` merges into a module-global catalogue, so calling it once
99
+ // per mount is idempotent.
100
+ extend(host.three as unknown as Parameters<typeof extend>[0]);
101
+
102
+ // W3 (docs/R3F-SURFACE-SPIKE.md) — the engine component runtime this
103
+ // world OWNS: a real SystemRunner + ComponentManager pair, provided to
104
+ // the R3F tree via `EngineBridge` so `<Behavior>` (./behavior.tsx) can
105
+ // attach registry GameComponents with the engine's REAL attach path.
106
+ // `update(dt)` below runs the phases under the host loop, so attached
107
+ // components tick in engine phase order and freeze under pause exactly
108
+ // like `useFrame` work does.
109
+ //
110
+ // HONESTY NOTE: the manager's ctx is still a PARTIAL GameContext, but
111
+ // the shared game-scoped subsystems are now REAL classic-adapter parity
112
+ // (`./world-context.ts`): `ctx.debug`/`ctx.random`/`ctx.game`/`ctx.roots`,
113
+ // and `ctx.input` (the game-owned InputManager, with this world's
114
+ // virtual-input/actions/trace debug seams registered and the project's
115
+ // input map loaded). The subsystems an R3F world genuinely does not
116
+ // build (Rapier, composer, audio, particles, debugDraw) are THROWING
117
+ // getters naming the limitation — never a silent `undefined`.
118
+ // `scene`/`camera` are backfilled right after fiber's first commit
119
+ // resolves them, before any tick can run.
120
+ const runtime = createR3FWorldContext(host, { id, inputMapPath });
121
+ const { systems, components } = runtime;
122
+ const bridge: EngineBridgeValue = { components, registry };
123
+ // Actions must exist before any Behavior component's init() reads them —
124
+ // wait for the (never-rejecting) input-map load before the first commit.
125
+ await runtime.inputMapReady;
126
+
127
+ const canvas = host.surface.canvas;
128
+ const root = createRoot(canvas);
129
+
130
+ // `RootState` (the live scene/camera/gl fiber built) only arrives via
131
+ // the `onCreated` callback — `root.render()`'s return value is
132
+ // TECHNICALLY the same store, but `onCreated` is the hook the design
133
+ // doc's §1.C sketch names, and waiting for it (rather than assuming
134
+ // the first commit already ran synchronously) is the honest choice
135
+ // under React 19's concurrent renderer, which does not guarantee a
136
+ // synchronous first commit the way legacy ReactDOM.render did.
137
+ let resolveState!: (state: RootState) => void;
138
+ const statePromise = new Promise<RootState>((resolve) => {
139
+ resolveState = resolve;
140
+ });
141
+
142
+ // `root.configure()` is ASYNC in fiber v9 (`Promise<ReconcilerRoot>`)
143
+ // — the design doc's §1.C sketch shows it called synchronously; the
144
+ // real v9 API (verified against
145
+ // `node_modules/@react-three/fiber/dist/declarations/src/core/index.d.ts`)
146
+ // requires awaiting it before `render()`.
147
+ await root.configure({
148
+ // The engine's renderer, not a second one (design §1.C) — fiber
149
+ // renders THROUGH `host.renderer` instead of constructing its own
150
+ // `WebGLRenderer`.
151
+ gl: host.renderer,
152
+ // The engine's gated loop is the ONLY loop — fiber must never run
153
+ // its own rAF (that would defeat editor pause; design §1.C's
154
+ // `drivesOwnLoop: false` contract, guide §2's loop-model note).
155
+ frameloop: 'never',
156
+ // `RenderProps.size` types as the FULL `Size` (width/height/top/left),
157
+ // not `Partial<Size>` — the design sketch's `{ width, height }` alone
158
+ // does not satisfy fiber v9's real type, so `top`/`left` are pinned
159
+ // to 0 (this bridge always fills its whole canvas; no offset
160
+ // viewport in v1).
161
+ size: { width: host.surface.width, height: host.surface.height, top: 0, left: 0 },
162
+ // `events` deliberately left at fiber's default (binds to
163
+ // `gl.domElement`, i.e. `host.surface.canvas`) — design O1's open
164
+ // question about a delegating multi-canvas input router only
165
+ // matters for a shell+net composite; this bridge is a genuine
166
+ // single-canvas world, the case O1 already says is unaffected.
167
+ onCreated: (state) => resolveState(state),
168
+ });
169
+ // No `<StrictMode>` (design §1.C: "no StrictMode: host mounts once")
170
+ // — this bridge mounts exactly once per `mount()` call; StrictMode's
171
+ // deliberate double-invoke of effects would double-subscribe
172
+ // `useFrame` callbacks against a host loop that only ticks once per
173
+ // frame. The `EngineBridge` provider threads the component runtime
174
+ // (above) to any `<Behavior>` wrapper in the tree (W3).
175
+ root.render(createElement(EngineBridge.Provider, { value: bridge }, content));
176
+ // A reconcile-time crash (e.g. a missing `extend` catalogue entry)
177
+ // surfaces as an uncaught window error and `onCreated` never fires —
178
+ // without this guard, `mount()` would await `statePromise` FOREVER and
179
+ // silently wedge every world declared after this one (roots mount
180
+ // sequentially). Convert that class of failure into a loud mount error.
181
+ const state = await new Promise<RootState>((resolve, reject) => {
182
+ const onError = (event: ErrorEvent) => {
183
+ cleanup();
184
+ root.unmount();
185
+ runtime.dispose();
186
+ reject(
187
+ new Error(
188
+ `r3f-adapter: fiber crashed before its first commit — ${event.message} ` +
189
+ '(mount() fails loudly instead of hanging on onCreated)',
190
+ ),
191
+ );
192
+ };
193
+ const timer = setTimeout(() => {
194
+ cleanup();
195
+ root.unmount();
196
+ runtime.dispose();
197
+ reject(
198
+ new Error(
199
+ 'r3f-adapter: onCreated did not fire within 10s — the R3F tree never reached ' +
200
+ 'its first commit (mount() fails loudly instead of hanging)',
201
+ ),
202
+ );
203
+ }, 10_000);
204
+ const cleanup = () => {
205
+ clearTimeout(timer);
206
+ window.removeEventListener('error', onError);
207
+ };
208
+ window.addEventListener('error', onError);
209
+ void statePromise.then((s) => {
210
+ cleanup();
211
+ resolve(s);
212
+ });
213
+ });
214
+
215
+ // Clock hardening (field-proven by the first external adopter,
216
+ // alien-stories' proto): fiber's internal `update()` calls
217
+ // `state.clock.getDelta()` BEFORE its `frameloop:'never'` branch, and a
218
+ // RUNNING (or autoStart) `THREE.Clock` accumulates WALL time into
219
+ // `elapsedTime` as a side effect — skewing the deltas the 'never'
220
+ // branch derives from the game timestamps `update(dt)` feeds below.
221
+ // Stopped + autoStart=false makes `getDelta()` a pure no-op, so
222
+ // `useFrame` deltas come from game time alone.
223
+ state.clock.autoStart = false;
224
+ state.clock.stop();
225
+
226
+ // Backfill the component runtime's ctx with fiber's real scene/camera
227
+ // (W3 — see the HONESTY NOTE above). This happens before the first
228
+ // `update(dt)` tick, so no component ever observes them missing.
229
+ runtime.setSceneCamera(state.scene, state.camera);
230
+
231
+ // The engine drives every `useFrame` through the mounted world's
232
+ // `update(dt)` hook — NOT `host.loop.onUpdate`. The distinction is the
233
+ // whole pause story: `runFrameImpl` (`runtime/game.ts`) calls
234
+ // `mounted.update?.(dt)` per substep for a host-driven
235
+ // (`drivesOwnLoop: false`) world and SKIPS it while that world is
236
+ // frozen, and `Game.play.step()` ticks it exactly once — whereas the
237
+ // raw loop's `onUpdate` extraUpdaters fire unconditionally, pause or
238
+ // not (they exist for host-level concerns; `create-runtime.ts` runs
239
+ // them outside `game.runFrame`'s gate). Advancing fiber from
240
+ // `onUpdate` therefore LOOKS right and silently breaks acceptance
241
+ // gate 2 — proven by the 36-r3f-first-party e2e, whose paused
242
+ // instance-matrix samples kept moving until this moved to `update`.
243
+ //
244
+ // `advance(timestamp, runGlobalEffects, state)`'s `timestamp` is
245
+ // consumed as `THREE.Clock.elapsedTime` DIRECTLY when
246
+ // `frameloop:'never'` (verified against fiber's `update()`
247
+ // implementation, not just its `.d.ts` — the declared signature alone
248
+ // doesn't say this): `delta = timestamp - clock.elapsedTime;
249
+ // clock.elapsedTime = timestamp`. So `timestamp` must be a
250
+ // monotonically increasing SECONDS value — GAME time, not wall time:
251
+ // `update` simply isn't called while this world is frozen, so
252
+ // accumulating its `dt` (seconds; the loop runs a fixed 1/60 timestep)
253
+ // means fiber's clock does not advance across a pause. Wall clock
254
+ // (`performance.now()`) would leak the pause duration into the first
255
+ // resumed frame as one giant `useFrame` delta — the exact "time passed
256
+ // while frozen" illusion acceptance gate 2 forbids.
257
+ let elapsed = 0;
258
+
259
+ return {
260
+ kind: 'threejs',
261
+ // Fiber's REAL `THREE.Scene`/`THREE.Camera` — `three` identity is
262
+ // deduped to the host's single instance by the project's Vite
263
+ // config (see this file's header comment), so these pass the
264
+ // editor's `instanceof THREE.Scene` / `instanceof THREE.Camera`
265
+ // gates unmodified (design acceptance gate 3). `state.camera`'s
266
+ // type (`Camera = (OrthographicCamera | PerspectiveCamera) & {
267
+ // manual?: boolean }`) is a structural subtype of `THREE.Camera`, so
268
+ // no cast is needed.
269
+ scene: state.scene,
270
+ camera: state.camera,
271
+ drivesOwnLoop: false,
272
+ // The engine's REAL ComponentManager for this world — what makes
273
+ // `Game.queryByComponent` see `<Behavior>`-attached components
274
+ // (`runtime/game.ts` aggregates this optional capability for every
275
+ // non-first-party mount that exposes one).
276
+ components,
277
+ // Adapter surface: `debug` pre-seeded (the shared game registry's
278
+ // adapter); game code adds capabilities via
279
+ // `ctx.registerSystemAdapter` exactly as in a classic world.
280
+ systems: runtime.systemAdapters,
281
+ update(dt: number): void {
282
+ elapsed += dt;
283
+ // Engine phases FIRST (attached GameComponents mutate transforms),
284
+ // then fiber's advance (useFrame callbacks + the actual render see
285
+ // the fresh state). Both behavior models — `useFrame` and
286
+ // `<Behavior>` — are host-gated: neither runs while frozen.
287
+ systems.run(dt);
288
+ advance(elapsed, true, state);
289
+ },
290
+ resize(width: number, height: number): void {
291
+ state.setSize(width, height);
292
+ },
293
+ dispose(): void {
294
+ // Unmount FIRST so <Behavior> effect cleanups detach through the
295
+ // live manager, then tear the runtime down (components.clear +
296
+ // scoped debug-registry strip).
297
+ root.unmount();
298
+ runtime.dispose();
299
+ },
300
+ };
301
+ },
302
+ };
303
+ }
@@ -0,0 +1,294 @@
1
+ /**
2
+ * `createR3FWorldContext` — the ENGINE runtime an R3F world owns, split out of
3
+ * `./r3f-adapter.tsx` so it stays react-free and headlessly unit-testable
4
+ * (`test/world3d-react-context-parity.test.tsx`).
5
+ *
6
+ * Context parity with the classic first-party adapter
7
+ * (`../adapter/vgai-scene-game-adapter.ts`) — closed here after a dogfood
8
+ * build had to shim around three silent gaps:
9
+ *
10
+ * 1. **`ctx.debug` / `ctx.random` / `ctx.game` / `ctx.roots` are wired**
11
+ * exactly the way the classic adapter wires them: the ONE game-scoped
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.
16
+ * 2. **`ctx.input` is the game-owned `InputManager`** (`host.game.input` —
17
+ * the same instance the classic adapter uses when hosted), and this
18
+ * module registers the SAME per-world debug-registry seams the classic
19
+ * adapter registers (`setVirtualInputTarget`/`setInputActionsSource`/
20
+ * `setInputTraceSource`), which is what makes `game.input.*` (the bot
21
+ * input doctrine) work instead of throwing `DEBUG_INPUT_UNAVAILABLE`.
22
+ * The project's input map is loaded through `Game.loadInputMap` (the
23
+ * game-owned, load-once path every root shares) with the SAME default
24
+ * path the classic adapter uses. A missing/unparseable map degrades
25
+ * LOUDLY (console.error naming the path and what breaks) without
26
+ * failing the mount — an R3F tree with no actions (e.g. a pure
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
+ */
39
+
40
+ import type * as THREE from 'three';
41
+ import type { HostContext } from '../adapter';
42
+ import type { SystemAdapters } from '../adapter/system-adapter';
43
+ import {
44
+ createSeededRandom,
45
+ DEFAULT_SEEDED_RANDOM_SEED,
46
+ getSeededRandom,
47
+ } from '../core/seeded-random';
48
+ import { createSystemRunner } from '../core/system-runner';
49
+ import { type ComponentManager, createComponentManager } from '../ecs/component-manager';
50
+ import {
51
+ createDebugRegistry,
52
+ type DebugRegistry,
53
+ getDebugRegistry,
54
+ } from '../runtime/debug-registry';
55
+ import type { GameContext } from '../runtime/types';
56
+
57
+ /** The classic adapter's default input-map path — the conventional location
58
+ * every scaffolded project ships (`public/inputmaps/default.inputmap.json`). */
59
+ export const DEFAULT_INPUT_MAP_PATH = '/inputmaps/default.inputmap.json';
60
+
61
+ export interface R3FWorldContextOptions {
62
+ /** World id — provenance for every debug-registry registration (matches the
63
+ * manifest root id by the same convention the classic adapter follows). */
64
+ readonly id: string;
65
+ /**
66
+ * Input map to load through `Game.loadInputMap` (game-owned, load-once —
67
+ * several roots asking for the SAME path share one load; competing paths
68
+ * throw there). Defaults to {@link DEFAULT_INPUT_MAP_PATH}, the classic
69
+ * adapter's own default. Pass `null` to skip loading (a world with no
70
+ * actions). Ignored when no `host.game` is present.
71
+ */
72
+ readonly inputMapPath?: string | null | undefined;
73
+ }
74
+
75
+ /** What {@link createR3FWorldContext} returns — the engine runtime one R3F
76
+ * 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;
81
+ /** The world-local phase runner `mount().update(dt)` ticks. */
82
+ 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
+ /** The mounted world's `SystemAdapters` bag (`mounted.systems`) —
88
+ * `ctx.registerSystemAdapter` writes into it; `debug` is pre-seeded. */
89
+ readonly systemAdapters: SystemAdapters;
90
+ /** Resolves once the input map load settles (immediately when skipped).
91
+ * Never rejects — a failed load reports loudly and resolves. */
92
+ readonly inputMapReady: Promise<void>;
93
+ /** Backfill fiber's real scene/camera onto `ctx` after the first commit
94
+ * resolves them — before any tick can run. `camera` is fiber's union
95
+ * (perspective OR orthographic) — the ctx surface exposes whatever the
96
+ * tree declared, exactly as the mounted world does. */
97
+ setSceneCamera(scene: THREE.Scene, camera: THREE.Camera): void;
98
+ /** Detach every component and strip this world's debug registrations
99
+ * (scoped, like the classic adapter's teardown — sibling roots and
100
+ * react-door registrations are untouched when a Game hosts this world). */
101
+ dispose(): void;
102
+ }
103
+
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
+ /**
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()`
132
+ * performs for the subsystems an R3F world shares with it (debug, input,
133
+ * random, components); loud throwing getters for the ones it doesn't.
134
+ */
135
+ export function createR3FWorldContext(
136
+ host: HostContext,
137
+ options: R3FWorldContextOptions,
138
+ ): R3FWorldRuntime {
139
+ const { id, inputMapPath = DEFAULT_INPUT_MAP_PATH } = options;
140
+
141
+ // Same observer threading as the classic adapter — profiler sees this
142
+ // world's phases under the shared game profiler when hosted.
143
+ const systems = createSystemRunner(host.game?.profiler.systemObserver, 'threejs');
144
+
145
+ // The mounted world's adapter surface (`mounted.systems`) — declared before
146
+ // ctx so `ctx.registerSystemAdapter` can close over it (classic parity: a
147
+ // game-owned capability like networking registers here from component code).
148
+ const systemAdapters: SystemAdapters = {};
149
+
150
+ const ctxRaw: Record<string, unknown> = {
151
+ systems,
152
+ assets: host.assets,
153
+ registerSystemAdapter: (kind: keyof SystemAdapters, adapter: SystemAdapters[typeof kind]) => {
154
+ // biome-ignore lint/suspicious/noExplicitAny: same per-key record write the classic adapter performs; correct by construction
155
+ (systemAdapters as any)[kind] = adapter;
156
+ },
157
+ };
158
+ const ctx = ctxRaw as unknown as GameContext;
159
+ if (host.game) {
160
+ ctx.game = host.game;
161
+ ctx.roots = host.game.roots;
162
+ }
163
+ const components = createComponentManager(ctx);
164
+ ctxRaw['components'] = components;
165
+
166
+ // --- ctx.debug (gap 1) — the ONE registry per Game root, shared across
167
+ // every world mounted onto it; a bare mount with no Game shell gets a
168
+ // private, mount-local registry instead (same absence precedent as the
169
+ // classic adapter). ---
170
+ const debugRegistry: DebugRegistry =
171
+ (host.game ? getDebugRegistry(host.game) : null) ??
172
+ createDebugRegistry({ getTick: () => 0, getSimT: () => 0 });
173
+ systemAdapters.debug = debugRegistry.adapter;
174
+ ctx.debug = debugRegistry.forWorld(id);
175
+ ctx.random =
176
+ (host.game ? getSeededRandom(host.game) : null) ??
177
+ createSeededRandom(DEFAULT_SEEDED_RANDOM_SEED);
178
+
179
+ // --- ctx.input + the debug-registry input seams (gap 3) — the game-owned
180
+ // InputManager, polled by the game-scoped runner each frame; this world
181
+ // registers the SAME per-world seams the classic adapter registers so
182
+ // `game.input.*` (virtual input) and the built-in `input.actions`/
183
+ // `input.trace` providers resolve to it. ---
184
+ let inputMapReady: Promise<void> = Promise.resolve();
185
+ if (host.game) {
186
+ const game = host.game;
187
+ const input = game.input;
188
+ ctxRaw['input'] = input;
189
+ debugRegistry.setInputActionsSource(id, () =>
190
+ input.actionNames().map((name) => ({ name, valueType: input.getActionValueType(name) })),
191
+ );
192
+ debugRegistry.setInputTraceSource(id, () => {
193
+ const raw = input.getInputTrace();
194
+ return {
195
+ version: raw.version,
196
+ seed: getSeededRandom(game)?.seed ?? null,
197
+ fixedDt: game.loop.fixedDt,
198
+ ticks: raw.ticks,
199
+ };
200
+ });
201
+ debugRegistry.setVirtualInputTarget(id, {
202
+ setVirtualAction: (action, value) => input.setVirtualAction(action, value),
203
+ tapVirtualAction: (action) => input.tapVirtualAction(action),
204
+ clearVirtualActions: () => input.clearVirtualActions(),
205
+ scheduleActionAtTick: (tick, action, value) =>
206
+ input.scheduleActionAtTick(tick, action, value),
207
+ startInputRecording: () => input.startInputRecording(),
208
+ stopInputRecording: () => input.stopInputRecording(),
209
+ isInputRecording: () => input.isInputRecording(),
210
+ injectAxis: (sourceId, value) => input.injectAxis(sourceId, value),
211
+ injectVector2: (sourceId, value) => input.injectVector2(sourceId, value),
212
+ injectPointerDelta: (sourceId, delta) => input.injectPointerDelta(sourceId, delta),
213
+ injectPointerPosition: (sourceId, value) => input.injectPointerPosition(sourceId, value),
214
+ });
215
+ if (!host.headless && inputMapPath !== null) {
216
+ // Load-once through the game-owned path (classic parity — competing
217
+ // paths across roots throw THERE, loudly). A FAILED load (missing/bad
218
+ // file) must not fail this mount: an R3F tree with no declared actions
219
+ // is legal. It degrades loudly instead — naming exactly what breaks.
220
+ inputMapReady = game.loadInputMap(inputMapPath).catch((err: unknown) => {
221
+ // biome-ignore lint/suspicious/noConsole: deliberate loud degrade — the documented alternative to failing the mount (see comment above)
222
+ console.error(
223
+ `R3F world "${id}": failed to load input map "${inputMapPath}" — declared input ` +
224
+ 'actions and `game.input.*` (bot/virtual input) will not work until a valid map ' +
225
+ 'loads. Ship one at the conventional path or pass `inputMapPath` to ' +
226
+ `createR3FAdapter (null to opt out). Cause: ${err instanceof Error ? err.message : String(err)}`,
227
+ );
228
+ });
229
+ }
230
+ } 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
+ );
238
+ }
239
+
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
+ return {
277
+ ctx,
278
+ systems,
279
+ components,
280
+ systemAdapters,
281
+ inputMapReady,
282
+ setSceneCamera(scene, camera): void {
283
+ ctxRaw['scene'] = scene;
284
+ ctxRaw['camera'] = camera;
285
+ },
286
+ dispose(): void {
287
+ components.clear();
288
+ // Scoped strip when a Game hosts this world (sibling roots stay live);
289
+ // a bare standalone mount owns its private registry and clears it all.
290
+ if (host.game) debugRegistry.strip(id);
291
+ else debugRegistry.strip();
292
+ },
293
+ };
294
+ }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 0beqz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,19 @@
1
+ # realism-effects compatibility fork
2
+
3
+ This directory vendors the published `realism-effects@1.1.2` distribution
4
+ (MIT license) so `@vgai/engine` remains installable as a self-contained source
5
+ package. The upstream package still imports Three's removed
6
+ `WebGLMultipleRenderTargets` API and has no release compatible with Three r180.
7
+
8
+ VGAI's fork contains only the mechanical r180 MRT migration:
9
+
10
+ - `WebGLMultipleRenderTargets(width, height, count, options)` becomes
11
+ `WebGLRenderTarget(width, height, { ...options, count })`.
12
+ - MRT attachment access moves from `.texture[index]` to `.textures[index]`.
13
+
14
+ Upstream repository: <https://github.com/0beqz/realism-effects>
15
+
16
+ Upstream package/version: `realism-effects@1.1.2`
17
+
18
+ Remove this fork when an upstream release supports Three r180 or newer and the
19
+ isolated packed-engine consumer proof passes against it.