@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.
Files changed (103) hide show
  1. package/README.md +48 -15
  2. package/package.json +11 -25
  3. package/schemas/engine-capabilities.json +10 -10
  4. package/schemas/entity2d.schema.json +468 -0
  5. package/schemas/mat.schema.json +2 -33
  6. package/schemas/prefab.schema.json +16 -172
  7. package/schemas/scn2d.schema.json +42 -23
  8. package/schemas/vgai-game.schema.json +34 -0
  9. package/schemas/vscn.schema.json +16 -172
  10. package/src/adapter/authoring.ts +152 -2
  11. package/src/adapter/colyseus-networking-adapter.ts +35 -1
  12. package/src/adapter/first-party-systems.ts +7 -1
  13. package/src/adapter/game-adapter.ts +13 -0
  14. package/src/adapter/index.ts +25 -0
  15. package/src/adapter/rapier-physics-adapter.ts +55 -2
  16. package/src/adapter/system-adapter.ts +249 -2
  17. package/src/adapter/vgai-scene-game-adapter.ts +149 -25
  18. package/src/ai/navigation.ts +28 -0
  19. package/src/animation/clip-map.ts +1 -8
  20. package/src/animation/theatre-director.ts +50 -0
  21. package/src/animation/xstate-animation-binding.ts +6 -0
  22. package/src/audio/audio-introspection.ts +290 -0
  23. package/src/audio/tone-context.ts +46 -0
  24. package/src/dev/chrome-trace.ts +153 -0
  25. package/src/dev/performance-profiler.ts +93 -6
  26. package/src/dev/render-debug-adapter.ts +199 -0
  27. package/src/dev/render-memory.ts +243 -0
  28. package/src/dev/webgl-frame-capture.ts +424 -0
  29. package/src/ecs/component-manager.ts +43 -10
  30. package/src/ecs/game-component.ts +39 -10
  31. package/src/input/input-manager.ts +24 -19
  32. package/src/input/input-types.ts +1 -1
  33. package/src/loader.ts +7 -0
  34. package/src/manifest/load.ts +14 -0
  35. package/src/manifest/schema.ts +65 -0
  36. package/src/react/game-state.tsx +1 -1
  37. package/src/render/render-batch-system.ts +26 -12
  38. package/src/render/spark-renderer-lifecycle.ts +64 -0
  39. package/src/runtime/create-runtime.ts +33 -5
  40. package/src/runtime/debug-bridge.ts +5 -5
  41. package/src/runtime/game.ts +102 -20
  42. package/src/runtime/mount-manifest.ts +1 -1
  43. package/src/runtime/render-control.ts +121 -0
  44. package/src/runtime/types.ts +1 -1
  45. package/src/scene/asset-loaders.ts +77 -3
  46. package/src/scene/instance-mesh.ts +25 -0
  47. package/src/scene/material-factory.ts +4 -15
  48. package/src/scene/mesh-shadow.ts +18 -0
  49. package/src/scene/particles-factory.ts +59 -0
  50. package/src/scene/scene-loader.ts +41 -16
  51. package/src/scene/schema/instances.ts +1 -2
  52. package/src/scene/schema/material.ts +83 -94
  53. package/src/scene/schema/mesh.ts +76 -90
  54. package/src/scene/schema/scene-file.ts +1 -2
  55. package/src/scene/user-data.ts +30 -14
  56. package/src/setup/setup-renderer.ts +6 -1
  57. package/src/world2d/asset-paths2d.ts +44 -0
  58. package/src/world2d/collision-2d.ts +7 -14
  59. package/src/world2d/entity2d-asset.ts +22 -0
  60. package/src/world2d/index.ts +27 -2
  61. package/src/world2d/physics2d-transform.ts +173 -0
  62. package/src/world2d/physics2d-units.ts +10 -0
  63. package/src/world2d/pixi-game-adapter.ts +148 -36
  64. package/src/world2d/scene2d-identity.ts +49 -0
  65. package/src/world2d/scene2d-loader.ts +243 -119
  66. package/src/world2d/schema/entity2d.ts +51 -33
  67. package/src/world2d/schema/physics2d.ts +14 -3
  68. package/src/world2d/schema/sprite.ts +32 -4
  69. package/src/world2d/schema/tilemap.ts +26 -9
  70. package/src/world2d/transform-writer-2d.ts +29 -11
  71. package/src/world2d/types.ts +21 -8
  72. package/src/world3d-react/behavior.tsx +138 -0
  73. package/src/world3d-react/engine-bridge.ts +48 -0
  74. package/src/world3d-react/index.ts +44 -0
  75. package/src/world3d-react/r3f-adapter.tsx +303 -0
  76. package/src/world3d-react/world-context.ts +294 -0
  77. package/vendor/realism-effects/LICENSE.md +21 -0
  78. package/vendor/realism-effects/UPSTREAM.md +19 -0
  79. package/vendor/realism-effects/dist/index.cjs +3447 -0
  80. package/vendor/realism-effects/dist/index.d.ts +59 -0
  81. package/vendor/realism-effects/dist/index.js +3434 -0
  82. package/vendor/realism-effects/package.json +23 -0
  83. package/src/character/cloth-sim.ts +0 -533
  84. package/src/character/spring-chain.ts +0 -307
  85. package/src/humanoid/body.ts +0 -663
  86. package/src/humanoid/clips.ts +0 -149
  87. package/src/humanoid/compose.ts +0 -209
  88. package/src/humanoid/generate.ts +0 -189
  89. package/src/humanoid/index.ts +0 -36
  90. package/src/humanoid/schema.ts +0 -108
  91. package/src/humanoid/skeleton.ts +0 -345
  92. package/src/react/humanoid-bake.document.tsx +0 -337
  93. package/src/scene/geometries/index.ts +0 -7
  94. package/src/scene/geometries/terrain.ts +0 -42
  95. package/src/scene/geometry-registry.ts +0 -42
  96. package/src/scene/instance-registry.ts +0 -84
  97. package/src/scene/instancers/grid.ts +0 -38
  98. package/src/scene/instancers/index.ts +0 -7
  99. package/src/scene/material-registry.ts +0 -73
  100. package/src/scene/materials/index.ts +0 -7
  101. package/src/scene/materials/water.ts +0 -56
  102. package/src/world2d/components-2d.ts +0 -86
  103. package/tools/humanoid-bake.tool.ts +0 -274
