@vgai/engine 0.5.7 → 0.5.9

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 (62) hide show
  1. package/dist/adapter/ingest/contract-debug-adapter.d.ts +35 -0
  2. package/dist/adapter/ingest/contract-debug-adapter.d.ts.map +1 -0
  3. package/dist/adapter/ingest/contract-debug-adapter.js +90 -0
  4. package/dist/adapter/ingest/game-contract.d.ts +60 -1
  5. package/dist/adapter/ingest/game-contract.d.ts.map +1 -1
  6. package/dist/adapter/ingest/game-contract.js +1 -1
  7. package/dist/adapter/setup-three-root-adapter.d.ts.map +1 -1
  8. package/dist/adapter/setup-three-root-adapter.js +94 -2
  9. package/dist/core/game-scoped-slot.d.ts +14 -0
  10. package/dist/core/game-scoped-slot.d.ts.map +1 -0
  11. package/dist/core/game-scoped-slot.js +24 -0
  12. package/dist/core/seeded-random.d.ts.map +1 -1
  13. package/dist/core/seeded-random.js +3 -2
  14. package/dist/core/sim-clock.d.ts.map +1 -1
  15. package/dist/core/sim-clock.js +3 -2
  16. package/dist/dev/performance-profiler.d.ts +13 -7
  17. package/dist/dev/performance-profiler.d.ts.map +1 -1
  18. package/dist/dev/performance-profiler.js +31 -3
  19. package/dist/dev/register-render-vitals.d.ts +95 -0
  20. package/dist/dev/register-render-vitals.d.ts.map +1 -0
  21. package/dist/dev/register-render-vitals.js +182 -0
  22. package/dist/dev/render-census.d.ts +135 -0
  23. package/dist/dev/render-census.d.ts.map +1 -0
  24. package/dist/dev/render-census.js +257 -0
  25. package/dist/dev/render-vitals.d.ts +181 -0
  26. package/dist/dev/render-vitals.d.ts.map +1 -0
  27. package/dist/dev/render-vitals.js +232 -0
  28. package/dist/dev/static-batch-advisor.d.ts +106 -0
  29. package/dist/dev/static-batch-advisor.d.ts.map +1 -0
  30. package/dist/dev/static-batch-advisor.js +141 -0
  31. package/dist/render/render-batch-system.d.ts.map +1 -1
  32. package/dist/render/render-batch-system.js +7 -18
  33. package/dist/render/structural-signature.d.ts +148 -0
  34. package/dist/render/structural-signature.d.ts.map +1 -0
  35. package/dist/render/structural-signature.js +193 -0
  36. package/dist/runtime/debug-registry.d.ts +2 -2
  37. package/dist/runtime/debug-registry.d.ts.map +1 -1
  38. package/dist/runtime/debug-registry.js +4 -3
  39. package/dist/runtime/dev-layers.d.ts.map +1 -1
  40. package/dist/runtime/dev-layers.js +6 -0
  41. package/dist/runtime/game.js +2 -2
  42. package/dist/runtime/gameplay-rng-trap.d.ts.map +1 -1
  43. package/dist/runtime/gameplay-rng-trap.js +2 -1
  44. package/package.json +1 -1
  45. package/schemas/engine-capabilities.json +5 -5
  46. package/src/adapter/ingest/contract-debug-adapter.ts +110 -0
  47. package/src/adapter/ingest/game-contract.ts +63 -1
  48. package/src/adapter/setup-three-root-adapter.ts +94 -2
  49. package/src/core/game-scoped-slot.ts +28 -0
  50. package/src/core/seeded-random.ts +3 -2
  51. package/src/core/sim-clock.ts +3 -2
  52. package/src/dev/performance-profiler.ts +47 -12
  53. package/src/dev/register-render-vitals.ts +249 -0
  54. package/src/dev/render-census.ts +351 -0
  55. package/src/dev/render-vitals.ts +338 -0
  56. package/src/dev/static-batch-advisor.ts +186 -0
  57. package/src/render/render-batch-system.ts +16 -19
  58. package/src/render/structural-signature.ts +231 -0
  59. package/src/runtime/debug-registry.ts +4 -3
  60. package/src/runtime/dev-layers.ts +7 -1
  61. package/src/runtime/game.ts +2 -2
  62. package/src/runtime/gameplay-rng-trap.ts +4 -2
