@vgai/engine 0.4.1 → 0.5.0-canary.20260719.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 +48 -15
- package/package.json +11 -25
- package/schemas/engine-capabilities.json +10 -10
- package/schemas/entity2d.schema.json +468 -0
- package/schemas/mat.schema.json +2 -33
- package/schemas/prefab.schema.json +16 -172
- package/schemas/scn2d.schema.json +42 -23
- package/schemas/vgai-game.schema.json +34 -0
- package/schemas/vscn.schema.json +16 -172
- package/src/adapter/authoring.ts +152 -2
- package/src/adapter/colyseus-networking-adapter.ts +35 -1
- package/src/adapter/first-party-systems.ts +7 -1
- package/src/adapter/game-adapter.ts +13 -0
- package/src/adapter/index.ts +25 -0
- package/src/adapter/rapier-physics-adapter.ts +55 -2
- package/src/adapter/system-adapter.ts +249 -2
- package/src/adapter/vgai-scene-game-adapter.ts +149 -25
- package/src/ai/navigation.ts +28 -0
- package/src/animation/clip-map.ts +1 -8
- package/src/animation/theatre-director.ts +50 -0
- package/src/animation/xstate-animation-binding.ts +6 -0
- package/src/audio/audio-introspection.ts +290 -0
- package/src/audio/tone-context.ts +46 -0
- package/src/dev/chrome-trace.ts +153 -0
- package/src/dev/performance-profiler.ts +93 -6
- package/src/dev/render-debug-adapter.ts +199 -0
- package/src/dev/render-memory.ts +243 -0
- package/src/dev/webgl-frame-capture.ts +424 -0
- package/src/ecs/component-manager.ts +43 -10
- package/src/ecs/game-component.ts +39 -10
- package/src/input/input-manager.ts +24 -19
- package/src/input/input-types.ts +1 -1
- package/src/loader.ts +7 -0
- package/src/manifest/load.ts +14 -0
- package/src/manifest/schema.ts +65 -0
- package/src/react/game-state.tsx +1 -1
- package/src/render/render-batch-system.ts +26 -12
- package/src/render/spark-renderer-lifecycle.ts +64 -0
- package/src/runtime/create-runtime.ts +33 -5
- package/src/runtime/debug-bridge.ts +5 -5
- package/src/runtime/game.ts +102 -20
- package/src/runtime/mount-manifest.ts +1 -1
- package/src/runtime/render-control.ts +121 -0
- package/src/runtime/types.ts +1 -1
- package/src/scene/asset-loaders.ts +77 -3
- package/src/scene/instance-mesh.ts +25 -0
- package/src/scene/material-factory.ts +4 -15
- package/src/scene/mesh-shadow.ts +18 -0
- package/src/scene/particles-factory.ts +59 -0
- package/src/scene/scene-loader.ts +41 -16
- package/src/scene/schema/instances.ts +1 -2
- package/src/scene/schema/material.ts +83 -94
- package/src/scene/schema/mesh.ts +76 -90
- package/src/scene/schema/scene-file.ts +1 -2
- package/src/scene/user-data.ts +30 -14
- package/src/setup/setup-renderer.ts +6 -1
- package/src/world2d/asset-paths2d.ts +44 -0
- package/src/world2d/collision-2d.ts +7 -14
- package/src/world2d/entity2d-asset.ts +22 -0
- package/src/world2d/index.ts +27 -2
- package/src/world2d/physics2d-transform.ts +173 -0
- package/src/world2d/physics2d-units.ts +10 -0
- package/src/world2d/pixi-game-adapter.ts +148 -36
- package/src/world2d/scene2d-identity.ts +49 -0
- package/src/world2d/scene2d-loader.ts +243 -119
- package/src/world2d/schema/entity2d.ts +51 -33
- package/src/world2d/schema/physics2d.ts +14 -3
- package/src/world2d/schema/sprite.ts +32 -4
- package/src/world2d/schema/tilemap.ts +26 -9
- package/src/world2d/transform-writer-2d.ts +29 -11
- package/src/world2d/types.ts +21 -8
- package/src/world3d-react/behavior.tsx +138 -0
- package/src/world3d-react/engine-bridge.ts +48 -0
- package/src/world3d-react/index.ts +44 -0
- package/src/world3d-react/r3f-adapter.tsx +303 -0
- package/src/world3d-react/world-context.ts +294 -0
- package/vendor/realism-effects/LICENSE.md +21 -0
- package/vendor/realism-effects/UPSTREAM.md +19 -0
- package/vendor/realism-effects/dist/index.cjs +3447 -0
- package/vendor/realism-effects/dist/index.d.ts +59 -0
- package/vendor/realism-effects/dist/index.js +3434 -0
- package/vendor/realism-effects/package.json +23 -0
- package/src/character/cloth-sim.ts +0 -533
- package/src/character/spring-chain.ts +0 -307
- package/src/humanoid/body.ts +0 -663
- package/src/humanoid/clips.ts +0 -149
- package/src/humanoid/compose.ts +0 -209
- package/src/humanoid/generate.ts +0 -189
- package/src/humanoid/index.ts +0 -36
- package/src/humanoid/schema.ts +0 -108
- package/src/humanoid/skeleton.ts +0 -345
- package/src/react/humanoid-bake.document.tsx +0 -337
- package/src/scene/geometries/index.ts +0 -7
- package/src/scene/geometries/terrain.ts +0 -42
- package/src/scene/geometry-registry.ts +0 -42
- package/src/scene/instance-registry.ts +0 -84
- package/src/scene/instancers/grid.ts +0 -38
- package/src/scene/instancers/index.ts +0 -7
- package/src/scene/material-registry.ts +0 -73
- package/src/scene/materials/index.ts +0 -7
- package/src/scene/materials/water.ts +0 -56
- package/src/world2d/components-2d.ts +0 -86
- package/tools/humanoid-bake.tool.ts +0 -274
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import RAPIER from '@dimforge/rapier3d-compat';
|
|
16
|
+
import type { SparkRenderer } from '@sparkjsdev/spark';
|
|
16
17
|
import type { EffectComposer } from 'postprocessing';
|
|
17
18
|
import * as THREE from 'three';
|
|
18
19
|
import {
|
|
@@ -22,6 +23,14 @@ import {
|
|
|
22
23
|
} from '../core/seeded-random';
|
|
23
24
|
import { createSystemRunner } from '../core/system-runner';
|
|
24
25
|
import { createDebugDraw } from '../dev/debug-draw';
|
|
26
|
+
import { log } from '../dev/logger';
|
|
27
|
+
import {
|
|
28
|
+
createRenderDebugAdapter,
|
|
29
|
+
frameCaptureContextFor,
|
|
30
|
+
type RenderDebugWiring,
|
|
31
|
+
} from '../dev/render-debug-adapter';
|
|
32
|
+
import { collectRenderMemory } from '../dev/render-memory';
|
|
33
|
+
import { createWebGLFrameCapture } from '../dev/webgl-frame-capture';
|
|
25
34
|
import { createWebGLGpuTimer } from '../dev/webgl-gpu-timer';
|
|
26
35
|
import { createComponentManager } from '../ecs/component-manager';
|
|
27
36
|
import { InputManager } from '../input/input-manager';
|
|
@@ -32,6 +41,12 @@ import { createTransformWriter } from '../physics/transform-writer';
|
|
|
32
41
|
import { createTriggerDispatch } from '../physics/trigger-dispatch';
|
|
33
42
|
import { RenderBatchSystem } from '../render/render-batch-system';
|
|
34
43
|
import { type RenderScope, resolveRenderSettings } from '../render/render-settings';
|
|
44
|
+
import {
|
|
45
|
+
disposeSparkRendererWhenIdle,
|
|
46
|
+
SPARK_DISCOVERY_INTERVAL_MS,
|
|
47
|
+
sceneHasGaussianSplat,
|
|
48
|
+
shouldDiscoverGaussianSplat,
|
|
49
|
+
} from '../render/spark-renderer-lifecycle';
|
|
35
50
|
import { type ViewportShadingMode, ViewportShadingRenderer } from '../render/viewport-shading';
|
|
36
51
|
import {
|
|
37
52
|
createDebugRegistry,
|
|
@@ -41,7 +56,6 @@ import {
|
|
|
41
56
|
import type { WorldFrameHooks } from '../runtime/game';
|
|
42
57
|
import type { EditorPreview, GameCleanup, GameContext, GameSetupFn } from '../runtime/types';
|
|
43
58
|
import type { ComponentRegistry } from '../scene/component-registry';
|
|
44
|
-
import { tickCustomMaterials } from '../scene/material-registry';
|
|
45
59
|
import {
|
|
46
60
|
clearAssetCaches,
|
|
47
61
|
loadScene,
|
|
@@ -206,8 +220,12 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
206
220
|
const writeTransforms = createTransformWriter(physicsRegistry, scene);
|
|
207
221
|
|
|
208
222
|
// --- Input (InputManager attaches window listeners; stub it headlessly) ---
|
|
209
|
-
const
|
|
210
|
-
|
|
223
|
+
const ownsInput = !host.game;
|
|
224
|
+
const input = host.game?.input ?? (headless ? headlessInput() : new InputManager());
|
|
225
|
+
if (!headless) {
|
|
226
|
+
if (host.game) await host.game.loadInputMap(inputMapPath);
|
|
227
|
+
else await input.loadMap(inputMapPath);
|
|
228
|
+
}
|
|
211
229
|
|
|
212
230
|
// --- Collision system ---
|
|
213
231
|
const collisions = createCollisionSystem(physics.rapierWorld, physics.eventQueue);
|
|
@@ -230,6 +248,22 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
230
248
|
!headless && rendererContext && typeof rendererContext.getExtension === 'function'
|
|
231
249
|
? createWebGLGpuTimer(rendererContext)
|
|
232
250
|
: null;
|
|
251
|
+
// W4b (F11 frame debugger): first-party WebGL2 frame capture, constructed
|
|
252
|
+
// under the SAME real-context guard as gpuTimer — a headless/stub mount
|
|
253
|
+
// (`frameCaptureContextFor` returns null) never builds it and never
|
|
254
|
+
// registers `renderDebug`, so the capability is honestly absent there. The
|
|
255
|
+
// capture only patches the context WHILE a `captureFrame()` is armed (see
|
|
256
|
+
// beginPass/endPass in the render system below); every non-capture frame
|
|
257
|
+
// is zero-overhead.
|
|
258
|
+
const frameCaptureContext = frameCaptureContextFor(headless, rendererContext);
|
|
259
|
+
const frameCapture = frameCaptureContext ? createWebGLFrameCapture(frameCaptureContext) : null;
|
|
260
|
+
const renderDebugWiring: RenderDebugWiring | null = frameCapture
|
|
261
|
+
? createRenderDebugAdapter({
|
|
262
|
+
capture: frameCapture,
|
|
263
|
+
scene,
|
|
264
|
+
memory: () => collectRenderMemory(scene, renderer.info),
|
|
265
|
+
})
|
|
266
|
+
: null;
|
|
233
267
|
const viewportShading = new ViewportShadingRenderer();
|
|
234
268
|
let viewportShadingMode: ViewportShadingMode = 'solid';
|
|
235
269
|
|
|
@@ -246,9 +280,7 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
246
280
|
// key off, so `InputManager.poll()` falls back to its own internal
|
|
247
281
|
// self-incrementing counter instead (unchanged pre-D15 behavior) rather
|
|
248
282
|
// than being pinned to a constant `0` forever.
|
|
249
|
-
systems.add('input', () => input.poll(
|
|
250
|
-
name: 'input.poll',
|
|
251
|
-
});
|
|
283
|
+
if (ownsInput) systems.add('input', () => input.poll(), { name: 'input.poll' });
|
|
252
284
|
systems.add('physics', () => physics.rapierWorld.step(physics.eventQueue), {
|
|
253
285
|
name: 'physics.step',
|
|
254
286
|
});
|
|
@@ -264,8 +296,28 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
264
296
|
'preRender',
|
|
265
297
|
(dt) => {
|
|
266
298
|
particles.batchedRenderer.update(dt);
|
|
267
|
-
tickCustomMaterials(scene, dt);
|
|
268
299
|
updateSceneLODs(scene, camera);
|
|
300
|
+
sparkDiscoveryElapsedMs += dt * 1_000;
|
|
301
|
+
const shouldDiscover = shouldDiscoverGaussianSplat(0, sparkDiscoveryElapsedMs);
|
|
302
|
+
if (!sparkRenderer && !sparkRendererLoading && !sparkRendererFailed && shouldDiscover) {
|
|
303
|
+
sparkDiscoveryElapsedMs = 0;
|
|
304
|
+
if (!sceneHasGaussianSplat(scene)) return;
|
|
305
|
+
sparkRendererLoading = true;
|
|
306
|
+
void import('@sparkjsdev/spark')
|
|
307
|
+
.then(({ SparkRenderer }) => {
|
|
308
|
+
if (disposed || sparkRenderer) return;
|
|
309
|
+
sparkRenderer = new SparkRenderer({ renderer, enableLod: false });
|
|
310
|
+
sparkRenderer.traverse((node) => setUserData(node, 'engineInternal', true));
|
|
311
|
+
})
|
|
312
|
+
.catch((error: unknown) => {
|
|
313
|
+
sparkRendererFailed = true;
|
|
314
|
+
// biome-ignore lint/suspicious/noConsole: A missing renderer chunk must fail once and loudly, not retry every frame.
|
|
315
|
+
console.error('Failed to install SparkRenderer for a live Gaussian splat.', error);
|
|
316
|
+
})
|
|
317
|
+
.finally(() => {
|
|
318
|
+
sparkRendererLoading = false;
|
|
319
|
+
});
|
|
320
|
+
}
|
|
269
321
|
},
|
|
270
322
|
{ name: 'materials-and-particles' },
|
|
271
323
|
);
|
|
@@ -274,6 +326,13 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
274
326
|
'render',
|
|
275
327
|
(dt) => {
|
|
276
328
|
if (host.game?.profiler.enabled) gpuTimer?.begin();
|
|
329
|
+
if (sparkRenderer) scene.add(sparkRenderer);
|
|
330
|
+
// W4b: arm-gated frame capture wraps the real draw calls. beginPass
|
|
331
|
+
// is a no-op unless a captureFrame() armed it; afterRender delivers
|
|
332
|
+
// the assembled capture to a pending captureFrame() and restores the
|
|
333
|
+
// patched context — in the SAME finally as gpuTimer.end(). Spark is
|
|
334
|
+
// added before this so its draws fall inside the captured frame.
|
|
335
|
+
renderDebugWiring?.beforeRender();
|
|
277
336
|
try {
|
|
278
337
|
viewportShading.render(
|
|
279
338
|
scene,
|
|
@@ -285,6 +344,8 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
285
344
|
(mesh) => camera.layers.test(mesh.layers),
|
|
286
345
|
);
|
|
287
346
|
} finally {
|
|
347
|
+
renderDebugWiring?.afterRender();
|
|
348
|
+
sparkRenderer?.removeFromParent();
|
|
288
349
|
gpuTimer?.end();
|
|
289
350
|
}
|
|
290
351
|
if (physics.debugEnabled) updatePhysicsDebug(physics.rapierWorld, physics.debugMesh);
|
|
@@ -303,7 +364,7 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
303
364
|
if (!headless) window.addEventListener('keydown', onDebugToggle);
|
|
304
365
|
|
|
305
366
|
const postFrame = () => {
|
|
306
|
-
input.endFrame();
|
|
367
|
+
if (ownsInput) input.endFrame();
|
|
307
368
|
if (host.game?.profiler.enabled) {
|
|
308
369
|
host.game.profiler.reportRender({
|
|
309
370
|
gpuMs: gpuTimer?.poll() ?? null,
|
|
@@ -377,8 +438,8 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
377
438
|
// resolves, not here (no NavMeshManager exists yet at this point in the
|
|
378
439
|
// mount). `networking` remains the one game-owned capability with no
|
|
379
440
|
// first-party implementer; it simply starts absent here.
|
|
380
|
-
systemAdapters.physics = createRapierPhysicsAdapter(physicsRegistry, physics
|
|
381
|
-
systemAdapters.input = createInputManagerAdapter(input);
|
|
441
|
+
systemAdapters.physics = createRapierPhysicsAdapter(physicsRegistry, physics);
|
|
442
|
+
if (ownsInput) systemAdapters.input = createInputManagerAdapter(input);
|
|
382
443
|
systemAdapters.assets = createVgaiAssetAdapter();
|
|
383
444
|
// D10/T7.6: the audio seam `Game.play.pause()` silences on pause. Works
|
|
384
445
|
// against the headless stand-in too (`headlessAudio()`'s plain
|
|
@@ -396,6 +457,11 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
396
457
|
(host.game ? getDebugRegistry(host.game) : null) ??
|
|
397
458
|
createDebugRegistry({ getTick: () => 0, getSimT: () => 0 });
|
|
398
459
|
systemAdapters.debug = debugRegistry.adapter;
|
|
460
|
+
// W4b: the render-debug capability is engine-owned and first-party, seeded
|
|
461
|
+
// here (like physics/input/assets) BEFORE the engineAdapterKinds snapshot
|
|
462
|
+
// below so a warm restart / stop never mistakes it for game content. Only
|
|
463
|
+
// present when a real WebGL2 context backed the mount (renderDebugWiring).
|
|
464
|
+
if (renderDebugWiring) systemAdapters.renderDebug = renderDebugWiring.adapter;
|
|
399
465
|
ctx.debug = debugRegistry.forWorld(this.id);
|
|
400
466
|
// T1.2: the InputManager is a first-party per-world handle the registry
|
|
401
467
|
// has no other way to reach — wired as a lazy supplier (not a direct
|
|
@@ -449,7 +515,7 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
449
515
|
// to THIS world's debug registry, same seed spot/precedent as
|
|
450
516
|
// `setInputActionsSource`/`setVirtualInputTarget` above (typeof-guarded:
|
|
451
517
|
// a headless `InputManager` stand-in may have no `setDebugEmit`).
|
|
452
|
-
if (typeof input.setDebugEmit === 'function') {
|
|
518
|
+
if (!host.game && typeof input.setDebugEmit === 'function') {
|
|
453
519
|
input.setDebugEmit((event, detail) => ctx.debug?.emit(event, detail));
|
|
454
520
|
}
|
|
455
521
|
// D15 (T-D15.1) — same "ONE per Game root, shared across every world"
|
|
@@ -490,11 +556,18 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
490
556
|
}
|
|
491
557
|
let currentCleanup: GameCleanup | null = null;
|
|
492
558
|
let batchSystem: RenderBatchSystem | null = null;
|
|
559
|
+
let sparkRenderer: SparkRenderer | null = null;
|
|
560
|
+
let sparkRendererLoading = false;
|
|
561
|
+
let sparkRendererFailed = false;
|
|
562
|
+
// Discovery is render-loop scheduling, so advance it from the loop's `dt`
|
|
563
|
+
// instead of introducing a second wall-clock frontier into engine code.
|
|
564
|
+
// Start at the threshold so the first rendered frame discovers eager splats.
|
|
565
|
+
let sparkDiscoveryElapsedMs = SPARK_DISCOVERY_INTERVAL_MS;
|
|
493
566
|
|
|
494
567
|
// Apply scene-level render settings + transparent auto-batching after a scene
|
|
495
568
|
// loads (GPU host only). Gated on the resolved `environment.rendering` cascade
|
|
496
569
|
// (defaults preserve prior behavior when a scene omits `rendering`).
|
|
497
|
-
const applySceneRendering = (inst: SceneInstance): void => {
|
|
570
|
+
const applySceneRendering = async (inst: SceneInstance): Promise<void> => {
|
|
498
571
|
if (headless) return;
|
|
499
572
|
const scope = inst.environment?.rendering as RenderScope | undefined;
|
|
500
573
|
const settings = resolveRenderSettings(scope);
|
|
@@ -509,6 +582,11 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
509
582
|
if (import.meta.env?.DEV)
|
|
510
583
|
(window as unknown as { __vgaiBatch?: unknown }).__vgaiBatch = batchSystem;
|
|
511
584
|
}
|
|
585
|
+
if (inst.splatCount > 0 && !sparkRenderer) {
|
|
586
|
+
const { SparkRenderer } = await import('@sparkjsdev/spark');
|
|
587
|
+
sparkRenderer = new SparkRenderer({ renderer, enableLod: false });
|
|
588
|
+
sparkRenderer.traverse((node) => setUserData(node, 'engineInternal', true));
|
|
589
|
+
}
|
|
512
590
|
};
|
|
513
591
|
|
|
514
592
|
// Wire the (first-party) navigation system adapter to whichever
|
|
@@ -536,7 +614,7 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
536
614
|
particleRenderer: ctx.particles.batchedRenderer,
|
|
537
615
|
...(headless ? {} : { renderer }),
|
|
538
616
|
});
|
|
539
|
-
applySceneRendering(inst);
|
|
617
|
+
await applySceneRendering(inst);
|
|
540
618
|
applySceneNavigation(inst);
|
|
541
619
|
ctx.systems.add('postPhysics', (dt) => inst.update(dt));
|
|
542
620
|
return inst;
|
|
@@ -546,8 +624,12 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
546
624
|
if (headless) throw new Error('VgaiSceneGameAdapter: setup-based games require a GPU host');
|
|
547
625
|
currentCleanup = await setup(ctx, editorPreview);
|
|
548
626
|
} else if (scenePath) {
|
|
549
|
-
//
|
|
550
|
-
//
|
|
627
|
+
// The editor viewport is only the fallback/initial play viewpoint. Apply
|
|
628
|
+
// it BEFORE capturing the load baseline so authored scene cameras and
|
|
629
|
+
// camera GameComponents can override it during/after scene load.
|
|
630
|
+
applyEditorCamera(editorPreview, camera);
|
|
631
|
+
// #139: captured immediately BEFORE load so adoptSceneCamera can tell
|
|
632
|
+
// whether a component (e.g. SceneCamera.init) framed the camera.
|
|
551
633
|
const preLoadPose = captureCameraPose(camera);
|
|
552
634
|
const inst = await loadScene(scenePath, {
|
|
553
635
|
scene,
|
|
@@ -560,8 +642,8 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
560
642
|
particleRenderer: ctx.particles.batchedRenderer,
|
|
561
643
|
...(headless ? {} : { renderer }),
|
|
562
644
|
});
|
|
563
|
-
|
|
564
|
-
applySceneRendering(inst);
|
|
645
|
+
adoptSceneCamera(inst, camera, preLoadPose);
|
|
646
|
+
await applySceneRendering(inst);
|
|
565
647
|
applySceneNavigation(inst);
|
|
566
648
|
ctx.systems.add('postPhysics', (dt) => inst.update(dt));
|
|
567
649
|
// A pre-baked navmesh (sibling .navmesh file — see scene-loader.ts) holds
|
|
@@ -574,9 +656,10 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
574
656
|
},
|
|
575
657
|
};
|
|
576
658
|
} else if (sceneData) {
|
|
659
|
+
applyEditorCamera(editorPreview, camera);
|
|
577
660
|
const preLoadPose = captureCameraPose(camera);
|
|
578
661
|
const inst = await loadFromData(sceneData);
|
|
579
|
-
|
|
662
|
+
adoptSceneCamera(inst, camera, preLoadPose);
|
|
580
663
|
currentCleanup = {
|
|
581
664
|
dispose: () => {
|
|
582
665
|
inst.navMesh?.dispose(scene);
|
|
@@ -590,6 +673,30 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
590
673
|
// adapter.
|
|
591
674
|
let disposed = false;
|
|
592
675
|
|
|
676
|
+
// W3c: attach the read-only Audio-debugger introspection capabilities
|
|
677
|
+
// (graph/transport/meters/events) onto the SAME first-party audio adapter
|
|
678
|
+
// seeded above. Dynamically imported so `tone` stays out of the
|
|
679
|
+
// mount-critical chunk (the Colyseus dynamic-import idiom); the editor
|
|
680
|
+
// panel polls, so a few-ms-late attachment is invisible. If the game
|
|
681
|
+
// replaced `systems.audio` with its OWN adapter during setup(), the
|
|
682
|
+
// first-party augmentation is skipped — the game's adapter is the truth
|
|
683
|
+
// the editor sees.
|
|
684
|
+
let disposeAudioIntrospection: (() => void) | null = null;
|
|
685
|
+
const firstPartyAudioAdapter = systemAdapters.audio;
|
|
686
|
+
if (firstPartyAudioAdapter) {
|
|
687
|
+
void import('../audio/audio-introspection').then(
|
|
688
|
+
(m) => {
|
|
689
|
+
if (disposed || systemAdapters.audio !== firstPartyAudioAdapter) return;
|
|
690
|
+
disposeAudioIntrospection = m.attachAudioIntrospection(firstPartyAudioAdapter, audio);
|
|
691
|
+
},
|
|
692
|
+
(err) => {
|
|
693
|
+
log.audio.warn('VgaiSceneGameAdapter: audio introspection unavailable', {
|
|
694
|
+
error: String(err),
|
|
695
|
+
});
|
|
696
|
+
},
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
|
|
593
700
|
// Run one teardown step in isolation: a throwing step (e.g. a
|
|
594
701
|
// GameComponent's dispose() misbehaving, or a WASM free() panicking) must
|
|
595
702
|
// not abort the remaining steps — otherwise a single bad component leaks
|
|
@@ -618,6 +725,12 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
618
725
|
currentCleanup?.dispose();
|
|
619
726
|
currentCleanup = null;
|
|
620
727
|
});
|
|
728
|
+
safeStep('sparkRenderer.dispose', () => {
|
|
729
|
+
if (!sparkRenderer) return;
|
|
730
|
+
sparkRenderer.removeFromParent();
|
|
731
|
+
disposeSparkRendererWhenIdle(sparkRenderer);
|
|
732
|
+
sparkRenderer = null;
|
|
733
|
+
});
|
|
621
734
|
// The scene instance's NavMeshManager (if any) was just disposed above
|
|
622
735
|
// (currentCleanup.dispose calls `inst.navMesh?.dispose(scene)`) — clear
|
|
623
736
|
// the wrapper too, or `mounted.systems.navigation` would keep pointing
|
|
@@ -689,12 +802,20 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
689
802
|
if (!engineAdapterKinds.has(kind)) delete systemAdapters[kind as keyof SystemAdapters];
|
|
690
803
|
}
|
|
691
804
|
});
|
|
692
|
-
//
|
|
693
|
-
//
|
|
694
|
-
//
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
805
|
+
// A game-hosted mount owns only its world-scoped registrations; sibling
|
|
806
|
+
// roots remain alive during root replacement. A bare standalone adapter
|
|
807
|
+
// owns the registry and can clear it globally.
|
|
808
|
+
safeStep('debugRegistry.strip', () =>
|
|
809
|
+
host.game ? debugRegistry.strip(this.id) : debugRegistry.strip(),
|
|
810
|
+
);
|
|
811
|
+
|
|
812
|
+
// W3c: detach introspection BEFORE the audio teardown below — releases
|
|
813
|
+
// Tone transport listeners (the transport singleton outlives this
|
|
814
|
+
// world), the context statechange hook, and any meter taps still alive.
|
|
815
|
+
safeStep('audio introspection dispose', () => {
|
|
816
|
+
disposeAudioIntrospection?.();
|
|
817
|
+
disposeAudioIntrospection = null;
|
|
818
|
+
});
|
|
698
819
|
|
|
699
820
|
if (!headless) {
|
|
700
821
|
safeStep('audio teardown', () => {
|
|
@@ -704,7 +825,10 @@ export class VgaiSceneGameAdapter implements GameAdapter {
|
|
|
704
825
|
composer.dispose();
|
|
705
826
|
});
|
|
706
827
|
}
|
|
707
|
-
safeStep('input.dispose', () => input.dispose());
|
|
828
|
+
if (ownsInput) safeStep('input.dispose', () => input.dispose());
|
|
829
|
+
// W4b: restore any pending capture wrappers and reject a capture in
|
|
830
|
+
// flight (a Stop mid-capture must not leave the context patched).
|
|
831
|
+
safeStep('renderDebug.dispose', () => renderDebugWiring?.dispose());
|
|
708
832
|
safeStep('gpuTimer.dispose', () => gpuTimer?.dispose());
|
|
709
833
|
safeStep('viewportShading.dispose', () => viewportShading.dispose());
|
|
710
834
|
};
|
package/src/ai/navigation.ts
CHANGED
|
@@ -24,6 +24,15 @@ export interface Vector3Like {
|
|
|
24
24
|
z: number;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/** One crowd agent's live state — a plain-data snapshot safe to hand across
|
|
28
|
+
* the adapter seam (no WASM handles), for editor debug draw. */
|
|
29
|
+
export interface CrowdAgentSnapshot {
|
|
30
|
+
position: Vector3Like;
|
|
31
|
+
velocity: Vector3Like;
|
|
32
|
+
radius: number;
|
|
33
|
+
height: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
27
36
|
export interface NavMeshBuildParams {
|
|
28
37
|
cellSize?: number | undefined;
|
|
29
38
|
cellHeight?: number | undefined;
|
|
@@ -130,6 +139,25 @@ export class NavMeshManager {
|
|
|
130
139
|
this.crowd?.update(dt);
|
|
131
140
|
}
|
|
132
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Plain-data snapshots of every active crowd agent (interpolated position,
|
|
144
|
+
* velocity, radius, height) — the introspection feed for the editor's
|
|
145
|
+
* play-mode crowd debug draw. Empty when no crowd exists.
|
|
146
|
+
*/
|
|
147
|
+
getCrowdAgents(): CrowdAgentSnapshot[] {
|
|
148
|
+
if (!this.crowd) return [];
|
|
149
|
+
return this.crowd.getAgents().map((a) => {
|
|
150
|
+
const p = a.interpolatedPosition;
|
|
151
|
+
const v = a.velocity();
|
|
152
|
+
return {
|
|
153
|
+
position: { x: p.x, y: p.y, z: p.z },
|
|
154
|
+
velocity: { x: v.x, y: v.y, z: v.z },
|
|
155
|
+
radius: a.radius,
|
|
156
|
+
height: a.height,
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
133
161
|
/** Get or create the wireframe debug mesh. Add to scene once, call update() after rebuilding. */
|
|
134
162
|
getDebugMesh(scene: Scene): NavMeshHelper | null {
|
|
135
163
|
if (!this.navMesh) return null;
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import type * as THREE from 'three';
|
|
2
2
|
|
|
3
|
-
/** Common aliases used by many free humanoid packs. Scene-authored aliases override these. */
|
|
4
|
-
const DEFAULT_CLIP_ALIASES: Record<string, string> = {
|
|
5
|
-
walking: 'walk',
|
|
6
|
-
running: 'run',
|
|
7
|
-
};
|
|
8
|
-
|
|
9
3
|
/**
|
|
10
4
|
* Build the clip lookup shared by runtime and editor.
|
|
11
5
|
*
|
|
@@ -29,8 +23,7 @@ export function buildClipMap(
|
|
|
29
23
|
if (!clips.has(raw.toLowerCase())) clips.set(raw.toLowerCase(), clip);
|
|
30
24
|
}
|
|
31
25
|
|
|
32
|
-
const
|
|
33
|
-
for (const [source, alias] of Object.entries(mergedAliases)) {
|
|
26
|
+
for (const [source, alias] of Object.entries(aliases)) {
|
|
34
27
|
const clip = canonical.get(source.toLowerCase());
|
|
35
28
|
if (!clip) continue;
|
|
36
29
|
if (!clips.has(alias)) clips.set(alias, clip);
|
|
@@ -256,6 +256,29 @@ export interface CameraOwnershipChangeEvent {
|
|
|
256
256
|
*/
|
|
257
257
|
export type CameraOwnershipHook = (event: CameraOwnershipChangeEvent) => void;
|
|
258
258
|
|
|
259
|
+
/**
|
|
260
|
+
* WF — one detected event-marker transition on the Director's `event` extra
|
|
261
|
+
* track (authored by the editor's Sequencer as HOLD string keyframes on the
|
|
262
|
+
* SAME Director object; see the sequencer's `EVENT_MARKER_PROP`).
|
|
263
|
+
*/
|
|
264
|
+
export interface EventMarkerEvent {
|
|
265
|
+
/** The marker name now active (the hold keyframe's string value). */
|
|
266
|
+
readonly name: string;
|
|
267
|
+
/** The previously active marker name, or `null` before the first marker. */
|
|
268
|
+
readonly previousName: string | null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* WF seam — fired once per DETECTED change of the Director's event-marker
|
|
273
|
+
* value, exactly the `activeCamera` detection semantics: markers are
|
|
274
|
+
* *directorial* state, so the hook reflects whatever value the clock's
|
|
275
|
+
* current time holds, however the clock got there (a seek across several
|
|
276
|
+
* markers fires only the one now active; a seek BACK re-fires the older
|
|
277
|
+
* marker as a change). Gameplay consequences that must distinguish seek from
|
|
278
|
+
* advance belong in a D5 `CueRunner` (`cinematic-cues.ts`), not here.
|
|
279
|
+
*/
|
|
280
|
+
export type EventMarkerHook = (event: EventMarkerEvent) => void;
|
|
281
|
+
|
|
259
282
|
export interface TheatreDirectorBindingOptions {
|
|
260
283
|
/**
|
|
261
284
|
* Which `AnimationClock` phase to evaluate the Director in. Defaults to
|
|
@@ -273,6 +296,20 @@ export interface TheatreDirectorBindingOptions {
|
|
|
273
296
|
* non-positive at the moment of a transition reports `mode: 'cut'`.
|
|
274
297
|
*/
|
|
275
298
|
blendSecondsKey?: string;
|
|
299
|
+
/**
|
|
300
|
+
* WF seam — event-marker hook (see {@link EventMarkerHook}). Fired when
|
|
301
|
+
* the string value of the Director's `eventMarkerKey` extra prop changes
|
|
302
|
+
* between evaluations (markers are authored as HOLD keyframes, so each
|
|
303
|
+
* marker's name holds until the next one). Optional; omit to ignore
|
|
304
|
+
* markers entirely.
|
|
305
|
+
*/
|
|
306
|
+
onEventMarker?: EventMarkerHook;
|
|
307
|
+
/**
|
|
308
|
+
* Name of the extra Director prop carrying event-marker names. Defaults to
|
|
309
|
+
* `'event'` — the track the editor's Sequencer writes. An empty string or
|
|
310
|
+
* a non-string value means "no marker active".
|
|
311
|
+
*/
|
|
312
|
+
eventMarkerKey?: string;
|
|
276
313
|
}
|
|
277
314
|
|
|
278
315
|
export interface TheatreDirectorBinding {
|
|
@@ -301,13 +338,26 @@ export function bindDirectorToClock<Props extends UnknownShorthandCompoundProps>
|
|
|
301
338
|
): TheatreDirectorBinding {
|
|
302
339
|
const phase = options.phase ?? 'preRender';
|
|
303
340
|
const blendSecondsKey = options.blendSecondsKey ?? 'blendSeconds';
|
|
341
|
+
const eventMarkerKey = options.eventMarkerKey ?? 'event';
|
|
304
342
|
|
|
305
343
|
let state: DirectorState = readDirectorState(sheetObject);
|
|
306
344
|
let previousCameraKey: string | null = null;
|
|
345
|
+
let previousMarker: string | null = null;
|
|
307
346
|
let hasEvaluatedOnClock = false;
|
|
308
347
|
|
|
348
|
+
function evaluateEventMarker(): void {
|
|
349
|
+
if (!options.onEventMarker) return;
|
|
350
|
+
const markerRaw = state.extras[eventMarkerKey];
|
|
351
|
+
const marker = typeof markerRaw === 'string' && markerRaw !== '' ? markerRaw : null;
|
|
352
|
+
if (marker !== null && marker !== previousMarker) {
|
|
353
|
+
options.onEventMarker({ name: marker, previousName: previousMarker });
|
|
354
|
+
}
|
|
355
|
+
previousMarker = marker ?? previousMarker;
|
|
356
|
+
}
|
|
357
|
+
|
|
309
358
|
function evaluate(): void {
|
|
310
359
|
state = readDirectorState(sheetObject);
|
|
360
|
+
evaluateEventMarker();
|
|
311
361
|
|
|
312
362
|
const changed = !hasEvaluatedOnClock || state.activeCamera !== previousCameraKey;
|
|
313
363
|
if (changed) {
|
|
@@ -44,6 +44,8 @@ export interface XStateAnimationLayerState {
|
|
|
44
44
|
readonly layer: string;
|
|
45
45
|
readonly stateId: string;
|
|
46
46
|
readonly clips: readonly string[];
|
|
47
|
+
/** Live native AnimationAction weights, including an in-progress crossfade. */
|
|
48
|
+
readonly clipWeights: readonly { readonly clip: string; readonly weight: number }[];
|
|
47
49
|
readonly weight: number;
|
|
48
50
|
readonly blendMode: 'override' | 'additive';
|
|
49
51
|
readonly boneMask?: AnimationBoneMask;
|
|
@@ -408,6 +410,10 @@ export function bindXStateAnimation(
|
|
|
408
410
|
layer: active.layer,
|
|
409
411
|
stateId: active.stateId,
|
|
410
412
|
clips: active.natural.map((weighted) => weighted.clip),
|
|
413
|
+
clipWeights: active.natural.map((weighted) => ({
|
|
414
|
+
clip: weighted.clip,
|
|
415
|
+
weight: actionCache.get(weighted.actionKey)?.action.getEffectiveWeight() ?? 0,
|
|
416
|
+
})),
|
|
411
417
|
weight: layerWeightOf(active.meta),
|
|
412
418
|
blendMode: blendModeOf(active.meta),
|
|
413
419
|
...(active.meta.boneMask ? { boneMask: active.meta.boneMask } : {}),
|