@vgai/engine 0.5.2 → 0.5.4

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
@@ -24,29 +24,28 @@
24
24
  * game uses. In a bundler/dev-server that dedupes `three` (one `node_modules/
25
25
  * three`), an external ESM game's `import 'three'` resolves to that one instance,
26
26
  * so passing the host's `THREE` here traps the game's renderer too. A game that
27
- * bundles its own copy of `three` cannot be captured this way (see
28
- * `docs/UNMODIFIED-GAME-INGESTION-STUDY.md` §4A — the module-identity gatekeeper).
27
+ * bundles its own copy of `three` cannot be captured this way (the module-identity
28
+ * gatekeeper).
29
29
  *
30
- * Wave 17 (`docs/WAVE17-F13-COMPOSER-RESIZE-FIX-DESIGN.md`) adds an OPTIONAL,
31
- * ADDITIVE composer capture: a game rendering through its own three.js addon
32
- * `EffectComposer` (`three/examples/jsm/postprocessing/EffectComposer.js`)
33
- * still trips the `render` trap above (its `RenderPass` calls
34
- * `renderer.render(scene,camera)` internally), but the renderer holds no
35
- * reference back to the composer, so the host previously could not resize the
36
- * composer's own (intentionally non-1:1, progressively-downsampled — see
37
- * `UnrealBloomPass`) render targets when the host resizes the game's pane.
38
- * Unlike `WebGLRenderer`, `EffectComposer` is a plain ES class whose methods
39
- * (including `render`) live on the PROTOTYPE, not assigned as own instance
40
- * properties in the constructor so a direct method-wrapper (no getter/
41
- * setter indirection) on `EffectComposer.prototype.render` is sufficient: it
42
- * calls through to the real `render`, then — AFTER that call, so any nested
30
+ * Wave 17 adds an OPTIONAL, ADDITIVE composer capture: a game rendering through
31
+ * its own three.js addon `EffectComposer`
32
+ * (`three/examples/jsm/postprocessing/EffectComposer.js`) still trips the `render`
33
+ * trap above (its `RenderPass` calls `renderer.render(scene,camera)` internally),
34
+ * but the renderer holds no reference back to the composer, so the host previously
35
+ * could not resize the composer's own (intentionally non-1:1,
36
+ * progressively-downsampled — see `UnrealBloomPass`) render targets when the host
37
+ * resizes the game's pane. Unlike `WebGLRenderer`, `EffectComposer` is a plain ES
38
+ * class whose methods (including `render`) live on the PROTOTYPE, not assigned as
39
+ * own instance properties in the constructor so a direct method-wrapper (no
40
+ * getter/ setter indirection) on `EffectComposer.prototype.render` is sufficient:
41
+ * it calls through to the real `render`, then AFTER that call, so any nested
43
42
  * `renderer.render()` the pass makes has already hit the trap above and set
44
- * `captured` — records `this` (the composer instance) if its `.renderer` is
45
- * the captured one. This is deduped/shared-trappable for the same reason
43
+ * `captured` — records `this` (the composer instance) if its `.renderer` is the
44
+ * captured one. This is deduped/shared-trappable for the same reason
46
45
  * `WebGLRenderer` is: `EffectComposer.js` is a FILE inside the same `three`
47
46
  * package tree Vite's `resolve.dedupe: ['three', …]` already collapses to one
48
- * instance — not a separate package with its own dedupe question. Proven
49
- * live against the `bloom-composer` fixture
47
+ * instance — not a separate package with its own dedupe question. Proven live
48
+ * against the `bloom-composer` fixture
50
49
  * (`docs/f13-bloom-composer-proof/record-fixed.mjs`).
51
50
  */
