@vgai/engine 0.2.0 → 0.4.0-canary.20260715.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/package.json +24 -4
- package/schemas/engine-api.json +124 -0
- package/schemas/engine-api.md +53 -0
- package/schemas/engine-capabilities.json +124 -0
- package/schemas/inputmap.schema.json +314 -0
- package/schemas/mat.schema.json +286 -0
- package/schemas/prefab.schema.json +10148 -0
- package/schemas/scn2d.schema.json +475 -0
- package/schemas/vgai-game.schema.json +383 -0
- package/schemas/vscn.schema.json +11007 -0
- package/src/adapter/{world-kind.ts → adapter-surface.ts} +6 -6
- package/src/adapter/authoring.ts +77 -0
- package/src/adapter/first-party-systems.ts +23 -34
- package/src/adapter/game-adapter.ts +8 -8
- package/src/adapter/host-context.ts +2 -4
- package/src/adapter/index.ts +4 -4
- package/src/adapter/system-adapter.ts +88 -22
- package/src/adapter/vgai-scene-game-adapter.ts +244 -194
- package/src/animation/anim-graph-types.ts +12 -43
- package/src/animation/animation-clock.ts +479 -0
- package/src/animation/camera-ownership.ts +467 -0
- package/src/animation/cinematic-cues.ts +451 -0
- package/src/animation/clip-map.ts +41 -0
- package/src/animation/gsap-registration.ts +184 -0
- package/src/animation/theatre-clock-binding.ts +111 -0
- package/src/animation/theatre-director.ts +347 -0
- package/src/animation/theatre-object-binding.ts +661 -0
- package/src/animation/xstate-animation-binding.ts +436 -0
- package/src/animation/xstate-animation-meta.ts +319 -0
- package/src/audio/index.ts +39 -7
- package/src/audio/tone-clock-binding.ts +98 -0
- package/src/audio/tone-context.ts +129 -0
- package/src/audio/tone-offline-render.ts +167 -0
- package/src/audio/wav-encode.ts +119 -0
- package/src/character/cloth-sim.ts +533 -0
- package/src/character/spring-chain.ts +307 -0
- package/src/core/game-loop.ts +57 -2
- package/src/core/seeded-random.ts +161 -0
- package/src/core/system-runner.ts +20 -3
- package/src/core/types.ts +50 -0
- package/src/data/data-asset.ts +167 -0
- package/src/data/data-check-core.ts +242 -0
- package/src/data/data-ref.ts +145 -0
- package/src/data/vite-plugin-data.ts +290 -0
- package/src/dev/performance-profiler.ts +213 -0
- package/src/dev/webgl-gpu-timer.ts +53 -0
- package/src/ecs/component-manager.ts +45 -12
- package/src/ecs/game-component.ts +95 -11
- package/src/humanoid/bake.operation.ts +326 -0
- package/src/humanoid/body.ts +663 -0
- package/src/humanoid/clips.ts +149 -0
- package/src/humanoid/compose.ts +209 -0
- package/src/humanoid/generate.ts +189 -0
- package/src/humanoid/index.ts +36 -0
- package/src/humanoid/schema.ts +108 -0
- package/src/humanoid/skeleton.ts +345 -0
- package/src/index.ts +48 -0
- package/src/input/input-manager.ts +1886 -33
- package/src/input/input-types.ts +158 -3
- package/src/input/prompt-labels.ts +122 -0
- package/src/input/rebind-controller.ts +105 -0
- package/src/input/schema.ts +206 -52
- package/src/manifest/index.ts +5 -5
- package/src/manifest/load.ts +125 -72
- package/src/manifest/schema.ts +362 -255
- package/src/react/game-state.tsx +135 -32
- package/src/react/root-adapter.tsx +49 -0
- package/src/react/unmanaged-root-detector.ts +66 -0
- package/src/react/use-data.ts +124 -0
- package/src/react/use-selection.tsx +135 -0
- package/src/runtime/create-runtime.ts +112 -273
- package/src/runtime/debug-bridge.ts +483 -0
- package/src/runtime/debug-registry.ts +856 -0
- package/src/runtime/game.ts +342 -93
- package/src/runtime/gameplay-rng-trap.ts +134 -0
- package/src/runtime/input-router.ts +7 -7
- package/src/runtime/mount-game.ts +40 -38
- package/src/runtime/mount-manifest.ts +169 -37
- package/src/runtime/render-audio-control.ts +168 -0
- package/src/runtime/render-control.ts +522 -0
- package/src/runtime/render-seed.ts +79 -0
- package/src/runtime/state-bridge.ts +24 -10
- package/src/runtime/types.ts +110 -33
- package/src/scene/asset-loaders.ts +10 -36
- package/src/scene/asset-paths.ts +0 -2
- package/src/scene/asset-ref-check.ts +248 -0
- package/src/scene/asset-registry.ts +22 -0
- package/src/scene/component-registry.ts +14 -3
- package/src/scene/defaults.ts +1 -0
- package/src/scene/light-camera-factory.ts +11 -3
- package/src/scene/parse.ts +133 -0
- package/src/scene/scene-apply.ts +55 -4
- package/src/scene/scene-loader.ts +91 -123
- package/src/scene/scene-types.ts +0 -1
- package/src/scene/schema/animation.ts +30 -79
- package/src/scene/schema/entity.ts +20 -0
- package/src/scene/schema/index.ts +2 -46
- package/src/scene/schema/light.ts +16 -1
- package/src/scene/schema/material.ts +96 -91
- package/src/scene/schema/scene-file.ts +1 -7
- package/src/scene/user-data.ts +22 -10
- package/src/setup/setup-renderer.ts +10 -3
- package/src/tools/define-tool.ts +191 -0
- package/src/world2d/authoring-2d.ts +17 -1
- package/src/world2d/collision-2d.ts +1 -1
- package/src/world2d/pixi-game-adapter.ts +19 -17
- package/src/world2d/scene2d-loader.ts +1 -0
- package/src/world2d/types.ts +8 -2
- package/src/animation/anim-graph.ts +0 -406
- package/src/animation/anim-system.ts +0 -28
- package/src/animation/property-track.ts +0 -178
- package/src/animation/schema.ts +0 -204
- package/src/audio/ambient.ts +0 -300
- package/src/audio/impacts.ts +0 -212
- package/src/audio/movement.ts +0 -140
- package/src/audio/musical.ts +0 -200
- package/src/audio/ui-sounds.ts +0 -171
- package/src/audio/vehicle.ts +0 -235
- package/src/audio/weapons.ts +0 -152
- package/src/runtime/scene-ui-bridge.ts +0 -86
- package/src/runtime/scene-ui-data.ts +0 -119
- package/src/scene/schema/ui.ts +0 -602
|
@@ -15,10 +15,14 @@
|
|
|
15
15
|
import RAPIER from '@dimforge/rapier3d-compat';
|
|
16
16
|
import type { EffectComposer } from 'postprocessing';
|
|
17
17
|
import * as THREE from 'three';
|
|
18
|
-
import
|
|
19
|
-
|
|
18
|
+
import {
|
|
19
|
+
createSeededRandom,
|
|
20
|
+
DEFAULT_SEEDED_RANDOM_SEED,
|
|
21
|
+
getSeededRandom,
|
|
22
|
+
} from '../core/seeded-random';
|
|
20
23
|
import { createSystemRunner } from '../core/system-runner';
|
|
21
24
|
import { createDebugDraw } from '../dev/debug-draw';
|
|
25
|
+
import { createWebGLGpuTimer } from '../dev/webgl-gpu-timer';
|
|
22
26
|
import { createComponentManager } from '../ecs/component-manager';
|
|
23
27
|
import { InputManager } from '../input/input-manager';
|
|
24
28
|
import { setAssetPrefix } from '../loader';
|
|
@@ -28,14 +32,12 @@ import { createTransformWriter } from '../physics/transform-writer';
|
|
|
28
32
|
import { createTriggerDispatch } from '../physics/trigger-dispatch';
|
|
29
33
|
import { RenderBatchSystem } from '../render/render-batch-system';
|
|
30
34
|
import { type RenderScope, resolveRenderSettings } from '../render/render-settings';
|
|
31
|
-
import type { WorldFrameHooks } from '../runtime/game';
|
|
32
35
|
import {
|
|
33
|
-
|
|
34
|
-
type
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
} from '../runtime/
|
|
38
|
-
import { gameStateDataSource } from '../runtime/scene-ui-data';
|
|
36
|
+
createDebugRegistry,
|
|
37
|
+
type DebugRegistry,
|
|
38
|
+
getDebugRegistry,
|
|
39
|
+
} from '../runtime/debug-registry';
|
|
40
|
+
import type { WorldFrameHooks } from '../runtime/game';
|
|
39
41
|
import type { EditorPreview, GameCleanup, GameContext, GameSetupFn } from '../runtime/types';
|
|
40
42
|
import type { ComponentRegistry } from '../scene/component-registry';
|
|
41
43
|
import { tickCustomMaterials } from '../scene/material-registry';
|
|
@@ -57,7 +59,6 @@ import {
|
|
|
57
59
|
createSceneView,
|
|
58
60
|
} from '../setup/setup-renderer';
|
|
59
61
|
import {
|
|
60
|
-
createAnimationAdapter,
|
|
61
62
|
createAudioSystemAdapter,
|
|
62
63
|
createInputManagerAdapter,
|
|
63
64
|
createNavigationAdapter,
|
|
@@ -91,24 +92,6 @@ export interface VgaiSceneConfig {
|
|
|
91
92
|
* that sets this today).
|
|
92
93
|
*/
|
|
93
94
|
componentRegistry?: ComponentRegistry | undefined;
|
|
94
|
-
/**
|
|
95
|
-
* The project's scene-UI registry (design/24-scene-ui.md D9/D11) —
|
|
96
|
-
* `components`/`transforms`/`strings` only, opaque records engine-side (the
|
|
97
|
-
* engine never imports `@vgai/scene-ui`'s real `ProjectUIRegistry` type;
|
|
98
|
-
* they're React things it never inspects). Threaded per-mount into the
|
|
99
|
-
* services `renderSceneUI` receives, mirroring `componentRegistry` above:
|
|
100
|
-
* the editor's default-three adapter resolver passes its cached project
|
|
101
|
-
* registry here for the editor-play path (D11); a standalone host that
|
|
102
|
-
* already registered a static registry via `registerSceneUIRenderer`
|
|
103
|
-
* typically omits this (registration-time registry is enough there).
|
|
104
|
-
*/
|
|
105
|
-
uiRegistry?:
|
|
106
|
-
| {
|
|
107
|
-
components?: Record<string, unknown>;
|
|
108
|
-
transforms?: Record<string, unknown>;
|
|
109
|
-
strings?: Record<string, Record<string, string>>;
|
|
110
|
-
}
|
|
111
|
-
| undefined;
|
|
112
95
|
/** Input map JSON path. Defaults to 'inputmaps/default.inputmap.json'. */
|
|
113
96
|
inputMapPath?: string | undefined;
|
|
114
97
|
/** URL prefix for relative asset paths. Defaults to '/'. */
|
|
@@ -142,10 +125,10 @@ export interface VgaiMountedGame extends MountedGame {
|
|
|
142
125
|
/**
|
|
143
126
|
* Phase-partitioned frame entry point (T7.1 slice 2,
|
|
144
127
|
* `docs/GAME-ROOT-DESIGN.md` §4) — wired onto this world's `WorldInstance`
|
|
145
|
-
* by `
|
|
128
|
+
* by `registerThreeWorld` (`create-runtime.ts`). `Game.runFrame` is
|
|
146
129
|
* the CANONICAL driver going forward: it calls `frame.runPhase` once per
|
|
147
130
|
* (phase, substep) for every world, then `frame.endFrame` once per
|
|
148
|
-
* substep after all
|
|
131
|
+
* substep after all roots finish all phases. `update` (below) remains
|
|
149
132
|
* the legacy/direct entry point to the exact same `SystemRunner` — direct
|
|
150
133
|
* callers (editor HMR, adapter conformance tests) keep using it unchanged.
|
|
151
134
|
*/
|
|
@@ -188,7 +171,6 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
188
171
|
scenePath,
|
|
189
172
|
sceneData,
|
|
190
173
|
componentRegistry,
|
|
191
|
-
uiRegistry,
|
|
192
174
|
inputMapPath = '/inputmaps/default.inputmap.json',
|
|
193
175
|
assetPrefix = '/',
|
|
194
176
|
} = this.config;
|
|
@@ -237,33 +219,69 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
237
219
|
// --- Particles (three.quarks BatchedRenderer) ---
|
|
238
220
|
const particles = setupParticles(scene);
|
|
239
221
|
|
|
240
|
-
// ---
|
|
241
|
-
const uiContainer = host.ui as HTMLDivElement;
|
|
242
|
-
|
|
243
|
-
// --- Debug draw / assets / anim graphs ---
|
|
222
|
+
// --- Debug draw / assets ---
|
|
244
223
|
const debugDraw = createDebugDraw(scene);
|
|
245
224
|
const assets = host.assets;
|
|
246
|
-
|
|
225
|
+
// Test/headless adapters may provide the renderer surface needed by the
|
|
226
|
+
// runtime without a real WebGL context. GPU timing is optional diagnostics:
|
|
227
|
+
// degrade honestly when the renderer/context cannot expose extensions.
|
|
228
|
+
const rendererContext =
|
|
229
|
+
typeof renderer.getContext === 'function' ? renderer.getContext() : undefined;
|
|
230
|
+
const gpuTimer =
|
|
231
|
+
!headless && rendererContext && typeof rendererContext.getExtension === 'function'
|
|
232
|
+
? createWebGLGpuTimer(rendererContext)
|
|
233
|
+
: null;
|
|
247
234
|
|
|
248
235
|
// --- System runner (engine-level systems) ---
|
|
249
|
-
const systems = createSystemRunner();
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
236
|
+
const systems = createSystemRunner(host.game?.profiler.systemObserver, 'threejs');
|
|
237
|
+
// D15/T-D15.3/.5: pass the LIVE shared game tick (not a locally-counted
|
|
238
|
+
// one) into every `poll()` call — `debugRegistry` is declared further
|
|
239
|
+
// down in this same `mount()` (it needs `ctx`/`systemAdapters` to exist
|
|
240
|
+
// first), but this callback only ever executes during real frame
|
|
241
|
+
// ticks, long after `mount()` has fully returned and `debugRegistry` is
|
|
242
|
+
// initialized (a safe closure-over-a-later-const, not a genuine
|
|
243
|
+
// use-before-init). `host.game` gates it: a bare mount with no `Game`
|
|
244
|
+
// shell behind it (test harnesses predating T7.1) has no shared tick to
|
|
245
|
+
// key off, so `InputManager.poll()` falls back to its own internal
|
|
246
|
+
// self-incrementing counter instead (unchanged pre-D15 behavior) rather
|
|
247
|
+
// than being pinned to a constant `0` forever.
|
|
248
|
+
systems.add('input', () => input.poll(host.game ? debugRegistry.getGameTick() : undefined), {
|
|
249
|
+
name: 'input.poll',
|
|
255
250
|
});
|
|
256
|
-
systems.add('
|
|
257
|
-
|
|
258
|
-
particles.batchedRenderer.update(dt);
|
|
259
|
-
tickCustomMaterials(scene, dt);
|
|
260
|
-
updateSceneLODs(scene, camera);
|
|
251
|
+
systems.add('physics', () => physics.rapierWorld.step(physics.eventQueue), {
|
|
252
|
+
name: 'physics.step',
|
|
261
253
|
});
|
|
254
|
+
systems.add(
|
|
255
|
+
'postPhysics',
|
|
256
|
+
() => {
|
|
257
|
+
collisions.drain();
|
|
258
|
+
writeTransforms();
|
|
259
|
+
},
|
|
260
|
+
{ name: 'physics.sync' },
|
|
261
|
+
);
|
|
262
|
+
systems.add(
|
|
263
|
+
'preRender',
|
|
264
|
+
(dt) => {
|
|
265
|
+
particles.batchedRenderer.update(dt);
|
|
266
|
+
tickCustomMaterials(scene, dt);
|
|
267
|
+
updateSceneLODs(scene, camera);
|
|
268
|
+
},
|
|
269
|
+
{ name: 'materials-and-particles' },
|
|
270
|
+
);
|
|
262
271
|
if (!headless) {
|
|
263
|
-
systems.add(
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
272
|
+
systems.add(
|
|
273
|
+
'render',
|
|
274
|
+
(dt) => {
|
|
275
|
+
if (host.game?.profiler.enabled) gpuTimer?.begin();
|
|
276
|
+
try {
|
|
277
|
+
composer.render(dt);
|
|
278
|
+
} finally {
|
|
279
|
+
gpuTimer?.end();
|
|
280
|
+
}
|
|
281
|
+
if (physics.debugEnabled) updatePhysicsDebug(physics.rapierWorld, physics.debugMesh);
|
|
282
|
+
},
|
|
283
|
+
{ name: 'three.render' },
|
|
284
|
+
);
|
|
267
285
|
}
|
|
268
286
|
|
|
269
287
|
// --- Debug toggle (browser only) ---
|
|
@@ -275,7 +293,18 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
275
293
|
};
|
|
276
294
|
if (!headless) window.addEventListener('keydown', onDebugToggle);
|
|
277
295
|
|
|
278
|
-
const postFrame = () =>
|
|
296
|
+
const postFrame = () => {
|
|
297
|
+
input.endFrame();
|
|
298
|
+
if (host.game?.profiler.enabled) {
|
|
299
|
+
host.game.profiler.reportRender({
|
|
300
|
+
gpuMs: gpuTimer?.poll() ?? null,
|
|
301
|
+
drawCalls: renderer.info.render.calls,
|
|
302
|
+
triangles: renderer.info.render.triangles,
|
|
303
|
+
geometries: renderer.info.memory.geometries,
|
|
304
|
+
textures: renderer.info.memory.textures,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
};
|
|
279
308
|
|
|
280
309
|
// The mounted game's adapter surface (`mounted.systems`). Declared before
|
|
281
310
|
// ctx so `ctx.registerSystemAdapter` can close over it: the engine fills
|
|
@@ -285,17 +314,6 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
285
314
|
// late registrations are visible to its panels at read time.
|
|
286
315
|
const systemAdapters: SystemAdapters = {};
|
|
287
316
|
|
|
288
|
-
// Game-registered scene-UI services (design/24-scene-ui.md D2/D7) — a `let`
|
|
289
|
-
// bag mutated by `ctx.setSceneUIServices`, declared here (BEFORE `ctx` is
|
|
290
|
-
// built) so the closure below has something to capture: a GameComponent
|
|
291
|
-
// calling `ctx.setSceneUIServices` from its `init()` runs DURING
|
|
292
|
-
// `componentManager.initAll()` (scene-loader.ts), which is awaited before
|
|
293
|
-
// `loadScene`/`loadFromData` return below — well before `sceneUIHandle`
|
|
294
|
-
// exists — so early calls just accumulate here and are folded into the
|
|
295
|
-
// FIRST render (`buildSceneUIServices`, below) rather than routed through
|
|
296
|
-
// the late-update path.
|
|
297
|
-
let sceneUIServices: SceneUIGameServices = {};
|
|
298
|
-
|
|
299
317
|
// --- GameContext (the first-party runtime surface) ---
|
|
300
318
|
const ctx: GameContext = {
|
|
301
319
|
scene,
|
|
@@ -309,42 +327,23 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
309
327
|
audio,
|
|
310
328
|
particles,
|
|
311
329
|
debugDraw,
|
|
312
|
-
animGraphs,
|
|
313
330
|
assets,
|
|
314
331
|
systems,
|
|
315
332
|
components: null!,
|
|
316
|
-
uiContainer,
|
|
317
333
|
registerSystemAdapter: (kind, adapter) => {
|
|
318
334
|
systemAdapters[kind] = adapter;
|
|
319
335
|
},
|
|
320
|
-
setSceneUIServices: (incoming) => {
|
|
321
|
-
sceneUIServices = { ...sceneUIServices, ...incoming };
|
|
322
|
-
// Late call (after the initial mount already resolved a sceneInstance
|
|
323
|
-
// and rendered it): route to the live handle instead of just banking
|
|
324
|
-
// the services for a mount that already happened (D7).
|
|
325
|
-
if (sceneUIHandle && sceneInstance) {
|
|
326
|
-
const merged = buildSceneUIServices(sceneInstance);
|
|
327
|
-
if (sceneUIHandle.update) {
|
|
328
|
-
sceneUIHandle.update(merged);
|
|
329
|
-
} else {
|
|
330
|
-
sceneUIHandle.dispose();
|
|
331
|
-
sceneUIHandle = sceneInstance.ui
|
|
332
|
-
? renderSceneUI(uiContainer, sceneInstance.ui, merged)
|
|
333
|
-
: null;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
},
|
|
337
336
|
};
|
|
338
337
|
// Game root (T7.1 slice 1): only present once the host has constructed a
|
|
339
338
|
// Game shell (createGameRuntime does this before mount; headless/foreign
|
|
340
|
-
// hosts may omit it — ctx.game/ctx.
|
|
341
|
-
// the documented zero-break behavior). `ctx.
|
|
339
|
+
// hosts may omit it — ctx.game/ctx.roots simply stay undefined, which is
|
|
340
|
+
// the documented zero-break behavior). `ctx.roots` is assigned the SAME
|
|
342
341
|
// live array `host.game` mutates via `registerWorld` (not a copy) — the
|
|
343
342
|
// default world is registered AFTER mount returns, and this reference
|
|
344
343
|
// must observe that later push.
|
|
345
344
|
if (host.game) {
|
|
346
345
|
ctx.game = host.game;
|
|
347
|
-
ctx.
|
|
346
|
+
ctx.roots = host.game.roots;
|
|
348
347
|
}
|
|
349
348
|
ctx.components = createComponentManager(ctx, physicsRegistry);
|
|
350
349
|
collisions.onCollision(
|
|
@@ -372,11 +371,89 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
372
371
|
systemAdapters.physics = createRapierPhysicsAdapter(physicsRegistry, physics.debugMesh);
|
|
373
372
|
systemAdapters.input = createInputManagerAdapter(input);
|
|
374
373
|
systemAdapters.assets = createVgaiAssetAdapter();
|
|
375
|
-
systemAdapters.animation = createAnimationAdapter(animGraphs);
|
|
376
374
|
// D10/T7.6: the audio seam `Game.play.pause()` silences on pause. Works
|
|
377
375
|
// against the headless stand-in too (`headlessAudio()`'s plain
|
|
378
376
|
// `masterGain.gain` object) — this world's `pause()` mutes it harmlessly.
|
|
379
377
|
systemAdapters.audio = createAudioSystemAdapter(audio);
|
|
378
|
+
// Debug/synthetic-player seam (T1.1, docs/SYNTHETIC-PLAYER-SPEC.md §3.1):
|
|
379
|
+
// ONE registry per Game root, shared across every world that mounts onto
|
|
380
|
+
// it — `getDebugRegistry(host.game)` reaches the SAME accumulator
|
|
381
|
+
// `createGame` filed, so a second/third world's `ctx.debug` feeds it too
|
|
382
|
+
// (see debug-registry.ts's module doc for the provenance/id note). A
|
|
383
|
+
// mount with no Game shell at all (bare test harnesses predating T7.1)
|
|
384
|
+
// gets a private, mount-local registry instead — same absence precedent
|
|
385
|
+
// as `ctx.game`/`ctx.roots` above.
|
|
386
|
+
const debugRegistry: DebugRegistry =
|
|
387
|
+
(host.game ? getDebugRegistry(host.game) : null) ??
|
|
388
|
+
createDebugRegistry({ getTick: () => 0, getSimT: () => 0 });
|
|
389
|
+
systemAdapters.debug = debugRegistry.adapter;
|
|
390
|
+
ctx.debug = debugRegistry.forWorld(this.id);
|
|
391
|
+
// T1.2: the InputManager is a first-party per-world handle the registry
|
|
392
|
+
// has no other way to reach — wired as a lazy supplier (not a direct
|
|
393
|
+
// read here), scoped to THIS world's id, so the built-in `input.actions`
|
|
394
|
+
// provider always reflects the CURRENT action set, not a snapshot taken
|
|
395
|
+
// at mount time. Per-world (not a single last-writer-wins slot) since
|
|
396
|
+
// D15/T-D15.5 — see `debug-registry.ts`'s `setInputActionsSource` doc.
|
|
397
|
+
debugRegistry.setInputActionsSource(this.id, () =>
|
|
398
|
+
typeof input.actionNames === 'function'
|
|
399
|
+
? input.actionNames().map((name) => ({ name, valueType: input.getActionValueType(name) }))
|
|
400
|
+
: [],
|
|
401
|
+
);
|
|
402
|
+
// D15/T-D15.5 — same lazy-supplier pattern (and same headless-stand-in
|
|
403
|
+
// guard) as `setInputActionsSource` immediately above: the built-in
|
|
404
|
+
// `input.trace` provider always reads the CURRENT recorded trace, never
|
|
405
|
+
// a snapshot taken at mount time. `seed`/`fixedDt` are assembled here
|
|
406
|
+
// (not inside `InputManager`, which has no business knowing about
|
|
407
|
+
// `ctx.random` or the loop) — the two replay-critical metadata fields
|
|
408
|
+
// the design doc's format sketch (§2.c) calls for beside the raw
|
|
409
|
+
// per-tick deltas; `null` only absent a Game/ctx.random behind this
|
|
410
|
+
// mount, matching every other "no Game" fallback in this file.
|
|
411
|
+
debugRegistry.setInputTraceSource(this.id, () => {
|
|
412
|
+
const raw: { version: 1; ticks: unknown[] } =
|
|
413
|
+
typeof input.getInputTrace === 'function'
|
|
414
|
+
? input.getInputTrace()
|
|
415
|
+
: { version: 1, ticks: [] };
|
|
416
|
+
const seed = (host.game ? getSeededRandom(host.game) : null)?.seed ?? null;
|
|
417
|
+
const fixedDt = host.game?.loop.fixedDt ?? null;
|
|
418
|
+
return { version: raw.version, seed, fixedDt, ticks: raw.ticks };
|
|
419
|
+
});
|
|
420
|
+
// Task 2.1 — the same InputManager instance, wired as the debug bridge's
|
|
421
|
+
// actuation target (`runtime/debug-bridge.ts`'s `input.*` methods) at the
|
|
422
|
+
// same seed spot as `setInputActionsSource` above. Per-world (D15 review
|
|
423
|
+
// objection 2 fix) — see `debug-registry.ts`'s `setVirtualInputTarget`
|
|
424
|
+
// doc for why this used to be a single last-writer-wins slot.
|
|
425
|
+
debugRegistry.setVirtualInputTarget(this.id, {
|
|
426
|
+
setVirtualAction: (action, value) => input.setVirtualAction(action, value),
|
|
427
|
+
tapVirtualAction: (action) => input.tapVirtualAction(action),
|
|
428
|
+
clearVirtualActions: () => input.clearVirtualActions(),
|
|
429
|
+
scheduleActionAtTick: (tick, action, value) =>
|
|
430
|
+
input.scheduleActionAtTick(tick, action, value),
|
|
431
|
+
startInputRecording: () => input.startInputRecording(),
|
|
432
|
+
stopInputRecording: () => input.stopInputRecording(),
|
|
433
|
+
isInputRecording: () => input.isInputRecording(),
|
|
434
|
+
injectAxis: (sourceId, value) => input.injectAxis(sourceId, value),
|
|
435
|
+
injectVector2: (sourceId, value) => input.injectVector2(sourceId, value),
|
|
436
|
+
injectPointerDelta: (sourceId, delta) => input.injectPointerDelta(sourceId, delta),
|
|
437
|
+
injectPointerPosition: (sourceId, value) => input.injectPointerPosition(sourceId, value),
|
|
438
|
+
});
|
|
439
|
+
// R4-class fix — wire the InputManager's `'input.schedule.dropped'` sink
|
|
440
|
+
// to THIS world's debug registry, same seed spot/precedent as
|
|
441
|
+
// `setInputActionsSource`/`setVirtualInputTarget` above (typeof-guarded:
|
|
442
|
+
// a headless `InputManager` stand-in may have no `setDebugEmit`).
|
|
443
|
+
if (typeof input.setDebugEmit === 'function') {
|
|
444
|
+
input.setDebugEmit((event, detail) => ctx.debug?.emit(event, detail));
|
|
445
|
+
}
|
|
446
|
+
// D15 (T-D15.1) — same "ONE per Game root, shared across every world"
|
|
447
|
+
// pattern as the debug registry immediately above: `getSeededRandom
|
|
448
|
+
// (host.game)` reaches the SAME `SeededRandom` `createGame` filed (boot-
|
|
449
|
+
// seeded by the manifest-aware mount path, `mount-manifest.ts`, before
|
|
450
|
+
// ANY world's `mount()`/`setup()` runs — see `createGame`'s own doc
|
|
451
|
+
// comment), so every world's `ctx.random` is the identical instance, not
|
|
452
|
+
// a per-world copy. Same absence precedent as `ctx.debug`: a mount with
|
|
453
|
+
// no Game shell at all gets a private, mount-local surface instead.
|
|
454
|
+
ctx.random =
|
|
455
|
+
(host.game ? getSeededRandom(host.game) : null) ??
|
|
456
|
+
createSeededRandom(DEFAULT_SEEDED_RANDOM_SEED);
|
|
380
457
|
|
|
381
458
|
// Snapshot of the engine-owned adapter kinds, taken right after seeding
|
|
382
459
|
// and before any setup() has had a chance to run. `hotReload`/`disposeGame`
|
|
@@ -403,13 +480,7 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
403
480
|
c.traverse((n) => setUserData(n, 'engineInternal', true));
|
|
404
481
|
}
|
|
405
482
|
let currentCleanup: GameCleanup | null = null;
|
|
406
|
-
let sceneUIHandle: SceneUIHandle | null = null;
|
|
407
483
|
let batchSystem: RenderBatchSystem | null = null;
|
|
408
|
-
// The scenePath/sceneData branch's resolved SceneInstance, if either ran
|
|
409
|
-
// (stays undefined for a `setup`-based mount — see the `sceneUI` deriva-
|
|
410
|
-
// tion below). Single source of truth for both the scene-graph `ui` tree
|
|
411
|
-
// and (potentially, in future) other instance-derived wiring.
|
|
412
|
-
let sceneInstance: SceneInstance | undefined;
|
|
413
484
|
|
|
414
485
|
// Apply scene-level render settings + transparent auto-batching after a scene
|
|
415
486
|
// loads (GPU host only). Gated on the resolved `environment.rendering` cascade
|
|
@@ -448,61 +519,6 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
448
519
|
}
|
|
449
520
|
};
|
|
450
521
|
|
|
451
|
-
// Scratch vector for the D5 default worldProjector — reused across calls
|
|
452
|
-
// (one per bound world-tracked canvas per render), never allocated per-frame.
|
|
453
|
-
const _sceneUIProjectorTmp = new THREE.Vector3();
|
|
454
|
-
|
|
455
|
-
// The D4 default data source subscribes to game.state for its whole
|
|
456
|
-
// lifetime with no unsubscribe path, so it must be built at most ONCE per
|
|
457
|
-
// adapter mount — buildSceneUIServices runs again on every late
|
|
458
|
-
// setSceneUIServices call, and rebuilding it there would both leak a
|
|
459
|
-
// subscription and force every bound node to resubscribe to a fresh store.
|
|
460
|
-
let defaultSceneUIData: SceneUIDataSourceLike | undefined;
|
|
461
|
-
|
|
462
|
-
/**
|
|
463
|
-
* Assemble the services `renderSceneUI` receives (design/24-scene-ui.md
|
|
464
|
-
* D2): engine defaults (D4 data source, D5 worldProjector, D6 onEvent) <-
|
|
465
|
-
* the project's per-mount UI registry (D11, editor-play threading) <-
|
|
466
|
-
* game-registered services (`ctx.setSceneUIServices`) — per key, each
|
|
467
|
-
* later layer winning over the one before it.
|
|
468
|
-
*/
|
|
469
|
-
const buildSceneUIServices = (inst: SceneInstance): SceneUIGameServices => {
|
|
470
|
-
const defaults: SceneUIGameServices = {
|
|
471
|
-
onEvent: (handlerId) => {
|
|
472
|
-
console.warn(
|
|
473
|
-
`scene-ui: unhandled UI event "${handlerId}" — register onEvent via ctx.setSceneUIServices`,
|
|
474
|
-
);
|
|
475
|
-
},
|
|
476
|
-
worldProjector: (id) => {
|
|
477
|
-
const obj = inst.entities.get(id);
|
|
478
|
-
if (!obj) return null;
|
|
479
|
-
const projected = obj.getWorldPosition(_sceneUIProjectorTmp).project(camera);
|
|
480
|
-
if (projected.z > 1) return null; // behind the camera -> hidden
|
|
481
|
-
const w = uiContainer.clientWidth;
|
|
482
|
-
const h = uiContainer.clientHeight;
|
|
483
|
-
// Rounded (not sub-pixel) — matches CrtScreen's integer-rounding
|
|
484
|
-
// (crt-screen.ts), which exists specifically to stop an asymptotic
|
|
485
|
-
// camera lerp from shimmering a tracked canvas every frame.
|
|
486
|
-
return {
|
|
487
|
-
x: Math.round(((projected.x + 1) / 2) * w),
|
|
488
|
-
y: Math.round(((1 - projected.y) / 2) * h),
|
|
489
|
-
};
|
|
490
|
-
},
|
|
491
|
-
};
|
|
492
|
-
// Lazily and at-most-once; skipped entirely while the game supplies its
|
|
493
|
-
// own `data` service (no point subscribing a default nobody reads).
|
|
494
|
-
if (!sceneUIServices.data && ctx.game && componentRegistry) {
|
|
495
|
-
defaultSceneUIData ??= gameStateDataSource(ctx.game, componentRegistry);
|
|
496
|
-
defaults.data = defaultSceneUIData;
|
|
497
|
-
}
|
|
498
|
-
const registryPortion: SceneUIGameServices = {
|
|
499
|
-
...(uiRegistry?.components ? { components: uiRegistry.components } : {}),
|
|
500
|
-
...(uiRegistry?.transforms ? { transforms: uiRegistry.transforms } : {}),
|
|
501
|
-
...(uiRegistry?.strings ? { strings: uiRegistry.strings } : {}),
|
|
502
|
-
};
|
|
503
|
-
return { ...defaults, ...registryPortion, ...sceneUIServices };
|
|
504
|
-
};
|
|
505
|
-
|
|
506
522
|
const loadFromData = async (data: SceneFile): Promise<SceneInstance> => {
|
|
507
523
|
const inst = await loadSceneFromData(data, {
|
|
508
524
|
scene,
|
|
@@ -510,7 +526,6 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
510
526
|
rapier: ctx.rapier,
|
|
511
527
|
physics: ctx.physics,
|
|
512
528
|
audioListener: ctx.audio.listener,
|
|
513
|
-
animGraphs: ctx.animGraphs,
|
|
514
529
|
...(componentRegistry ? { componentRegistry } : {}),
|
|
515
530
|
componentManager: ctx.components,
|
|
516
531
|
particleRenderer: ctx.particles.batchedRenderer,
|
|
@@ -526,23 +541,24 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
526
541
|
if (headless) throw new Error('VgaiSceneGameAdapter: setup-based games require a GPU host');
|
|
527
542
|
currentCleanup = await setup(ctx, editorPreview);
|
|
528
543
|
} else if (scenePath) {
|
|
544
|
+
// #139: captured BEFORE load so adoptSceneCamera can tell whether a
|
|
545
|
+
// component (e.g. SceneCamera.init) already framed the camera.
|
|
546
|
+
const preLoadPose = captureCameraPose(camera);
|
|
529
547
|
const inst = await loadScene(scenePath, {
|
|
530
548
|
scene,
|
|
531
549
|
rapierWorld: ctx.rapierWorld,
|
|
532
550
|
rapier: ctx.rapier,
|
|
533
551
|
physics: ctx.physics,
|
|
534
552
|
audioListener: ctx.audio.listener,
|
|
535
|
-
animGraphs: ctx.animGraphs,
|
|
536
553
|
...(componentRegistry ? { componentRegistry } : {}),
|
|
537
554
|
componentManager: ctx.components,
|
|
538
555
|
particleRenderer: ctx.particles.batchedRenderer,
|
|
539
556
|
...(headless ? {} : { renderer }),
|
|
540
557
|
});
|
|
541
|
-
applyEditorCamera(editorPreview, camera) || adoptSceneCamera(inst, camera);
|
|
558
|
+
applyEditorCamera(editorPreview, camera) || adoptSceneCamera(inst, camera, preLoadPose);
|
|
542
559
|
applySceneRendering(inst);
|
|
543
560
|
applySceneNavigation(inst);
|
|
544
561
|
ctx.systems.add('postPhysics', (dt) => inst.update(dt));
|
|
545
|
-
sceneInstance = inst;
|
|
546
562
|
// A pre-baked navmesh (sibling .navmesh file — see scene-loader.ts) holds
|
|
547
563
|
// live recast-navigation WASM handles that leak unless destroyed
|
|
548
564
|
// explicitly; dispose it alongside the scene instance on teardown.
|
|
@@ -553,9 +569,9 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
553
569
|
},
|
|
554
570
|
};
|
|
555
571
|
} else if (sceneData) {
|
|
572
|
+
const preLoadPose = captureCameraPose(camera);
|
|
556
573
|
const inst = await loadFromData(sceneData);
|
|
557
|
-
applyEditorCamera(editorPreview, camera) || adoptSceneCamera(inst, camera);
|
|
558
|
-
sceneInstance = inst;
|
|
574
|
+
applyEditorCamera(editorPreview, camera) || adoptSceneCamera(inst, camera, preLoadPose);
|
|
559
575
|
currentCleanup = {
|
|
560
576
|
dispose: () => {
|
|
561
577
|
inst.navMesh?.dispose(scene);
|
|
@@ -564,19 +580,6 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
564
580
|
};
|
|
565
581
|
}
|
|
566
582
|
|
|
567
|
-
// Render scene-graph UI (the `ui` tree) into the overlay via the injected React
|
|
568
|
-
// renderer (B1). Sourced from the loaded `SceneInstance` — the single source of
|
|
569
|
-
// truth for BOTH the scenePath and sceneData branches (R8 item 7 / B: previously
|
|
570
|
-
// only sceneData's raw `SceneFile.ui` was read via a `resolvedSceneData` special-
|
|
571
|
-
// case, so a scenePath mount's `ui` tree was silently dropped even though
|
|
572
|
-
// `loadScene` fully parses it). A `setup`-based mount has no `sceneInstance` at
|
|
573
|
-
// all — its game owns any UI mounting itself. No-op if no UI or no renderer
|
|
574
|
-
// registered. Engine stays React-free.
|
|
575
|
-
const sceneUI = sceneInstance?.ui;
|
|
576
|
-
if (sceneUI && sceneInstance && !headless) {
|
|
577
|
-
sceneUIHandle = renderSceneUI(uiContainer, sceneUI, buildSceneUIServices(sceneInstance));
|
|
578
|
-
}
|
|
579
|
-
|
|
580
583
|
// Idempotency guard for disposeGame — see below. Declared here (not inside
|
|
581
584
|
// disposeGame) so hotReload can also refuse to run on an already-disposed
|
|
582
585
|
// adapter.
|
|
@@ -663,14 +666,6 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
663
666
|
});
|
|
664
667
|
safeStep('clearAssetCaches', () => clearAssetCaches());
|
|
665
668
|
safeStep('debugDraw.clear', () => debugDraw.clear());
|
|
666
|
-
safeStep('animGraphs.clear', () => animGraphs.clear());
|
|
667
|
-
safeStep('sceneUIHandle.dispose', () => {
|
|
668
|
-
sceneUIHandle?.dispose();
|
|
669
|
-
sceneUIHandle = null;
|
|
670
|
-
});
|
|
671
|
-
safeStep('uiContainer reset', () => {
|
|
672
|
-
uiContainer.innerHTML = '';
|
|
673
|
-
});
|
|
674
669
|
|
|
675
670
|
// R8 item 7 (C) — disposeGame (Stop) used to skip both of these, unlike
|
|
676
671
|
// hotReload (warm restart), which already ran them: a game lifecycle
|
|
@@ -679,7 +674,7 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
679
674
|
// systemAdapters kind (e.g. a `setup`-registered 'networking') survived
|
|
680
675
|
// a Stop and would be visible on a STALE `mounted.systems` even though
|
|
681
676
|
// the game that owned it is gone. Same two steps, same relative
|
|
682
|
-
// ordering as hotReload (
|
|
677
|
+
// ordering as hotReload (before the
|
|
683
678
|
// engine-owned audio/input teardown below, which hotReload doesn't
|
|
684
679
|
// touch at all since it warm-restarts rather than tearing engine
|
|
685
680
|
// subsystems down).
|
|
@@ -689,6 +684,12 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
689
684
|
if (!engineAdapterKinds.has(kind)) delete systemAdapters[kind as keyof SystemAdapters];
|
|
690
685
|
}
|
|
691
686
|
});
|
|
687
|
+
// Defect 2 fix: a full Stop means the whole GAME is going away (every
|
|
688
|
+
// world it mounted, plus any react-door registrations from its HUDs)
|
|
689
|
+
// — unlike hotReload below (a SINGLE mount warm-restarting), so this is
|
|
690
|
+
// the one call site that legitimately wants strip()'s global, no-id
|
|
691
|
+
// form rather than scoping to `this.id`.
|
|
692
|
+
safeStep('debugRegistry.strip', () => debugRegistry.strip());
|
|
692
693
|
|
|
693
694
|
if (!headless) {
|
|
694
695
|
safeStep('audio teardown', () => {
|
|
@@ -699,6 +700,7 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
699
700
|
});
|
|
700
701
|
}
|
|
701
702
|
safeStep('input.dispose', () => input.dispose());
|
|
703
|
+
safeStep('gpuTimer.dispose', () => gpuTimer?.dispose());
|
|
702
704
|
};
|
|
703
705
|
|
|
704
706
|
const hotReload = async (
|
|
@@ -764,21 +766,6 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
764
766
|
scene.environment = null;
|
|
765
767
|
});
|
|
766
768
|
safeStep('debugDraw.clear', () => debugDraw.clear());
|
|
767
|
-
safeStep('animGraphs.clear', () => animGraphs.clear());
|
|
768
|
-
safeStep('sceneUIHandle.dispose', () => {
|
|
769
|
-
sceneUIHandle?.dispose();
|
|
770
|
-
sceneUIHandle = null;
|
|
771
|
-
});
|
|
772
|
-
// Strip game-registered scene-UI services back to none before the
|
|
773
|
-
// incoming game's setup/init runs (D7) — same contract as the
|
|
774
|
-
// systemAdapters strip below: a disposed game's onEvent/data/
|
|
775
|
-
// worldProjector must never leak onto the next mount.
|
|
776
|
-
safeStep('reset sceneUIServices', () => {
|
|
777
|
-
sceneUIServices = {};
|
|
778
|
-
});
|
|
779
|
-
safeStep('uiContainer reset', () => {
|
|
780
|
-
uiContainer.innerHTML = '';
|
|
781
|
-
});
|
|
782
769
|
|
|
783
770
|
// Bulk-remove every system the outgoing game registered (this restart's
|
|
784
771
|
// predecessor) so warm restarts never accumulate duplicate systems —
|
|
@@ -798,6 +785,16 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
798
785
|
if (!engineAdapterKinds.has(kind)) delete systemAdapters[kind as keyof SystemAdapters];
|
|
799
786
|
}
|
|
800
787
|
});
|
|
788
|
+
// Hot-reload re-seed (spec §3.1): the incoming game's setup (below)
|
|
789
|
+
// re-registers its providers/commands under the SAME worldId — strip()
|
|
790
|
+
// clears the outgoing game's registrations first so that re-seed is
|
|
791
|
+
// silent (no stale duplicate-name warning) rather than "replacing" a
|
|
792
|
+
// dead game's entries. Defect 2 fix: scoped to THIS mount's worldId
|
|
793
|
+
// (`this.id`) — a blind global strip() here wiped every OTHER live
|
|
794
|
+
// world's registrations on every warm restart of just this one, plus
|
|
795
|
+
// every react-door registration (whose `useEffect` cleanup never
|
|
796
|
+
// re-fires to restore them after someone else's strip).
|
|
797
|
+
safeStep('debugRegistry.strip', () => debugRegistry.strip(this.id));
|
|
801
798
|
|
|
802
799
|
currentCleanup = await newSetup(ctx, newEditorPreview);
|
|
803
800
|
};
|
|
@@ -813,8 +810,10 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
813
810
|
postFrame();
|
|
814
811
|
},
|
|
815
812
|
resize: (w: number, h: number) => {
|
|
816
|
-
|
|
817
|
-
|
|
813
|
+
const safeWidth = Math.max(1, w);
|
|
814
|
+
const safeHeight = Math.max(1, h);
|
|
815
|
+
composer.setSize(safeWidth, safeHeight);
|
|
816
|
+
camera.aspect = safeWidth / safeHeight;
|
|
818
817
|
camera.updateProjectionMatrix();
|
|
819
818
|
},
|
|
820
819
|
dispose: disposeGame,
|
|
@@ -842,17 +841,68 @@ function applyEditorCamera(
|
|
|
842
841
|
return true;
|
|
843
842
|
}
|
|
844
843
|
|
|
844
|
+
/** The pose `adoptSceneCamera` compares against to detect that a component
|
|
845
|
+
* already framed the runtime camera during scene load (#139). Capture with
|
|
846
|
+
* `captureCameraPose(camera)` immediately BEFORE `loadScene`. */
|
|
847
|
+
export interface CameraPose {
|
|
848
|
+
position: THREE.Vector3;
|
|
849
|
+
quaternion: THREE.Quaternion;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
export function captureCameraPose(camera: THREE.PerspectiveCamera): CameraPose {
|
|
853
|
+
return { position: camera.position.clone(), quaternion: camera.quaternion.clone() };
|
|
854
|
+
}
|
|
855
|
+
|
|
845
856
|
export function adoptSceneCamera(
|
|
846
857
|
sceneInstance: SceneInstance,
|
|
847
858
|
runtimeCamera: THREE.PerspectiveCamera,
|
|
859
|
+
preLoadPose?: CameraPose,
|
|
848
860
|
): boolean {
|
|
849
861
|
const sceneCam = sceneInstance.cameras[0];
|
|
850
862
|
if (!sceneCam) return false;
|
|
863
|
+
// #133 (blind run #4): `camera.type: 'orthographic'` builds a real
|
|
864
|
+
// OrthographicCamera in the scene graph (`createCamera`), but the RUNTIME
|
|
865
|
+
// renders through a hardcoded PerspectiveCamera — the authored projection
|
|
866
|
+
// silently never reached the screen. Until ortho support is ranked
|
|
867
|
+
// (implement-vs-reject, owner call on #133), say so loudly instead of
|
|
868
|
+
// letting a coplanar game render in unexplained perspective.
|
|
869
|
+
if ((sceneCam as THREE.Camera & { isOrthographicCamera?: boolean }).isOrthographicCamera) {
|
|
870
|
+
console.warn(
|
|
871
|
+
"adoptSceneCamera (#133): this scene's camera is ORTHOGRAPHIC, but the runtime camera is " +
|
|
872
|
+
'perspective-only — the orthographic projection is NOT applied (only the pose is). ' +
|
|
873
|
+
'Use type: "perspective" (a long-lens fake: far position + small fov) until #133 lands.',
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
// #139 (blind run #5): if a GameComponent already positioned/oriented the
|
|
877
|
+
// runtime camera during scene load (`SceneCamera.init`'s
|
|
878
|
+
// `ctx.camera.position.set` + `lookAt` is the blessed shape), the camera
|
|
879
|
+
// ENTITY's transform must not stomp it — an unauthored `transform.rotation`
|
|
880
|
+
// is identity ("stare down −Z"), which silently blanked an entire correct
|
|
881
|
+
// game while every state-based probe spec passed. Components win the pose;
|
|
882
|
+
// the entity still contributes the LENS (fov/near/far below), which
|
|
883
|
+
// `SceneCamera` never sets.
|
|
884
|
+
const componentFramedCamera =
|
|
885
|
+
preLoadPose !== undefined &&
|
|
886
|
+
(!runtimeCamera.position.equals(preLoadPose.position) ||
|
|
887
|
+
!runtimeCamera.quaternion.equals(preLoadPose.quaternion));
|
|
851
888
|
const parent = sceneCam.parent;
|
|
852
|
-
if (parent) {
|
|
889
|
+
if (parent && !componentFramedCamera) {
|
|
853
890
|
parent.updateMatrixWorld(true);
|
|
854
891
|
runtimeCamera.position.copy(parent.getWorldPosition(new THREE.Vector3()));
|
|
855
|
-
|
|
892
|
+
const worldQuat = parent.getWorldQuaternion(new THREE.Quaternion());
|
|
893
|
+
runtimeCamera.quaternion.copy(worldQuat);
|
|
894
|
+
// Identity orientation on an adopted scene camera is almost never intent
|
|
895
|
+
// (it means "look at the horizon down −Z", not "look at my scene") — the
|
|
896
|
+
// exact silent-blank-viewport footgun above. Adopt it anyway (authored
|
|
897
|
+
// data is truth) but say so loudly, with the two blessed remedies.
|
|
898
|
+
if (Math.abs(1 - Math.abs(worldQuat.w)) < 1e-6) {
|
|
899
|
+
console.warn(
|
|
900
|
+
'adoptSceneCamera (#139): the scene camera entity has no rotation — the runtime camera ' +
|
|
901
|
+
'will stare horizontally down −Z and may show nothing. Either attach the SceneCamera ' +
|
|
902
|
+
'component (position + lookAt, no quaternion math) or author transform.rotation on ' +
|
|
903
|
+
'the camera entity.',
|
|
904
|
+
);
|
|
905
|
+
}
|
|
856
906
|
}
|
|
857
907
|
if (sceneCam instanceof THREE.PerspectiveCamera) {
|
|
858
908
|
runtimeCamera.fov = sceneCam.fov;
|