@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.
- package/README.md +18 -11
- package/package.json +10 -6
- package/schemas/engine-api.json +1 -68
- package/schemas/engine-api.md +1 -32
- package/schemas/engine-capabilities.json +30 -42
- package/schemas/{vgai-game.schema.json → vgai-project.schema.json} +52 -34
- package/src/adapter/adapter-surface.ts +5 -5
- package/src/adapter/authoring.ts +168 -226
- package/src/adapter/colyseus-networking-adapter.ts +44 -5
- package/src/adapter/first-party-systems.ts +156 -42
- package/src/adapter/host-context.ts +177 -46
- package/src/adapter/index.ts +47 -51
- package/src/adapter/ingest/game-contract.ts +2 -2
- package/src/adapter/ingest/scene-capture.ts +18 -19
- package/src/adapter/ingest/structural-ids.ts +127 -0
- package/src/adapter/ingest/upstream-pin.ts +9 -12
- package/src/adapter/loop-gate-report.ts +11 -11
- package/src/adapter/rapier-physics-adapter.ts +27 -9
- package/src/adapter/root-adapter.ts +217 -0
- package/src/adapter/{vgai-scene-game-adapter.ts → setup-three-root-adapter.ts} +173 -351
- package/src/adapter/system-adapter.ts +80 -63
- package/src/ai/navigation.ts +1 -1
- package/src/animation/animation-clock.ts +1 -1
- package/src/animation/camera-ownership.ts +1 -2
- package/src/animation/cubic-spline-interpolant.ts +132 -0
- package/src/animation/theatre-clock-binding.ts +2 -2
- package/src/animation/theatre-object-binding.ts +4 -4
- package/src/animation/xstate-animation-binding.ts +75 -5
- package/src/{scene/schema → asset-formats}/camera.ts +2 -5
- package/src/{scene/schema → asset-formats}/collider.ts +2 -5
- package/src/asset-formats/index.ts +54 -0
- package/src/{scene/schema → asset-formats}/instances.ts +10 -6
- package/src/{scene/schema → asset-formats}/light.ts +3 -6
- package/src/{scene/schema → asset-formats}/material.ts +4 -7
- package/src/{scene/schema → asset-formats}/mesh.ts +4 -7
- package/src/asset-formats/parse.ts +39 -0
- package/src/{scene/schema → asset-formats}/particles.ts +3 -6
- package/src/{scene/schema/environment.ts → asset-formats/render-env.ts} +23 -86
- package/src/{scene/schema → asset-formats}/tuples.ts +1 -1
- package/src/{scene/asset-loaders.ts → asset-loaders.ts} +8 -9
- package/src/asset-parse-error.ts +33 -0
- package/src/{scene/asset-registry.ts → asset-registry.ts} +1 -1
- package/src/assets.ts +1 -1
- package/src/audio/wav-encode.ts +9 -9
- package/src/canvas-react/engine-bridge.ts +59 -0
- package/src/canvas-react/index.ts +50 -0
- package/src/canvas-react/pixi-primitive.tsx +202 -0
- package/src/canvas-react/pixi-react-adapter.tsx +290 -0
- package/src/canvas-react/pixi-react-root-factory.tsx +88 -0
- package/src/canvas-react/world-context.ts +328 -0
- package/src/core/frame-pacing.ts +100 -0
- package/src/core/game-loop.ts +50 -28
- package/src/core/seeded-random.ts +7 -7
- package/src/core/sim-clock.ts +388 -0
- package/src/core/system-runner.ts +17 -63
- package/src/core/types.ts +34 -15
- package/src/data/data-asset.ts +3 -3
- package/src/data/data-check-core.ts +6 -7
- package/src/data/data-ref.ts +11 -11
- package/src/data/vite-plugin-data.ts +10 -10
- package/src/{scene/defaults.ts → defaults.ts} +18 -40
- package/src/dev/render-debug-adapter.ts +1 -1
- package/src/dev/webgl-frame-capture.ts +1 -1
- package/src/ecs/scene-index.ts +439 -0
- package/src/ecs/scene-query.ts +43 -0
- package/src/{scene → ecs}/user-data.ts +17 -36
- package/src/index.ts +7 -9
- package/src/input/input-manager.ts +29 -32
- package/src/input/input-types.ts +2 -2
- package/src/input/schema.ts +5 -5
- package/src/loader.ts +57 -0
- package/src/manifest/editor-port.ts +69 -0
- package/src/manifest/filename.ts +49 -0
- package/src/manifest/index.ts +8 -2
- package/src/manifest/load-file.ts +11 -0
- package/src/manifest/load.ts +65 -77
- package/src/manifest/locate.ts +55 -0
- package/src/manifest/schema.ts +400 -233
- package/src/{scene → physics}/collider-dimensions.ts +3 -3
- package/src/physics/physics-registry.ts +1 -1
- package/src/{world2d/authoring-2d.ts → pixi/authoring.ts} +24 -11
- package/src/pixi/index.ts +43 -0
- package/src/{world2d/ingest-iframe-2d.ts → pixi/ingest-iframe.ts} +9 -9
- package/src/{world2d/ingest2d.ts → pixi/ingest.ts} +28 -28
- package/src/{world2d/physics2d-registry.ts → pixi/physics-registry.ts} +1 -1
- package/src/{world2d/scene-capture-2d.ts → pixi/scene-capture.ts} +3 -3
- package/src/{world2d/system-adapters-2d.ts → pixi/system-adapters.ts} +2 -2
- package/src/react/unmanaged-root-detector.ts +26 -1
- package/src/react/use-data.ts +5 -5
- package/src/react/use-selection.tsx +15 -42
- package/src/react/{game-state.tsx → world-state.tsx} +44 -46
- package/src/render/auto-batcher.ts +1 -2
- package/src/{scene → render}/instance-mesh.ts +1 -1
- package/src/{scene → render}/light-camera-factory.ts +14 -13
- package/src/render/lod.ts +17 -0
- package/src/{scene → render}/material-factory.ts +8 -7
- package/src/{scene → render}/particles-factory.ts +62 -12
- package/src/render/render-batch-system.ts +14 -41
- package/src/render/render-features.ts +1 -1
- package/src/render/render-settings.ts +1 -2
- package/src/render/spark-renderer-lifecycle.ts +1 -1
- package/src/runtime/create-runtime.ts +352 -463
- package/src/runtime/debug-bridge.ts +148 -93
- package/src/runtime/debug-registry.ts +79 -54
- package/src/runtime/dev-layers.ts +40 -0
- package/src/runtime/frame-selector-cache.ts +4 -4
- package/src/runtime/game.ts +618 -369
- package/src/runtime/gameplay-rng-trap.ts +6 -7
- package/src/runtime/input-router.ts +11 -11
- package/src/runtime/mount-game.ts +54 -55
- package/src/runtime/mount-manifest.ts +154 -150
- package/src/runtime/presentation.ts +141 -0
- package/src/runtime/render-audio-control.ts +64 -53
- package/src/runtime/render-control.ts +45 -51
- package/src/runtime/render-seed.ts +3 -4
- package/src/runtime/state-bridge.ts +17 -18
- package/src/runtime/types.ts +94 -61
- package/src/setup/setup-renderer.ts +11 -6
- package/src/world3d-react/engine-bridge.ts +46 -33
- package/src/world3d-react/index.ts +31 -26
- package/src/world3d-react/r3f-adapter.tsx +211 -96
- package/src/world3d-react/r3f-root-factory.tsx +91 -0
- package/src/world3d-react/renderer-config.ts +137 -0
- package/src/world3d-react/world-context.ts +141 -111
- package/schemas/entity2d.schema.json +0 -468
- package/schemas/prefab.schema.json +0 -9992
- package/schemas/scn2d.schema.json +0 -494
- package/schemas/vscn.schema.json +0 -10851
- package/src/adapter/game-adapter.ts +0 -164
- package/src/adapter/ingest/overlay-applier.ts +0 -207
- package/src/adapter/ingest/overlay-apply.ts +0 -168
- package/src/adapter/ingest/overlay-file.ts +0 -126
- package/src/adapter/ingest/overlay-report.ts +0 -176
- package/src/animation/gsap-registration.ts +0 -184
- package/src/audio/audio-introspection.ts +0 -290
- package/src/audio/index.ts +0 -39
- package/src/audio/tone-clock-binding.ts +0 -98
- package/src/audio/tone-context.ts +0 -175
- package/src/audio/tone-offline-render.ts +0 -167
- package/src/ecs/component-manager.ts +0 -814
- package/src/ecs/game-component.ts +0 -260
- package/src/ecs/hmr-swap-report.ts +0 -65
- package/src/physics/trigger-dispatch.ts +0 -97
- package/src/react/root-adapter.tsx +0 -49
- package/src/scene/asset-paths.ts +0 -121
- package/src/scene/asset-ref-check.ts +0 -248
- package/src/scene/component-registry.ts +0 -51
- package/src/scene/parse.ts +0 -204
- package/src/scene/scene-apply.ts +0 -407
- package/src/scene/scene-diff-schema.ts +0 -115
- package/src/scene/scene-diff-types.ts +0 -29
- package/src/scene/scene-loader.ts +0 -1526
- package/src/scene/scene-query.ts +0 -63
- package/src/scene/scene-types.ts +0 -33
- package/src/scene/scene-version.ts +0 -40
- package/src/scene/schema/animation.ts +0 -46
- package/src/scene/schema/audio.ts +0 -25
- package/src/scene/schema/entity-ref.ts +0 -78
- package/src/scene/schema/entity.ts +0 -189
- package/src/scene/schema/index.ts +0 -51
- package/src/scene/schema/joint.ts +0 -26
- package/src/scene/schema/physics.ts +0 -49
- package/src/scene/schema/scene-file.ts +0 -292
- package/src/scene/schema/shadow.ts +0 -24
- package/src/scene/schema/spline.ts +0 -21
- package/src/world2d/asset-paths2d.ts +0 -44
- package/src/world2d/capture-to-scene2d.ts +0 -52
- package/src/world2d/collision-2d.ts +0 -99
- package/src/world2d/entity2d-asset.ts +0 -22
- package/src/world2d/index.ts +0 -91
- package/src/world2d/physics2d-transform.ts +0 -173
- package/src/world2d/physics2d-units.ts +0 -10
- package/src/world2d/pixi-game-adapter.ts +0 -439
- package/src/world2d/pixi-surface.ts +0 -78
- package/src/world2d/scene2d-identity.ts +0 -49
- package/src/world2d/scene2d-loader.ts +0 -433
- package/src/world2d/schema/entity2d.ts +0 -163
- package/src/world2d/schema/physics2d.ts +0 -64
- package/src/world2d/schema/sprite.ts +0 -99
- package/src/world2d/schema/tilemap.ts +0 -39
- package/src/world2d/schema/tuples2d.ts +0 -25
- package/src/world2d/transform-writer-2d.ts +0 -42
- package/src/world2d/types.ts +0 -74
- package/src/world3d-react/behavior.tsx +0 -146
- /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 (
|
|
28
|
-
*
|
|
27
|
+
* bundles its own copy of `three` cannot be captured this way (the module-identity
|
|
28
|
+
* gatekeeper).
|
|
29
29
|
*
|
|
30
|
-
* Wave 17
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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 (
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `version`
|
|
10
|
-
*
|
|
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.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
|
|
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
|
|
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(
|
|
69
|
-
const 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(
|
|
75
|
-
const 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(
|
|
82
|
-
const 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(
|
|
91
|
-
const 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'>;
|