@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
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * System adapters — optional subsystem capability providers hung off a mounted
3
- * game (`MountedGame.systems`). Each is a thin capability boundary the editor can
3
+ * game (`MountedThreeRoot.systems`). Each is a thin capability boundary the editor can
4
4
  * coordinate with WITHOUT owning the implementation. The first-party
5
- * implementers wrap Rapier / Colyseus / the InputManager; an external game
6
- * supplies its own or none, and the editor degrades per `capabilities`.
5
+ * implementers wrap Rapier / Colyseus / recast; an external game supplies its
6
+ * own or none, and the editor degrades per `capabilities`.
7
7
  *
8
8
  * These are introspection/coordination boundaries, NOT re-implementations: e.g.
9
9
  * `NetworkingAdapter` is not a transport, and `PhysicsAdapter` does not replace
@@ -14,22 +14,29 @@
14
14
  import type * as THREE from 'three';
15
15
  import type { RenderMemorySnapshot } from '../dev/render-memory';
16
16
  import type { FrameCapture } from '../dev/webgl-frame-capture';
17
- import type { ActionValueType, Vector2 } from '../input/input-types';
18
17
  import type { Transform, TransformOwner } from './transform';
19
18
 
20
19
  /**
21
20
  * Physics coordination so the editor can stably edit an object a simulation
22
21
  * would otherwise overwrite every frame: `freeze → apply → unfreeze`.
22
+ *
23
+ * Keyed by NODE ID (the `EditorNode` id), not `THREE.Object3D` (P-4). The
24
+ * Object3D keying made this seam structurally unreachable for any non-three
25
+ * world: a Pixi or React world could never expose a physics inspector, because
26
+ * the seam spoke a vocabulary it has no values in — which is why the Pixi
27
+ * surface grew a parallel `PhysicsAdapter2D` (`pixi/system-adapters.ts`)
28
+ * instead of implementing this. Substrate-specific resolution (id → the native
29
+ * object) is the IMPLEMENTER's job, done once at its own boundary.
23
30
  */