@@ -1,19 +1,32 @@
1
1
  import RAPIER from '@dimforge/rapier2d-compat';
2
2
  import type { Container } from 'pixi.js';
3
+ import { createInputManagerAdapter } from '../adapter/first-party-systems';
3
4
  import type { MountedPixiWorld } from '../adapter/game-adapter';
5
+ import type { SystemAdapters } from '../adapter/system-adapter';
4
6
  import { createAssetCache } from '../assets';
5
7
  import { createGameLoop } from '../core/game-loop';
8
+ import {
9
+ createSeededRandom,
10
+ DEFAULT_SEEDED_RANDOM_SEED,
11
+ getSeededRandom,
12
+ } from '../core/seeded-random';
6
13
  import { createSystemRunner } from '../core/system-runner';
7
14
  import { createComponentManager } from '../ecs/component-manager';
15
+ import { InputManager } from '../input/input-manager';
16
+ import {
17
+ createDebugRegistry,
18
+ type DebugRegistry,
19
+ getDebugRegistry,
20
+ } from '../runtime/debug-registry';
8
21
  import {
9
22
  createGame,
10
23
  createWorldInstance,
11
24
  type GameInternal,
12
25
  type WorldFrameHooks,
13
26
  } from '../runtime/game';
14
- import type { GameContext } from '../runtime/types';
15
27
  import { createCollision2DSystem, createTriggerDispatch2D } from './collision-2d';