52
51
 
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Structural identity — the deterministic id scheme (and the small material
3
+ * reflection helper that rides along with it) the editor's live-three authoring
4
+ * adapter uses to address the objects of a world whose source carries no
5
+ * serve-time identity stamps
6
+ * (`packages/editor/src/authoring/live-three-identity.ts`, `structuralIdentity`).
7
+ *
8
+ * Identity: each object gets a **structural-path id** — deterministic from the
9
+ * scene's shape (position in the tree + three.js type + name), so the SAME id
10
+ * re-binds to the SAME object after the game rebuilds its scene within a
11
+ * session. It is also written to `userData.entityId` so the editor's object map
12
+ * / gizmo / selection (which key on `entityId`) bind to foreign objects with no
13
+ * further shim.
14
+ *
15
+ * This module used to be the shared core of a per-game JSON sidecar
16
+ * persistence system, which was deleted outright (2026-08-02) — ingest edits
17
+ * are LIVE-ONLY now. The id scheme survives because it is what makes the live
18
+ * hierarchy/selection/inspector work at all; nothing here reads or writes a
19
+ * file.
20
+ */
21
+
22
+ import type * as THREE from 'three';
23
+ import { setUserData } from '../../ecs/user-data';
24
+
25
+ /** Fixed id for the captured render camera (not a scene child, so no structural path). */
26
+ export const CAMERA_ID = 'ingest:camera';
27
+
28
+ /**
29
+ * The editor parks its OWN objects — grid, its two lights, the particle
30
+ * BatchedRenderer, every TransformControls/gizmo helper — on layer 31
31
+ * (`packages/editor/src/editor-layers.ts`'s `EDITOR_LAYER`) so the game camera
32
+ * never sees them. In ingest mode those objects are added to the GAME'S OWN
33
+ * scene, which is the same tree this walk indexes.
34
+ */
35
+ const EDITOR_ONLY_LAYER_MASK = 1 << 31;
36
+
37
+ /**
38
+ * Editor furniture, not game content. Excluded from the walk entirely — see
39
+ * {@link assignStructuralIds} for why that exclusion is what makes the id
40
+ * scheme's central promise true.
41
+ */
42
+ function isEditorOnly(o: THREE.Object3D): boolean {
43
+ return (o.layers.mask & EDITOR_ONLY_LAYER_MASK) !== 0;
44
+ }
45
+
46
+ /**
47
+ * The material whose colour a mesh's swatch reflects, if it has one.
48
+ *
49
+ * A multi-material mesh reflects its FIRST slot — that is what a single swatch
50
+ * can honestly stand for, and it is also the slot a creation-site anchor for
51
+ * `material.color` resolves against. Returns `null` for anything with no
52
+ * `color` at all rather than fabricating one.
53
+ */
54
+ export function colorMaterialOf(o: THREE.Object3D): THREE.MeshStandardMaterial | null {
55
+ const mat = (o as THREE.Mesh).material;
56
+ if (!mat) return null;
57
+ const m = (Array.isArray(mat) ? mat[0] : mat) as THREE.MeshStandardMaterial | undefined;
58
+ return m?.color ? m : null;
59
+ }
60
+
61
+ /** Result of a structural-path walk: the id→object map plus a few reflection stats. */
62
+ export interface StructuralIdWalk {
63
+ byId: Map<string, THREE.Object3D>;
64
+ count: number;
65
+ meshes: number;
66
+ lights: number;
67
+ }
68
+
69
+ /**
70
+ * (Re)assign structural-path ids to every object under `scene` (and, if
71
+ * given, the separately-captured render `camera`, under the fixed
72
+ * {@link CAMERA_ID}). Idempotent and deterministic from scene structure
73
+ * (position + type + name) — so a re-walk after the game rebuilds part of its
74
+ * tree re-binds the same ids to the same objects.
75
+ *
76
+ * EDITOR FURNITURE IS EXCLUDED, and that exclusion is load-bearing rather than
77
+ * cosmetic. In ingest mode the editor adds its grid, lights, particle
78
+ * BatchedRenderer and TransformControls gizmo to the GAME'S OWN scene, and
79
+ * they arrive asynchronously — some before the walk, some after. Indexing them
80
+ * made a game object's path depend on how much editor furniture happened to be
81
+ * attached at walk time, so the same mesh changed id across a re-walk (observed
82
+ * in the games-fps ingest: an object addressed as `ingest:106/0/0/0:…` came
83
+ * back as `ingest:102/0:Mesh:Cube004` — four root slots earlier, exactly the
84
+ * grid + two editor lights + BatchedRenderer).
85
+ *
86
+ * Skipping the furniture entirely — not merely declining to give it an id —
87
+ * is what fixes that: the game's own children occupy 0..N-1 whatever else is
88
+ * parented alongside them. It also keeps gizmo handles and the grid out of the
89
+ * hierarchy/inspector projections built on this walk, where they were
90
+ * selectable and colorable as if they were game content.
91
+ */
92
+ export function assignStructuralIds(
93
+ scene: THREE.Object3D,
94
+ camera?: THREE.Object3D | undefined,
95
+ ): StructuralIdWalk {
96
+ const byId = new Map<string, THREE.Object3D>();
97
+ let count = 0;
98
+ let meshes = 0;
99
+ let lights = 0;
100
+ const visit = (
101
+ o: THREE.Object3D & { isMesh?: boolean; isLight?: boolean },
102
+ path: string,
103
+ ): void => {
104
+ const id = `ingest:${path}:${o.type}:${o.name || ''}`;
105
+ setUserData(o, 'entityId', id);
106
+ byId.set(id, o);
107
+ count++;
108
+ if (o.isLight) lights++;
109
+ else if (o.isMesh) meshes++;
110
+ visitChildren(o, path);
111
+ };
112
+ const visitChildren = (o: THREE.Object3D, path: string): void => {
113
+ let i = 0;
114
+ for (const c of o.children) {
115
+ if (isEditorOnly(c)) continue;
116
+ visit(c, path ? `${path}/${i}` : `${i}`);
117
+ i++;
118
+ }
119
+ };
120
+ visitChildren(scene, '');
121
+ if (camera) {
122
+ setUserData(camera, 'entityId', CAMERA_ID);
123
+ byId.set(CAMERA_ID, camera);
124
+ count++;
125
+ }
126
+ return { byId, count, meshes, lights };
127
+ }
@@ -1,16 +1,13 @@
1
1
  /**
2
- * Upstream-pin extraction — R5 (docs/MASTER-ARCHITECTURE-REVIEW.md §4(c)/
3
- * §7.1 item 9): the fix for the overlay identity gap the structural-id +
4
- * `authoredAgainst.gameVersion` scheme (T7.7/D9) does not close. A re-vendor
5
- * of a vendored upstream game that keeps a slot's shape (index + three.js
6
- * type + name unchanged) reapplies a saved overlay onto a semantically
7
- * different object with `applied: 1, orphanedIds: []` silent wrong-meaning
8
- * success because `authoredAgainst.gameVersion` is the MANIFEST's own
9
- * `version` (hand-set by whoever wrote `vgai.game.json`, e.g. every
10
- * `public/ingest/*` vendored game ships `"version": "0.1.0"` regardless of
11
- * which upstream commit it vendors — see `public/ingest/{tanks,racing-game,
12
- * simcity}/vgai.game.json`), never tied to the thing that actually changes on
13
- * a re-vendor: the `UPSTREAM.md` pin.
2
+ * Upstream-pin extraction — R5 ((c)/ §7.1 item 9): the fix for the overlay identity
3
+ * gap the structural-id + `authoredAgainst.gameVersion` scheme (T7.7/D9) does not
4
+ * close. A re-vendor of a vendored upstream game that keeps a slot's shape (index +
5
+ * three.js type + name unchanged) reapplies a saved overlay onto a semantically
6
+ * different object with `applied: 1, orphanedIds: []` silent wrong-meaning success
7
+ * because `authoredAgainst.gameVersion` is the MANIFEST's own `version` (hand-set
8
+ * by whoever wrote the ingest manifest, e.g. every `public/ingest/*` vendored game
9
+ * ships `"version": "0.1.0"` regardless of which upstream commit it vendors), never
10
+ * tied to the thing that actually changes on a re-vendor: the `UPSTREAM.md` pin.
14
11
  *
15
12
  * This module is the pure, dependency-free half (parse text -> pin, or
16
13
  * `null`): NO file I/O here, so it is usable from any context (editor
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * Capability-gate shortfall reporting (D10, T7.6) — the honesty half of the
3
- * play-control contract. `docs/CAPABILITY-TIERS.md` §(d) draws the `loop` axis
4
- * (`gated | self-driven`) as independent of capability tier: a `self-driven`
5
- * world (an ingested game driving its own rAF/ticker) pauses for real ONLY if
6
- * its adapter implements an explicit loop-gate capability
7
- * (`MountedWorldBase.setPaused`/`step`) — otherwise it "honestly reports so"
8
- * rather than a silent no-op (the D5/COMPOSITION-DESIGN.md §5.2 finding: gating
9
- * a raw-rAF loop from outside was demonstrated and REJECTED). The audio seam
10
- * (`SystemAdapters.AudioAdapter`) has the identical shape: absent or unable to
11
- * silence a world's audio on pause must report, not pretend.
3
+ * play-control contract. §(d) draws the `loop` axis (`gated | self-driven`) as
4
+ * independent of capability tier: a `self-driven` world (an ingested game
5
+ * driving its own rAF/ticker) pauses for real ONLY if its adapter implements an
6
+ * explicit loop-gate capability (`MountedRootBase.setPaused`/`step`) —
7
+ * otherwise it "honestly reports so" rather than a silent no-op (the
8
+ * D5 §5.2 finding: gating a raw-rAF loop from outside was
9
+ * demonstrated and REJECTED). The audio seam (`SystemAdapters.AudioAdapter`)
10
+ * has the identical shape: absent or unable to silence a world's audio on pause
11
+ * must report, not pretend.
12
12
  *
13
13
  * Pure compute + message-formatting, same split as `achieved-tier.ts`
14
14
  * (`packages/editor/src/achieved-tier.ts`) — callers own the actual
@@ -36,8 +36,8 @@ function formatGateMessage(
36
36
  ): string {
37
37
  return (
38
38
  `${prefix} world "${report.worldId}": pause requested but ${capability} cannot be gated ` +
39
- `(${report.reason}) — reporting honestly instead of silently no-op-ing ` +
40
- `(docs/CAPABILITY-TIERS.md §d). ${prefix} ${JSON.stringify(report)}`
39
+ `(${report.reason}) — reporting honestly instead of silently no-op-ing. ` +
40
+ `${prefix} ${JSON.stringify(report)}`
41
41
  );
42
42
  }
43
43
 
@@ -4,6 +4,11 @@
4
4
  * behind-the-interface form of the old `setEcsSyncTransform` teleport: it lets
5
5
  * the editor freeze a body, apply a gizmo edit, and resume — without the editor
6
6
  * knowing Rapier exists.
7
+ *
8
+ * The seam is keyed by NODE ID (P-4), so THIS is where the three-specific
9
+ * `id → Object3D` step happens: once, at the implementer's own boundary,
10
+ * through the `resolve` function its constructor is handed. Nothing above this
11
+ * line — not the editor, not the seam — needs to know an `Object3D` exists.
7
12
  */
