@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
|
@@ -1,52 +1,47 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
2
|
import { assertNever } from '../adapter/adapter-surface';
|
|
3
|
+
import type { CanvasHostContext, DomHostContext, ThreeHostContext } from '../adapter/host-context';
|
|
3
4
|
import type {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
MountedPixiRoot,
|
|
6
|
+
MountedReactRoot,
|
|
7
|
+
MountedRoot,
|
|
8
|
+
MountedThreeRoot,
|
|
9
|
+
RootAdapter,
|
|
10
|
+
} from '../adapter/root-adapter';
|
|
10
11
|
import { createAssetCache } from '../assets';
|
|
11
12
|
import { createGameLoop } from '../core/game-loop';
|
|
12
13
|
import { createHostRenderer } from '../setup/setup-renderer';
|
|
13
|
-
// TYPE-ONLY
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// supplied ALREADY-CONSTRUCTED by the caller (a `PixiSceneGameAdapter` or any
|
|
17
|
-
// structurally-compatible adapter), so the host only ever needs these types.
|
|
18
|
-
import type { MountedGame2D, World2DHost } from '../world2d/pixi-game-adapter';
|
|
14
|
+
// TYPE-ONLY: `create-runtime.ts` must never value-import `pixi.js` — a canvas
|
|
15
|
+
// `RootMountSpec`'s `adapter` is supplied ALREADY-CONSTRUCTED by the caller, so
|
|
16
|
+
// the host only ever needs the seam's types.
|
|
19
17
|
import {
|
|
20
18
|
createGame,
|
|
21
|
-
|
|
19
|
+
createRootInstance,
|
|
22
20
|
type Game,
|
|
23
21
|
type GameInternal,
|
|
24
22
|
isFirstPartyMounted,
|
|
25
|
-
type
|
|
23
|
+
type RootInstance,
|
|
26
24
|
} from './game';
|
|
27
25
|
import { createInputRouter, type RouterAdapterRoot, stackOrder } from './input-router';
|
|
28
26
|
import { installRenderControlHarness, isRenderModeRequested } from './render-control';
|
|
29
|
-
|
|
30
|
-
// Re-export the camera-precedence helper from its new home so existing importers
|
|
31
|
-
// (`import { adoptSceneCamera } from '@engine/runtime/create-runtime'`) keep working.
|
|
32
|
-
export { adoptSceneCamera } from '../adapter/vgai-scene-game-adapter';
|
|
27
|
+
import type { PlaytestContext } from './types';
|
|
33
28
|
|
|
34
29
|
/**
|
|
35
|
-
* Register a
|
|
36
|
-
*
|
|
30
|
+
* Register a three world for a freshly-mounted game onto the Game shell. Shared
|
|
31
|
+
* by `createGameRuntime` and every headless test harness
|
|
37
32
|
* (`test/game-root.test.ts`, `test/frame-order.test.ts`,
|
|
38
33
|
* `test/game-two-roots.test.ts`) so there is exactly one code path for this
|
|
39
|
-
* wiring (T7.1 slice 1, id-generalized in slice 3 for the two-world proof —
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* `
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
34
|
+
* wiring (T7.1 slice 1, id-generalized in slice 3 for the two-world proof — stage
|
|
35
|
+
* 3) — `physics`/`collisions`/`camera`/ `frame` are populated from the mount's
|
|
36
|
+
* first-party `GameContext`/ `MountedSetupThreeRoot` when available, left
|
|
37
|
+
* `undefined` otherwise (an external adapter's mount has none of these
|
|
38
|
+
* first-party handles — `frame` in particular is what makes
|
|
39
|
+
* `GameInternal.runFrame` (T7.1 slice 2) fall back to a single opaque
|
|
40
|
+
* `mounted.update` call per substep for such a world).
|
|
46
41
|
*/
|
|
47
42
|
function firstPartyExtras(
|
|
48
|
-
mounted:
|
|
49
|
-
): Pick<Parameters<typeof
|
|
43
|
+
mounted: MountedRoot,
|
|
44
|
+
): Pick<Parameters<typeof createRootInstance>[0], 'physics' | 'collisions' | 'camera' | 'frame'> {
|
|
50
45
|
if (!isFirstPartyMounted(mounted)) return {};
|
|
51
46
|
return {
|
|
52
47
|
physics: mounted.ctx.physics,
|
|
@@ -57,192 +52,122 @@ function firstPartyExtras(
|
|
|
57
52
|
}
|
|
58
53
|
|
|
59
54
|
/**
|
|
60
|
-
* Register a
|
|
55
|
+
* Register a three world onto the Game shell. `opts.id` defaults to
|
|
61
56
|
* `'main'` — the single-world callers (`createGameRuntime`, slice-1/2 tests)
|
|
62
57
|
* are unaffected; a second/third world (T7.1 slice 3's two-world proof, and
|
|
63
58
|
* real multi-world manifests later) pass an explicit id. This is the ONE
|
|
64
|
-
* registration code path for a
|
|
59
|
+
* registration code path for a three world, whatever its id.
|
|
65
60
|
*/
|
|
66
|
-
export function
|
|
61
|
+
export function registerThreeRoot(
|
|
67
62
|
game: GameInternal,
|
|
68
|
-
adapter:
|
|
69
|
-
mounted:
|
|
63
|
+
adapter: RootAdapter,
|
|
64
|
+
mounted: MountedThreeRoot,
|
|
70
65
|
opts?: { id?: string | undefined; pausable?: boolean | undefined },
|
|
71
|
-
):
|
|
72
|
-
const world =
|
|
66
|
+
): RootInstance {
|
|
67
|
+
const world = createRootInstance({
|
|
73
68
|
id: opts?.id ?? 'main',
|
|
74
|
-
kind: '
|
|
69
|
+
kind: 'three',
|
|
75
70
|
pausable: opts?.pausable ?? true,
|
|
76
71
|
adapter,
|
|
77
72
|
mounted,
|
|
78
73
|
scene: mounted.scene as THREE.Scene,
|
|
79
74
|
...firstPartyExtras(mounted),
|
|
80
75
|
});
|
|
81
|
-
|
|
82
|
-
// documented on `resolveWorldInstance` in `ecs/component-manager.ts`):
|
|
83
|
-
// scene-authored components were attached by the scene loader DURING the
|
|
84
|
-
// `mount()` call that just returned `mounted`, before this `WorldInstance`
|
|
85
|
-
// existed — so `instance.world` was left `undefined` for every one of
|
|
86
|
-
// them. `adoptWorld` backfills it now, once, for every already-attached
|
|
87
|
-
// instance still missing a world. Attaches made AFTER this point (e.g.
|
|
88
|
-
// runtime-spawned entities once the game is ticking) still resolve their
|
|
89
|
-
// world directly at attach time, unaffected by this call.
|
|
90
|
-
if (isFirstPartyMounted(mounted)) {
|
|
91
|
-
mounted.ctx.components.adoptWorld(world);
|
|
92
|
-
} else if (mounted.components) {
|
|
93
|
-
// Same one-time backfill for a non-first-party mount exposing the
|
|
94
|
-
// optional `MountedWorldBase.components` capability (e.g.
|
|
95
|
-
// `@engine/world3d-react` — its `<Behavior>` attaches happened during
|
|
96
|
-
// `mount()`, before this `WorldInstance` existed).
|
|
97
|
-
mounted.components.adoptWorld(world);
|
|
98
|
-
}
|
|
99
|
-
game.registerWorld(world);
|
|
76
|
+
game.registerRoot(world);
|
|
100
77
|
return world;
|
|
101
78
|
}
|
|
102
79
|
|
|
103
80
|
/**
|
|
104
|
-
* Register a
|
|
105
|
-
* {@link
|
|
106
|
-
* `
|
|
107
|
-
* OWN (separate) Game, and `test/game-three-plus-pixi.test.ts` performs by
|
|
108
|
-
* hand onto a SHARED Game. `createGameRuntime`'s roots path
|
|
109
|
-
* (T6.1 slice 1) is the first PRODUCTION caller that registers a pixijs
|
|
110
|
-
* world alongside other roots on one Game; this is its one wiring code
|
|
111
|
-
* path, so a second/third pixijs world (or a future caller) never
|
|
112
|
-
* re-derives it. `adapter` is typed as the real {@link Pixi2DGameAdapter}
|
|
113
|
-
* shape (T7.5 — previously the generic `GameAdapter`, which forced a cast at
|
|
114
|
-
* every call site since a pixi adapter's `mount` takes a `World2DHost`, not
|
|
115
|
-
* `HostContext`); `WorldInstance.adapter` itself only needs `.id`
|
|
81
|
+
* Register a canvas world onto the Game shell — the pixi analog of
|
|
82
|
+
* {@link registerThreeRoot}. `adapter` is the seam's own
|
|
83
|
+
* `RootAdapter<'canvas'>`; `RootInstance.adapter` itself only needs `.id`
|
|
116
84
|
* (`AdapterHandle`, `runtime/game.ts`), so this passes through with zero cast.
|
|
117
|
-
*
|
|
118
|
-
* Section 7.4-5 (design-gap catalog), UPDATED by wave 2 (D-Y1,
|
|
119
|
-
* docs/EXTERNAL-REACT-AND-MODULE-DESIGN.md §1/§4 slice S1): this guard used
|
|
120
|
-
* to THROW for a non-first-party mount, reasoning "today this can never
|
|
121
|
-
* actually fire (Pixi2DGameAdapter.mount is typed to always return a
|
|
122
|
-
* MountedGame2D, whose ctx/physics2d/collisions2d/frame are all REQUIRED
|
|
123
|
-
* fields, so there is no foreign-mount shape that could reach here)". Wave 2
|
|
124
|
-
* proved that reasoning stale: a `kind: 'pixijs'` `{ module }` adapter's mount
|
|
125
|
-
* result IS exactly such a foreign shape — `Pixi2DGameAdapter`'s TYPE promises
|
|
126
|
-
* `MountedGame2D`, but `adapter-resolver.ts`'s `resolveAllWorlds` reaches it
|
|
127
|
-
* only via `adapter as unknown as Pixi2DGameAdapter` (a custom module's actual
|
|
128
|
-
* return value is merely `MountedPixiWorld`-shaped, T7.5's cross-kind union),
|
|
129
|
-
* so the "never actually fire" premise no longer holds the moment a custom
|
|
130
|
-
* pixi module world reaches the roots path. Rather than keep the throw (which
|
|
131
|
-
* would re-create exactly the kind of dead end D-X6 named and D-Y1 exists to
|
|
132
|
-
* remove), this now degrades EXACTLY like `registerThreeWorld`'s own foreign-
|
|
133
|
-
* mount handling above (`firstPartyExtras`): `physics2d`/`collisions2d`/
|
|
134
|
-
* `frame`/component-adoption are omitted for a non-first-party mount instead
|
|
135
|
-
* of read from a `.ctx` that doesn't exist — `stage` (the one field every
|
|
136
|
-
* `MountedPixiWorld`, first-party or not, actually carries) is always real.
|
|
85
|
+
* `stage` is the one field every `MountedPixiRoot` actually carries.
|
|
137
86
|
*/
|
|
138
|
-
function
|
|
139
|
-
mounted: MountedGame2D,
|
|
140
|
-
): Pick<Parameters<typeof createWorldInstance>[0], 'physics2d' | 'collisions2d' | 'frame'> {
|
|
141
|
-
// Same structural check `firstPartyExtras`/`isFirstPartyMounted` use — see
|
|
142
|
-
// this function's own doc comment (above `registerPixiWorld`) for why this
|
|
143
|
-
// stays a plain boolean read rather than a type-guarded `isFirstPartyMounted`
|
|
144
|
-
// call (that guard's threejs-shaped return type would narrow `mounted` to
|
|
145
|
-
// `never` here).
|
|
146
|
-
if ((mounted as { firstParty?: unknown }).firstParty !== true) return {};
|
|
147
|
-
return {
|
|
148
|
-
physics2d: mounted.ctx.physics2d,
|
|
149
|
-
collisions2d: mounted.ctx.collisions2d,
|
|
150
|
-
frame: mounted.frame,
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export function registerPixiWorld(
|
|
87
|
+
export function registerPixiRoot(
|
|
155
88
|
game: GameInternal,
|
|
156
|
-
adapter:
|
|
157
|
-
mounted:
|
|
89
|
+
adapter: RootAdapter<'canvas'>,
|
|
90
|
+
mounted: MountedPixiRoot,
|
|
158
91
|
opts?: { id?: string | undefined; pausable?: boolean | undefined },
|
|
159
|
-
):
|
|
92
|
+
): RootInstance {
|
|
160
93
|
const id = opts?.id ?? 'main';
|
|
161
|
-
const
|
|
162
|
-
const world = createWorldInstance({
|
|
94
|
+
const world = createRootInstance({
|
|
163
95
|
id,
|
|
164
|
-
kind: '
|
|
96
|
+
kind: 'canvas',
|
|
165
97
|
pausable: opts?.pausable ?? true,
|
|
166
98
|
adapter,
|
|
167
99
|
mounted,
|
|
168
100
|
stage: mounted.stage,
|
|
169
|
-
...pixiFirstPartyExtras(mounted),
|
|
170
101
|
});
|
|
171
|
-
|
|
172
|
-
// world's scene-authored components attach during `mount()`, before this
|
|
173
|
-
// `WorldInstance` exists. Skipped for a non-first-party mount (a foreign
|
|
174
|
-
// `{ module }` adapter's `ComponentManager` doesn't exist to adopt into —
|
|
175
|
-
// same "nothing for a custom adapter to backfill" reasoning
|
|
176
|
-
// `registerReactWorld`'s doc comment records for react).
|
|
177
|
-
if (mountedIsFirstParty) mounted.ctx.components.adoptWorld(world);
|
|
178
|
-
game.registerWorld(world);
|
|
102
|
+
game.registerRoot(world);
|
|
179
103
|
return world;
|
|
180
104
|
}
|
|
181
105
|
|
|
182
106
|
/**
|
|
183
|
-
* Register a react world onto the Game shell (T6.2 slice 1
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
* `
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
* `
|
|
191
|
-
* `MountedReactGame` declares no `update`). `adapter` is typed as the real
|
|
192
|
-
* {@link ReactRootAdapter} shape (T7.5, same reasoning as
|
|
193
|
-
* `registerPixiWorld`'s doc comment above) — `WorldInstance.adapter` only
|
|
107
|
+
* Register a react world onto the Game shell (T6.2 slice 1) — the react
|
|
108
|
+
* analog of {@link registerThreeRoot}/{@link registerPixiRoot}. A react
|
|
109
|
+
* world has no `frame` hooks (react's own
|
|
110
|
+
* `createRoot` schedules its commits; `GameInternal.runFrame` correctly
|
|
111
|
+
* leaves a world with no `frame` untouched by its opaque-`update` fallback
|
|
112
|
+
* too, since `MountedReactGame` declares no `update`). `adapter` is typed
|
|
113
|
+
* as the real {@link ReactRootAdapter} shape (T7.5, same reasoning as
|
|
114
|
+
* `registerPixiRoot`'s doc comment above) — `RootInstance.adapter` only
|
|
194
115
|
* needs `.id`, so this passes through with zero cast.
|
|
195
116
|
*/
|
|
196
|
-
export function
|
|
117
|
+
export function registerReactRoot(
|
|
197
118
|
game: GameInternal,
|
|
198
|
-
adapter:
|
|
199
|
-
mounted:
|
|
119
|
+
adapter: RootAdapter<'dom'>,
|
|
120
|
+
mounted: MountedReactRoot,
|
|
200
121
|
container: HTMLElement,
|
|
201
122
|
opts?: { id?: string | undefined; pausable?: boolean | undefined },
|
|
202
|
-
):
|
|
203
|
-
const world =
|
|
123
|
+
): RootInstance {
|
|
124
|
+
const world = createRootInstance({
|
|
204
125
|
id: opts?.id ?? 'main',
|
|
205
|
-
kind: '
|
|
126
|
+
kind: 'dom',
|
|
206
127
|
pausable: opts?.pausable ?? true,
|
|
207
128
|
adapter,
|
|
208
129
|
mounted,
|
|
209
130
|
container,
|
|
210
131
|
});
|
|
211
|
-
game.
|
|
132
|
+
game.registerRoot(world);
|
|
212
133
|
return world;
|
|
213
134
|
}
|
|
214
135
|
|
|
215
136
|
/**
|
|
216
|
-
*
|
|
217
|
-
* `pixijs` variant requires. `PixiSceneGameAdapter` (`world2d/
|
|
218
|
-
* pixi-game-adapter.ts`) satisfies this today; it is deliberately NOT named
|
|
219
|
-
* here as a concrete type so an external pixi adapter can satisfy the same
|
|
220
|
-
* shape without importing the first-party implementer.
|
|
221
|
-
*/
|
|
222
|
-
export interface Pixi2DGameAdapter {
|
|
223
|
-
readonly id: string;
|
|
224
|
-
mount(host: World2DHost): Promise<MountedGame2D>;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* One world to mount in the {@link WorldsRuntimeConfig} roots path (T6.1
|
|
137
|
+
* One world to mount in the {@link RootsRuntimeConfig} roots path (T6.1
|
|
229
138
|
* slice 1) — the host-facing mirror of `manifest/load.ts`'s
|
|
230
139
|
* `ResolvedAdapterRoot` (same `id`/`zOrder`/`pausable`/`loop` fields; the
|
|
231
140
|
* manifest-to-host translation itself is the editor's job, not this file's).
|
|
232
141
|
*
|
|
233
|
-
* `react` (T6.2 slice 1
|
|
234
|
-
*
|
|
235
|
-
*
|
|
142
|
+
* `react` (T6.2 slice 1) mounts as a DOM-root layer `<div>` in the SAME
|
|
143
|
+
* stack instead of a canvas — see {@link ReactRootMountSpec}/{@link
|
|
144
|
+
* ReactRootAdapter} below.
|
|
236
145
|
*/
|
|
237
|
-
export interface
|
|
146
|
+
export interface RootMountSpecBase {
|
|
238
147
|
/** Manifest id — must be unique within one `roots` array. */
|
|
239
148
|
readonly id: string;
|
|
240
|
-
/** Canvas stacking order (
|
|
149
|
+
/** Canvas stacking order (D5 §1); ties broken by array
|
|
241
150
|
* order, mirroring `manifest/load.ts`'s `loadGameManifest` sort. Defaults
|
|
242
151
|
* to `0`. */
|
|
243
152
|
readonly zOrder?: number | undefined;
|
|
244
153
|
/** Whether play-mode pause/step applies to this world (D10). Defaults to `true`. */
|
|
245
154
|
readonly pausable?: boolean | undefined;
|
|
155
|
+
/**
|
|
156
|
+
* `vgai.project.json`'s `dev: true` — this root is a DEV LAYER (the game's
|
|
157
|
+
* own dev GUI), not shipped game content. Two things happen here: the
|
|
158
|
+
* surface is stacked ABOVE every non-dev root (a dev layer sits topmost),
|
|
159
|
+
* and it is marked `data-vgai-dev-layer="true"` so the play compositor can
|
|
160
|
+
* leave it out of a capture (`composite-screenshot.ts`). Whether a dev root
|
|
161
|
+
* reaches this function at all is the CALLER's gate —
|
|
162
|
+
* `mountManifestRoots` asks `devLayersEnabled()` before building the spec.
|
|
163
|
+
*
|
|
164
|
+
* It steals no input from the game while closed for a reason that predates
|
|
165
|
+
* this flag and needs no new mechanism: a dev layer is a `dom` root, DOM
|
|
166
|
+
* layers are `pointer-events:none` (see `mountOneReactRoot`) with the
|
|
167
|
+
* mounted tree opting individual elements back in, and the input router
|
|
168
|
+
* never sees DOM roots at all.
|
|
169
|
+
*/
|
|
170
|
+
readonly dev?: boolean | undefined;
|
|
246
171
|
/** `'gated'` (host-driven, default) or `'self-driven'` (this world drives
|
|
247
172
|
* its own loop — D5's "composited, unsynchronized" tier). Carried through
|
|
248
173
|
* for parity with `ResolvedAdapterRoot`; T6.1 slice 1 does not yet
|
|
@@ -253,133 +178,104 @@ export interface WorldMountSpecBase {
|
|
|
253
178
|
* Optional claim predicate for the delegating input router (D5 §2a), over
|
|
254
179
|
* a point RELATIVE TO THE CONTAINER. Absent means: this world claims only
|
|
255
180
|
* if it ends up the bottom (lowest zOrder) world — see
|
|
256
|
-
* `input-router.ts`'s `
|
|
257
|
-
* explicit `hitTest` gets a REAL default derived from its own mounted
|
|
258
|
-
* stage's `EventBoundary.hitTest` (see `derivePixiHitTest` below) — a
|
|
259
|
-
* threejs world with no explicit `hitTest` gets no default (three has no
|
|
260
|
-
* cheap universal "is this pixel interactive" answer).
|
|
181
|
+
* `input-router.ts`'s `resolveClaimingRoot`.
|
|
261
182
|
*/
|
|
262
183
|
readonly hitTest?: ((x: number, y: number) => boolean) | undefined;
|
|
263
184
|
}
|
|
264
185
|
|
|
265
|
-
export interface
|
|
266
|
-
readonly kind: '
|
|
267
|
-
readonly adapter:
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
export interface PixiWorldMountSpec extends WorldMountSpecBase {
|
|
271
|
-
readonly kind: 'pixijs';
|
|
272
|
-
readonly adapter: Pixi2DGameAdapter;
|
|
186
|
+
export interface ThreeRootMountSpec extends RootMountSpecBase {
|
|
187
|
+
readonly kind: 'three';
|
|
188
|
+
readonly adapter: RootAdapter;
|
|
273
189
|
}
|
|
274
190
|
|
|
275
191
|
/**
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
* DOM-root layer `<div>` the host already created and stacked (same box/
|
|
280
|
-
* z-order rules as a canvas per COMPOSITION-DESIGN D5 §1) — the adapter's
|
|
281
|
-
* `mount` renders its react tree INTO this exact element via `createRoot`;
|
|
282
|
-
* it must never create its own root element (mirrors `HostContext`'s
|
|
283
|
-
* "the world's runtime renders into the surface it is handed" rule for
|
|
284
|
-
* canvases, `docs/GAME-ROOT-DESIGN.md` §2).
|
|
192
|
+
* `adapter` is the SEAM's `RootAdapter<'canvas'>` — a canvas root's mount
|
|
193
|
+
* honestly returns only a `MountedPixiRoot`, and naming the real contract
|
|
194
|
+
* here is what keeps the resolver cast-free.
|
|
285
195
|
*/
|
|
286
|
-
export interface
|
|
287
|
-
readonly
|
|
288
|
-
|
|
289
|
-
* The Game this world is being mounted into — symmetric with
|
|
290
|
-
* `HostContext.game`; what a react adapter hands to `<GameProvider>` so
|
|
291
|
-
* `useGameState` selectors read live state (T7.4 bridge).
|
|
292
|
-
*
|
|
293
|
-
* Optional as of D-V3 (docs/WAVE5-MULTIWORLD-INGEST-DESIGN.md, F24
|
|
294
|
-
* composite): a `default-react` sibling mounted BESIDE an ingest world
|
|
295
|
-
* (`packages/editor/src/ingest-siblings.ts`) has no native `Game` to hand
|
|
296
|
-
* it — there is no first-party `GameContext`/loop for the sibling to join,
|
|
297
|
-
* only the ingested world's own foreign runtime — so its host carries no
|
|
298
|
-
* `game` at all rather than fabricating an empty one (anti-shim rule); the
|
|
299
|
-
* sibling mounts its entry component bare, with no `<GameProvider>` wrap.
|
|
300
|
-
* `mountOneReactWorld` below (the NATIVE multi-world runtime path) still
|
|
301
|
-
* ALWAYS supplies a real `game` — this optionality is reached only by the
|
|
302
|
-
* composite sibling's own hand-built host, never by weakening the native
|
|
303
|
-
* path's guarantee.
|
|
304
|
-
*/
|
|
305
|
-
readonly game?: Game;
|
|
196
|
+
export interface PixiRootMountSpec extends RootMountSpecBase {
|
|
197
|
+
readonly kind: 'canvas';
|
|
198
|
+
readonly adapter: RootAdapter<'canvas'>;
|
|
306
199
|
}
|
|
307
200
|
|
|
308
201
|
/**
|
|
309
|
-
* A live, mounted react world (T6.2 slice 1) — the react analog of
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
* manager) and render from game state via the T7.4 bridge instead of a
|
|
313
|
-
* per-frame `update`, so this shape carries no `update`/`fixedUpdate`/
|
|
202
|
+
* A live, mounted react world (T6.2 slice 1) — the react analog of a mounted
|
|
203
|
+
* canvas world. React roots render from game state via the T7.4 bridge
|
|
204
|
+
* instead of a per-frame `update`, so this shape carries no `update`/`fixedUpdate`/
|
|
314
205
|
* `ctx`/`frame` — `drivesOwnLoop` is always `false` (react's `createRoot`
|
|
315
206
|
* schedules its OWN commits; the host's fixed-step loop never drives it,
|
|
316
207
|
* and it is correctly skipped by `GameInternal.runFrame`'s per-world
|
|
317
|
-
* `frame`-hooks/opaque-`update` dispatch — see `
|
|
318
|
-
* which registers this `
|
|
208
|
+
* `frame`-hooks/opaque-`update` dispatch — see `registerReactRoot` below,
|
|
209
|
+
* which registers this `RootInstance` with no `frame`, same as any other
|
|
319
210
|
* opaque mount with nothing to tick).
|
|
320
211
|
*
|
|
321
212
|
* Deliberately carries NO `firstParty: true` brand: that brand specifically
|
|
322
213
|
* means "has a `.ctx: GameContext`" (`isFirstPartyMounted`,
|
|
323
214
|
* `runtime/game.ts`) — a react world has no Rapier/ComponentManager/
|
|
324
215
|
* GameContext at all, so branding it first-party would be a type lie.
|
|
325
|
-
* `Game.
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
* for first-party-observable state), never a per-world `observe`.
|
|
216
|
+
* `Game.registerRoot`'s "no state bridge" console warning explicitly exempts
|
|
217
|
+
* `kind: 'dom'` (§7.1-15): a react world has no `observe` BY DESIGN — that
|
|
218
|
+
* hook is scoped to `useRootObservation` (the ingested/foreign-world case),
|
|
219
|
+
* not `useWorldState` (`ui/game-state.tsx`), which a react world's own
|
|
220
|
+
* mounted tree uses instead — it reads `Game.state` directly (T7.4's actual
|
|
221
|
+
* bridge for first-party-observable state), never a per-world `observe`.
|
|
332
222
|
*
|
|
333
|
-
* `kind`/`container` (T7.5) satisfy `
|
|
334
|
-
*
|
|
335
|
-
* adapter's `mount` was handed (identity matters, mirroring `
|
|
336
|
-
* `
|
|
223
|
+
* `kind`/`container` (T7.5) satisfy `MountedReactRoot` (`adapter/
|
|
224
|
+
* root-adapter.ts`) — `container` is the SAME `DomHostContext.container` the
|
|
225
|
+
* adapter's `mount` was handed (identity matters, mirroring `threeScene()`/
|
|
226
|
+
* `pixiStage()`'s "same instance the adapter mounted" contract); every
|
|
337
227
|
* `ReactRootAdapter` implementer echoes it back here so `mounted` alone
|
|
338
228
|
* (with no separately-threaded `container`) satisfies the union
|
|
339
|
-
* `
|
|
229
|
+
* `RootInstanceInit.mounted`/`RootInstance.mounted` with zero cast.
|
|
340
230
|
*/
|
|
341
|
-
export interface MountedReactGame extends
|
|
231
|
+
export interface MountedReactGame extends MountedReactRoot {
|
|
342
232
|
readonly drivesOwnLoop: false;
|
|
343
233
|
}
|
|
344
234
|
|
|
345
235
|
/**
|
|
346
|
-
* A react-shaped adapter — the structural contract `
|
|
347
|
-
* `react` variant requires.
|
|
236
|
+
* A react-shaped adapter — the structural contract `RootMountSpec`'s
|
|
237
|
+
* `react` variant requires. Deliberately
|
|
348
238
|
* NOT tied to a concrete implementer here so an editor-resolved adapter (the
|
|
349
239
|
* `default-react` resolver branch, T6.2's editor-side follow-up) can satisfy
|
|
350
240
|
* this shape without this file importing react-dom or any editor code.
|
|
241
|
+
*
|
|
242
|
+
* A genuine `RootAdapter<'dom'>` refinement (`HostContextFor<'dom'>` =
|
|
243
|
+
* `DomHostContext` = {@link DomHostContext}), narrowing only the return type to
|
|
244
|
+
* {@link MountedReactGame}.
|
|
351
245
|
*/
|
|
352
|
-
export interface ReactRootAdapter {
|
|
246
|
+
export interface ReactRootAdapter extends RootAdapter<'dom'> {
|
|
353
247
|
readonly id: string;
|
|
354
|
-
mount(host:
|
|
248
|
+
mount(host: DomHostContext): Promise<MountedReactGame>;
|
|
355
249
|
}
|
|
356
250
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
251
|
+
/** WO-34, same reasoning as {@link PixiRootMountSpec} above: the seam's
|
|
252
|
+
* `RootAdapter<'dom'>` is what a DOM root actually guarantees. */
|
|
253
|
+
export interface ReactRootMountSpec extends RootMountSpecBase {
|
|
254
|
+
readonly kind: 'dom';
|
|
255
|
+
readonly adapter: RootAdapter<'dom'>;
|
|
360
256
|
}
|
|
361
257
|
|
|
362
|
-
export type
|
|
258
|
+
export type RootMountSpec = ThreeRootMountSpec | PixiRootMountSpec | ReactRootMountSpec;
|
|
363
259
|
|
|
364
260
|
/**
|
|
365
|
-
* Mount N adapter roots (
|
|
366
|
-
* on ONE `Game`, stacked in `container` per
|
|
367
|
-
* `roots[0]` participates in `Game.
|
|
261
|
+
* Mount N adapter roots (three + canvas + react)
|
|
262
|
+
* on ONE `Game`, stacked in `container` per D5 §1.
|
|
263
|
+
* `roots[0]` participates in `Game.defaultRoot`'s existing "first three
|
|
368
264
|
* root, else first root" rule.
|
|
369
265
|
*/
|
|
370
|
-
export interface
|
|
266
|
+
export interface RootsRuntimeConfig {
|
|
371
267
|
/** The host creates one absolutely-positioned surface per world inside
|
|
372
|
-
* this element (D5 §1) — a canvas for
|
|
268
|
+
* this element (D5 §1) — a canvas for three/canvas, a DOM-root `<div>`
|
|
373
269
|
* layer for react — plus one shared UI overlay above all of them. */
|
|
374
270
|
container: HTMLElement;
|
|
375
|
-
roots:
|
|
271
|
+
roots: RootMountSpec[];
|
|
376
272
|
width?: number | undefined;
|
|
377
273
|
height?: number | undefined;
|
|
378
274
|
/**
|
|
379
|
-
* Mirrors `
|
|
380
|
-
* real `WebGLRenderer` construction for every
|
|
275
|
+
* Mirrors `ThreeHostContext.headless` (Node conformance tests — no GPU): skips
|
|
276
|
+
* real `WebGLRenderer` construction for every three world in this
|
|
381
277
|
* session (a stand-in renderer is used instead, exactly as
|
|
382
|
-
* `
|
|
278
|
+
* `SetupThreeRootAdapter.mount` already special-cases `host.headless`
|
|
383
279
|
* internally). Pixijs roots are unaffected — Pixi already falls back to
|
|
384
280
|
* a 2D canvas renderer with no GPU. Never set `true` in a real host.
|
|
385
281
|
*/
|
|
@@ -387,21 +283,23 @@ export interface WorldsRuntimeConfig {
|
|
|
387
283
|
/** D15 (T-D15.1) — the root seed `ctx.random` boots from on every world
|
|
388
284
|
* mounted onto this session's Game, forwarded to `createGame` BEFORE any
|
|
389
285
|
* world's `mount()`/`setup()` runs (this function constructs the Game
|
|
390
|
-
* first — see `
|
|
391
|
-
* (`mount-manifest.ts`'s `
|
|
286
|
+
* first — see `createRootsGameRuntime`). The manifest-aware boot path
|
|
287
|
+
* (`mount-manifest.ts`'s `mountManifestRoots`) is the real caller that
|
|
392
288
|
* resolves this from `manifest.determinism`/`?vgai-seed=`/its own
|
|
393
|
-
* explicit-config leg; a caller building `
|
|
289
|
+
* explicit-config leg; a caller building `RootsRuntimeConfig` by hand
|
|
394
290
|
* (a test, a bespoke host) may also set it directly. Omitting it falls
|
|
395
291
|
* back to `createGame`'s own fixed default. */
|
|
396
292
|
seed?: number | undefined;
|
|
293
|
+
/** Private-play or Team Test identity supplied by the host. Networking
|
|
294
|
+
* remains game-owned; games may use `roomKey` in their direct room join. */
|
|
295
|
+
playtest?: PlaytestContext | null | undefined;
|
|
397
296
|
/**
|
|
398
|
-
*
|
|
399
|
-
* for
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
* in a unit test to avoid touching the global object; mirrors
|
|
297
|
+
* Test-only overrides for the render-control seam this host now wires up
|
|
298
|
+
* for EVERY session (see `createRootsGameRuntime`): `location` is where
|
|
299
|
+
* `?vgai-render=1` is read from (defaults to the real `window.location`;
|
|
300
|
+
* a Node test has no `window` and must inject one to exercise the seam),
|
|
301
|
+
* `target` is where the harness publishes (defaults to the real `window`
|
|
302
|
+
* — override in a unit test to avoid touching the global object; mirrors
|
|
405
303
|
* `RenderControlHarnessOptions.target`/`.location` and the
|
|
406
304
|
* `debugBridge.url` override precedent in `mount-manifest.ts`).
|
|
407
305
|
*/
|
|
@@ -418,12 +316,12 @@ export interface WorldsRuntimeConfig {
|
|
|
418
316
|
*
|
|
419
317
|
* This is the GENERIC host handle — it has no first-party concepts (no
|
|
420
318
|
* `GameContext`, no `GameSetupFn`). First-party features (the live `GameContext`,
|
|
421
|
-
* warm-restart hot reload) are reached by casting `mounted` to `
|
|
319
|
+
* warm-restart hot reload) are reached by casting `mounted` to `MountedSetupThreeRoot`
|
|
422
320
|
* (the editor does this for HMR/physics-sync — those are inherently first-party).
|
|
423
321
|
*
|
|
424
|
-
* `scene`/`camera`/`mounted` alias the Game's `
|
|
425
|
-
*
|
|
426
|
-
* world) by DECLARATION order, per `Game.
|
|
322
|
+
* `scene`/`camera`/`mounted` alias the Game's `defaultRoot`: it is the first
|
|
323
|
+
* three root (else the first
|
|
324
|
+
* world) by DECLARATION order, per `Game.defaultRoot`'s existing rule —
|
|
427
325
|
* independent of `zOrder`/canvas stacking, which is a rendering-only concern.
|
|
428
326
|
*/
|
|
429
327
|
export interface GameSession {
|
|
@@ -432,13 +330,20 @@ export interface GameSession {
|
|
|
432
330
|
resume(): void;
|
|
433
331
|
step(): void;
|
|
434
332
|
/** Resize every world's render buffer. `pixelRatio` (W2c device preview)
|
|
435
|
-
* optionally re-pins the
|
|
333
|
+
* optionally re-pins the three renderers' DPR in the same pass; omitted,
|
|
436
334
|
* the mount-time ratio is left untouched (legacy behavior). */
|
|
437
335
|
resize(width: number, height: number, pixelRatio?: number): void;
|
|
438
336
|
readonly scene: THREE.Scene;
|
|
439
337
|
readonly camera: THREE.PerspectiveCamera;
|
|
440
|
-
/** The mounted
|
|
441
|
-
|
|
338
|
+
/** The default world's mounted root, as the honest `MountedRoot` union —
|
|
339
|
+
* narrow on `.kind` before reaching for a surface's own members (the same
|
|
340
|
+
* discriminant `scene`/`camera` above use). This deliberately does NOT
|
|
341
|
+
* claim `MountedThreeRoot`: a roots-path session whose default world is
|
|
342
|
+
* canvas or dom returns that world's mount, and the old
|
|
343
|
+
* `as MountedThreeRoot` cast made every such value a type-level lie. For a
|
|
344
|
+
* first-party three mount, narrow to `'three'` and cast to
|
|
345
|
+
* `MountedSetupThreeRoot` for the first-party extras. */
|
|
346
|
+
readonly mounted: MountedRoot;
|
|
442
347
|
/** The Game root (T6.1 slice 1) — the multi-world entry point
|
|
443
348
|
* (`game.roots`/`game.world(id)`/`game.queryByComponent`) for callers
|
|
444
349
|
* that need more than the default-world aliases above. */
|
|
@@ -452,17 +357,17 @@ export interface GameSession {
|
|
|
452
357
|
* renderers, loop, and asset cache. Every game — including a one-root game —
|
|
453
358
|
* uses the same explicit adapter-root path.
|
|
454
359
|
*/
|
|
455
|
-
export async function createGameRuntime(config:
|
|
456
|
-
return
|
|
360
|
+
export async function createGameRuntime(config: RootsRuntimeConfig): Promise<GameSession> {
|
|
361
|
+
return createRootsGameRuntime(config);
|
|
457
362
|
}
|
|
458
363
|
|
|
459
364
|
// ---------------------------------------------------------------------------
|
|
460
|
-
// Worlds path (T6.1 slice 1
|
|
365
|
+
// Worlds path (T6.1 slice 1)
|
|
461
366
|
// ---------------------------------------------------------------------------
|
|
462
367
|
|
|
463
|
-
/** A stand-in `THREE.WebGLRenderer` for headless (`headless:true`)
|
|
464
|
-
* roots — mirrors `
|
|
465
|
-
* pattern (`adapter/
|
|
368
|
+
/** A stand-in `THREE.WebGLRenderer` for headless (`headless:true`) three
|
|
369
|
+
* roots — mirrors `SetupThreeRootAdapter.mount`'s own `headlessComposer`
|
|
370
|
+
* pattern (`adapter/setup-three-root-adapter.ts`): a headless first-party
|
|
466
371
|
* mount never calls a render-phase method on `host.renderer` at all, so
|
|
467
372
|
* this only needs to satisfy the handful of calls THIS file itself makes
|
|
468
373
|
* (`setPixelRatio`/`setClearColor`/`setSize` at mount, `dispose`/
|
|
@@ -478,75 +383,30 @@ function createHeadlessRendererStub(): THREE.WebGLRenderer {
|
|
|
478
383
|
} as unknown as THREE.WebGLRenderer;
|
|
479
384
|
}
|
|
480
385
|
|
|
481
|
-
/**
|
|
482
|
-
* Derive a REAL default `hitTest` for a pixijs world from its own mounted
|
|
483
|
-
* stage (D5 §2a/COMPOSITION-DESIGN.md proof item 2: "pixi `EventBoundary
|
|
484
|
-
* .hitTest`") — used only when the caller's `WorldMountSpec` didn't declare
|
|
485
|
-
* an explicit `hitTest`. `(x, y)` are container-relative pixels, matching
|
|
486
|
-
* the coordinates the pixi surface renders at (the stage itself carries no
|
|
487
|
-
* pan/zoom — that lives on `pixi-surface.ts`'s child `world` container, per
|
|
488
|
-
* its own doc comment), so this is a correct global hit test with no extra
|
|
489
|
-
* coordinate mapping. Duck-typed against `mounted.ctx.app` (never a
|
|
490
|
-
* value-import of `pixi.js`) so this file stays type-only w.r.t. Pixi;
|
|
491
|
-
* returns `undefined` (no default claim) if the mount's `app`/event
|
|
492
|
-
* boundary isn't shaped as expected — degrade to "does not claim" rather
|
|
493
|
-
* than throw. A non-first-party mount (D-Y1, wave 2 — same finding
|
|
494
|
-
* `registerPixiWorld`'s doc comment records: a `{ module }` adapter's mount
|
|
495
|
-
* has no `.ctx` at all) has nothing to duck-type here either, so this checks
|
|
496
|
-
* the SAME `firstParty` brand FIRST and degrades identically — never claims,
|
|
497
|
-
* never throws.
|
|
498
|
-
*/
|
|
499
|
-
function derivePixiHitTest(
|
|
500
|
-
mounted: MountedGame2D,
|
|
501
|
-
): ((x: number, y: number) => boolean) | undefined {
|
|
502
|
-
if ((mounted as { firstParty?: unknown }).firstParty !== true) return undefined;
|
|
503
|
-
const app = (mounted.ctx as unknown as { app?: unknown }).app as
|
|
504
|
-
| {
|
|
505
|
-
renderer?: {
|
|
506
|
-
events?: {
|
|
507
|
-
rootBoundary?: {
|
|
508
|
-
rootTarget?: unknown;
|
|
509
|
-
hitTest?: (x: number, y: number) => unknown;
|
|
510
|
-
};
|
|
511
|
-
};
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
| undefined;
|
|
515
|
-
const rootBoundary = app?.renderer?.events?.rootBoundary;
|
|
516
|
-
if (!rootBoundary || typeof rootBoundary.hitTest !== 'function') return undefined;
|
|
517
|
-
// Pixi assigns `rootTarget` lazily on the first render. Pointer movement
|
|
518
|
-
// can reach the host router during that small window (or while a world is
|
|
519
|
-
// tearing down); EventBoundary.hitTest dereferences it unconditionally.
|
|
520
|
-
// An unready boundary cannot claim input, so degrade to no hit instead of
|
|
521
|
-
// surfacing Pixi's internal `undefined.eventMode` exception.
|
|
522
|
-
return (x: number, y: number) =>
|
|
523
|
-
rootBoundary.rootTarget != null && rootBoundary.hitTest!(x, y) != null;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
386
|
/** One already-mounted world, tracked for disposal + the router. `element`
|
|
527
|
-
* is the world's stacked surface — an `HTMLCanvasElement` for
|
|
528
|
-
*
|
|
387
|
+
* is the world's stacked surface — an `HTMLCanvasElement` for three/
|
|
388
|
+
* canvas, or the DOM-root layer `<div>` for react (T6.2 slice 1) — kept
|
|
529
389
|
* under one field name so `fullCleanup`'s disposal loop stays kind-generic
|
|
530
390
|
* (`container.removeChild(entry.element)` needs no branch). */
|
|
531
391
|
interface MountedAdapterRoot {
|
|
532
392
|
readonly id: string;
|
|
533
|
-
readonly kind: '
|
|
393
|
+
readonly kind: 'three' | 'canvas' | 'dom';
|
|
534
394
|
readonly element: HTMLElement;
|
|
535
|
-
readonly mounted:
|
|
536
|
-
/** Only
|
|
395
|
+
readonly mounted: MountedRoot;
|
|
396
|
+
/** Only three roots own a renderer this file constructed. */
|
|
537
397
|
readonly renderer: THREE.WebGLRenderer | undefined;
|
|
538
398
|
}
|
|
539
399
|
|
|
540
|
-
interface
|
|
400
|
+
interface OneRootResult {
|
|
541
401
|
readonly mountedEntry: MountedAdapterRoot;
|
|
542
402
|
readonly routerEntry: RouterAdapterRoot;
|
|
543
403
|
}
|
|
544
404
|
|
|
545
|
-
/** Shared per-world mount inputs, computed once in `
|
|
405
|
+
/** Shared per-world mount inputs, computed once in `createRootsGameRuntime`'s
|
|
546
406
|
* loop (canvas stacking + one dpr) and threaded into whichever of
|
|
547
|
-
* `
|
|
407
|
+
* `mountOneThreeRoot`/`mountOnePixiRoot` this world's `kind` needs — split
|
|
548
408
|
* out so the orchestrating loop itself stays a simple dispatch. */
|
|
549
|
-
interface
|
|
409
|
+
interface OneRootContext {
|
|
550
410
|
readonly game: GameInternal;
|
|
551
411
|
readonly canvas: HTMLCanvasElement;
|
|
552
412
|
readonly w: number;
|
|
@@ -554,19 +414,18 @@ interface OneWorldContext {
|
|
|
554
414
|
readonly dpr: number;
|
|
555
415
|
readonly isBottom: boolean;
|
|
556
416
|
readonly headless: boolean;
|
|
557
|
-
readonly loopHandle: LoopHandle;
|
|
558
417
|
readonly assets: ReturnType<typeof createAssetCache>;
|
|
559
418
|
}
|
|
560
419
|
|
|
561
|
-
/** Mount one
|
|
562
|
-
* D5 §1/§4, then `
|
|
563
|
-
* `
|
|
564
|
-
* simple — see `
|
|
565
|
-
async function
|
|
566
|
-
spec:
|
|
567
|
-
ctx:
|
|
568
|
-
): Promise<
|
|
569
|
-
const { game, canvas, w, h, dpr, isBottom, headless,
|
|
420
|
+
/** Mount one three `RootMountSpec` (canvas + renderer construction, per
|
|
421
|
+
* D5 §1/§4, then `registerThreeRoot`). Split out of
|
|
422
|
+
* `createRootsGameRuntime` purely to keep that function's own branching
|
|
423
|
+
* simple — see `mountOnePixiRoot` for the canvas sibling. */
|
|
424
|
+
async function mountOneThreeRoot(
|
|
425
|
+
spec: ThreeRootMountSpec,
|
|
426
|
+
ctx: OneRootContext,
|
|
427
|
+
): Promise<OneRootResult> {
|
|
428
|
+
const { game, canvas, w, h, dpr, isBottom, headless, assets } = ctx;
|
|
570
429
|
const renderer = headless
|
|
571
430
|
? createHeadlessRendererStub()
|
|
572
431
|
: createHostRenderer(canvas, w, h, undefined, {
|
|
@@ -582,7 +441,7 @@ async function mountOneThreeWorld(
|
|
|
582
441
|
// loop above just set. The `renderer.setSize(w, h, false)` line right above
|
|
583
442
|
// this comment does NOT undo that stamp (updateStyle:false only skips
|
|
584
443
|
// TOUCHING style, it can't un-stamp a previous call) — so without this
|
|
585
|
-
// re-assertion, every roots-path
|
|
444
|
+
// re-assertion, every roots-path three canvas' on-screen size was
|
|
586
445
|
// permanently pinned to whatever `w`/`h` it happened to mount at (usually
|
|
587
446
|
// the manifest's `resolution`, since standalone builds mount before a real
|
|
588
447
|
// container size is known — see `mount-manifest.ts`). Re-asserting here
|
|
@@ -604,49 +463,46 @@ async function mountOneThreeWorld(
|
|
|
604
463
|
renderer.setPixelRatio(dpr);
|
|
605
464
|
if (!isBottom) renderer.setClearColor(0x000000, 0); // D5 §1: alpha-clear above the bottom layer
|
|
606
465
|
|
|
607
|
-
const host:
|
|
466
|
+
const host: ThreeHostContext = {
|
|
608
467
|
three: THREE,
|
|
609
468
|
surface: { canvas, width: w, height: h },
|
|
610
469
|
renderer,
|
|
611
|
-
loop: loopHandle,
|
|
612
470
|
assets,
|
|
613
471
|
headless,
|
|
614
472
|
game,
|
|
615
|
-
requestSystem: () => null,
|
|
616
473
|
};
|
|
617
474
|
const mounted = await spec.adapter.mount(host);
|
|
618
|
-
|
|
475
|
+
registerThreeRoot(game, spec.adapter, mounted, { id: spec.id, pausable: spec.pausable });
|
|
619
476
|
return {
|
|
620
|
-
mountedEntry: { id: spec.id, kind: '
|
|
477
|
+
mountedEntry: { id: spec.id, kind: 'three', element: canvas, mounted, renderer },
|
|
621
478
|
routerEntry: { id: spec.id, zOrder: spec.zOrder ?? 0, canvas, hitTest: spec.hitTest },
|
|
622
479
|
};
|
|
623
480
|
}
|
|
624
481
|
|
|
625
|
-
/** Mount one
|
|
626
|
-
* §1/§3/§4, then `
|
|
627
|
-
* `
|
|
628
|
-
* mounted stage when the spec didn't declare one (see `derivePixiHitTest`).
|
|
482
|
+
/** Mount one canvas `RootMountSpec` (via its own `CanvasHostContext`, per D5
|
|
483
|
+
* §1/§3/§4, then `registerPixiRoot`) — the pixi sibling of
|
|
484
|
+
* `mountOneThreeRoot` above.
|
|
629
485
|
*
|
|
630
|
-
* Unlike `
|
|
486
|
+
* Unlike `mountOneThreeRoot`, this needs no explicit `canvas.style.width`/
|
|
631
487
|
* `.height` re-assertion (E4.R1). `pixi-surface.ts` constructs its
|
|
632
488
|
* `Application` with `autoDensity: true`, which makes PIXI ITSELF re-stamp
|
|
633
489
|
* `canvas.style.width`/`.height` (real CSS px, matching the LOGICAL
|
|
634
490
|
* width/height passed to `resize()`) on every `app.renderer.resize()` call —
|
|
635
491
|
* including the one this world's `mounted.resize?.()` triggers from the
|
|
636
492
|
* roots-path `resize()` below. So while a pixi world's canvas can start
|
|
637
|
-
* pinned to the mount-time `w`/`h` (same as
|
|
493
|
+
* pinned to the mount-time `w`/`h` (same as three, until PIXI's own
|
|
638
494
|
* construction-time resize runs), it self-heals the moment ANY real
|
|
639
495
|
* `session.resize(rw, rh)` fires — every shipped standalone entry
|
|
640
496
|
* (`packages/editor/template/src/main.ts`, `examples/tri-world/src/main.ts`)
|
|
641
497
|
* already calls `session.resize()` unconditionally right after mount, so
|
|
642
|
-
* this never surfaces as a lasting bug the way
|
|
498
|
+
* this never surfaces as a lasting bug the way three' buffer-only resize
|
|
643
499
|
* did (never self-healing, by design — see above). */
|
|
644
|
-
async function
|
|
645
|
-
spec:
|
|
646
|
-
ctx: Omit<
|
|
647
|
-
): Promise<
|
|
500
|
+
async function mountOnePixiRoot(
|
|
501
|
+
spec: PixiRootMountSpec,
|
|
502
|
+
ctx: Omit<OneRootContext, 'assets'>,
|
|
503
|
+
): Promise<OneRootResult> {
|
|
648
504
|
const { game, canvas, w, h, dpr, headless, isBottom } = ctx;
|
|
649
|
-
const pixiHost:
|
|
505
|
+
const pixiHost: CanvasHostContext = {
|
|
650
506
|
canvas,
|
|
651
507
|
width: w,
|
|
652
508
|
height: h,
|
|
@@ -657,14 +513,14 @@ async function mountOnePixiWorld(
|
|
|
657
513
|
preserveDrawingBuffer: true,
|
|
658
514
|
};
|
|
659
515
|
const mounted = await spec.adapter.mount(pixiHost);
|
|
660
|
-
|
|
516
|
+
registerPixiRoot(game, spec.adapter, mounted, {
|
|
661
517
|
id: spec.id,
|
|
662
518
|
pausable: spec.pausable,
|
|
663
519
|
});
|
|
664
520
|
return {
|
|
665
521
|
mountedEntry: {
|
|
666
522
|
id: spec.id,
|
|
667
|
-
kind: '
|
|
523
|
+
kind: 'canvas',
|
|
668
524
|
element: canvas,
|
|
669
525
|
mounted,
|
|
670
526
|
renderer: undefined,
|
|
@@ -673,38 +529,38 @@ async function mountOnePixiWorld(
|
|
|
673
529
|
id: spec.id,
|
|
674
530
|
zOrder: spec.zOrder ?? 0,
|
|
675
531
|
canvas,
|
|
676
|
-
hitTest: spec.hitTest
|
|
532
|
+
hitTest: spec.hitTest,
|
|
677
533
|
},
|
|
678
534
|
};
|
|
679
535
|
}
|
|
680
536
|
|
|
681
537
|
/**
|
|
682
|
-
* Mount one react `
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
538
|
+
* Mount one react `RootMountSpec` (T6.2 slice 1) — the react sibling of
|
|
539
|
+
* `mountOneThreeRoot`/`mountOnePixiRoot`. Unlike its canvas-backed siblings
|
|
540
|
+
* this returns NO `routerEntry`: a react world's DOM-root layer participates in
|
|
541
|
+
* D5's z-order/box stacking (the caller still creates and positions its `<div>`
|
|
542
|
+
* exactly like a canvas — see `createRootsGameRuntime`'s stack-building loop)
|
|
543
|
+
* but needs no entry in the delegating router's hit-test loop (§1.C — "DOM
|
|
544
|
+
* layers need no entry in the router's hit-test loop"): the layer's own
|
|
545
|
+
* `pointer-events` discipline (this file sets `none` on the layer by default;
|
|
546
|
+
* the mounted React tree opts specific elements back in with
|
|
547
|
+
* `pointer-events:auto`) is what lets its interactive elements claim events
|
|
548
|
+
* NATIVELY, via the real DOM, with zero router involvement — and lets a click
|
|
549
|
+
* over its non-interactive (transparent) area fall through to the canvas below
|
|
550
|
+
* it via the SAME native DOM hit-testing (a `pointer-events:none` element is
|
|
551
|
+
* invisible to hit-testing entirely, so the click lands on whatever real DOM
|
|
552
|
+
* element is beneath it — the router's normal canvas-vs-canvas forwarding,
|
|
553
|
+
* unaffected by this layer's presence).
|
|
698
554
|
*/
|
|
699
|
-
async function
|
|
700
|
-
spec:
|
|
555
|
+
async function mountOneReactRoot(
|
|
556
|
+
spec: ReactRootMountSpec,
|
|
701
557
|
game: GameInternal,
|
|
702
558
|
/** The ALREADY-created, already-stacked (position/z-index set, appended to
|
|
703
559
|
* `container`) DOM-root layer for this world — see
|
|
704
|
-
* `
|
|
560
|
+
* `createRootsGameRuntime`'s surface-stack loop, which builds a `<div>`
|
|
705
561
|
* for every react-kind spec up front, in the SAME pass that builds every
|
|
706
562
|
* other world's canvas. This function must reuse that exact element (never
|
|
707
|
-
* create its own) so `
|
|
563
|
+
* create its own) so `RootInstance.reactRoot()` returns the SAME node
|
|
708
564
|
* that is actually positioned in the stack. */
|
|
709
565
|
layer: HTMLElement,
|
|
710
566
|
): Promise<{ mountedEntry: MountedAdapterRoot }> {
|
|
@@ -715,16 +571,16 @@ async function mountOneReactWorld(
|
|
|
715
571
|
// block (found by the T6.2 slice-3 e2e — clicks landed outside the game).
|
|
716
572
|
layer.style.width = '100%';
|
|
717
573
|
layer.style.height = '100%';
|
|
718
|
-
const reactHost:
|
|
574
|
+
const reactHost: DomHostContext = { container: layer, game };
|
|
719
575
|
const mounted = await spec.adapter.mount(reactHost);
|
|
720
|
-
|
|
576
|
+
registerReactRoot(game, spec.adapter, mounted, layer, {
|
|
721
577
|
id: spec.id,
|
|
722
578
|
pausable: spec.pausable,
|
|
723
579
|
});
|
|
724
580
|
return {
|
|
725
581
|
mountedEntry: {
|
|
726
582
|
id: spec.id,
|
|
727
|
-
kind: '
|
|
583
|
+
kind: 'dom',
|
|
728
584
|
element: layer,
|
|
729
585
|
mounted,
|
|
730
586
|
renderer: undefined,
|
|
@@ -735,24 +591,24 @@ async function mountOneReactWorld(
|
|
|
735
591
|
/**
|
|
736
592
|
* The roots-path implementer behind {@link createGameRuntime} (T6.1 slice
|
|
737
593
|
* 1; react roots added T6.2 slice 1). Builds ONE surface per world — a
|
|
738
|
-
* canvas for
|
|
739
|
-
* per
|
|
594
|
+
* canvas for three/canvas, a DOM-root `<div>` layer for react — stacked
|
|
595
|
+
* per D5 §1, z-order/ties exactly matching
|
|
740
596
|
* `manifest/load.ts`'s sort, ONE `Game`, and registers every world onto it
|
|
741
|
-
* via `
|
|
597
|
+
* via `registerThreeRoot`/`registerPixiRoot`/`registerReactRoot` — the
|
|
742
598
|
* SAME wiring `test/game-three-plus-pixi.test.ts` proves by hand for the
|
|
743
|
-
* three/pixi pair. Worlds MOUNT in `roots` ARRAY order (
|
|
744
|
-
*
|
|
599
|
+
* three/pixi pair. Worlds MOUNT in `roots` ARRAY order ("manifest
|
|
600
|
+
* declaration order" — the frame/registration axis), independent of
|
|
745
601
|
* `zOrder` (the canvas-stacking/rendering axis) — the two orders can differ
|
|
746
602
|
* and both are honored correctly.
|
|
747
603
|
*/
|
|
748
604
|
|
|
749
605
|
/**
|
|
750
606
|
* Dev/e2e-only `window.__vgaiScene`/`__vgaiCamera` exposure for the roots
|
|
751
|
-
* path's DEFAULT world (E4) — split out of `
|
|
607
|
+
* path's DEFAULT world (E4) — split out of `createRootsGameRuntime` purely
|
|
752
608
|
* to keep that function's own cyclomatic complexity down. Mirrors the
|
|
753
609
|
* legacy single-world path's identical exposure (above, in this same file),
|
|
754
|
-
* using the SAME "first
|
|
755
|
-
* `GameSession.scene`/`.camera` getters alias (`Game.
|
|
610
|
+
* using the SAME "first three world, else none" default-world rule the
|
|
611
|
+
* `GameSession.scene`/`.camera` getters alias (`Game.defaultRoot`): a
|
|
756
612
|
* non-threejs default world (or none at all) publishes neither global,
|
|
757
613
|
* exactly like those getters return `undefined` in that case. Returns a
|
|
758
614
|
* retraction callback — identity-guarded (a newer session's globals must
|
|
@@ -760,74 +616,87 @@ async function mountOneReactWorld(
|
|
|
760
616
|
* path's own cleanup guards it) — that is a no-op when nothing was
|
|
761
617
|
* published (non-DEV build, or non-threejs default world).
|
|
762
618
|
*/
|
|
763
|
-
function
|
|
619
|
+
function installDefaultRootDevGlobals(game: GameInternal): () => void {
|
|
764
620
|
if (!import.meta.env?.DEV) return () => {};
|
|
765
|
-
const defaultMounted = game.
|
|
766
|
-
if (defaultMounted.kind !== '
|
|
621
|
+
const defaultMounted = game.defaultRoot.mounted;
|
|
622
|
+
if (defaultMounted.kind !== 'three') return () => {};
|
|
767
623
|
const scene = defaultMounted.scene;
|
|
768
|
-
const camera = defaultMounted.camera as THREE.PerspectiveCamera;
|
|
769
624
|
const w = window as unknown as Record<string, unknown>;
|
|
770
625
|
w['__vgaiScene'] = scene;
|
|
771
|
-
|
|
626
|
+
// An ACCESSOR, not a value read once: a world may replace its camera after
|
|
627
|
+
// mount (fiber's `set({ camera })` — drei's `makeDefault`, or a translated
|
|
628
|
+
// Godot world installing the camera its `.tscn` authors), and a snapshot
|
|
629
|
+
// here reads as authoritative to the probe that reaches for it while naming
|
|
630
|
+
// a camera the frame no longer uses. The retraction guard compares the
|
|
631
|
+
// GETTER's identity, which is the same "don't clobber a newer session"
|
|
632
|
+
// rule the value comparison was.
|
|
633
|
+
const readCamera = (): THREE.PerspectiveCamera =>
|
|
634
|
+
defaultMounted.camera as THREE.PerspectiveCamera;
|
|
635
|
+
Object.defineProperty(w, '__vgaiCamera', {
|
|
636
|
+
get: readCamera,
|
|
637
|
+
configurable: true,
|
|
638
|
+
enumerable: true,
|
|
639
|
+
});
|
|
772
640
|
return () => {
|
|
773
641
|
if (w['__vgaiScene'] === scene) delete w['__vgaiScene'];
|
|
774
|
-
if (w
|
|
642
|
+
if (Object.getOwnPropertyDescriptor(w, '__vgaiCamera')?.get === readCamera)
|
|
643
|
+
delete w['__vgaiCamera'];
|
|
775
644
|
};
|
|
776
645
|
}
|
|
777
646
|
|
|
778
|
-
/** Shared inputs `
|
|
779
|
-
* everything `
|
|
647
|
+
/** Shared inputs `mountAllRootSpecs` needs beyond each individual spec —
|
|
648
|
+
* everything `OneRootContext` needs except the per-world `canvas`/
|
|
780
649
|
* `isBottom`, plus the surface lookup and bottom-id needed to derive them. */
|
|
781
|
-
interface
|
|
650
|
+
interface MountAllRootsInputs extends Omit<OneRootContext, 'canvas' | 'isBottom'> {
|
|
782
651
|
readonly surfacesById: Map<string, HTMLElement>;
|
|
783
652
|
}
|
|
784
653
|
|
|
785
|
-
interface
|
|
654
|
+
interface MountAllRootsResult {
|
|
786
655
|
readonly mountedEntries: MountedAdapterRoot[];
|
|
787
656
|
readonly routerEntries: RouterAdapterRoot[];
|
|
788
657
|
}
|
|
789
658
|
|
|
790
659
|
/**
|
|
791
660
|
* Mount + register every world spec, in ARRAY (declaration) order — split
|
|
792
|
-
* out of `
|
|
793
|
-
* cyclomatic complexity down (E4, same reason `
|
|
794
|
-
* `
|
|
795
|
-
* router entry (
|
|
796
|
-
*
|
|
797
|
-
*
|
|
798
|
-
*
|
|
661
|
+
* out of `createRootsGameRuntime` purely to keep that function's own
|
|
662
|
+
* cyclomatic complexity down (E4, same reason `mountOneThreeRoot`/
|
|
663
|
+
* `mountOnePixiRoot` are already split out). React roots contribute NO
|
|
664
|
+
* router entry ("DOM layers need no entry in the router's hit-test loop"):
|
|
665
|
+
* their layer's own `pointer-events` discipline handles claim/fall-through
|
|
666
|
+
* natively, with zero router involvement (see `mountOneReactRoot`'s doc
|
|
667
|
+
* comment).
|
|
799
668
|
*/
|
|
800
|
-
async function
|
|
801
|
-
mountSpecs: (
|
|
669
|
+
async function mountAllRootSpecs(
|
|
670
|
+
mountSpecs: (ThreeRootMountSpec | PixiRootMountSpec | ReactRootMountSpec)[],
|
|
802
671
|
bottomId: string | undefined,
|
|
803
|
-
inputs:
|
|
804
|
-
): Promise<
|
|
672
|
+
inputs: MountAllRootsInputs,
|
|
673
|
+
): Promise<MountAllRootsResult> {
|
|
805
674
|
const { surfacesById, ...shared } = inputs;
|
|
806
675
|
const mountedEntries: MountedAdapterRoot[] = [];
|
|
807
676
|
const routerEntries: RouterAdapterRoot[] = [];
|
|
808
677
|
|
|
809
678
|
for (const spec of mountSpecs) {
|
|
810
679
|
const isBottom = spec.id === bottomId;
|
|
811
|
-
if (spec.kind === '
|
|
680
|
+
if (spec.kind === 'dom') {
|
|
812
681
|
const layer = surfacesById.get(spec.id)!;
|
|
813
|
-
const { mountedEntry } = await
|
|
682
|
+
const { mountedEntry } = await mountOneReactRoot(spec, shared.game, layer);
|
|
814
683
|
mountedEntries.push(mountedEntry);
|
|
815
684
|
continue;
|
|
816
685
|
}
|
|
817
686
|
const canvas = surfacesById.get(spec.id)! as HTMLCanvasElement;
|
|
818
|
-
const oneCtx:
|
|
687
|
+
const oneCtx: OneRootContext = { ...shared, canvas, isBottom };
|
|
819
688
|
let mountedEntry: MountedAdapterRoot;
|
|
820
689
|
let routerEntry: RouterAdapterRoot;
|
|
821
|
-
if (spec.kind === '
|
|
822
|
-
({ mountedEntry, routerEntry } = await
|
|
823
|
-
} else if (spec.kind === '
|
|
824
|
-
({ mountedEntry, routerEntry } = await
|
|
690
|
+
if (spec.kind === 'three') {
|
|
691
|
+
({ mountedEntry, routerEntry } = await mountOneThreeRoot(spec, oneCtx));
|
|
692
|
+
} else if (spec.kind === 'canvas') {
|
|
693
|
+
({ mountedEntry, routerEntry } = await mountOnePixiRoot(spec, oneCtx));
|
|
825
694
|
} else {
|
|
826
695
|
// Exhaustiveness guard (§7.4-2): 'react' was already handled by the
|
|
827
696
|
// early `continue` above, so only a hypothetical 4th `AdapterSurface` can
|
|
828
697
|
// reach here — fail loudly rather than silently defaulting. `spec`
|
|
829
698
|
// itself (not `spec.kind`) is what TS has narrowed to `never`, since
|
|
830
|
-
// `
|
|
699
|
+
// `RootMountSpec` is a discriminated union at the object level.
|
|
831
700
|
assertNever(spec, 'create-runtime mount loop');
|
|
832
701
|
}
|
|
833
702
|
mountedEntries.push(mountedEntry);
|
|
@@ -842,9 +711,9 @@ async function mountAllWorldSpecs(
|
|
|
842
711
|
* `stop()`, same leave-nothing-live rule as the debug bridge's `uninstall()`
|
|
843
712
|
* in mount-manifest.ts — render-control has no uninstall surface of its own,
|
|
844
713
|
* a render page being a single-load host by design, so the retraction is
|
|
845
|
-
* the publication's inverse). Split out of `
|
|
714
|
+
* the publication's inverse). Split out of `createRootsGameRuntime` purely
|
|
846
715
|
* to keep that function's cyclomatic complexity down (the same reason
|
|
847
|
-
* `
|
|
716
|
+
* `mountAllRootSpecs`/`installDefaultRootDevGlobals` are split out).
|
|
848
717
|
*
|
|
849
718
|
* The clock is a documented no-op: a manifest-mounted game has no global
|
|
850
719
|
* cinematic AnimationClock to seek (a cinematic fixture that HAS one
|
|
@@ -874,7 +743,7 @@ function installSessionRenderHarness(
|
|
|
874
743
|
},
|
|
875
744
|
readiness: {
|
|
876
745
|
// Mount is complete by the time this harness exists (the install site
|
|
877
|
-
// in `
|
|
746
|
+
// in `createRootsGameRuntime` is after `mountAllRootSpecs`
|
|
878
747
|
// resolved) — the hook resolving immediately IS the readiness
|
|
879
748
|
// statement, not an assumption.
|
|
880
749
|
scene() {},
|
|
@@ -887,12 +756,12 @@ function installSessionRenderHarness(
|
|
|
887
756
|
};
|
|
888
757
|
}
|
|
889
758
|
|
|
890
|
-
async function
|
|
891
|
-
const { container, roots: specs, width, height, headless = false, seed } = config;
|
|
759
|
+
async function createRootsGameRuntime(config: RootsRuntimeConfig): Promise<GameSession> {
|
|
760
|
+
const { container, roots: specs, width, height, headless = false, seed, playtest } = config;
|
|
892
761
|
if (specs.length === 0) {
|
|
893
762
|
throw new Error('createGameRuntime: `roots` must contain at least one root.');
|
|
894
763
|
}
|
|
895
|
-
const mountSpecs = specs as (
|
|
764
|
+
const mountSpecs = specs as (ThreeRootMountSpec | PixiRootMountSpec | ReactRootMountSpec)[];
|
|
896
765
|
|
|
897
766
|
const w = Math.max(
|
|
898
767
|
1,
|
|
@@ -908,24 +777,42 @@ async function createWorldsGameRuntime(config: WorldsRuntimeConfig): Promise<Gam
|
|
|
908
777
|
|
|
909
778
|
if (!container.style.position) container.style.position = 'relative';
|
|
910
779
|
|
|
911
|
-
// --- Surface stack (D5 §1): DOM/z-order follows zOrder, ties -> array
|
|
912
|
-
//
|
|
913
|
-
//
|
|
914
|
-
//
|
|
915
|
-
//
|
|
916
|
-
//
|
|
917
|
-
//
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
780
|
+
// --- Surface stack (D5 §1): DOM/z-order follows zOrder, ties -> array order
|
|
781
|
+
// — computed FIRST (bottom -> top) so both the z-index assignment below and
|
|
782
|
+
// the router's default-claim rule share one definition. A react world's
|
|
783
|
+
// DOM-root layer shares this SAME stacking pass (T6.2 slice 1 — "one
|
|
784
|
+
// stacking model, no special case") even though it is a `<div>`, not a
|
|
785
|
+
// canvas, and carries no `hitTest` (the router never sees react entries at
|
|
786
|
+
// all — see the dispatch loop below). ---
|
|
787
|
+
// Dev layers sit TOPMOST, above every non-dev root, whatever zOrder either
|
|
788
|
+
// declares: a dev GUI the game can paint over is not a dev GUI. Expressed as
|
|
789
|
+
// two independent `stackOrder` passes concatenated rather than a magic
|
|
790
|
+
// zOrder value, so `stackOrder`'s one rule (ascending zOrder, ties -> array
|
|
791
|
+
// order) still governs WITHIN each band and nothing has to reason about what
|
|
792
|
+
// number is "high enough".
|
|
793
|
+
const claimEntries = mountSpecs.map((spec) => ({
|
|
794
|
+
id: spec.id,
|
|
795
|
+
zOrder: spec.zOrder ?? 0,
|
|
796
|
+
hitTest: spec.hitTest,
|
|
797
|
+
dev: spec.dev === true,
|
|
798
|
+
}));
|
|
799
|
+
const stacked = [
|
|
800
|
+
...stackOrder(claimEntries.filter((e) => !e.dev)),
|
|
801
|
+
...stackOrder(claimEntries.filter((e) => e.dev)),
|
|
802
|
+
];
|
|
921
803
|
const bottomId = stacked[0]?.id;
|
|
922
804
|
const kindById = new Map(mountSpecs.map((spec) => [spec.id, spec.kind] as const));
|
|
923
805
|
|
|
924
806
|
const surfacesById = new Map<string, HTMLElement>();
|
|
925
807
|
stacked.forEach((entry, i) => {
|
|
926
|
-
const isReact = kindById.get(entry.id) === '
|
|
808
|
+
const isReact = kindById.get(entry.id) === 'dom';
|
|
927
809
|
const surface = document.createElement(isReact ? 'div' : 'canvas') as HTMLElement;
|
|
928
|
-
surface.dataset['
|
|
810
|
+
surface.dataset['vgaiRootSurface'] = 'true';
|
|
811
|
+
// The capture's marker (`composite-screenshot.ts` reads exactly this
|
|
812
|
+
// attribute): a dev layer is excluded from a play capture unless the
|
|
813
|
+
// capture asks for it, because a contaminated "does the game look right"
|
|
814
|
+
// frame is worthless.
|
|
815
|
+
if (entry.dev) surface.dataset['vgaiDevLayer'] = 'true';
|
|
929
816
|
if (!isReact) {
|
|
930
817
|
const canvas = surface as unknown as HTMLCanvasElement;
|
|
931
818
|
canvas.width = w;
|
|
@@ -938,10 +825,10 @@ async function createWorldsGameRuntime(config: WorldsRuntimeConfig): Promise<Gam
|
|
|
938
825
|
// concern only). Without this, a canvas with no CSS size falls back to
|
|
939
826
|
// its `width`/`height` attribute as its CSS layout size too, so whatever
|
|
940
827
|
// stamped those attributes (`createHostRenderer`'s construction-time
|
|
941
|
-
// `setSize`, `
|
|
828
|
+
// `setSize`, `mountOneThreeRoot` below) pins the ON-SCREEN size — see
|
|
942
829
|
// that function's doc comment for the concrete bug this caused (template
|
|
943
830
|
// standalone at any viewport ≠ the manifest resolution). Setting it HERE
|
|
944
|
-
// too (not just in `
|
|
831
|
+
// too (not just in `mountOneThreeRoot`) means every surface, whatever
|
|
945
832
|
// kind, starts container-relative from its very first paint, before any
|
|
946
833
|
// per-kind mount work has even run.
|
|
947
834
|
// `contain:layout paint` makes each surface the CONTAINING BLOCK for
|
|
@@ -958,22 +845,15 @@ async function createWorldsGameRuntime(config: WorldsRuntimeConfig): Promise<Gam
|
|
|
958
845
|
});
|
|
959
846
|
|
|
960
847
|
const assets = createAssetCache();
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
//
|
|
969
|
-
//
|
|
970
|
-
// seam (`?vgai-render=1`, render-control.ts) is wired HERE, at the one
|
|
971
|
-
// host every boot path shares (mountGameFromManifest, mountManifestWorlds,
|
|
972
|
-
// and direct createGameRuntime callers all reach this function), instead
|
|
973
|
-
// of asking every project's entry page to install it the way the e2e
|
|
974
|
-
// fixtures do. Production-protected twice over: `isRenderModeRequested`
|
|
975
|
-
// gates on the query param, and `installRenderControlHarness` re-checks it
|
|
976
|
-
// internally (its AC 4), so a normal gameplay page sees zero change.
|
|
848
|
+
// Every manifest/host-mounted game is a deterministic-capture candidate —
|
|
849
|
+
// the render-control seam (`?vgai-render=1`, render-control.ts) is wired
|
|
850
|
+
// HERE, at the one host every boot path shares (mountGameFromManifest,
|
|
851
|
+
// mountManifestRoots, and direct createGameRuntime callers all reach this
|
|
852
|
+
// function), instead of asking every project's entry page to install it
|
|
853
|
+
// the way the e2e fixtures do. Production-protected twice over:
|
|
854
|
+
// `isRenderModeRequested` gates on the query param, and
|
|
855
|
+
// `installRenderControlHarness` re-checks it internally (its AC 4), so a
|
|
856
|
+
// normal gameplay page sees zero change.
|
|
977
857
|
const renderModeLocation =
|
|
978
858
|
config.renderControl?.location ?? (typeof window !== 'undefined' ? window.location : undefined);
|
|
979
859
|
const renderMode = renderModeLocation !== undefined && isRenderModeRequested(renderModeLocation);
|
|
@@ -986,26 +866,35 @@ async function createWorldsGameRuntime(config: WorldsRuntimeConfig): Promise<Gam
|
|
|
986
866
|
// host's explicit `simulateSubsteps`/`renderOnce` calls — wall-clock time
|
|
987
867
|
// passing while a screenshot is taken must not move the world.
|
|
988
868
|
externalDrive: renderMode,
|
|
869
|
+
// WO-P13 — sim and presentation are wired to different callbacks here:
|
|
870
|
+
// `update` runs the gameplay phases once per consumed fixed substep with
|
|
871
|
+
// `preRender`/`render` withheld, and `render` runs those two once per real
|
|
872
|
+
// display frame with the interpolation alpha. Both closures are invoked
|
|
873
|
+
// only from the loop's own rAF arm, which `externalDrive` never arms — so
|
|
874
|
+
// a capture/offline-export page (`renderMode`) reaches neither, and its
|
|
875
|
+
// `simulateSubsteps` → `game.runFrame(fixedDt)` path stays frame-exact,
|
|
876
|
+
// rendering inside the substep exactly as it did before this flip.
|
|
989
877
|
update: (dt) => {
|
|
990
|
-
if (started) game.runFrame(dt);
|
|
991
|
-
|
|
878
|
+
if (started) game.runFrame(dt, { skipRenderPhases: true });
|
|
879
|
+
},
|
|
880
|
+
render: (alpha, displayDt) => {
|
|
881
|
+
if (started) game.runRenderFrame(alpha, displayDt);
|
|
992
882
|
},
|
|
993
883
|
});
|
|
994
|
-
const game = createGame({ loop, assets, seed });
|
|
884
|
+
const game = createGame({ loop, assets, seed, playtest });
|
|
995
885
|
|
|
996
886
|
// --- Mount + register every world, in ARRAY (declaration) order. ---
|
|
997
887
|
// Split out into its own top-level function purely to keep
|
|
998
|
-
// `
|
|
999
|
-
// reason `
|
|
888
|
+
// `createRootsGameRuntime`'s own cyclomatic complexity down (E4) — same
|
|
889
|
+
// reason `mountOneThreeRoot`/`mountOnePixiRoot` are already split out
|
|
1000
890
|
// below.
|
|
1001
|
-
const { mountedEntries, routerEntries } = await
|
|
891
|
+
const { mountedEntries, routerEntries } = await mountAllRootSpecs(mountSpecs, bottomId, {
|
|
1002
892
|
game,
|
|
1003
893
|
surfacesById,
|
|
1004
894
|
w,
|
|
1005
895
|
h,
|
|
1006
896
|
dpr,
|
|
1007
897
|
headless,
|
|
1008
|
-
loopHandle,
|
|
1009
898
|
assets,
|
|
1010
899
|
});
|
|
1011
900
|
|
|
@@ -1017,16 +906,16 @@ async function createWorldsGameRuntime(config: WorldsRuntimeConfig): Promise<Gam
|
|
|
1017
906
|
|
|
1018
907
|
// Expose scene & camera for dev tools / e2e tests — mirrors the legacy
|
|
1019
908
|
// single-world path's identical dev-only exposure above, generalized to
|
|
1020
|
-
// the roots path's DEFAULT world (E4
|
|
1021
|
-
//
|
|
1022
|
-
//
|
|
1023
|
-
//
|
|
1024
|
-
//
|
|
1025
|
-
//
|
|
1026
|
-
//
|
|
1027
|
-
//
|
|
909
|
+
// the roots path's DEFAULT world (E4): `mountGameFromManifest`/
|
|
910
|
+
// `mountManifestRoots` route every caller (including a
|
|
911
|
+
// single-threejs-world scaffold project) through THIS path, so a caller
|
|
912
|
+
// migrating off the legacy `{canvas, adapter}` call must not silently
|
|
913
|
+
// lose `window.__vgaiScene`/`__vgaiCamera` — real e2e/dev tooling
|
|
914
|
+
// depends on them
|
|
915
|
+
// (`packages/editor/e2e/tests/04-standalone-game.spec.ts`). Split into
|
|
916
|
+
// its own top-level helper (with its retraction counterpart below)
|
|
1028
917
|
// purely to keep this function's own cyclomatic complexity down.
|
|
1029
|
-
const retractDevGlobals =
|
|
918
|
+
const retractDevGlobals = installDefaultRootDevGlobals(game);
|
|
1030
919
|
|
|
1031
920
|
// G3/FT-11 — publish `window.__vgaiRender` for the deterministic capture
|
|
1032
921
|
// host. Installed at the TAIL of the mount (same position as
|
|
@@ -1093,23 +982,23 @@ async function createWorldsGameRuntime(config: WorldsRuntimeConfig): Promise<Gam
|
|
|
1093
982
|
entry.mounted.resize?.(safeWidth, safeHeight);
|
|
1094
983
|
}
|
|
1095
984
|
},
|
|
1096
|
-
// `GameSession.scene`/`.camera`/`.mounted` alias `Game.
|
|
1097
|
-
// slice 1's "first
|
|
985
|
+
// `GameSession.scene`/`.camera`/`.mounted` alias `Game.defaultRoot` (T6.1
|
|
986
|
+
// slice 1's "first three world, else first world" rule) — a legacy
|
|
1098
987
|
// convenience shaped for the threejs-only past. T7.5 narrows the read via
|
|
1099
988
|
// the `kind` discriminant instead of a blind `.scene`/`.camera` cast
|
|
1100
989
|
// through a nonexistent property (identical behavior to before: still
|
|
1101
990
|
// `undefined` for a roots-path session whose default world isn't
|
|
1102
|
-
//
|
|
991
|
+
// three — real per-world surface routing for that case is T7.6's).
|
|
1103
992
|
get scene() {
|
|
1104
|
-
const m = game.
|
|
1105
|
-
return (m.kind === '
|
|
993
|
+
const m = game.defaultRoot.mounted;
|
|
994
|
+
return (m.kind === 'three' ? m.scene : undefined) as THREE.Scene;
|
|
1106
995
|
},
|
|
1107
996
|
get camera() {
|
|
1108
|
-
const m = game.
|
|
1109
|
-
return (m.kind === '
|
|
997
|
+
const m = game.defaultRoot.mounted;
|
|
998
|
+
return (m.kind === 'three' ? m.camera : undefined) as THREE.PerspectiveCamera;
|
|
1110
999
|
},
|
|
1111
1000
|
get mounted() {
|
|
1112
|
-
return game.
|
|
1001
|
+
return game.defaultRoot.mounted;
|
|
1113
1002
|
},
|
|
1114
1003
|
game,
|
|
1115
1004
|
};
|