16
28
  import { createPhysics2DRegistry } from './physics2d-registry';
29
+ import { DEFAULT_WORLD2D_PIXELS_PER_METER, defaultWorld2DGravityY } from './physics2d-units';
17
30
  import { createPixiSurface, type PixiSurface } from './pixi-surface';
18
31
  import { loadScene2DFromData, type Scene2DInstance } from './scene2d-loader';
19
32
  import type { Scene2DFile } from './schema/entity2d';
@@ -25,6 +38,8 @@ export interface World2DHost {
25
38
  canvas: HTMLCanvasElement;
26
39
  width: number;
27
40
  height: number;
41
+ /** Headless validation mount: skip browser-only asset/input initialization. */
42
+ headless?: boolean;
28
43
  /** Shared Game root when mounted by the universal multi-world host. */
29
44
  game?: GameInternal;
30
45
  /** DPR override (T6.1 slice 1, COMPOSITION-DESIGN.md D5 §3 — "one DPR
@@ -85,6 +100,9 @@ export interface PixiSceneConfig {
85
100
  setup?: World2DSetupFn;
86
101
  /** Component registry the scene's `components` reference. */
87
102
  registry?: Component2DRegistry;
103
+ /** Input map JSON path. Defaults to the same conventional map as Three;
104
+ * explicit `null` disables loading for deliberately input-free/test mounts. */
105
+ inputMapPath?: string | null;
88
106
  id?: string;
89
107
  }
90
108
 
@@ -101,7 +119,12 @@ export class PixiSceneGameAdapter {
101
119
  }
102
120
 