8
13
 
9
14
  import type RAPIER from '@dimforge/rapier3d-compat';
@@ -15,7 +20,14 @@ import type { Transform, TransformOwner } from './transform';
15
20
 
16
21
  export function createRapierPhysicsAdapter(
17
22
  registry: PhysicsRegistry,
18
- physics?: PhysicsContext | null,
23
+ physics: PhysicsContext | null | undefined,
24
+ /**
25
+ * The world's own `id → Object3D` map. Injected rather than derived here so
26
+ * this file never hard-codes an identity convention (e.g. reading the
27
+ * first-party `userData.entityId` key off foreign objects, which is the
28
+ * ingest-stamping leak the retirement playbook deliberately parked).
29
+ */
30
+ resolve: (nodeId: string) => THREE.Object3D | null,
19
31
  ): PhysicsAdapter {
20
32
  /** Saved body types while frozen, so unfreeze can restore them. */
21
33
  const frozen = new Map<RAPIER.RigidBody, number>();
@@ -64,22 +76,28 @@ export function createRapierPhysicsAdapter(
64
76
  return contactBuf.subarray(0, contactCount * 3);
65
77
  };
66
78
 
79
+ /** The one place this adapter crosses from node id into Three's vocabulary. */
80
+ const bodyFor = (nodeId: string): RAPIER.RigidBody | undefined => {
81
+ const o = resolve(nodeId);
82
+ return o ? registry.get(o)?.body : undefined;
83
+ };
84
+
67
85
  return {
68
- ownerOf(o: THREE.Object3D): TransformOwner {
69
- const body = registry.get(o)?.body;
86
+ ownerOf(nodeId: string): TransformOwner {
87
+ const body = bodyFor(nodeId);
70
88
  if (!body) return 'editor';
71
89
  // A fixed body never moves on its own → the editor owns its transform.
72
90
  return body.isFixed() ? 'editor' : 'physics';
73
91
  },
74
- freeze(o: THREE.Object3D): void {
75
- const body = registry.get(o)?.body;
92
+ freeze(nodeId: string): void {
93
+ const body = bodyFor(nodeId);
76
94
  if (!body || frozen.has(body)) return;
77
95
  frozen.set(body, body.bodyType());
78
96
  // Kinematic-position: the body stops simulating but tracks the pose we set.
79
97
  body.setBodyType(2 /* KinematicPositionBased */, true);
80
98
  },
81
- commit(o: THREE.Object3D, t: Transform): void {
82
- const body = registry.get(o)?.body;
99
+ commit(nodeId: string, t: Transform): void {
100
+ const body = bodyFor(nodeId);
83
101
  if (!body) return;
84
102
  body.setTranslation({ x: t.position[0], y: t.position[1], z: t.position[2] }, true);
85
103
  body.setRotation(
@@ -87,8 +105,8 @@ export function createRapierPhysicsAdapter(
87
105
  true,
88
106
  );
89
107
  },
90
- unfreeze(o: THREE.Object3D): void {
91
- const body = registry.get(o)?.body;
108
+ unfreeze(nodeId: string): void {
109
+ const body = bodyFor(nodeId);
92
110
  if (!body) return;
93
111
  const prev = frozen.get(body);
94
112
  if (prev !== undefined) {
@@ -0,0 +1,217 @@
1
+ /**
2
+ * RootAdapter — the host ⇄ runtime contract. A ROOT is *anything that mounts*.
3
+ *
4
+ * This is the interface the HOST depends on. It replaces the old
5
+ * `{ setup, components }` shape: the host no longer knows about `GameSetupFn`
6
+ * — that is the internal of ONE implementer
7
+ * (`SetupThreeRootAdapter`). First-party content and an unmodified external game
8
+ * are peer implementers of THIS interface; the host has no branch on which.
9
+ *
10
+ * It was `GameAdapter` until WO-9 #60, and the rename is an ONTOLOGY fix, not
11
+ * taste. Under D20 a *game* is the manifest's whole `roots` array — that is
12
+ * what `runtime/game.ts`'s `Game` is. Every implementer of THIS interface
13
+ * adapts exactly ONE root. So `Game` and `GameAdapter` sat at different levels
14
+ * of the ontology while sharing a name-stem, which reads as though one were
15
+ * the adapter for the other. The surrounding vocabulary had already moved:
16
+ * `MountedRootBase`, `MountedRoot`, `MountedRootFor`, `RootStateObserver`,
17
+ * `SurfaceAdapterFor`, `RootInstance`, `mountManifestRoots` — and, decisively,
18
+ * the resolver that PRODUCES these (`resolveRootAdapter`, `RootAdapterSchema`,
19
+ * `resolveThreeRootAdapter`) already said root. The interface was the last
20
+ * holdout, out of step with its own resolver.
21
+ *
22
+ * The `K = 'three'` default below is KEPT deliberately (see the note on the
23
+ * interface).
24
+ */
25
+
26
+ import type { Container } from 'pixi.js';
27
+ import type * as THREE from 'three';
28
+ import type { AdapterSurface } from './adapter-surface';
29
+ import type { AuthoringAdapter } from './authoring';
30
+ import type { HostContextFor } from './host-context';
31
+ import type { SystemAdapters } from './system-adapter';
32
+
33
+ /**
34
+ * The ingested-world observation contract (T7.4 slice 2, "the
35
+ * must-answer"). Cross-world data flow with a
36
+ * foreign game is observation via an adapter-provided interface, not ordinary
37
+ * shared state (that's D7's answer for first-party↔first-party flow).
38
+ * Optional on {@link MountedThreeRoot} —
39
+ * absence means the adapter has nothing genuinely observable to expose (the
40
+ * anti-shim rule: never fabricate state), and the world takes the honest
41
+ * "no state bridge" tier hit (see `Game.registerRoot` in `runtime/game.ts`)
42
+ * rather than silently offering an `undefined`-forever subscription.
43
+ */
44
+ export interface RootStateObserver {
45
+ /**
46
+ * Notified at most once per frame IF the adapter can hook the game's own
47
+ * update (`loop: 'gated'` roots, §(d)); self-driven roots (`loop:
48
+ * 'self-driven'`, raw-rAF) may notify on their OWN rAF cadence instead —
49
+ * consumers must not assume our frame timing. Returns an unsubscribe
50
+ * function.
51
+ */
52
+ subscribe(onChange: () => void): () => void;
53
+ /**
54
+ * A cheap, adapter-chosen snapshot of this world's observable state.
55
+ * Returns a STABLE reference while nothing has changed (so a selector run
56
+ * over it can cheaply bail out by reference, same spirit as
57
+ * `GameStateBridge`'s frame-version cache). Shape is adapter-defined —
58
+ * not centrally schematized in v1.
59
+ */
60
+ snapshot(): unknown;
61
+ }
62
+
63
+ /**
64
+ * Everything a mounted world provides EXCEPT its render surface: THREE.Scene/
65
+ * THREE.Camera typing is replaced with kind-tagged world surfaces. Split out
66
+ * of the old flat
67
+ * `MountedThreeRoot` so a non-threejs world can carry its own kind-appropriate
68
+ * surface field (a pixi `stage`, a react `container`) instead of being
69
+ * force-fit through THREE `scene`/`camera` fields it doesn't have.
70
+ */
71
+ export interface MountedRootBase {
72
+ /**
73
+ * Loop model:
74
+ * - `false` (host-driven): first-party + "clean" externals. The host ticks
75
+ * `update(dt)` in its loop.
76
+ * - `true` (self-driven): an unmodified game that owns its renderer + rAF.
77
+ * The host does NOT tick it. Pause/step control is a DECLARED CAPABILITY,
78
+ * not a promise: implement `setPaused`/`step` only where the game exposes
79
+ * a sanctioned pause/step mechanism. Gating a raw-rAF loop from outside
80
+ * was demonstrated and REJECTED (D5 — it halts the loop rather than
81
+ * pausing it); such games are the "composited, unsynchronized" tier:
82
+ * `setPaused` absent, host degrades loudly (T7.6 owns the tier surface).
83
+ */
84
+ readonly drivesOwnLoop: boolean;
85
+
86
+ update?(dt: number): void; // host-driven only
87
+ fixedUpdate?(dt: number): void;
88
+ setPaused?(paused: boolean): void; // capability, not promise — see loop-model note above
89
+ step?(): void;
90
+
91
+ resize?(width: number, height: number): void;
92
+ dispose(): void;
93
+
94
+ /** Optional capability providers — absence = "not supported", host degrades. */
95
+ readonly authoring?: AuthoringAdapter;
96
+ readonly systems?: SystemAdapters;
97
+ /** Optional state-observation capability (T7.4 slice 2, §4) — absent means
98
+ * "no state bridge"; `Game.registerRoot` reports this loudly, once, for
99
+ * any non-first-party mount. First-party mounts are exempt — their state
100
+ * is observed through `Game.state`/`useWorldState` instead (§3), not this
101
+ * field. */
102
+ readonly observe?: RootStateObserver;
103
+ }
104
+
105
+ /** A live, mounted three world. The host obtains `scene`/`camera` to
106
+ * render+author. Until WO-9 task #9 this type also had a `MountedGame` alias,
107
+ * which every pre-T7.5 threejs call site named; the alias is deleted, because
108
+ * what it aliased is one three ROOT and a game is the manifest's whole set of
109
+ * roots (D20). */
110
+ export interface MountedThreeRoot extends MountedRootBase {
111
+ readonly kind: 'three';
112
+ /** The live scene + camera the editor inspects/renders for authoring. */
113
+ readonly scene: THREE.Scene;
114
+ readonly camera: THREE.Camera;
115
+ }
116
+
117
+ /** A live, mounted canvas world (T7.3) — the pixi analog of
118
+ * {@link MountedThreeRoot}. `stage` is the pixi world container the host
119
+ * renders/authors, the surface `RootInstance.pixiStage()` returns. */
120
+ export interface MountedPixiRoot extends MountedRootBase {
121
+ readonly kind: 'canvas';
122
+ readonly stage: Container;
123
+ }
124
+
125
+ /** A live, mounted react world (T6.2) — the react analog of
126
+ * {@link MountedThreeRoot}. `container` is the DOM-root layer the host
127
+ * handed the adapter's `mount` (the SAME element `RootInstance.reactRoot()`
128
+ * returns) — a react world's tree renders into it via `createRoot`. */
129
+ export interface MountedReactRoot extends MountedRootBase {
130
+ readonly kind: 'dom';
131
+ readonly container: HTMLElement;
132
+ }
133
+
134
+ /** Every kind of live, mounted world (T7.5) — the union `RootInstance.mounted`
135
+ * is typed against now, replacing the THREE-only `MountedThreeRoot`. */
136
+ export type MountedRoot = MountedThreeRoot | MountedPixiRoot | MountedReactRoot;
137
+
138
+ /** Map a {@link AdapterSurface} to its mounted-world shape (mirrors `NodeOf`/
139
+ * `BodyOf`/`ColliderOf` in `ecs/behavior.ts`) — lets generic code over
140
+ * `K extends AdapterSurface` name the right surface without a manual union. */
141
+ export type MountedRootFor<K extends AdapterSurface> = K extends 'three'
142
+ ? MountedThreeRoot
143
+ : K extends 'canvas'
144
+ ? MountedPixiRoot
145
+ : K extends 'dom'
146
+ ? MountedReactRoot
147
+ : never;
148
+
149
+ /**
150
+ * The interface every game implements to run on the host. Generic over
151
+ * {@link AdapterSurface} (T7.5) so a non-threejs implementer's `mount` returns
152
+ * its OWN kind-tagged surface instead of being cast through the three shape —
153
+ * defaults to `'three'` so every pre-T7.5 implementer/call site
154
+ * (`RootAdapter`, unparameterized) keeps compiling unchanged.
155
+ *
156
+ * `K` parameterizes BOTH halves of `mount` (P-8): until then only the RETURN
157
+ * type was kind-tagged, while the PARAMETER was always the three-shaped
158
+ * `ThreeHostContext` — so a `RootAdapter<'canvas'>` was, by its own type, handed a
159
+ * `THREE.WebGLRenderer` it cannot use, and the production Pixi/React adapters
160
+ * could not be `RootAdapter`s at all (they were separate structural types over
161
+ * `CanvasHostContext`/`DomHostContext`, and `RootAdapter<'canvas'>` existed only in
162
+ * tests). With `HostContextFor<K>` the surface tag picks the context type, and
163
+ * those production adapters are now genuine `RootAdapter<K>` subtypes —
164
+ * `Pixi2DRootAdapter extends RootAdapter<'canvas'>`, `ReactRootAdapter extends
165
+ * RootAdapter<'dom'>` (`runtime/create-runtime.ts`).
166
+ */
167
+ /**
168
+ * On the `= 'three'` default (WO-9 #60 considered removing it, and did not):
169
+ * this default is type-level only: an author who writes bare `RootAdapter` for
170
+ * a pixi root gets a compile error on `mount`'s return type, because
171
+ * `MountedRootFor<'three'>` demands `scene`/`camera` a `MountedPixiRoot` has
172
+ * not got. Wrong-by-default is caught by tsc, so removing this default would
173
+ * be churn across ~125 bare call sites for no defect it could catch.
174
+ */
175
+ export interface RootAdapter<K extends AdapterSurface = 'three'> {
176
+ /** Stable id (telemetry/registry/conformance). */
177
+ readonly id: string;
178
+ /** Build/start the game against the host context for ITS surface; return the
179
+ * handle. */
180
+ mount(host: HostContextFor<K>): Promise<MountedRootFor<K>>;
181
+ }
182
+
183
+ /**
184
+ * One surface tag paired with an adapter typed FOR that surface (WO-34).
185
+ *
186
+ * Why this exists: a resolver that turns a manifest root into a mountable
187
+ * adapter handles every surface in one function, so its return type has to
188
+ * span all three. Returning the bare union `RootAdapter<'three'> |
189
+ * RootAdapter<'canvas'> | RootAdapter<'dom'>` does NOT work — a union of
190
+ * function types INTERSECTS its parameters, so `.mount(host)` becomes
191
+ * uncallable with any single host value (no one object is simultaneously a
192
+ * `ThreeHostContext`, a `CanvasHostContext` and a `DomHostContext`). The historical
193
+ * workaround was to declare the whole channel `RootAdapter<'three'>` and
194
+ * re-assert the real kind at each call site with `as unknown as` — kind
195
+ * ERASURE: the surface tag the manifest already knew was thrown away and
196
+ * guessed back.
197
+ *
198
+ * Carrying the tag ALONGSIDE the adapter fixes that structurally: this is a
199
+ * discriminated union on `surface`, so `switch (resolved.surface)` narrows
200
+ * `resolved.adapter` to ONE `RootAdapter<K>` inside each branch — a single
201
+ * signature again, hence `.mount(host)` is callable with that surface's own
202
+ * host context, checked by the compiler instead of asserted.
203
+ */
204
+ export type SurfaceAdapterFor<K extends AdapterSurface> = {
205
+ readonly surface: K;
206
+ readonly adapter: RootAdapter<K>;
207
+ };
208
+
209
+ /** Every surface's {@link SurfaceAdapterFor}, as one discriminated union.
210
+ * Written out member-by-member (rather than as a distributive conditional)
211
+ * so `assertNever` sees exactly three variants and a hypothetical 4th
212
+ * `AdapterSurface` member fails to compile HERE, at the vocabulary, rather
213
+ * than silently widening every consumer's exhaustiveness guard. */
214
+ export type SurfaceAdapter =
215
+ | SurfaceAdapterFor<'three'>
216
+ | SurfaceAdapterFor<'canvas'>
217
+ | SurfaceAdapterFor<'dom'>;