24
31
  export interface PhysicsAdapter {
25
- /** Returns `'physics'` when a body drives this object, else another owner. */
26
- ownerOf(o: THREE.Object3D): TransformOwner;
27
- /** Pause the body driving `o` (kinematic / sleep / detach) for editing. */
28
- freeze(o: THREE.Object3D): void;
32
+ /** Returns `'physics'` when a body drives this node, else another owner. */
33
+ ownerOf(nodeId: string): TransformOwner;
34
+ /** Pause the body driving `nodeId` (kinematic / sleep / detach) for editing. */
35
+ freeze(nodeId: string): void;
29
36
  /** Teleport the body to the edited pose (so the sim continues from there). */
30
- commit(o: THREE.Object3D, t: Transform): void;
31
- /** Resume simulation of `o`. */
32
- unfreeze(o: THREE.Object3D): void;
37
+ commit(nodeId: string, t: Transform): void;
38
+ /** Resume simulation of `nodeId`. */
39
+ unfreeze(nodeId: string): void;
33
40
  /** Optional debug-draw object (collider wireframes). */
34
41
  debugDraw?(): THREE.Object3D | null;
35
42
  /**
@@ -53,6 +60,19 @@ export interface NetPeer {
53
60
  label?: string;
54
61
  }
55
62
 
63
+ /** The project's declared multiplayer server, readable with the game NOT
64
+ * running. `null` from `getServerConfig` means no server is declared. */
65
+ export interface NetServerConfig {
66
+ endpoint: string;
67
+ roomName: string;
68
+ }
69
+
70
+ /** The signed-in player's authored identity, used as the connect default. */
71
+ export interface NetPlayerIdentity {
72
+ name?: string;
73
+ id?: string;
74
+ }
75
+
56
76
  /** The live connection lifecycle, for the editor's network inspector. */
57
77
  export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'error';
58
78
 
@@ -132,10 +152,11 @@ export interface NetConditioning {
132
152
  */
133
153
  export interface NetworkingAdapter {
134
154
  peers(): NetPeer[];
135
- networkId(o: THREE.Object3D): string | null;
136
- authority(o: THREE.Object3D): 'local' | 'remote' | 'server';
137
- /** Remote/server-authoritative objects inspect-only in the editor. */
138
- editable(o: THREE.Object3D): boolean;
155
+ /** Keyed by NODE ID, not `THREE.Object3D` see {@link PhysicsAdapter} (P-4). */
156
+ networkId(nodeId: string): string | null;
157
+ authority(nodeId: string): 'local' | 'remote' | 'server';
158
+ /** Remote/server-authoritative nodes → inspect-only in the editor. */
159
+ editable(nodeId: string): boolean;
139
160
  /** Current connection lifecycle state. */
140
161
  getConnectionState(): ConnectionState;
141
162
  /** The active room's identity, or `null` when not connected to a room. */
@@ -160,29 +181,28 @@ export interface NetworkingAdapter {
160
181
  getConditioning?(): NetConditioning;
161
182
  /** Optional capability: apply link conditioning at the adapter seam. */
162
183
  setConditioning?(c: NetConditioning): void;
163
- }
164
-
165
- /** One action's current value, tagged with its declared shape (F1 `ActionValueType`) so a
166
- * consumer (editor input inspector, conformance suite) can render/assert on the real typed
167
- * value instead of a lossy digital coercion. `value` is a `number` for `'digital'`
168
- * (1 = pressed, 0 = not) and `'scalar'`, or a `Vector2` for `'vector2'`/`'pointerDelta'`/
169
- * `'pointerPosition'`narrow on `type` to know which. */
170
- export interface ActionValueSnapshot {
171
- type: ActionValueType;
172
- value: number | Vector2;
173
- }
174
-
175
- export interface InputAdapter {
176
- poll(): void;
177
- /** Every named action's current value, faithfully typed per its declared `ActionValueType`
178
- * (F1, spec §12) digital reads as 1/0, scalar as its float, vector2/pointerDelta/
179
- * pointerPosition as `{x,y}`. Never coerces a non-digital action down to 0/1. */
180
- actions(): Readonly<Record<string, ActionValueSnapshot>>;
181
- }
182
-
183
- /** URL remap so an ingested game's relative asset paths resolve under the host. */
184
- export interface AssetAdapter {
185
- resolve(url: string): string;
184
+ /** Optional edit-time capability: the project's declared server, readable
185
+ * with the game NOT running (`null` = no server declared). Hides on
186
+ * absence, like the other optionals. */
187
+ getServerConfig?(): NetServerConfig | null;
188
+ /** Optional capability: the local player's identity, as the GAME defines it
189
+ * (e.g. read off its own room state). The editor consumes this for display —
190
+ * a seat label ONLY when a real implementer provides it, falling back to a
191
+ * generic label otherwise. */
192
+ getPlayerIdentity?(): NetPlayerIdentity;
193
+ /** Optional capability, PAIRED with {@link getPlayerIdentity}: set the local
194
+ * player's identity through the game's OWN multiplayer mechanism. The editor
195
+ * renders an editable name field ONLY when a real implementer provides this
196
+ * (consume-by-presence, exactly like {@link setConditioning}); its absence
197
+ * means the game does not support an author-settable name and no field shows.
198
+ *
199
+ * This is NOT the editor imposing a name: whether a name is settable, and how
200
+ * it reaches the server, is entirely the game's decision a game that names
201
+ * players by join order simply never provides this member. When it does, the
202
+ * implementation drives the game's real path (a join option honoured by the
203
+ * server, a rename message, …), so what the editor sets is the same value the
204
+ * multiplayer build actually uses never a placeholder the server discards. */
205
+ setPlayerIdentity?(identity: NetPlayerIdentity): void | Promise<void>;
186
206
  }
187
207
 
188
208
  export interface NavPoint {
@@ -315,11 +335,11 @@ export interface AudioDebugEvent {
315
335
  /**
316
336
  * Silence/restore a world's own audio (D10, T7.6) — NOT a mixer/bus API. This
317
337
  * is the seam `Game.play.pause()` calls so a paused world's audio goes quiet
318
- * too (`docs/DECISIONS-PENDING.md` §D10: "an audio seam in `SystemAdapters` so
319
- * pause can silence game audio (howler et al.) or explicitly report it
320
- * cannot"). Absence on a world's `mounted.systems` means exactly that:
321
- * `Game.play.pause()` reports it loudly once per world rather than silently
322
- * leaving that world's audio playing under a "paused" game.
338
+ * too ("an audio seam in `SystemAdapters` so pause can silence game audio
339
+ * (howler et al.) or explicitly report it cannot"). Absence on a world's
340
+ * `mounted.systems` means exactly that: `Game.play.pause()` reports it loudly
341
+ * once per world rather than silently leaving that world's audio playing under
342
+ * a "paused" game.
323
343
  *
324
344
  * The OPTIONAL methods below are the W3c read-only INTROSPECTION capabilities
325
345
  * feeding the editor's Audio debugger tab (graph / transport / meters /
@@ -374,7 +394,7 @@ export interface AudioAdapter {
374
394
  * other emission at that instant would), so two DIFFERENT events — or an
375
395
  * event and a consumer's fence point — can legitimately share one `tick`;
376
396
  * `seq` never collides the same way, so "everything after what I've already
377
- * observed" is unambiguous. See `events(sinceTick, sinceSeq)` below. */
397
+ * observed" is unambiguous. See `events(sinceSeq)` below. */
378
398
  export interface TickStampedEvent {
379
399
  tick: number;
380
400
  simT: number;
@@ -395,11 +415,11 @@ export interface DebugCommandInfo {
395
415
  }
396
416
 
397
417
  /**
398
- * The debug/synthetic-player seam (`docs/SYNTHETIC-PLAYER-SPEC.md` §3.1):
399
- * game-scoped introspection + actuation over whatever a game registers via
400
- * `ctx.debug` (`registerStateProvider`/`registerCommand`/`emit`). NOT a
401
- * gameplay API — this is the one seam the debug bridge, the editor's Debug
402
- * Console/State Watch panels, and `@vgai/e2e` all read/drive through.
418
+ * The debug/synthetic-player seam: game-scoped introspection + actuation
419
+ * over whatever a game registers via `ctx.debug`
420
+ * (`registerStateProvider`/`registerCommand`/`emit`). NOT a gameplay API —
421
+ * this is the one seam the debug bridge, the editor's Debug Console/State
422
+ * Watch panels, and `@vgai/live` all read/drive through.
403
423
  */
404
424
  export interface DebugAdapter {
405
425
  providers(): { name: string; tier: 'observable' | 'assisted' }[];
@@ -420,18 +440,17 @@ export interface DebugAdapter {
420
440
  /** The tick-stamped event ring (cap 500, drops oldest).
421
441
  *
422
442
  * - `sinceSeq` given (run-4 friction #5 fix): filters to `seq > sinceSeq`
423
- * — the unambiguous fence a consumer should use to mean "everything
424
- * emitted after what I've already observed" (see `TickStampedEvent.seq`'s
425
- * doc comment for why `tick` alone can't do this: a debug-command
426
- * emission and a consumer's fence point can share one tick, and the
427
- * old `tick > sinceTick` filter silently dropped same-tick events).
428
- * Takes precedence over `sinceTick` when both are given.
429
- * - `sinceTick` only: the ORIGINAL (pre-fix) filter, `tick > sinceTick` —
430
- * kept for back-compat callers that only ever had a tick to fence on;
431
- * still has the same-tick blind spot by construction, so a new caller
432
- * should prefer `sinceSeq`.
433
- * - Neither given: the whole ring. */
434
- events(sinceTick?: number, sinceSeq?: number): TickStampedEvent[];
443
+ * — the unambiguous fence for "everything emitted after what I've
444
+ * already observed" (see `TickStampedEvent.seq`'s doc comment for why
445
+ * `tick` alone can't do this: a debug-command emission and a consumer's
446
+ * fence point can share one tick).
447
+ * - Omitted: the whole ring.
448
+ *
449
+ * There was a second, KNOWN-DEFECTIVE `sinceTick` parameter (`tick >
450
+ * sinceTick`, which silently dropped same-tick events) kept for
451
+ * back-compat after the seq fix. It is REMOVED a fence is `seq` or
452
+ * nothing. */
453
+ events(sinceSeq?: number): TickStampedEvent[];
435
454
  }
436
455
 
437
456
  /**
@@ -458,8 +477,6 @@ export interface RenderDebugAdapter {
458
477
  export interface SystemAdapters {
459
478
  physics?: PhysicsAdapter;
460
479
  networking?: NetworkingAdapter;
461
- input?: InputAdapter;
462
- assets?: AssetAdapter;
463
480
  navigation?: NavigationAdapter;
464
481
  audio?: AudioAdapter;
465
482
  debug?: DebugAdapter;
@@ -14,7 +14,7 @@ import { NavMeshHelper, threeToSoloNavMesh } from '@recast-navigation/three';
14
14
  import type { CrowdAgent, NavMesh } from 'recast-navigation';
15
15
  import { Crowd, exportNavMesh, importNavMesh, init, NavMeshQuery } from 'recast-navigation';
16
16
  import type { Mesh, Scene } from 'three';
17
- import { DEFAULTS } from '../scene/defaults';
17
+ import { DEFAULTS } from '../defaults';
18
18
 
19
19
  export type { CrowdAgent };
20
20
 
@@ -19,7 +19,7 @@ import { PHASE_ORDER, type SystemPhaseName } from '../core/types';
19
19
  * what makes `seek(t)` and frame-index seeking reproducible offline.
20
20
  * - No parallel scheduler. Evaluator registration reuses the engine's own
21
21
  * `SystemPhaseName`/`PHASE_ORDER` vocabulary (`core/types.ts`) — the same
22
- * phase names every `GameComponent`/system in the engine already uses —
22
+ * phase names every system in the engine already uses —
23
23
  * so a Theatre binding (D2), a GSAP registration (D7), and a cue
24
24
  * evaluator (D5) can each declare which phase they conceptually belong
25
25
  * to and be invoked in one deterministic, engine-wide order instead of
@@ -11,8 +11,7 @@ import type { CameraOwnershipChangeEvent } from './theatre-director';
11
11
  * that control is released. The actual camera math (cuts, blends, lens
12
12
  * changes, orbit/follow/look-at/dolly behavior) is expressed entirely with
13
13
  * native `THREE.PerspectiveCamera` objects — Object3D-as-truth, no camera
14
- * wrapper (spec §4, `ARCHITECTURE.md`'s "the world node IS the entity"
15
- * rule). This module reads and writes real Three.js camera properties
14
+ * wrapper (spec §4, the "world node IS the entity" rule). This module reads and writes real Three.js camera properties
16
15
  * (`position`, `quaternion`, `fov`, `near`, `far`) and nothing else.
17
16
  *
18
17
  * ## Two cooperating pieces
@@ -0,0 +1,132 @@
1
+ /**
2
+ * The glTF CUBICSPLINE interpolant, and the factory that installs it on a
3
+ * `KeyframeTrack`.
4
+ *
5
+ * `GLTFLoader` has both of these and exports NEITHER — `GLTFCubicSplineInterpolant`
6
+ * and `_createCubicSplineTrackInterpolant` are module-private. Anything that
7
+ * builds a CUBICSPLINE track without going through a GLB parse therefore has
8
+ * to supply them, and the D25 migration does exactly that: it bakes Theatre's
9
+ * bezier easing into tangent triples, and a track carrying triples without the
10
+ * interpolant reads as three times as many components and plays garbage.
11
+ *
12
+ * The flag is also load-bearing on the way OUT. `GLTFExporter` decides a track
13
+ * is CUBICSPLINE by testing `isInterpolantFactoryMethodGLTFCubicSpline` and
14
+ * nothing else (`GLTFExporter.js:2252`), so a baked track without it exports
15
+ * as LINEAR over tangent triples — silently wrong. Installing it is what makes
16
+ * a converted clip round-trip through the industry format instead of needing
17
+ * one of ours.
18
+ *
19
+ * This is a port of Three's own algorithm, not an abstraction over it — the
20
+ * math below is theirs, kept deliberately line-comparable to
21
+ * `GLTFLoader.js`'s `GLTFCubicSplineInterpolant.interpolate_` so a future
22
+ * upstream fix is a readable diff. It exists because of a missing export, and
23
+ * should be deleted the day Three publishes one.
24
+ *
25
+ * Layout, which is the whole reason this is subtle:
26
+ *
27
+ * [ inTangent_1, splineVertex_1, outTangent_1, inTangent_2, splineVertex_2, … ]
28
+ *
29
+ * so `track.getValueSize()` reports 3x the component count and
30
+ * `track.getInterpolation()` returns undefined (Three's enum cannot name a
31
+ * custom interpolant). The `isInterpolantFactoryMethodGLTFCubicSpline` flag on
32
+ * the factory is the only marker, and it is what the editor's authoring
33
+ * projection keys off — see `three-animation-authoring.ts`'s `trackLayout`.
34
+ */
35
+
36
+ import * as THREE from 'three';
37
+
38
+ /** Hermite evaluation over glTF's `[inTangent, vertex, outTangent]` triples. */
39
+ export class CubicSplineInterpolant extends THREE.Interpolant {
40
+ copySampleValue_(index: number): Float32Array | number[] {
41
+ const result = this.resultBuffer as number[];
42
+ const values = this.sampleValues as number[];
43
+ const valueSize = this.valueSize;
44
+ // The VERTEX sits one component-stride into its triple.
45
+ const offset = index * valueSize * 3 + valueSize;
46
+ for (let i = 0; i !== valueSize; i++) result[i] = values[offset + i]!;
47
+ return result;
48
+ }
49
+
50
+ // Not `override`: Three's published typings do not declare `interpolate_`,
51
+ // though every built-in interpolant implements it.
52
+ interpolate_(i1: number, t0: number, t: number, t1: number): Float32Array | number[] {
53
+ const result = this.resultBuffer as number[];
54
+ const values = this.sampleValues as number[];
55
+ const stride = this.valueSize;
56
+
57
+ const stride2 = stride * 2;
58
+ const stride3 = stride * 3;
59
+
60
+ const td = t1 - t0;
61
+ const p = (t - t0) / td;
62
+ const pp = p * p;
63
+ const ppp = pp * p;
64
+
65
+ const offset1 = i1 * stride3;
66
+ const offset0 = offset1 - stride3;
67
+
68
+ const s2 = -2 * ppp + 3 * pp;
69
+ const s3 = ppp - pp;
70
+ const s0 = 1 - s2;
71
+ const s1 = s3 - pp + p;
72
+
73
+ for (let i = 0; i !== stride; i++) {
74
+ const p0 = values[offset0 + i + stride]!; // splineVertex_k
75
+ const m0 = values[offset0 + i + stride2]! * td; // outTangent_k · Δt
76
+ const p1 = values[offset1 + i + stride]!; // splineVertex_k+1
77
+ const m1 = values[offset1 + i]! * td; // inTangent_k+1 · Δt
78
+ result[i] = s0 * p0 + s1 * m0 + s2 * p1 + s3 * m1;
79
+ }
80
+ return result;
81
+ }
82
+ }
83
+
84
+ const _quaternion = new THREE.Quaternion();
85
+
86
+ /** Quaternion tracks renormalize after the Hermite blend, as glTF requires. */
87
+ export class CubicSplineQuaternionInterpolant extends CubicSplineInterpolant {
88
+ override interpolate_(i1: number, t0: number, t: number, t1: number): Float32Array | number[] {
89
+ const result = super.interpolate_(i1, t0, t, t1) as number[];
90
+ _quaternion
91
+ .fromArray(result as number[])
92
+ .normalize()
93
+ .toArray(result as number[]);
94
+ return result;
95
+ }
96
+ }
97
+
98
+ /** The marker `GLTFLoader` sets, and the only way to detect a CUBICSPLINE track. */
99
+ interface CubicSplineFactory {
100
+ (this: THREE.KeyframeTrack, result: Float32Array): THREE.Interpolant;
101
+ isInterpolantFactoryMethodGLTFCubicSpline?: boolean;
102
+ }
103
+
104
+ /**
105
+ * Install the CUBICSPLINE interpolant on a track whose values are tangent
106
+ * triples — byte-identical in effect to what `GLTFLoader` does to a track it
107
+ * parses from a GLB, so a track from an authored file and a track from a GLB
108
+ * are indistinguishable downstream.
109
+ */
110
+ export function installCubicSplineInterpolant(track: THREE.KeyframeTrack): THREE.KeyframeTrack {
111
+ const factory: CubicSplineFactory = function InterpolantFactoryMethodGLTFCubicSpline(
112
+ this: THREE.KeyframeTrack,
113
+ result: Float32Array,
114
+ ) {
115
+ const Interpolant =
116
+ this instanceof THREE.QuaternionKeyframeTrack
117
+ ? CubicSplineQuaternionInterpolant
118
+ : CubicSplineInterpolant;
119
+ // getValueSize() counts the whole triple; the interpolant wants components.
120
+ return new Interpolant(this.times, this.values, this.getValueSize() / 3, result);
121
+ };
122
+ factory.isInterpolantFactoryMethodGLTFCubicSpline = true;
123
+ (track as unknown as { createInterpolant: CubicSplineFactory }).createInterpolant = factory;
124
+ return track;
125
+ }
126
+
127
+ /** True when `track` carries glTF tangent triples rather than plain values. */
128
+ export function isCubicSplineTrack(track: THREE.KeyframeTrack): boolean {
129
+ const factory = (track as unknown as { createInterpolant?: CubicSplineFactory })
130
+ .createInterpolant;
131
+ return factory?.isInterpolantFactoryMethodGLTFCubicSpline === true;
132
+ }
@@ -51,8 +51,8 @@ import type { AnimationClock, ClockEvaluatorHandle } from './animation-clock';
51
51
  * frame.
52
52
  *
53
53
  * Registered by default at the `'animation'` phase — the same phase
54
- * XState-bound `AnimationMixer` playback ticks in (see `ARCHITECTURE.md`'s
55
- * phase table: `... gameLogic → animation → preRender → render`). Object
54
+ * XState-bound `AnimationMixer` playback ticks in (the phase table:
55
+ * `... gameLogic → animation → preRender → render`). Object
56
56
  * bindings (`bindObject3DTransform`/`bindCameraLens`) default to `'preRender'`
57
57
  * (the documented home for camera placement/material tick) specifically so
58
58
  * `PHASE_ORDER` — not registration order — guarantees they observe the
@@ -1,5 +1,5 @@
1
1
  import type { ISheetObject, UnknownShorthandCompoundProps } from '@theatre/core';
2
- // Type-only import — matches `world2d/types.ts`'s own rule (the engine core
2
+ // Type-only import — the engine core
3
3
  // never VALUE-imports `pixi.js`; a caller supplies the constructed
4
4
  // Container/Sprite, this module only types against it).
5
5
  import type { Container } from 'pixi.js';
@@ -106,8 +106,8 @@ export interface TransformProps {
106
106
  export interface TheatreObjectBindingOptions {
107
107
  /**
108
108
  * Which `AnimationClock` phase to apply values in. Defaults to
109
- * `'preRender'` — the documented home for camera placement/material tick
110
- * (`ARCHITECTURE.md`), and guaranteed by `PHASE_ORDER` to run after the
109
+ * `'preRender'` — the documented home for camera placement/material
110
+ * tick, and guaranteed by `PHASE_ORDER` to run after the
111
111
  * `'animation'`-phase default of `bindClockToTheatreSequence`, so the
112
112
  * sheet's sequence position is always current by the time this reads
113
113
  * `sheetObject.value`, regardless of registration order.
@@ -624,7 +624,7 @@ export interface PixiTransformProps {
624
624
  /**
625
625
  * Bind a Theatre Sheet Object's `x`/`y`/`rotation`/`scaleX`/`scaleY`/`alpha`
626
626
  * sub-props (any subset) to a real `PIXI.Container`/`PIXI.Sprite` (reusing
627
- * the world2d stack's own `pixi.js` dependency — see this module's
627
+ * the Pixi surface's own `pixi.js` dependency — see this module's
628
628
  * type-only `pixi.js` import above and its doc note on why the engine core
629
629
  * never value-imports it). Plain `Container`/`Sprite` construction needs no
630
630
  * canvas/WebGL context, so this is proven against a REAL Pixi object in
@@ -1,7 +1,7 @@
1
1
  import * as THREE from 'three';
2
- import type { Actor, AnyActor, AnyStateMachine } from 'xstate';
2
+ import type { AnyStateMachine } from 'xstate';
3
3
  import type { createSystemRunner } from '../core/system-runner';
4
- import { deleteUserData, getUserData, setUserData } from '../scene/user-data';
4
+ import { deleteUserData, getUserData, setUserData } from '../ecs/user-data';
5
5
  import type { BlendTreeDef } from './anim-graph-types';
6
6
  import { evaluateBlendTree } from './blend-node';
7
7
  import {
@@ -14,6 +14,61 @@ import {
14
14
  type StateAnimationMeta,
15
15
  } from './xstate-animation-meta';
16
16
 
17
+ /**
18
+ * The snapshot half of {@link XStateAnimationActor} — the three members this
19
+ * binder and the editor's machine inspectors actually read off a snapshot.
20
+ * Named here rather than imported so the seam stays XState-free; see the
21
+ * actor's own note for why that matters.
22
+ */
23
+ export interface XStateAnimationSnapshot {
24
+ /** Active state value (`'idle'`, `{ locomotion: 'run' }`). XState's
25
+ * `StateValue`, kept as `unknown` here — only the inspector interprets it. */
26
+ readonly value: unknown;
27
+ /** Live machine context, read for blend-tree parameters. */
28
+ readonly context: unknown;
29
+ /** Meta of every active state, keyed by state id — how the binder learns
30
+ * which states are active and what `meta.animation` they carry. */
31
+ readonly getMeta: () => Record<string, unknown>;
32
+ }
33
+
34
+ /**
35
+ * What this binder needs from a running actor — a MINIMAL STRUCTURAL shape
36
+ * that names no XState type at all. It is satisfied by `Actor<TMachine>`, by
37
+ * `ActorRefFrom<typeof machine>`, and by anything else that can report a
38
+ * snapshot and be subscribed to.
39
+ *
40
+ * TWO REASONS IT IS SPELLED OUT INSTEAD OF IMPORTED, both measured:
41
+ *
42
+ * - A nominal `Actor<AnyStateMachine>` parameter rejects the type XState's own
43
+ * React docs reach for — `ActorRefFrom<typeof machine>`, e.g. in a
44
+ * `useRef<ActorRefFrom<typeof machine> | null>` — which carries no `logic`,
45
+ * `clock` or `options`. The call then fails with a wall of "missing the
46
+ * following properties" and the only obvious escape is
47
+ * `bindXStateAnimation(actor as any, …)`, a cast that also disables checking
48
+ * of the clip map and options.
49
+ * - XState's own `AnyActorRef` fixes that but is DEEP, and a game does not
50
+ * compile against the same physical copy of `xstate` the engine does: a
51
+ * scaffolded project installs its own, and the moment those two copies
52
+ * differ in version TypeScript stops deduplicating them by package id (the
53
+ * id is name + subpath + VERSION) and has to
54
+ * compare `Actor<StateMachine<…>>` from one copy against `AnyActorRef` from
55
+ * the other — structurally, through `system`, `src`, `_parent` and a generic
56
+ * `select`, until it gives up with `TS2321: Excessive stack depth`. Measured
57
+ * on a fresh `vgai create` + `vgai add humanoid`, where every in-repo gate
58
+ * stayed green because the monorepo resolves exactly one copy. A shape this
59
+ * shallow cannot blow that budget however many copies are in play.
60
+ *
61
+ * Both actor shapes still carry the machine — `Actor.logic`, and `machine` on
62
+ * every `MachineSnapshot` — so {@link machineOfXStateActor} reads it and no
63
+ * caller casts.
64
+ */
65
+ export interface XStateAnimationActor {
66
+ readonly getSnapshot: () => XStateAnimationSnapshot;
67
+ readonly subscribe: (next: (snapshot: XStateAnimationSnapshot) => void) => {
68
+ unsubscribe: () => void;
69
+ };
70
+ }
71
+
17
72
  /** A binding-time problem such as a missing clip, bone, or ambiguous layer. */
18
73
  export class AnimationBindingError extends Error {
19
74
  constructor(
@@ -25,6 +80,21 @@ export class AnimationBindingError extends Error {
25
80
  }
26
81
  }
27
82
 
83
+ /** The state machine behind a bound actor, from whichever half of the actor
84
+ * shape carries it. Throws rather than silently binding nothing when the
85
+ * actor is not a state-machine actor (a promise/callback actor has no
86
+ * `meta.animation` to collect). */
87
+ export function machineOfXStateActor(actor: XStateAnimationActor): AnyStateMachine {
88
+ const logic = (actor as { logic?: unknown }).logic as AnyStateMachine | undefined;
89
+ if (logic?.root) return logic;
90
+ const snapshot = actor.getSnapshot() as XStateAnimationSnapshot & { machine?: AnyStateMachine };
91
+ if (snapshot?.machine?.root) return snapshot.machine;
92
+ throw new AnimationBindingError(
93
+ '[bindXStateAnimation] the actor exposes no state machine — only a state-machine ' +
94
+ 'actor carries the `meta.animation` this binder reads',
95
+ );
96
+ }
97
+
28
98
  export interface XStateAnimationBindingOptions {
29
99
  /** Select live numeric/boolean blend-tree parameters from actor context. */
30
100
  selectParameters?: (context: unknown) => Record<string, number | boolean>;
@@ -54,7 +124,7 @@ export interface XStateAnimationLayerState {
54
124
  export interface XStateAnimationBinding {
55
125
  /** Advance native actions/mixer. Register this in the engine animation phase. */
56
126
  tick: (dt: number) => void;
57
- readonly actor: AnyActor;
127
+ readonly actor: XStateAnimationActor;
58
128
  /** Current native composition, useful to the editor and game diagnostics. */
59
129
  getActiveLayers: () => readonly XStateAnimationLayerState[];
60
130
  /** Unsubscribe, stop/uncache actions, and remove owner inspection data. Idempotent. */
@@ -185,7 +255,7 @@ function dominantOf(weights: WeightedClip[]): WeightedClip {
185
255
  * blend mode. Single unlayered machines retain the original clip/action path.
186
256
  */
187
257
  export function bindXStateAnimation(
188
- actor: Actor<AnyStateMachine>,
258
+ actor: XStateAnimationActor,
189
259
  mixer: THREE.AnimationMixer,
190
260
  clips: Map<string, THREE.AnimationClip>,
191
261
  options: XStateAnimationBindingOptions = {},
@@ -198,7 +268,7 @@ export function bindXStateAnimation(
198
268
  );
199
269
  }
200
270
  const owner = options.owner ?? root;
201
- const machineRoot = actor.logic.root as unknown as AnimationMetaStateNodeLike;
271
+ const machineRoot = machineOfXStateActor(actor).root as unknown as AnimationMetaStateNodeLike;
202
272
  const entries: MachineAnimationMetaEntry[] = collectMachineAnimationMeta(machineRoot);
203
273
  const metaByStateId = new Map(entries.map((entry) => [entry.stateId, entry.meta]));
204
274
 
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
 
3
- export const SceneCameraSchema = z
3
+ export const CameraDescriptorSchema = z
4
4
  .object({
5
5
  type: z.enum(['perspective', 'orthographic']).describe('Camera projection type'),
6
6
  fov: z.number().optional().describe('Vertical field of view in degrees (perspective only)'),
@@ -15,7 +15,4 @@ export const SceneCameraSchema = z
15
15
  })
16
16
  .describe('Camera configuration');
17
17
 
18
- /** Partial schema for prefab instance overrides (type not required). */
19
- export const SceneCameraOverrideSchema = SceneCameraSchema.partial();
20
-
21
- export type SceneCamera = z.infer<typeof SceneCameraSchema>;
18
+ export type CameraDescriptor = z.infer<typeof CameraDescriptorSchema>;
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { Vec3Schema } from './tuples';
3
3
 
4
- export const SceneColliderSchema = z
4
+ export const ColliderDescriptorSchema = z
5
5
  .object({
6
6
  type: z
7
7
  .enum(['cuboid', 'ball', 'capsule', 'trimesh'])
@@ -63,7 +63,4 @@ export const SceneColliderSchema = z
63
63
  })
64
64
  .describe('Physics collider shape and properties');
65
65
 
66
- /** Partial schema for prefab instance overrides (type not required). */
67
- export const SceneColliderOverrideSchema = SceneColliderSchema.partial();
68
-
69
- export type SceneCollider = z.infer<typeof SceneColliderSchema>;
66
+ export type ColliderDescriptor = z.infer<typeof ColliderDescriptorSchema>;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The engine's remaining authored asset formats and descriptor schemas.
3
+ *
4
+ * WO-8 deleted the `.vscn.json` scene format and everything under
5
+ * `engine/src/scene/`. What survived is here, and each module is here because a
6
+ * real runtime reader still consumes it:
7
+ *
8
+ * - `material.ts` — the `.mat.json` shared-material format (`parseMaterialFile`)
9
+ * plus the inline material descriptor `render/material-factory.ts` reads
10
+ * - `instances.ts` — the `.instances.json` transform format (`parseInstancesFile`),
11
+ * read by `render/instance-mesh.ts`
12
+ * - `mesh.ts` — the geometry descriptor `render/material-factory.ts`'s `createGeometry` reads
13
+ * - `light.ts` / `camera.ts` — the descriptors `render/light-camera-factory.ts` reads
14
+ * - `collider.ts` — the descriptor `physics/collider-dimensions.ts` and the editor's
15
+ * collider gizmos read
16
+ * - `particles.ts` — the descriptor `render/particles-factory.ts` reads
17
+ * - `render-settings.ts` — the tone-mapping / post-processing / render-feature scopes
18
+ * `setup/setup-renderer.ts` reads
19
+ * - `tuples.ts` — the shared Vec3/Quat/Transform tuples the above build on
20
+ *
21
+ * These are DESCRIPTORS, not documents: each names a slice of authored data a
22
+ * render/physics factory reads, and none of them is a scene. They carried a
23
+ * `Scene*` prefix until WO-9 task #6 renamed them to `*Descriptor` — the prefix
24
+ * was a leftover of the deleted `.vscn.json` format, not a statement about what
25
+ * they describe.
26
+ */
27
+
28
+ export type { CameraDescriptor } from './camera';
29
+ export { CameraDescriptorSchema } from './camera';
30
+ export type { ColliderDescriptor } from './collider';
31
+ export { ColliderDescriptorSchema } from './collider';
32
+ export type { InstancesFile } from './instances';
33
+ export { InstancesFileSchema } from './instances';
34
+ export type { LightDescriptor } from './light';
35
+ export { LightDescriptorSchema } from './light';
36
+ export type { MaterialDescriptor, MaterialFile } from './material';
37
+ export { MaterialDescriptorSchema, MaterialFileSchema } from './material';
38
+ export type { MeshDescriptor } from './mesh';
39
+ export { MeshDescriptorSchema } from './mesh';
40
+ export { parseInstancesFile, parseMaterialFile } from './parse';
41
+ export type { ParticlesDescriptor } from './particles';
42
+ export { ParticlesDescriptorSchema } from './particles';
43
+ export type {
44
+ PostProcessingDescriptor,
45
+ RenderEnvironment,
46
+ ToneMappingDescriptor,
47
+ } from './render-env';
48
+ export {
49
+ PostProcessingDescriptorSchema,
50
+ RenderEnvironmentSchema,
51
+ ToneMappingDescriptorSchema,
52
+ } from './render-env';
53
+ export type { Quat, Transform, Vec3 } from './tuples';
54
+ export { QuatSchema, TransformSchema, Vec3Schema } from './tuples';