@@ -1 +1 @@
1
- {"version":3,"file":"debug-registry.d.ts","sourceRoot":"","sources":["../../src/runtime/debug-registry.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"debug-registry.d.ts","sourceRoot":"","sources":["../../src/runtime/debug-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,YAAY,EAAsC,MAAM,2BAA2B,CAAC;AAElG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAmB,MAAM,SAAS,CAAC;AAElF;;;8CAG8C;AAC9C,qBAAa,UAAW,SAAQ,KAAK;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;gBAExC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAM1E;AA6BD;;;;;sEAKsE;AACtE,MAAM,WAAW,uBAAuB;IACtC,gBAAgB,CACd,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GACjD;QAAE,SAAS,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3C,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG;QAAE,SAAS,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1E,mBAAmB,IAAI,IAAI,CAAC;IAC5B;;4BAEwB;IACxB,oBAAoB,CAClB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GACjD,IAAI,CAAC;IACR,mBAAmB,IAAI,IAAI,CAAC;IAC5B,kBAAkB,IAAI,IAAI,CAAC;IAC3B,gBAAgB,IAAI,OAAO,CAAC;IAC5B;;;8EAG0E;IAC1E,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAClD,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACvE,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC5E,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CAChF;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,CAAC,CAAC;IACX,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE,OAAO,EAAE,CAAC;CAClB;AAED;;;;iEAIiE;AACjE,MAAM,WAAW,eAAe;IAC9B;;kCAE8B;IAC9B,MAAM,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,CAAC;CAClC;AAED;;;kEAGkE;AAClE,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;CACnD;AA+CD;;8EAE8E;AAC9E,MAAM,WAAW,aAAa;IAC5B;0CACsC;IACtC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B;iFAC6E;IAC7E,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,CAAC;IAC1C;;;sDAGkD;IAClD,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACjD;;;;;;;;;;;;;;;;;;OAkBG;IACH,KAAK,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B;kEAC8D;IAC9D,eAAe,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAC;IACzC;;;;;;6EAMyE;IACzE,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,EAAE,GAAG,IAAI,CAAC;IAC9F;;;;;;;;;;;;iFAY6E;IAC7E,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,kBAAkB,GAAG,IAAI,CAAC;IACzE;;;;;;;;;;;;;;;;gFAgB4E;IAC5E,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,uBAAuB,GAAG,IAAI,CAAC;IAC9E;;;;;;;;;;;;OAYG;IACH,qBAAqB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,uBAAuB,GAAG,IAAI,CAAC;IACxE;;;;;;;;;OASG;IACH,iBAAiB,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAAC;IAChD;;;;iDAI6C;IAC7C,iBAAiB,IAAI,cAAc,GAAG,IAAI,CAAC;IAC3C;;;;;;sDAMkD;IAClD,WAAW,IAAI,MAAM,CAAC;IACtB;;;;;;;;iBAQa;IACb,qBAAqB,CACnB,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,OAAO,EACjB,IAAI,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,YAAY,GAAG,UAAU,GAAG,SAAS,CAAA;KAAE,GACtD,MAAM,IAAI,CAAC;IACd;;sCAEkC;IAClC,oBAAoB,CAAC,CAAC,SAAS,CAAC,CAAC,QAAQ,GAAG,SAAS,GAAG,SAAS,EAC/D,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAA;KAAE,EACrE,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,gBAAgB,CAAC,CAAC,CAAC,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,GAC/D,MAAM,IAAI,CAAC;CACf;AAcD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,OAAO,IAAI,MAAM,CAAC;IAClB,OAAO,IAAI,MAAM,CAAC;IAClB,gBAAgB,CAAC,IAAI,MAAM,GAAG,IAAI,CAAC;IACnC,eAAe,CAAC,IAAI,gBAAgB,CAAC;CACtC,GAAG,aAAa,CAwfhB;AAID;kFACkF;AAClF,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,aAAa,GAAG,IAAI,CAE/E;AAED;;8EAE8E;AAC9E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,GAAG,aAAa,GAAG,IAAI,CAEjE"}
@@ -4,8 +4,8 @@
4
4
  * SAME registry via {@link DebugRegistry.forRoot}, so a name a game registers is
5
5
  * visible (and name-collision-checked) across every world, not just the one that
6
6
  * registered it. `createGame` (`runtime/game.ts`) constructs the registry
7
- * alongside the state bridge and files it in the `Game -> DebugRegistry` WeakMap
8
- * this module owns; later consumers (the react `useDebugProvider`/
7
+ * alongside the state bridge and files it in a non-enumerable game-scoped slot;
8
+ * later consumers (the react `useDebugProvider`/
9
9
  * `useDebugCommand` hooks, editor panels) reach it via {@link getDebugRegistry}
10
10
  * rather than threading it through every call site.
11
11
  *
@@ -20,6 +20,7 @@
20
20
  * by convention, so collision messages stay meaningful in practice.
21
21
  */
22
22
  import { z } from 'zod';
23
+ import { createGameScopedSlot } from '../core/game-scoped-slot';
23
24
  /** Engine-local error for the debug seam — mirrors the `code` + `data` shape
24
25
  * `packages/vgai-sdk/src/errors.ts` uses (no import: the engine does not
25
26
  * depend on `@vgai/sdk`). `code` is always machine-readable; nothing reading
@@ -514,7 +515,7 @@ export function createDebugRegistry(opts) {
514
515
  },
515
516
  };
516
517
  }
517
- const registryByGame = new WeakMap();
518
+ const registryByGame = createGameScopedSlot('debug-registry');
518
519
  /** Called once by `createGame`, right after both the registry and the Game
519
520
  * shell object exist, to file the association {@link getDebugRegistry} reads. */
520
521
  export function registerDebugRegistry(game, registry) {
@@ -1 +1 @@
1
- {"version":3,"file":"dev-layers.d.ts","sourceRoot":"","sources":["../../src/runtime/dev-layers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,OAAO,CAOxE"}
1
+ {"version":3,"file":"dev-layers.d.ts","sourceRoot":"","sources":["../../src/runtime/dev-layers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,OAAO,CAaxE"}
@@ -33,6 +33,12 @@
33
33
  export function devLayersEnabled(override) {
34
34
  if (override !== undefined)
35
35
  return override;
36
+ // `import.meta` is cast whole, not just its `.env`: this module is reachable
37
+ // from programs whose tsconfig does not pull in `vite/client` (the session
38
+ // client's, for one, which reaches the three adapter transitively), and there
39
+ // `ImportMeta` has no declared `env` at all. The cast keeps the single owner
40
+ // of the dev answer importable from ANY program rather than forcing every
41
+ // downstream tsconfig to adopt Vite's ambient types.
36
42
  const env = import.meta.env;
37
43
  if (env?.DEV === true)
38
44
  return true;
@@ -297,7 +297,7 @@ export function createGame(opts) {
297
297
  // same `advanced` guard as the two bumps above, so a paused/frozen frame
298
298
  // fires no timers). Every world's `ctx.clock` is THIS instance, reached the
299
299
  // same way `ctx.random`/`ctx.debug` reach their game-scoped singletons —
300
- // `getSimClock(host.game)` over the WeakMap filed below.
300
+ // `getSimClock(host.game)` over the game-scoped slot filed below.
301
301
  //
302
302
  // The disposer is supplied HERE rather than inside the clock because
303
303
  // `core/sim-clock.ts` deliberately knows nothing about Rapier or shared
@@ -979,7 +979,7 @@ export function createGame(opts) {
979
979
  debugRegistry.strip();
980
980
  },
981
981
  };
982
- // Filed AFTER the shell exists (the WeakMap keys on the Game object
982
+ // Filed AFTER the shell exists (the slot lives on the Game object
983
983
  // itself) so `getDebugRegistry(game)` — the react hooks' and any later
984
984
  // consumer's reach-in — works from the moment `createGame` returns.
985
985
  registerDebugRegistry(gameInternal, debugRegistry);
@@ -1 +1 @@
1
- {"version":3,"file":"gameplay-rng-trap.d.ts","sourceRoot":"","sources":["../../src/runtime/gameplay-rng-trap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,MAAM,IAAI,IAAI,CAAC;IACf;;;0CAGsC;IACtC,OAAO,IAAI,IAAI,CAAC;CACjB;AAcD;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,GAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAG1B,GACA,eAAe,CA6CjB;AAaD,MAAM,WAAW,sBAAsB;IACrC,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAID;+BAC+B;AAC/B,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,sBAAsB,GAC9B,IAAI,CAEN;AAED;4CAC4C;AAC5C,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,sBAAsB,GAAG,IAAI,CAEtF"}
1
+ {"version":3,"file":"gameplay-rng-trap.d.ts","sourceRoot":"","sources":["../../src/runtime/gameplay-rng-trap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,MAAM,IAAI,IAAI,CAAC;IACf;;;0CAGsC;IACtC,OAAO,IAAI,IAAI,CAAC;CACjB;AAcD;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,GAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAG1B,GACA,eAAe,CA6CjB;AAaD,MAAM,WAAW,sBAAsB;IACrC,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAID;+BAC+B;AAC/B,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,sBAAsB,GAC9B,IAAI,CAEN;AAED;4CAC4C;AAC5C,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,sBAAsB,GAAG,IAAI,CAEtF"}
@@ -19,6 +19,7 @@
19
19
  * Never throws: a cosmetic third-party draw is legal, just outside the
20
20
  * contract (the design doc is explicit about this — "warn, never throw").
21
21
  */
22
+ import { createGameScopedSlot } from '../core/game-scoped-slot';
22
23
  /** Pull a single representative "call site" line out of a captured stack —
23
24
  * the warn-once dedupe key. `stack[0]` is the `Error:` header line,
24
25
  * `stack[1]` is this trap's OWN wrapper frame — the caller's frame is the
@@ -82,7 +83,7 @@ export function createGameplayRngTrap(warn = (message) => {
82
83
  },
83
84
  };
84
85
  }
85
- const controlByOwner = new WeakMap();
86
+ const controlByOwner = createGameScopedSlot('gameplay-rng-trap');
86
87
  /** Called once by `createGame`, alongside `registerSeededRandom`/
87
88
  * `registerDebugRegistry`. */
88
89
  export function registerGameplayRngTrapControl(owner, control) {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@vgai/engine",
3
3
  "author": "Volter AI, Inc.",
4
4
  "license": "Apache-2.0",
5
- "version": "0.5.7",
5
+ "version": "0.5.9",
6
6
  "description": "Readable TypeScript game engine and universal host for Three.js, PixiJS, and React games.",
7
7
  "keywords": [
8
8
  "game-engine",
@@ -15,7 +15,7 @@
15
15
  ],
16
16
  "notes": "Grepped packages/engine/src for ccd/CCD/setCcdEnabled: zero matches. The engine constructs no rigid bodies itself — games build their own descs via ctx.RAPIER — and nothing engine-side ever enables ccd. Workaround: keep dynamic-body per-step travel small relative to the thinnest collider, or add intermediate colliders; a game may also call setCcdEnabled on its own bodies directly.",
17
17
  "evidenceHashes": {
18
- "packages/engine/src/adapter/setup-three-root-adapter.ts": "fd3035e557db23e002e52707145a6e17bb3ad15767da74ec8fbafd40879d1d34"
18
+ "packages/engine/src/adapter/setup-three-root-adapter.ts": "b55b5aa1b032a813ddeaa1dedb6488326ed4cf3497e8cf76a07bada1e40243ea"
19
19
  }
20
20
  },
21
21
  {
@@ -67,12 +67,12 @@
67
67
  "status": "supported-with-tiers",
68
68
  "claim": "Unmodified OSS PixiJS games run via the canvas ingest seam: manifest 'iframe-reachable' + bundleUrl — in-realm capture, editor edits, overlay persistence. extraDeps must be registry-known (unknown throws); capture failure degrades loudly.",
69
69
  "evidence": [
70
- "vendor/games/verify-unaltered.mjs (zero-diff vendoring gate)",
70
+ "vendor/games/verify-unaltered.mjs (vendored-bytes gate: zero UNRECORDED diff byte-identical files, plus any recorded per-file patch that must reverse-apply to the pinned upstream)",
71
71
  "packages/engine/src/manifest/schema.ts"
72
72
  ],
73
73
  "notes": "Track P shipped 2026-07-03; proven: bubbo-bubbo/puzzling-potions/flappy-pixi. Tiers/limits: ARCHITECTURE-CORE.md.",
74
74
  "evidenceHashes": {
75
- "vendor/games/verify-unaltered.mjs": "c41ce285aaa26a8fd0e6f5e20385b434d6fc73fc3f39ac1a61204d7141166201",
75
+ "vendor/games/verify-unaltered.mjs": "df875b09d698b3cfc03443ff3dba4929f5f08ea8913009d4fae253d478282312",
76
76
  "packages/engine/src/manifest/schema.ts": "63d3f6fdbe3e9323ecbd4910c540d8a77e57d95e9044016424b46ca15a5c0bea"
77
77
  }
78
78
  },
@@ -82,12 +82,12 @@
82
82
  "status": "supported-with-caveats",
83
83
  "claim": "Unmodified native-React DOM games run via the manifest `ingest-react` identity: vendored byte-for-byte (vendor/games/, zero-diff CI), mounted through the react world stack under the host React 19 (deduped). Embed-only in v1 — hosted, sized, disposed; NO authoring (the editor never writes JSX back into an unmodified game, D-N4).",
84
84
  "evidence": [
85
- "vendor/games/verify-unaltered.mjs (zero-diff vendoring gate)",
85
+ "vendor/games/verify-unaltered.mjs (vendored-bytes gate: zero UNRECORDED diff byte-identical files, plus any recorded per-file patch that must reverse-apply to the pinned upstream)",
86
86
  "packages/editor/src/adapter-resolver.ts"
87
87
  ],
88
88
  "notes": "Scope is native-DOM React ONLY (D-N1): R3F/@pixi/react games are canvas games and ride the three.js/pixi seams instead. Generality limit: ONE proven game (react-rpg — menu-to-dungeon e2e under React 19; both game-2 candidates rejected on evidence, R-N7). Game deps must be host-installed root deps (D-N6). Open R-N6: vendored react games' raw window listeners bypass the play-mode input gate.",
89
89
  "evidenceHashes": {
90
- "vendor/games/verify-unaltered.mjs": "c41ce285aaa26a8fd0e6f5e20385b434d6fc73fc3f39ac1a61204d7141166201",
90
+ "vendor/games/verify-unaltered.mjs": "df875b09d698b3cfc03443ff3dba4929f5f08ea8913009d4fae253d478282312",
91
91
  "packages/editor/src/adapter-resolver.ts": "ce88071c9a4061df0a3b7edef24031bf81704ae2c3ede62ed6bf43d4f34ac556"
92
92
  }
93
93
  },
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Projects a game's DECLARED system surface (`window.vgaiGame.systems`, see
3
+ * `game-contract.ts`) onto the host's existing {@link DebugAdapter} — the one
4
+ * seam `game.commands()` / `game.state()` / `game.providers()` already read
5
+ * through for first-party content (`command-listener.ts`'s
6
+ * `dispatchBridgeMethod` → `getActiveSystems().debug`).
7
+ *
8
+ * Why a projection rather than a second door: an agent driving an ingested
9
+ * game must not have to learn a parallel vocabulary. A first-party game
10
+ * registers verbs with `ctx.debug.registerCommand`; an ingested game declares
11
+ * them in its entry shim. Both arrive at the SAME `DebugAdapter`, so every
12
+ * consumer downstream — the CLI's `vgai eval`, `@vgai/e2e`'s `GameClient`, the
13
+ * editor's Debug Console and State Watch panels — works unchanged and unaware
14
+ * of the provenance.
15
+ *
16
+ * What this deliberately does NOT do:
17
+ * - **Validate arguments.** The first-party path validates against a declared
18
+ * Zod tuple; a shim beside a foreign game is plain JS and has none. A verb
19
+ * validates itself and throws its own error, which surfaces wrapped as
20
+ * `DEBUG_COMMAND_FAILED` — the host never invents a schema to check against.
21
+ * - **Fabricate an event ring.** There is no tick loop behind an ingested
22
+ * game's declarations, so `events()` is empty. That is accurate (nothing was
23
+ * emitted), not a stub standing in for a missing feature.
24
+ */
25
+
26
+ import { DebugError } from '../../runtime/debug-registry';
27
+ import type { DebugAdapter, DebugCommandInfo, TickStampedEvent } from '../system-adapter';
28
+ import type { VgaiGameSystems } from './game-contract';
29
+
30
+ /**
31
+ * Build a {@link DebugAdapter} over a declared system surface, or `null` when
32
+ * the game declared no verbs and no state. `null` is the honest floor: the
33
+ * bridge then reports `DEBUG_ADAPTER_UNAVAILABLE` exactly as it did before,
34
+ * rather than serving an empty adapter that reads as "this game has no state"
35
+ * when the truth is "this game declared nothing".
36
+ */
37
+ export function createContractDebugAdapter(
38
+ systems: VgaiGameSystems | undefined | null,
39
+ ): DebugAdapter | null {
40
+ const commandList = systems?.commands ?? [];
41
+ const providerList = systems?.state ?? [];
42
+ if (commandList.length === 0 && providerList.length === 0) return null;
43
+
44
+ // Snapshot into name-keyed maps once. A later duplicate shadows an earlier
45
+ // one and the listing de-duplicates with it, so `commands()` can never
46
+ // advertise a name `invoke()` would resolve differently.
47
+ const byCommand = new Map(commandList.map((c) => [c.name, c]));
48
+ const byProvider = new Map(providerList.map((p) => [p.name, p]));
49
+
50
+ return {
51
+ providers: () =>
52
+ [...byProvider.values()].map((p) => ({ name: p.name, tier: p.tier ?? 'observable' })),
53
+
54
+ state: (name: string): unknown => {
55
+ const provider = byProvider.get(name);
56
+ if (!provider) {
57
+ throw new DebugError(
58
+ 'STATE_PROVIDER_NOT_FOUND',
59
+ `No state provider named "${name}" is declared by this game.`,
60
+ { registered: [...byProvider.keys()] },
61
+ );
62
+ }
63
+ return provider.read();
64
+ },
65
+
66
+ stateAll: (): Record<string, unknown> => {
67
+ const out: Record<string, unknown> = {};
68
+ for (const [name, provider] of byProvider) {
69
+ // One throwing provider must not cost the caller every other reading —
70
+ // the same per-key isolation the first-party registry gives.
71
+ try {
72
+ out[name] = provider.read();
73
+ } catch (err) {
74
+ out[name] = { __error: String(err) };
75
+ }
76
+ }
77
+ return out;
78
+ },
79
+
80
+ commands: (): DebugCommandInfo[] =>
81
+ [...byCommand.values()].map((c) => ({
82
+ name: c.name,
83
+ description: c.description,
84
+ argsJsonSchema: c.argsJsonSchema,
85
+ // An ingested game runs entirely in the browser realm the host mounted
86
+ // it in; there is no server leg for its verbs to route to.
87
+ locus: 'client' as const,
88
+ })),
89
+
90
+ invoke: async (name: string, args: unknown[]): Promise<unknown> => {
91
+ const command = byCommand.get(name);
92
+ if (!command) {
93
+ throw new DebugError(
94
+ 'DEBUG_COMMAND_NOT_REGISTERED',
95
+ `No command named "${name}" is declared by this game.`,
96
+ { registered: [...byCommand.keys()] },
97
+ );
98
+ }
99
+ try {
100
+ return await command.run(...args);
101
+ } catch (err) {
102
+ throw new DebugError('DEBUG_COMMAND_FAILED', `Command "${name}" threw: ${String(err)}`, {
103
+ name,
104
+ });
105
+ }
106
+ },
107
+
108
+ events: (): TickStampedEvent[] => [],
109
+ };
110
+ }
@@ -15,7 +15,7 @@
15
15
  * endpoint by endpoint: capture infers what it can (the scene), the game
16
16
  * declares what inference can't reach (its DOM root, its session lifecycle).
17
17
  *
18
- * Origin (root), F17+F21 (lifecycle).
18
+ * Origin (root), F17+F21 (lifecycle), WO-SYS1 (systems).
19
19
  */
20
20
 
21
21
  /**
@@ -34,6 +34,62 @@ export interface VgaiGameLifecycle {
34
34
  resume?(): void;
35
35
  }
36
36
 
37
+ /**
38
+ * One invokable verb the game exposes. Deliberately the SAME listing shape a
39
+ * first-party game gets from `ctx.debug.registerCommand`
40
+ * (`DebugCommandInfo`/`DebugAdapter.invoke` in `adapter/system-adapter.ts`) —
41
+ * an ingested game's verbs must enumerate through `game.commands()` and run
42
+ * through `game.command(...)` with no second vocabulary for agents to learn.
43
+ *
44
+ * `run` receives the positional argument list exactly as invoked. There is no
45
+ * host-side validation: the contract is plain JS declared beside a foreign
46
+ * game, so it cannot carry a Zod tuple the way `registerCommand` does.
47
+ * `argsJsonSchema` is therefore DOCUMENTATION for listing surfaces, not a
48
+ * gate — a verb validates its own arguments and throws its own errors.
49
+ */
50
+ export interface VgaiGameCommand {
51
+ name: string;
52
+ description?: string;
53
+ /** JSON-Schema projection of the argument list, when the game declares one. */
54
+ argsJsonSchema?: unknown;
55
+ run(...args: unknown[]): unknown | Promise<unknown>;
56
+ }
57
+
58
+ /**
59
+ * One named, JSON-serializable state read — the `ctx.debug.registerStateProvider`
60
+ * shape. `tier` carries the same meaning as the first-party one: `observable`
61
+ * is a value the game already computes, `assisted` is one the shim derives.
62
+ */
63
+ export interface VgaiGameStateProvider {
64
+ name: string;
65
+ tier?: 'observable' | 'assisted';
66
+ read(): unknown;
67
+ }
68
+
69
+ /**
70
+ * The game's SYSTEM surface: what its content actually IS, as opposed to how
71
+ * it renders. An ingested game's content is not always its scene graph — in a
72
+ * system-driven game the authorable content lives in the game's own model and
73
+ * is reached through the game's own verbs, which `root`/`lifecycle` could not
74
+ * express.
75
+ *
76
+ * Both members are read by exactly one consumer,
77
+ * `adapter/ingest/contract-debug-adapter.ts`, which projects them onto the
78
+ * host's ordinary `DebugAdapter` (`adapter/system-adapter.ts`) — so
79
+ * `game.commands()`/`game.state()` reach an ingested game through the same
80
+ * bridge first-party content uses, with no second vocabulary for agents to
81
+ * learn. That is deliberate scope: this
82
+ * interface describes what a foreign game's entry shim may declare, so every
83
+ * member here is a promise the host must already keep. A member nothing reads
84
+ * would be a shim author implementing a hook that is never called.
85
+ */
86
+ export interface VgaiGameSystems {
87
+ /** Verbs, enumerated by `game.commands()` and run by `game.command(...)`. */
88
+ commands?: VgaiGameCommand[];
89
+ /** State reads, enumerated by `game.providers()` and read by `game.state(...)`. */
90
+ state?: VgaiGameStateProvider[];
91
+ }
92
+
37
93
  export interface VgaiGameContract {
38
94
  /** Bump only on breaking shape changes; additive endpoints keep version 1. */
39
95
  contractVersion: 1;
@@ -45,6 +101,12 @@ export interface VgaiGameContract {
45
101
  */
46
102
  root?: HTMLElement;
47
103
  lifecycle?: VgaiGameLifecycle;
104
+ /**
105
+ * The game's own systems. Absent means "this game declared no system
106
+ * surface" — every dependent editor surface then shows a named absence, not
107
+ * an empty pretend-palette.
108
+ */
109
+ systems?: VgaiGameSystems;
48
110
  }
49
111
 
50
112
  /**
@@ -33,12 +33,14 @@ import {
33
33
  import { createSimClock, getSimClock, type SimClockInternal } from '../core/sim-clock';
34
34
  import { createSystemRunner } from '../core/system-runner';
35
35
  import { createDebugDraw } from '../dev/debug-draw';
36
+ import { type RenderVitalsRegistration, registerRenderVitals } from '../dev/register-render-vitals';
36
37
  import {
37
38
  createRenderDebugAdapter,
38
39
  frameCaptureContextFor,
39
40
  type RenderDebugWiring,
40
41
  } from '../dev/render-debug-adapter';
41
42
  import { collectRenderMemory } from '../dev/render-memory';
43
+ import { RENDER_SUBMIT_PHASE } from '../dev/render-vitals';
42
44
  import { createWebGLFrameCapture } from '../dev/webgl-frame-capture';
43
45
  import { createWebGLGpuTimer } from '../dev/webgl-gpu-timer';
44
46
  import { createSceneIndex } from '../ecs/scene-index';
@@ -61,6 +63,7 @@ import {
61
63
  type DebugRegistry,
62
64
  getDebugRegistry,
63
65
  } from '../runtime/debug-registry';
66
+ import { devLayersEnabled } from '../runtime/dev-layers';
64
67
  import { disposeDebrisSubtree, type RootFrameHooks } from '../runtime/game';
65
68
  import type { EditorPreview, GameCleanup, GameContext, GameSetupFn } from '../runtime/types';
66
69
  import { type AudioContext as GameAudio, setupAudio } from '../setup/setup-audio';
@@ -237,6 +240,10 @@ export class SetupThreeRootAdapter implements RootAdapter {
237
240
  : null;
238
241
  const viewportShading = new ViewportShadingRenderer();
239
242
  let viewportShadingMode: ViewportShadingMode = 'solid';
243
+ /** Top-level `renderer.render()` calls the last presentation cost — written
244
+ * by the render system, read by `postFrame`. See both for why the count is
245
+ * latched rather than differenced at report time. */
246
+ let lastPresentationPasses = 0;
240
247
 
241
248
  // --- System runner (engine-level systems) ---
242
249
  const systems = createSystemRunner(host.game?.profiler.systemObserver, 'three');
@@ -296,7 +303,8 @@ export class SetupThreeRootAdapter implements RootAdapter {
296
303
  systems.add(
297
304
  'render',
298
305
  (dt) => {
299
- if (host.game?.profiler.enabled) gpuTimer?.begin();
306
+ const profiler = host.game?.profiler;
307
+ if (profiler?.enabled) gpuTimer?.begin();
300
308
  if (sparkRenderer) scene.add(sparkRenderer);
301
309
  // W4b: arm-gated frame capture wraps the real draw calls. beginPass
302
310
  // is a no-op unless a captureFrame() armed it; afterRender delivers
@@ -304,6 +312,21 @@ export class SetupThreeRootAdapter implements RootAdapter {
304
312
  // patched context — in the SAME finally as gpuTimer.end(). Spark is
305
313
  // added before this so its draws fall inside the captured frame.
306
314
  renderDebugWiring?.beforeRender();
315
+ // Issue #1504: CPU render submission, bracketed as its OWN profiler
316
+ // phase nested inside the frame's enclosing `render` phase. This is
317
+ // the frame's only isolated measurement of "how long did it take to
318
+ // hand the draws to the driver" — the `render` phase around it also
319
+ // contains physics-debug redraw and every other world's render work,
320
+ // and a system span would key the reading to this adapter's system
321
+ // NAME. The profiler's phase clock is a stack precisely so this
322
+ // bracket cannot truncate its parent (`dev/performance-profiler.ts`).
323
+ // It is also what makes a frame a PRESENTATION as far as the vitals
324
+ // fold is concerned: no draw, no bracket, no display frame.
325
+ profiler?.beginPhase();
326
+ // `info.render.frame` counts top-level renderer.render() calls and
327
+ // survives `info.reset()`, so its delta across the draw is how many
328
+ // passes the composer chain (plus shadows) actually cost.
329
+ const passesBefore = renderer.info.render.frame;
307
330
  try {
308
331
  viewportShading.render(
309
332
  scene,
@@ -315,6 +338,8 @@ export class SetupThreeRootAdapter implements RootAdapter {
315
338
  (mesh) => camera.layers.test(mesh.layers),
316
339
  );
317
340
  } finally {
341
+ profiler?.endPhase(RENDER_SUBMIT_PHASE);
342
+ lastPresentationPasses = renderer.info.render.frame - passesBefore;
318
343
  renderDebugWiring?.afterRender();
319
344
  sparkRenderer?.removeFromParent();
320
345
  gpuTimer?.end();
@@ -336,13 +361,25 @@ export class SetupThreeRootAdapter implements RootAdapter {
336
361
 
337
362
  const postFrame = () => {
338
363
  if (ownsInput) input.endFrame();
339
- if (host.game?.profiler.enabled) {
364
+ // `renderer.info` is checked, not assumed: a headless/stub renderer
365
+ // supplies only what the runtime needs to mount, and a mount with no
366
+ // counters must report NOTHING rather than throw once per frame. This
367
+ // used to be unreachable because only the editor ever enabled the
368
+ // profiler; the engine enables it itself under the dev gate now (see the
369
+ // render-vitals seed below), so the assumption has to be paid for.
370
+ if (host.game?.profiler.enabled && renderer.info) {
340
371
  host.game.profiler.reportRender({
341
372
  gpuMs: gpuTimer?.poll() ?? null,
342
373
  drawCalls: renderer.info.render.calls,
343
374
  triangles: renderer.info.render.triangles,
344
375
  geometries: renderer.info.memory.geometries,
345
376
  textures: renderer.info.memory.textures,
377
+ // The LAST presentation's pass count, not a delta taken here: under
378
+ // the display-rate loop this hook runs once per fixed SUBSTEP, so a
379
+ // delta measured at this point would read as 0 on every substep that
380
+ // did not draw. Same one-frame-warm semantics as the renderer.info
381
+ // counters beside it.
382
+ renderPasses: lastPresentationPasses,
346
383
  });
347
384
  }
348
385
  };
@@ -538,6 +575,48 @@ export class SetupThreeRootAdapter implements RootAdapter {
538
575
  (host.game ? getSeededRandom(host.game) : null) ??
539
576
  createSeededRandom(DEFAULT_SEEDED_RANDOM_SEED);
540
577
 
578
+ // --- Live render vitals (issue #1504) -------------------------------------
579
+ // Engine-owned and first-party, seeded HERE like `renderDebug` above: a
580
+ // running game must be able to explain its own frame cost through the debug
581
+ // registry, with no capability to install and nothing for a game to write.
582
+ //
583
+ // THE GATE, in one place. Three conditions, and the reason for each:
584
+ // 1. `devLayersEnabled()` — the ONE owner of "is this a dev/editor
585
+ // context" (`runtime/dev-layers.ts`). A ship build registers nothing,
586
+ // subscribes to nothing, and never enables the profiler.
587
+ // 2. a `Game` shell exists — the readings are folded out of that game's
588
+ // profiler frames, and a bare mount has no profiler to fold.
589
+ // 3. not headless — a headless mount registers no render system at all,
590
+ // so it never brackets a submission and never presents a frame. Its
591
+ // vitals could only ever read empty, and registering a door that is
592
+ // structurally incapable of answering is worse than not having one.
593
+ //
594
+ // The enabled-gating question, resolved: the profiler is AUTO-ENABLED here
595
+ // rather than given a second, cheaper always-on counter tier. A counter
596
+ // tier would be a parallel measurement path — exactly the side ledger this
597
+ // work exists to avoid — and the profiler already has the one property
598
+ // that made the choice: `enabled` is a plain flag whose cost is per-frame
599
+ // bookkeeping the editor ALREADY pays (it sets `profiler.enabled = true`
600
+ // on play, `CenterDocuments.tsx`), so under the dev gate this is not a new
601
+ // cost, it is the same cost arriving a little earlier. Nothing here ever
602
+ // turns it OFF: the profiler's other owners (the Performance panel,
603
+ // `render-control.ts`'s perfSample) save and restore the prior state, so a
604
+ // dev session that enabled it at mount keeps it enabled.
605
+ let renderVitals: RenderVitalsRegistration | null = null;
606
+ const seedRenderVitals = (): void => {
607
+ if (!host.game || headless || !devLayersEnabled()) return;
608
+ renderVitals?.dispose();
609
+ host.game.profiler.enabled = true;
610
+ renderVitals = registerRenderVitals({
611
+ registry: debugRegistry,
612
+ worldId: this.id,
613
+ profiler: host.game.profiler,
614
+ scene,
615
+ renderer,
616
+ });
617
+ };
618
+ seedRenderVitals();
619
+
541
620
  // Snapshot of the engine-owned adapter kinds, taken right after seeding
542
621
  // and before any setup() has had a chance to run. `hotReload`/`disposeGame`
543
622
  // use this to strip every GAME-registered kind (including an override of a
@@ -740,6 +819,13 @@ export class SetupThreeRootAdapter implements RootAdapter {
740
819
  safeStep('debugRegistry.strip', () =>
741
820
  host.game ? debugRegistry.strip(this.id) : debugRegistry.strip(),
742
821
  );
822
+ // The strip above removes the vitals REGISTRATIONS (their provenance is
823
+ // this world). This ends the fold's profiler subscription — the one
824
+ // resource `registerRenderVitals` allocates that a strip cannot reach.
825
+ safeStep('renderVitals.dispose', () => {
826
+ renderVitals?.dispose();
827
+ renderVitals = null;
828
+ });
743
829
 
744
830
  // Release analyser taps BEFORE the audio teardown below. A consumer
745
831
  // (the editor's meter poll) should dispose its own handle, but Stop must
@@ -855,6 +941,12 @@ export class SetupThreeRootAdapter implements RootAdapter {
855
941
  // every react-door registration (whose `useEffect` cleanup never
856
942
  // re-fires to restore them after someone else's strip).
857
943
  safeStep('debugRegistry.strip', () => debugRegistry.strip(this.id));
944
+ // The strip above takes the ENGINE's own vitals registrations with it
945
+ // (they carry this world's provenance, which is what makes the game's
946
+ // own re-seed silent). Re-seed them before the incoming setup runs, so a
947
+ // warm restart does not silently cost a game its render vitals — and so
948
+ // the fold starts clean rather than carrying the outgoing game's frames.
949
+ safeStep('renderVitals re-seed', seedRenderVitals);
858
950
 
859
951
  currentCleanup = await newSetup(ctx, newEditorPreview);
860
952
  };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * A non-enumerable slot carried by the game object itself.
3
+ *
4
+ * Packaged editor play can legitimately involve two copies of `@vgai/engine`:
5
+ * the editor bundle creates the Game, while the project's Vite graph creates
6
+ * its root adapter. Module-local WeakMaps cannot cross that boundary. A
7
+ * registry-backed symbol can, while keeping the internal value off the public
8
+ * string-keyed Game surface.
9
+ */
10
+ export function createGameScopedSlot<T>(name: string): {
11
+ set(owner: object, value: T): void;
12
+ get(owner: object): T | null;
13
+ } {
14
+ const key = Symbol.for(`@vgai/engine/game-scoped/${name}`);
15
+
16
+ return {
17
+ set(owner, value) {
18
+ Object.defineProperty(owner, key, {
19
+ configurable: true,
20
+ enumerable: false,
21
+ value,
22
+ });
23
+ },
24
+ get(owner) {
25
+ return (owner as { [key: symbol]: T | undefined })[key] ?? null;
26
+ },
27
+ };
28
+ }
@@ -27,6 +27,7 @@
27
27
  */
28
28
 
29
29
  import { createMulberry32 } from '../runtime/render-seed';
30
+ import { createGameScopedSlot } from './game-scoped-slot';
30
31
 
31
32
  /** The name `ctx.random()` (called with no `.stream(...)`) draws from. */
32
33
  export const GAMEPLAY_STREAM = 'gameplay';
@@ -135,7 +136,7 @@ export function createSeededRandom(initialSeed: number): SeededRandom {
135
136
 
136
137
  // ---------------------------------------------------------------------------
137
138
  // Game-scoped registry — mirrors `runtime/debug-registry.ts`'s
138
- // `registerDebugRegistry`/`getDebugRegistry` WeakMap pattern exactly, but
139
+ // `registerDebugRegistry`/`getDebugRegistry` game-slot pattern exactly, but
139
140
  // keyed on a bare `object` (not `Game`) so this module never needs to import
140
141
  // `runtime/game.ts` even as a type — `core/` stays independent of `runtime/`
141
142
  // except for the one explicit, documented `render-seed.ts` reuse above.
@@ -144,7 +145,7 @@ export function createSeededRandom(initialSeed: number): SeededRandom {
144
145
  // `registerDebugRegistry(gameInternal, debugRegistry)`.
145
146
  // ---------------------------------------------------------------------------
146
147
 
147
- const registryByOwner = new WeakMap<object, SeededRandom>();
148
+ const registryByOwner = createGameScopedSlot<SeededRandom>('seeded-random');
148
149
 
149
150
  /** Called once by `createGame`, right after both the seeded-random surface
150
151
  * and the Game shell object exist — mirrors `registerDebugRegistry`. */
@@ -114,6 +114,7 @@
114
114
  */
115
115
 
116
116
  import type * as THREE from 'three';
117
+ import { createGameScopedSlot } from './game-scoped-slot';
117
118
 
118
119
  /** Cancel handle returned by {@link SimClock.after}/{@link SimClock.disposeAfter}. */
119
120
  export interface SimTimerHandle {
@@ -362,12 +363,12 @@ export function createSimClock(options: SimClockOptions): SimClockInternal {
362
363
 
363
364
  // ---------------------------------------------------------------------------
364
365
  // Game-scoped registry — mirrors `core/seeded-random.ts`'s
365
- // `registerSeededRandom`/`getSeededRandom` WeakMap pattern exactly, keyed on a
366
+ // `registerSeededRandom`/`getSeededRandom` slot pattern exactly, keyed on a
366
367
  // bare `object` (not `Game`) so `core/` never imports `runtime/`. `createGame`
367
368
  // is the one real registrant, passing the `GameInternal` shell as the key.
368
369
  // ---------------------------------------------------------------------------
369
370
 
370
- const clockByOwner = new WeakMap<object, SimClockInternal>();
371
+ const clockByOwner = createGameScopedSlot<SimClockInternal>('sim-clock');
371
372
 
372
373
  /** Called once by `createGame`, right after the clock and the Game shell exist. */
373
374
  export function registerSimClock(owner: object, clock: SimClockInternal): void {