103
121
  async mount(host: World2DHost): Promise<MountedGame2D> {
104
- const { sceneData, setup, registry = {} } = this.config;
122
+ const {
123
+ inputMapPath = '/inputmaps/default.inputmap.json',
124
+ sceneData,
125
+ setup,
126
+ registry = {},
127
+ } = this.config;
105
128
 
106
129
  // --- Pixi surface (autoStart:false; engine loop drives render) ---
107
130
  const surface: PixiSurface = await createPixiSurface({
@@ -120,15 +143,43 @@ export class PixiSceneGameAdapter {
120
143
 
121
144
  // --- Rapier 2D world (separate from the 3D world) ---
122
145
  await RAPIER.init();
123
- const gravity = sceneData?.gravity ?? [0, 980];
146
+ const pixelsPerMeter = sceneData?.pixelsPerMeter ?? DEFAULT_WORLD2D_PIXELS_PER_METER;
147
+ const gravity = sceneData?.gravity ?? [0, defaultWorld2DGravityY(pixelsPerMeter)];
124
148
  const rapier2dWorld = new RAPIER.World({ x: gravity[0], y: gravity[1] });
149
+ // Keep Rapier's public API in authored pixels. Its native length-unit
150
+ // setting scales solver/contact tolerances for those pixel-sized bodies.
151
+ rapier2dWorld.lengthUnit = pixelsPerMeter;
125
152
  const eventQueue = new RAPIER.EventQueue(true);
126
153
 
127
154
  const physics2d = createPhysics2DRegistry();
128
- const writeTransforms2D = createTransformWriter2D(physics2d);
155
+ const writeTransforms2D = createTransformWriter2D(physics2d, surface.world);
129
156
  const collisions2d = createCollision2DSystem(rapier2dWorld, eventQueue);
157
+ const ownsInput = !host.game;
158
+ const input = host.game?.input ?? new InputManager();
159
+ if (!host.headless && inputMapPath !== null) {
160
+ if (host.game) await host.game.loadInputMap(inputMapPath);
161
+ else await input.loadMap(inputMapPath);
162
+ }
163
+
164
+ const systemAdapters: SystemAdapters = {};
165
+ const debugRegistry: DebugRegistry =
166
+ (host.game ? getDebugRegistry(host.game) : null) ??
167
+ createDebugRegistry({ getTick: () => 0, getSimT: () => 0 });
168
+ if (ownsInput) systemAdapters.input = createInputManagerAdapter(input);
169
+ systemAdapters.debug = debugRegistry.adapter;
170
+ // W4b (F11 frame debugger): the pixijs adapter deliberately does NOT
171
+ // register a `renderDebug` SystemAdapter. The first-party WebGL2 frame
172
+ // capture (`dev/webgl-frame-capture.ts`) and its `onBeforeRender`-based
173
+ // attribution are three.js-shaped (three's own per-object pre-draw hook,
174
+ // a THREE.Scene traversal, `renderer.info` memory). PixiJS draws through
175
+ // its own batch renderer with no equivalent per-object seam, so a faithful
176
+ // pixi capture is a separate instrument — deferred, not faked. Absence of
177
+ // `renderDebug` here is the honest "capability not supported" signal; the
178
+ // editor's frame-debugger panel marks a pixi world's section absent rather
179
+ // than showing fabricated draws.
130
180
 
131
181
  const systems = createSystemRunner(host.game?.profiler.systemObserver, 'pixijs');
182
+ if (ownsInput) systems.add('input', () => input.poll(), { name: 'input.poll' });
132
183
  systems.add('physics', () => rapier2dWorld.step(eventQueue), { name: 'physics2d.step' });
133
184
  systems.add(
134
185
  'postPhysics',
@@ -149,44 +200,110 @@ export class PixiSceneGameAdapter {
149
200
  collisions2d,
150
201
  components: null!,
151
202
  systems,
152
- ...(host.game ? { game: host.game } : {}),
203
+ input,
204
+ debug: debugRegistry.forWorld(this.id),
205
+ random:
206
+ (host.game ? getSeededRandom(host.game) : null) ??
207
+ createSeededRandom(DEFAULT_SEEDED_RANDOM_SEED),
208
+ registerSystemAdapter: (kind, adapter) => {
209
+ systemAdapters[kind] = adapter;
210
+ },
211
+ ...(host.game ? { game: host.game, roots: host.game.roots } : {}),
153
212
  };
154
213
 
214
+ debugRegistry.setInputActionsSource(this.id, () =>
215
+ input.actionNames().map((name) => ({ name, valueType: input.getActionValueType(name) })),
216
+ );
217
+ debugRegistry.setInputTraceSource(this.id, () => {
218
+ const raw = input.getInputTrace();
219
+ return {
220
+ version: raw.version,
221
+ seed: (host.game ? getSeededRandom(host.game) : null)?.seed ?? null,
222
+ fixedDt: host.game?.loop.fixedDt ?? null,
223
+ ticks: raw.ticks,
224
+ };
225
+ });
226
+ debugRegistry.setVirtualInputTarget(this.id, {
227
+ setVirtualAction: (action, value) => input.setVirtualAction(action, value),
228
+ tapVirtualAction: (action) => input.tapVirtualAction(action),
229
+ clearVirtualActions: () => input.clearVirtualActions(),
230
+ scheduleActionAtTick: (tick, action, value) =>
231
+ input.scheduleActionAtTick(tick, action, value),
232
+ startInputRecording: () => input.startInputRecording(),
233
+ stopInputRecording: () => input.stopInputRecording(),
234
+ isInputRecording: () => input.isInputRecording(),
235
+ injectAxis: (sourceId, value) => input.injectAxis(sourceId, value),
236
+ injectVector2: (sourceId, value) => input.injectVector2(sourceId, value),
237
+ injectPointerDelta: (sourceId, delta) => input.injectPointerDelta(sourceId, delta),
238
+ injectPointerPosition: (sourceId, value) => input.injectPointerPosition(sourceId, value),
239
+ });
240
+ if (!host.game) input.setDebugEmit((event, detail) => ctx.debug?.emit(event, detail));
241
+
155
242
  // T7.3 slice 1/2: the pixi world's components attach onto the SAME
156
243
  // unified `ComponentManager` (`ecs/component-manager.ts`) the threejs
157
244
  // path uses, constructed for kind `'pixijs'` — the world2d-silo manager
158
- // is deleted (T7.3 slice 2). `ctx` is cast to `GameContext` for the
159
- // manager's internal typing only: at runtime this object IS the real
160
- // `World2DContext` above, so `GameComponent<'pixijs'>` subclasses that
161
- // need real 2D ctx fields (e.g. `Camera2DFollow`, which widens its own
162
- // `update`'s ctx param to `GameContext | World2DContext` and narrows
163
- // back down) still receive their real ctx, unchanged behavior — the
164
- // same cast `test/mirror-tree-guard.test.ts` uses for its unit-level
165
- // pixi manager. `createComponentManager`'s positional `physics`
245
+ // is deleted (T7.3 slice 2). The manager accepts this native
246
+ // `World2DContext`, so Pixi component lifecycles need no union/cast.
247
+ // `createComponentManager`'s positional `physics`
166
248
  // parameter (the 3D `PhysicsRegistry`) is optional (T7.3 slice 2) and
167
249
  // omitted here — a `'pixijs'`-kind manager never reads it (rigidBody/
168
250
  // collider resolve from `physics2d` instead — see `resolvePhysicsRefs`
169
251
  // in `ecs/component-manager.ts`).
170
- ctx.components = createComponentManager(ctx as unknown as GameContext, undefined, {
252
+ ctx.components = createComponentManager(ctx, undefined, {
171
253
  kind: 'pixijs',
172
254
  physics2d,
173
255
  });
174
256
  collisions2d.onCollision(
175
- createTriggerDispatch2D(
176
- physics2d,
177
- ctx.components,
178
- rapier2dWorld,
179
- ctx as unknown as GameContext,
180
- ),
257
+ createTriggerDispatch2D(physics2d, ctx.components, rapier2dWorld, ctx),
181
258
  );
182
259
 
183
260
  // --- Load scene or run setup ---
184
261
  let sceneInstance: Scene2DInstance | null = null;
185
262
  let cleanup: { dispose: () => void } | null = null;
186
- if (sceneData) {
187
- sceneInstance = await loadScene2DFromData(sceneData, ctx, registry);
188
- } else if (setup) {
189
- cleanup = await setup(ctx);
263
+ let disposed = false;
264
+ systems.add('animation', (dt) => sceneInstance?.updateAnimations(dt), {
265
+ name: 'pixi.animated-sprites',
266
+ });
267
+ const disposeResources = (): void => {
268
+ if (disposed) return;
269
+ disposed = true;
270
+ const errors: unknown[] = [];
271
+ const attempt = (dispose: () => void): void => {
272
+ try {
273
+ dispose();
274
+ } catch (error) {
275
+ errors.push(error);
276
+ }
277
+ };
278
+ attempt(() => cleanup?.dispose());
279
+ attempt(() => sceneInstance?.dispose());
280
+ attempt(() => ctx.components.clear());
281
+ attempt(() => debugRegistry.strip(this.id));
282
+ if (ownsInput) attempt(() => input.dispose());
283
+ attempt(() => physics2d.clear());
284
+ attempt(() => rapier2dWorld.free());
285
+ attempt(() => eventQueue.free());
286
+ attempt(() => surface.dispose());
287
+ if (errors.length > 0) {
288
+ throw new AggregateError(errors, `Failed to dispose Pixi world "${this.id}" cleanly`);
289
+ }
290
+ };
291
+ try {
292
+ if (sceneData) {
293
+ sceneInstance = await loadScene2DFromData(sceneData, ctx, registry);
294
+ } else if (setup) {
295
+ cleanup = await setup(ctx);
296
+ }
297
+ } catch (error) {
298
+ try {
299
+ disposeResources();
300
+ } catch (cleanupError) {
301
+ throw new AggregateError(
302
+ [error, cleanupError],
303
+ `Pixi world "${this.id}" failed to mount and roll back cleanly`,
304
+ );
305
+ }
306
+ throw error;
190
307
  }
191
308
 
192
309
  return {
@@ -199,21 +316,15 @@ export class PixiSceneGameAdapter {
199
316
  ctx,
200
317
  update: (dt: number) => systems.run(dt),
201
318
  resize: (w: number, h: number) => surface.resize(w, h),
202
- // T7.3 slice 1: no `endFrame` hook — world2d has no per-substep
203
- // post-phase work today (no `input.endFrame()` analog; HUD input is
204
- // plain DOM events, not routed through an engine InputManager).
319
+ // Bare adapters own and close their private input frame here. A
320
+ // game-hosted adapter shares the one game-owned manager, whose frame is
321
+ // closed once after every root has advanced.
205
322
  frame: {
206
323
  runPhase: (phase, dt) => systems.runPhase(phase, dt),
324
+ ...(ownsInput ? { endFrame: () => input.endFrame() } : {}),
207
325
  },
208
- dispose: () => {
209
- cleanup?.dispose();
210
- sceneInstance?.dispose();
211
- ctx.components.clear();
212
- physics2d.clear();
213
- rapier2dWorld.free();
214
- eventQueue.free();
215
- surface.dispose();
216
- },
326
+ systems: systemAdapters,
327
+ dispose: disposeResources,
217
328
  };
218
329
  }
219
330
  }
@@ -298,6 +409,7 @@ export async function createWorld2DRuntime(
298
409
  stop() {
299
410
  loop.stop();
300
411
  mounted.dispose();
412
+ game.dispose();
301
413
  },
302
414
  // D10/T7.6: route through the real `Game.play` control surface instead of
303
415
  // `loop.timeScale = 0/1` — that starved the accumulator entirely (per D1,
@@ -0,0 +1,49 @@
1
+ import type { Entity2D, Scene2DFile } from './schema/entity2d';
2
+
3
+ /** Resolve every entity to one collision-free identity without mutating source. */
4
+ export function resolveScene2DEntityIds(scene: Scene2DFile): Map<Entity2D, string> {
5
+ const authored = new Set<string>();
6
+ const collect = (entities: Entity2D[]): void => {
7
+ for (const entity of entities) {
8
+ if (entity.id) {
9
+ if (authored.has(entity.id)) throw new Error(`Duplicate world2d entity id: "${entity.id}"`);
10
+ authored.add(entity.id);
11
+ }
12
+ if (entity.children) collect(entity.children);
13
+ }
14
+ };
15
+ collect(scene.entities);
16
+
17
+ const used = new Set(authored);
18
+ const result = new Map<Entity2D, string>();
19
+ const visit = (entities: Entity2D[], parentPath: readonly number[]): void => {
20
+ entities.forEach((entity, index) => {
21
+ const path = [...parentPath, index];
22
+ let id = entity.id;
23
+ if (!id) {
24
+ const base = `scn2d:${path.join('.')}`;
25
+ id = base;
26
+ let suffix = 1;
27
+ while (used.has(id)) id = `${base}~${suffix++}`;
28
+ used.add(id);
29
+ }
30
+ result.set(entity, id);
31
+ if (entity.children) visit(entity.children, path);
32
+ });
33
+ };
34
+ visit(scene.entities, []);
35
+ return result;
36
+ }
37
+
38
+ /** Materialize the same identities the preview/hierarchy already project. */
39
+ export function materializeScene2DEntityIds(scene: Scene2DFile): boolean {
40
+ const ids = resolveScene2DEntityIds(scene);
41
+ let changed = false;
42
+ for (const [entity, id] of ids) {
43
+ if (!entity.id) {
44
+ entity.id = id;
45
+ changed = true;
46
+ }
47
+ }
48
+ return changed;
49
+ }