@vgai/engine 0.5.5 → 0.5.7

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 (446) hide show
  1. package/dist/adapter/adapter-surface.d.ts +30 -0
  2. package/dist/adapter/adapter-surface.d.ts.map +1 -0
  3. package/dist/adapter/adapter-surface.js +18 -0
  4. package/dist/adapter/authoring.d.ts +597 -0
  5. package/dist/adapter/authoring.d.ts.map +1 -0
  6. package/dist/adapter/authoring.js +22 -0
  7. package/dist/adapter/colyseus-networking-adapter.d.ts +77 -0
  8. package/dist/adapter/colyseus-networking-adapter.d.ts.map +1 -0
  9. package/dist/adapter/colyseus-networking-adapter.js +76 -0
  10. package/dist/adapter/first-party-systems.d.ts +23 -0
  11. package/dist/adapter/first-party-systems.d.ts.map +1 -0
  12. package/dist/adapter/first-party-systems.js +196 -0
  13. package/dist/adapter/host-context.d.ts +194 -0
  14. package/dist/adapter/host-context.d.ts.map +1 -0
  15. package/dist/adapter/host-context.js +80 -0
  16. package/dist/adapter/index.d.ts +33 -0
  17. package/dist/adapter/index.d.ts.map +1 -0
  18. package/dist/adapter/index.js +25 -0
  19. package/dist/adapter/ingest/game-contract.d.ts +53 -0
  20. package/dist/adapter/ingest/game-contract.d.ts.map +1 -0
  21. package/dist/adapter/ingest/game-contract.js +30 -0
  22. package/dist/adapter/ingest/scene-capture.d.ts +121 -0
  23. package/dist/adapter/ingest/scene-capture.d.ts.map +1 -0
  24. package/dist/adapter/ingest/scene-capture.js +270 -0
  25. package/dist/adapter/ingest/structural-ids.d.ts +64 -0
  26. package/dist/adapter/ingest/structural-ids.d.ts.map +1 -0
  27. package/dist/adapter/ingest/structural-ids.js +110 -0
  28. package/dist/adapter/ingest/upstream-pin.d.ts +44 -0
  29. package/dist/adapter/ingest/upstream-pin.d.ts.map +1 -0
  30. package/dist/adapter/ingest/upstream-pin.js +47 -0
  31. package/dist/adapter/loop-gate-report.d.ts +34 -0
  32. package/dist/adapter/loop-gate-report.d.ts.map +1 -0
  33. package/dist/adapter/loop-gate-report.js +37 -0
  34. package/dist/adapter/rapier-physics-adapter.d.ts +69 -0
  35. package/dist/adapter/rapier-physics-adapter.d.ts.map +1 -0
  36. package/dist/adapter/rapier-physics-adapter.js +135 -0
  37. package/dist/adapter/root-adapter.d.ts +195 -0
  38. package/dist/adapter/root-adapter.d.ts.map +1 -0
  39. package/dist/adapter/root-adapter.js +25 -0
  40. package/dist/adapter/setup-three-root-adapter.d.ts +87 -0
  41. package/dist/adapter/setup-three-root-adapter.d.ts.map +1 -0
  42. package/dist/adapter/setup-three-root-adapter.js +791 -0
  43. package/dist/adapter/system-adapter.d.ts +453 -0
  44. package/dist/adapter/system-adapter.d.ts.map +1 -0
  45. package/dist/adapter/system-adapter.js +13 -0
  46. package/dist/adapter/transform.d.ts +18 -0
  47. package/dist/adapter/transform.d.ts.map +1 -0
  48. package/dist/adapter/transform.js +1 -0
  49. package/dist/ai/navigation.d.ts +78 -0
  50. package/dist/ai/navigation.d.ts.map +1 -0
  51. package/dist/ai/navigation.js +152 -0
  52. package/dist/animation/anim-graph-types.d.ts +25 -0
  53. package/dist/animation/anim-graph-types.d.ts.map +1 -0
  54. package/dist/animation/anim-graph-types.js +1 -0
  55. package/dist/animation/animation-clock.d.ts +213 -0
  56. package/dist/animation/animation-clock.d.ts.map +1 -0
  57. package/dist/animation/animation-clock.js +301 -0
  58. package/dist/animation/blend-node.d.ts +22 -0
  59. package/dist/animation/blend-node.d.ts.map +1 -0
  60. package/dist/animation/blend-node.js +97 -0
  61. package/dist/animation/camera-ownership.d.ts +249 -0
  62. package/dist/animation/camera-ownership.d.ts.map +1 -0
  63. package/dist/animation/camera-ownership.js +169 -0
  64. package/dist/animation/cinematic-cues.d.ts +200 -0
  65. package/dist/animation/cinematic-cues.d.ts.map +1 -0
  66. package/dist/animation/cinematic-cues.js +213 -0
  67. package/dist/animation/clip-map.d.ts +12 -0
  68. package/dist/animation/clip-map.d.ts.map +1 -0
  69. package/dist/animation/clip-map.js +31 -0
  70. package/dist/animation/cubic-spline-interpolant.d.ts +54 -0
  71. package/dist/animation/cubic-spline-interpolant.d.ts.map +1 -0
  72. package/dist/animation/cubic-spline-interpolant.js +111 -0
  73. package/dist/animation/theatre-clock-binding.d.ts +89 -0
  74. package/dist/animation/theatre-clock-binding.d.ts.map +1 -0
  75. package/dist/animation/theatre-clock-binding.js +29 -0
  76. package/dist/animation/theatre-director.d.ts +277 -0
  77. package/dist/animation/theatre-director.d.ts.map +1 -0
  78. package/dist/animation/theatre-director.js +248 -0
  79. package/dist/animation/theatre-object-binding.d.ts +305 -0
  80. package/dist/animation/theatre-object-binding.d.ts.map +1 -0
  81. package/dist/animation/theatre-object-binding.js +351 -0
  82. package/dist/animation/xstate-animation-binding.d.ts +119 -0
  83. package/dist/animation/xstate-animation-binding.d.ts.map +1 -0
  84. package/dist/animation/xstate-animation-binding.js +349 -0
  85. package/dist/animation/xstate-animation-meta.d.ts +266 -0
  86. package/dist/animation/xstate-animation-meta.d.ts.map +1 -0
  87. package/dist/animation/xstate-animation-meta.js +252 -0
  88. package/dist/asset-formats/camera.d.ts +15 -0
  89. package/dist/asset-formats/camera.d.ts.map +1 -0
  90. package/dist/asset-formats/camera.js +15 -0
  91. package/dist/asset-formats/collider.d.ts +21 -0
  92. package/dist/asset-formats/collider.d.ts.map +1 -0
  93. package/dist/asset-formats/collider.js +53 -0
  94. package/dist/asset-formats/index.d.ts +46 -0
  95. package/dist/asset-formats/index.d.ts.map +1 -0
  96. package/dist/asset-formats/index.js +36 -0
  97. package/dist/asset-formats/instances.d.ts +17 -0
  98. package/dist/asset-formats/instances.d.ts.map +1 -0
  99. package/dist/asset-formats/instances.js +31 -0
  100. package/dist/asset-formats/light.d.ts +21 -0
  101. package/dist/asset-formats/light.d.ts.map +1 -0
  102. package/dist/asset-formats/light.js +35 -0
  103. package/dist/asset-formats/material.d.ts +129 -0
  104. package/dist/asset-formats/material.d.ts.map +1 -0
  105. package/dist/asset-formats/material.js +88 -0
  106. package/dist/asset-formats/mesh.d.ts +22 -0
  107. package/dist/asset-formats/mesh.d.ts.map +1 -0
  108. package/dist/asset-formats/mesh.js +64 -0
  109. package/dist/asset-formats/parse.d.ts +20 -0
  110. package/dist/asset-formats/parse.d.ts.map +1 -0
  111. package/dist/asset-formats/parse.js +34 -0
  112. package/dist/asset-formats/particles.d.ts +1390 -0
  113. package/dist/asset-formats/particles.d.ts.map +1 -0
  114. package/dist/asset-formats/particles.js +319 -0
  115. package/dist/asset-formats/render-env.d.ts +387 -0
  116. package/dist/asset-formats/render-env.d.ts.map +1 -0
  117. package/dist/asset-formats/render-env.js +300 -0
  118. package/dist/asset-formats/tuples.d.ts +12 -0
  119. package/dist/asset-formats/tuples.d.ts.map +1 -0
  120. package/dist/asset-formats/tuples.js +14 -0
  121. package/dist/asset-loaders.d.ts +97 -0
  122. package/dist/asset-loaders.d.ts.map +1 -0
  123. package/dist/asset-loaders.js +209 -0
  124. package/dist/asset-parse-error.d.ts +24 -0
  125. package/dist/asset-parse-error.d.ts.map +1 -0
  126. package/dist/asset-parse-error.js +29 -0
  127. package/dist/asset-registry.d.ts +38 -0
  128. package/dist/asset-registry.d.ts.map +1 -0
  129. package/dist/asset-registry.js +66 -0
  130. package/dist/assets.d.ts +26 -0
  131. package/dist/assets.d.ts.map +1 -0
  132. package/dist/assets.js +55 -0
  133. package/dist/audio/wav-encode.d.ts +38 -0
  134. package/dist/audio/wav-encode.d.ts.map +1 -0
  135. package/dist/audio/wav-encode.js +97 -0
  136. package/dist/canvas-react/engine-bridge.d.ts +45 -0
  137. package/dist/canvas-react/engine-bridge.d.ts.map +1 -0
  138. package/dist/canvas-react/engine-bridge.js +45 -0
  139. package/dist/canvas-react/index.d.ts +42 -0
  140. package/dist/canvas-react/index.d.ts.map +1 -0
  141. package/dist/canvas-react/index.js +41 -0
  142. package/dist/canvas-react/pixi-primitive.d.ts +85 -0
  143. package/dist/canvas-react/pixi-primitive.d.ts.map +1 -0
  144. package/dist/canvas-react/pixi-primitive.js +181 -0
  145. package/dist/canvas-react/pixi-react-adapter.d.ts +71 -0
  146. package/dist/canvas-react/pixi-react-adapter.d.ts.map +1 -0
  147. package/dist/canvas-react/pixi-react-adapter.js +229 -0
  148. package/dist/canvas-react/pixi-react-root-factory.d.ts +56 -0
  149. package/dist/canvas-react/pixi-react-root-factory.d.ts.map +1 -0
  150. package/dist/canvas-react/pixi-react-root-factory.js +73 -0
  151. package/dist/canvas-react/world-context.d.ts +88 -0
  152. package/dist/canvas-react/world-context.d.ts.map +1 -0
  153. package/dist/canvas-react/world-context.js +212 -0
  154. package/dist/config.d.ts +9 -0
  155. package/dist/config.d.ts.map +1 -0
  156. package/dist/config.js +8 -0
  157. package/dist/core/frame-pacing.d.ts +90 -0
  158. package/dist/core/frame-pacing.d.ts.map +1 -0
  159. package/dist/core/frame-pacing.js +89 -0
  160. package/dist/core/game-loop.d.ts +62 -0
  161. package/dist/core/game-loop.d.ts.map +1 -0
  162. package/dist/core/game-loop.js +189 -0
  163. package/dist/core/seeded-random.d.ts +69 -0
  164. package/dist/core/seeded-random.d.ts.map +1 -0
  165. package/dist/core/seeded-random.js +121 -0
  166. package/dist/core/sim-clock.d.ts +199 -0
  167. package/dist/core/sim-clock.d.ts.map +1 -0
  168. package/dist/core/sim-clock.js +291 -0
  169. package/dist/core/system-runner.d.ts +96 -0
  170. package/dist/core/system-runner.d.ts.map +1 -0
  171. package/dist/core/system-runner.js +253 -0
  172. package/dist/core/types.d.ts +111 -0
  173. package/dist/core/types.d.ts.map +1 -0
  174. package/dist/core/types.js +25 -0
  175. package/dist/data/data-asset.d.ts +101 -0
  176. package/dist/data/data-asset.d.ts.map +1 -0
  177. package/dist/data/data-asset.js +122 -0
  178. package/dist/data/data-check-core.d.ts +109 -0
  179. package/dist/data/data-check-core.d.ts.map +1 -0
  180. package/dist/data/data-check-core.js +177 -0
  181. package/dist/data/data-ref.d.ts +79 -0
  182. package/dist/data/data-ref.d.ts.map +1 -0
  183. package/dist/data/data-ref.js +130 -0
  184. package/dist/data/vite-plugin-data.d.ts +79 -0
  185. package/dist/data/vite-plugin-data.d.ts.map +1 -0
  186. package/dist/data/vite-plugin-data.js +217 -0
  187. package/dist/defaults.d.ts +233 -0
  188. package/dist/defaults.d.ts.map +1 -0
  189. package/dist/defaults.js +137 -0
  190. package/dist/dev/chrome-trace.d.ts +45 -0
  191. package/dist/dev/chrome-trace.d.ts.map +1 -0
  192. package/dist/dev/chrome-trace.js +114 -0
  193. package/dist/dev/console-bridge.d.ts +22 -0
  194. package/dist/dev/console-bridge.d.ts.map +1 -0
  195. package/dist/dev/console-bridge.js +71 -0
  196. package/dist/dev/debug-draw.d.ts +24 -0
  197. package/dist/dev/debug-draw.d.ts.map +1 -0
  198. package/dist/dev/debug-draw.js +73 -0
  199. package/dist/dev/instruments.d.ts +133 -0
  200. package/dist/dev/instruments.d.ts.map +1 -0
  201. package/dist/dev/instruments.js +231 -0
  202. package/dist/dev/logger.d.ts +30 -0
  203. package/dist/dev/logger.d.ts.map +1 -0
  204. package/dist/dev/logger.js +90 -0
  205. package/dist/dev/performance-profiler.d.ts +111 -0
  206. package/dist/dev/performance-profiler.d.ts.map +1 -0
  207. package/dist/dev/performance-profiler.js +215 -0
  208. package/dist/dev/render-debug-adapter.d.ts +57 -0
  209. package/dist/dev/render-debug-adapter.d.ts.map +1 -0
  210. package/dist/dev/render-debug-adapter.js +141 -0
  211. package/dist/dev/render-memory.d.ts +53 -0
  212. package/dist/dev/render-memory.d.ts.map +1 -0
  213. package/dist/dev/render-memory.js +190 -0
  214. package/dist/dev/webgl-frame-capture.d.ts +134 -0
  215. package/dist/dev/webgl-frame-capture.d.ts.map +1 -0
  216. package/dist/dev/webgl-frame-capture.js +270 -0
  217. package/dist/dev/webgl-gpu-timer.d.ts +8 -0
  218. package/dist/dev/webgl-gpu-timer.d.ts.map +1 -0
  219. package/dist/dev/webgl-gpu-timer.js +52 -0
  220. package/dist/ecs/scene-index.d.ts +179 -0
  221. package/dist/ecs/scene-index.d.ts.map +1 -0
  222. package/dist/ecs/scene-index.js +331 -0
  223. package/dist/ecs/scene-query.d.ts +6 -0
  224. package/dist/ecs/scene-query.d.ts.map +1 -0
  225. package/dist/ecs/scene-query.js +40 -0
  226. package/dist/ecs/user-data.d.ts +193 -0
  227. package/dist/ecs/user-data.d.ts.map +1 -0
  228. package/dist/ecs/user-data.js +145 -0
  229. package/dist/index.d.ts +35 -0
  230. package/dist/index.d.ts.map +1 -0
  231. package/dist/index.js +29 -0
  232. package/dist/input/input-manager.d.ts +944 -0
  233. package/dist/input/input-manager.d.ts.map +1 -0
  234. package/dist/input/input-manager.js +2209 -0
  235. package/dist/input/input-types.d.ts +174 -0
  236. package/dist/input/input-types.d.ts.map +1 -0
  237. package/dist/input/input-types.js +1 -0
  238. package/dist/input/prompt-labels.d.ts +21 -0
  239. package/dist/input/prompt-labels.d.ts.map +1 -0
  240. package/dist/input/prompt-labels.js +121 -0
  241. package/dist/input/rebind-controller.d.ts +59 -0
  242. package/dist/input/rebind-controller.d.ts.map +1 -0
  243. package/dist/input/rebind-controller.js +85 -0
  244. package/dist/input/schema.d.ts +16 -0
  245. package/dist/input/schema.d.ts.map +1 -0
  246. package/dist/input/schema.js +228 -0
  247. package/dist/loader.d.ts +76 -0
  248. package/dist/loader.d.ts.map +1 -0
  249. package/dist/loader.js +114 -0
  250. package/dist/manifest/editor-port.d.ts +53 -0
  251. package/dist/manifest/editor-port.d.ts.map +1 -0
  252. package/dist/manifest/editor-port.js +67 -0
  253. package/dist/manifest/filename.d.ts +14 -0
  254. package/dist/manifest/filename.d.ts.map +1 -0
  255. package/dist/manifest/filename.js +43 -0
  256. package/dist/manifest/index.d.ts +6 -0
  257. package/dist/manifest/index.d.ts.map +1 -0
  258. package/dist/manifest/index.js +9 -0
  259. package/dist/manifest/load-file.d.ts +7 -0
  260. package/dist/manifest/load-file.d.ts.map +1 -0
  261. package/dist/manifest/load-file.js +23 -0
  262. package/dist/manifest/load.d.ts +161 -0
  263. package/dist/manifest/load.d.ts.map +1 -0
  264. package/dist/manifest/load.js +269 -0
  265. package/dist/manifest/locate.d.ts +24 -0
  266. package/dist/manifest/locate.d.ts.map +1 -0
  267. package/dist/manifest/locate.js +48 -0
  268. package/dist/manifest/schema.d.ts +293 -0
  269. package/dist/manifest/schema.d.ts.map +1 -0
  270. package/dist/manifest/schema.js +633 -0
  271. package/dist/physics/collider-dimensions.d.ts +63 -0
  272. package/dist/physics/collider-dimensions.d.ts.map +1 -0
  273. package/dist/physics/collider-dimensions.js +98 -0
  274. package/dist/physics/collision-system.d.ts +41 -0
  275. package/dist/physics/collision-system.d.ts.map +1 -0
  276. package/dist/physics/collision-system.js +64 -0
  277. package/dist/physics/physics-registry.d.ts +45 -0
  278. package/dist/physics/physics-registry.d.ts.map +1 -0
  279. package/dist/physics/physics-registry.js +65 -0
  280. package/dist/physics/transform-writer.d.ts +16 -0
  281. package/dist/physics/transform-writer.d.ts.map +1 -0
  282. package/dist/physics/transform-writer.js +38 -0
  283. package/dist/pixi/authoring.d.ts +93 -0
  284. package/dist/pixi/authoring.d.ts.map +1 -0
  285. package/dist/pixi/authoring.js +207 -0
  286. package/dist/pixi/index.d.ts +16 -0
  287. package/dist/pixi/index.d.ts.map +1 -0
  288. package/dist/pixi/index.js +15 -0
  289. package/dist/pixi/ingest-iframe.d.ts +82 -0
  290. package/dist/pixi/ingest-iframe.d.ts.map +1 -0
  291. package/dist/pixi/ingest-iframe.js +166 -0
  292. package/dist/pixi/ingest.d.ts +97 -0
  293. package/dist/pixi/ingest.d.ts.map +1 -0
  294. package/dist/pixi/ingest.js +40 -0
  295. package/dist/pixi/physics-registry.d.ts +24 -0
  296. package/dist/pixi/physics-registry.d.ts.map +1 -0
  297. package/dist/pixi/physics-registry.js +38 -0
  298. package/dist/pixi/scene-capture.d.ts +43 -0
  299. package/dist/pixi/scene-capture.d.ts.map +1 -0
  300. package/dist/pixi/scene-capture.js +92 -0
  301. package/dist/pixi/system-adapters.d.ts +16 -0
  302. package/dist/pixi/system-adapters.d.ts.map +1 -0
  303. package/dist/pixi/system-adapters.js +37 -0
  304. package/dist/react/unmanaged-root-detector.d.ts +18 -0
  305. package/dist/react/unmanaged-root-detector.d.ts.map +1 -0
  306. package/dist/react/unmanaged-root-detector.js +70 -0
  307. package/dist/react/use-data.d.ts +60 -0
  308. package/dist/react/use-data.d.ts.map +1 -0
  309. package/dist/react/use-data.js +101 -0
  310. package/dist/react/use-selection.d.ts +54 -0
  311. package/dist/react/use-selection.d.ts.map +1 -0
  312. package/dist/react/use-selection.js +71 -0
  313. package/dist/react/world-state.d.ts +154 -0
  314. package/dist/react/world-state.d.ts.map +1 -0
  315. package/dist/react/world-state.js +220 -0
  316. package/dist/render/auto-batcher.d.ts +34 -0
  317. package/dist/render/auto-batcher.d.ts.map +1 -0
  318. package/dist/render/auto-batcher.js +140 -0
  319. package/dist/render/instance-mesh.d.ts +5 -0
  320. package/dist/render/instance-mesh.d.ts.map +1 -0
  321. package/dist/render/instance-mesh.js +19 -0
  322. package/dist/render/light-camera-factory.d.ts +24 -0
  323. package/dist/render/light-camera-factory.d.ts.map +1 -0
  324. package/dist/render/light-camera-factory.js +71 -0
  325. package/dist/render/lod.d.ts +13 -0
  326. package/dist/render/lod.d.ts.map +1 -0
  327. package/dist/render/lod.js +16 -0
  328. package/dist/render/material-factory.d.ts +15 -0
  329. package/dist/render/material-factory.d.ts.map +1 -0
  330. package/dist/render/material-factory.js +186 -0
  331. package/dist/render/mesh-shadow.d.ts +11 -0
  332. package/dist/render/mesh-shadow.d.ts.map +1 -0
  333. package/dist/render/mesh-shadow.js +19 -0
  334. package/dist/render/particles-factory.d.ts +99 -0
  335. package/dist/render/particles-factory.d.ts.map +1 -0
  336. package/dist/render/particles-factory.js +361 -0
  337. package/dist/render/render-batch-system.d.ts +33 -0
  338. package/dist/render/render-batch-system.d.ts.map +1 -0
  339. package/dist/render/render-batch-system.js +223 -0
  340. package/dist/render/render-features.d.ts +47 -0
  341. package/dist/render/render-features.d.ts.map +1 -0
  342. package/dist/render/render-features.js +105 -0
  343. package/dist/render/render-settings.d.ts +35 -0
  344. package/dist/render/render-settings.d.ts.map +1 -0
  345. package/dist/render/render-settings.js +51 -0
  346. package/dist/render/spark-renderer-lifecycle.d.ts +18 -0
  347. package/dist/render/spark-renderer-lifecycle.d.ts.map +1 -0
  348. package/dist/render/spark-renderer-lifecycle.js +57 -0
  349. package/dist/render/viewport-shading.d.ts +17 -0
  350. package/dist/render/viewport-shading.d.ts.map +1 -0
  351. package/dist/render/viewport-shading.js +87 -0
  352. package/dist/runtime/create-runtime.d.ts +259 -0
  353. package/dist/runtime/create-runtime.d.ts.map +1 -0
  354. package/dist/runtime/create-runtime.js +666 -0
  355. package/dist/runtime/debug-bridge.d.ts +288 -0
  356. package/dist/runtime/debug-bridge.d.ts.map +1 -0
  357. package/dist/runtime/debug-bridge.js +277 -0
  358. package/dist/runtime/debug-registry.d.ts +287 -0
  359. package/dist/runtime/debug-registry.d.ts.map +1 -0
  360. package/dist/runtime/debug-registry.js +528 -0
  361. package/dist/runtime/dev-layers.d.ts +34 -0
  362. package/dist/runtime/dev-layers.d.ts.map +1 -0
  363. package/dist/runtime/dev-layers.js +40 -0
  364. package/dist/runtime/frame-selector-cache.d.ts +41 -0
  365. package/dist/runtime/frame-selector-cache.d.ts.map +1 -0
  366. package/dist/runtime/frame-selector-cache.js +65 -0
  367. package/dist/runtime/game.d.ts +576 -0
  368. package/dist/runtime/game.d.ts.map +1 -0
  369. package/dist/runtime/game.js +1003 -0
  370. package/dist/runtime/gameplay-rng-trap.d.ts +47 -0
  371. package/dist/runtime/gameplay-rng-trap.d.ts.map +1 -0
  372. package/dist/runtime/gameplay-rng-trap.js +95 -0
  373. package/dist/runtime/input-router.d.ts +78 -0
  374. package/dist/runtime/input-router.d.ts.map +1 -0
  375. package/dist/runtime/input-router.js +171 -0
  376. package/dist/runtime/mount-game.d.ts +126 -0
  377. package/dist/runtime/mount-game.d.ts.map +1 -0
  378. package/dist/runtime/mount-game.js +159 -0
  379. package/dist/runtime/mount-manifest.d.ts +174 -0
  380. package/dist/runtime/mount-manifest.d.ts.map +1 -0
  381. package/dist/runtime/mount-manifest.js +304 -0
  382. package/dist/runtime/presentation.d.ts +85 -0
  383. package/dist/runtime/presentation.d.ts.map +1 -0
  384. package/dist/runtime/presentation.js +92 -0
  385. package/dist/runtime/render-audio-control.d.ts +126 -0
  386. package/dist/runtime/render-audio-control.d.ts.map +1 -0
  387. package/dist/runtime/render-audio-control.js +116 -0
  388. package/dist/runtime/render-control.d.ts +347 -0
  389. package/dist/runtime/render-control.d.ts.map +1 -0
  390. package/dist/runtime/render-control.js +317 -0
  391. package/dist/runtime/render-seed.d.ts +56 -0
  392. package/dist/runtime/render-seed.d.ts.map +1 -0
  393. package/dist/runtime/render-seed.js +73 -0
  394. package/dist/runtime/state-bridge.d.ts +60 -0
  395. package/dist/runtime/state-bridge.d.ts.map +1 -0
  396. package/dist/runtime/state-bridge.js +62 -0
  397. package/dist/runtime/types.d.ts +285 -0
  398. package/dist/runtime/types.d.ts.map +1 -0
  399. package/dist/runtime/types.js +1 -0
  400. package/dist/runtime/unexported-game-trap.d.ts +36 -0
  401. package/dist/runtime/unexported-game-trap.d.ts.map +1 -0
  402. package/dist/runtime/unexported-game-trap.js +80 -0
  403. package/dist/setup/setup-audio.d.ts +20 -0
  404. package/dist/setup/setup-audio.d.ts.map +1 -0
  405. package/dist/setup/setup-audio.js +41 -0
  406. package/dist/setup/setup-particles.d.ts +17 -0
  407. package/dist/setup/setup-particles.d.ts.map +1 -0
  408. package/dist/setup/setup-particles.js +16 -0
  409. package/dist/setup/setup-physics.d.ts +25 -0
  410. package/dist/setup/setup-physics.d.ts.map +1 -0
  411. package/dist/setup/setup-physics.js +51 -0
  412. package/dist/setup/setup-renderer.d.ts +82 -0
  413. package/dist/setup/setup-renderer.d.ts.map +1 -0
  414. package/dist/setup/setup-renderer.js +381 -0
  415. package/dist/world3d-react/engine-bridge.d.ts +47 -0
  416. package/dist/world3d-react/engine-bridge.d.ts.map +1 -0
  417. package/dist/world3d-react/engine-bridge.js +48 -0
  418. package/dist/world3d-react/index.d.ts +47 -0
  419. package/dist/world3d-react/index.d.ts.map +1 -0
  420. package/dist/world3d-react/index.js +46 -0
  421. package/dist/world3d-react/r3f-adapter.d.ts +51 -0
  422. package/dist/world3d-react/r3f-adapter.d.ts.map +1 -0
  423. package/dist/world3d-react/r3f-adapter.js +368 -0
  424. package/dist/world3d-react/r3f-root-factory.d.ts +62 -0
  425. package/dist/world3d-react/r3f-root-factory.d.ts.map +1 -0
  426. package/dist/world3d-react/r3f-root-factory.js +79 -0
  427. package/dist/world3d-react/rapier-physics-bridge.d.ts +76 -0
  428. package/dist/world3d-react/rapier-physics-bridge.d.ts.map +1 -0
  429. package/dist/world3d-react/rapier-physics-bridge.js +120 -0
  430. package/dist/world3d-react/renderer-config.d.ts +58 -0
  431. package/dist/world3d-react/renderer-config.d.ts.map +1 -0
  432. package/dist/world3d-react/renderer-config.js +96 -0
  433. package/dist/world3d-react/world-context.d.ts +123 -0
  434. package/dist/world3d-react/world-context.d.ts.map +1 -0
  435. package/dist/world3d-react/world-context.js +204 -0
  436. package/dist-config/config.js +303 -0
  437. package/dist-config/config.js.map +7 -0
  438. package/package.json +14 -3
  439. package/src/adapter/rapier-physics-adapter.ts +77 -34
  440. package/src/config.ts +8 -0
  441. package/src/core/frame-pacing.ts +26 -0
  442. package/src/core/game-loop.ts +4 -3
  443. package/src/dev/instruments.ts +302 -0
  444. package/src/setup/setup-renderer.ts +2 -0
  445. package/src/world3d-react/index.ts +9 -0
  446. package/src/world3d-react/rapier-physics-bridge.tsx +137 -0
@@ -0,0 +1,1003 @@
1
+ /**
2
+ * Game root — T7.1 slice 1 ("introduce Game internally") + slice 2 ("world
3
+ * registry + ordered frame algorithm"), extended in T7.3 slice 1 with real
4
+ * `'canvas'`-kind `RootInstance` support (`stage`/`physics2d`, a real
5
+ * `pixiStage()`) so a canvas adapter can register its world onto a real `Game`
6
+ * instead of driving its own loop, and in T7.3 slice 2 with a
7
+ * construction-time stage check (symmetric with the three/scene check).
8
+ * T6.2 slice 1 adds real `'react'`-kind
9
+ * `RootInstance` support (`container`, a real `reactRoot()`) — the DOM-root
10
+ * world surface `runtime/create-runtime.ts`'s roots path now mounts, with
11
+ * `physics`/`collisions`/`camera`/`frame` `undefined` for them exactly
12
+ * like an opaque/foreign mount.
13
+ *
14
+ * (D6), particularly §4 (the frame algorithm) and §8 stage 2, for the full
15
+ * target shape. This file holds the Game/ RootInstance shell, the NEW
16
+ * game-scoped `SystemRunner`, and the host-facing `runFrame` frame executor.
17
+ * The public surface is NOT frozen yet — do not add these exports to a
18
+ * barrel/index; only the in-scope runtime/adapter files import this module
19
+ * directly. The `gpu` per-world cache is the one remaining slice-1 omission.
20
+ */
21
+ import { formatAudioGateMessage, formatLoopGateMessage } from '../adapter/loop-gate-report';
22
+ import { createSeededRandom, DEFAULT_SEEDED_RANDOM_SEED, registerSeededRandom, } from '../core/seeded-random';
23
+ import { createSimClock, registerSimClock } from '../core/sim-clock';
24
+ import { createSystemRunner } from '../core/system-runner';
25
+ import { PHASE_ORDER, SystemPhase } from '../core/types';
26
+ import { createPerformanceProfiler } from '../dev/performance-profiler';
27
+ import { hasUserData } from '../ecs/user-data';
28
+ import { InputManager } from '../input/input-manager';
29
+ import { createDebugRegistry, DebugError, registerDebugRegistry, } from './debug-registry';
30
+ import { createGameplayRngTrap, registerGameplayRngTrapControl } from './gameplay-rng-trap';
31
+ import { createStateBridge } from './state-bridge';
32
+ /**
33
+ * WO-P13 — the tail of `PHASE_ORDER` that presents rather than simulates, and
34
+ * therefore runs once per DISPLAY frame (`runRenderFrameImpl`) rather than
35
+ * once per fixed substep. The same two phases `runTicks`' `skipRenderPhases`
36
+ * fast-forward skips and `render-control.ts`'s `renderOnce()` drives — one
37
+ * definition of "the render phases" across all three, kept in `PHASE_ORDER`'s
38
+ * own order.
39
+ */
40
+ const DISPLAY_RATE_PHASES = PHASE_ORDER.filter((phase) => phase === SystemPhase.PRE_RENDER || phase === SystemPhase.RENDER);
41
+ /**
42
+ * Build a `RootInstance` whose kind-narrowed accessors throw descriptively
43
+ * on kind mismatch. This is the one place that builds
44
+ * `threeScene`/`pixiStage`, so every world (however it's constructed, in this
45
+ * slice or later ones) gets identical throw behavior.
46
+ */
47
+ export function createRootInstance(init) {
48
+ const { id, kind, pausable = true, adapter, mounted, scene, stage, container, physics, collisions, physics2d, camera, frame, } = init;
49
+ // A three world with no scene has nothing for `threeScene()` to return —
50
+ // fail loudly HERE, at construction, rather than letting `threeScene()`
51
+ // throw its generic "not a three world" message later for a world whose
52
+ // kind IS three (checklist item 5; a misleading error for this case).
53
+ if (kind === 'three' && !scene) {
54
+ throw new Error(`RootInstance "${id}" (kind: three): a three world requires a scene — ` +
55
+ 'pass `scene` in RootInstanceInit.');
56
+ }
57
+ // Symmetric check for canvas (T7.3 slice 2) — a canvas world with no stage
58
+ // has nothing for `pixiStage()` to return; fail loudly here, at
59
+ // construction, same as the three/scene check above.
60
+ if (kind === 'canvas' && !stage) {
61
+ throw new Error(`RootInstance "${id}" (kind: canvas): a canvas world requires a stage — ` +
62
+ 'pass `stage` in RootInstanceInit.');
63
+ }
64
+ // Symmetric check for react (T6.2 slice 1) — a react world with no
65
+ // container has nothing for `reactRoot()` to return; fail loudly here,
66
+ // at construction, same as the two checks above.
67
+ if (kind === 'dom' && !container) {
68
+ throw new Error(`RootInstance "${id}" (kind: react): a react world requires a container — ` +
69
+ 'pass `container` in RootInstanceInit.');
70
+ }
71
+ // Disposed-world guard (T7.1 slice 3). There
72
+ // is no `unregisterRoot` (decision 4 — roots are manifest-declared; a
73
+ // whole Game is disposed, not one world out of its registry), so a caller
74
+ // that disposes ONE world's `mounted` directly (e.g. ending a sub-session)
75
+ // leaves that `RootInstance` sitting in `Game.roots` — and `runFrame`
76
+ // would otherwise keep invoking its (now-torn-down) frame hooks every
77
+ // subsequent frame. `MountedThreeRoot` has no public "am I disposed" flag to
78
+ // read, so this wraps `mounted.dispose` in place (mutating the SAME mount
79
+ // object every holder of `mounted` shares — calling `mounted.dispose()`
80
+ // directly, exactly like calling `world.mounted.dispose()`, trips this)
81
+ // to flip a private flag, and wraps the frame hooks so they silently no-op
82
+ // once that flag is set. `runFrame` itself needs no knowledge of disposal
83
+ // — a disposed world is skipped by construction from the next call
84
+ // onward. (The opaque-world `mounted.update` fallback path is unaffected
85
+ // by this guard — no opaque-mount two-world scenario exists yet to need it.)
86
+ let disposed = false;
87
+ const originalDispose = mounted.dispose.bind(mounted);
88
+ mounted.dispose = () => {
89
+ disposed = true;
90
+ originalDispose();
91
+ };
92
+ const guardedFrame = frame
93
+ ? {
94
+ runPhase(phase, dt) {
95
+ if (disposed)
96
+ return;
97
+ frame.runPhase(phase, dt);
98
+ },
99
+ endFrame() {
100
+ if (disposed)
101
+ return;
102
+ frame.endFrame?.();
103
+ },
104
+ }
105
+ : undefined;
106
+ return {
107
+ id,
108
+ kind,
109
+ pausable,
110
+ adapter,
111
+ mounted,
112
+ physics,
113
+ collisions,
114
+ physics2d,
115
+ camera,
116
+ frame: guardedFrame,
117
+ threeScene() {
118
+ if (kind !== 'three' || !scene) {
119
+ throw new Error(`RootInstance "${id}" (kind: ${kind}): threeScene() requested but this world is not ` +
120
+ 'a three world');
121
+ }
122
+ return scene;
123
+ },
124
+ pixiStage() {
125
+ // The `!stage` branch a canvas world could hit here is now unreachable
126
+ // (T7.3 slice 2): construction above throws for `kind === 'canvas'`
127
+ // with no `stage`, symmetric with `threeScene()`'s `scene` guard.
128
+ if (kind !== 'canvas' || !stage) {
129
+ throw new Error(`RootInstance "${id}" (kind: ${kind}): pixiStage() requested but this world is not ` +
130
+ 'a canvas world');
131
+ }
132
+ return stage;
133
+ },
134
+ reactRoot() {
135
+ // The `!container` branch a react world could hit here is now
136
+ // unreachable (T6.2 slice 1): construction above throws for
137
+ // `kind === 'dom'` with no `container`, symmetric with
138
+ // `threeScene()`/`pixiStage()`'s guards.
139
+ if (kind !== 'dom' || !container) {
140
+ throw new Error(`RootInstance "${id}" (kind: ${kind}): reactRoot() requested but this world is not ` +
141
+ 'a react world');
142
+ }
143
+ return container;
144
+ },
145
+ };
146
+ }
147
+ /**
148
+ * Type guard for whether a `MountedThreeRoot` is a first-party
149
+ * `SetupThreeRootAdapter` mount (has a live `GameContext` at `.ctx`). Used to
150
+ * decide whether a world's `physics`/`collisions`/`camera` (and, on `Game`,
151
+ * `input`/`audio`) can be populated from it — an external
152
+ * adapter's mount has none of these first-party handles.
153
+ *
154
+ * Checks the `firstParty: true` brand (checklist item 1), NOT `'ctx' in
155
+ * mounted` — a foreign mount may carry an unrelated `ctx` key, so a
156
+ * structural `'ctx' in mounted` check would misfire. The probe is a plain property read, not an
157
+ * `instanceof`/value import of `setup-three-root-adapter.ts` — the
158
+ * `MountedSetupThreeRoot` import above stays type-only.
159
+ */
160
+ export function isFirstPartyMounted(mounted) {
161
+ return mounted.firstParty === true;
162
+ }
163
+ /**
164
+ * Read an owner's Rapier handles, treating an absent pair as "this owner holds
165
+ * no rigid bodies" — the honest reading for a world (R3F, or a bare
166
+ * `ComponentManager`-only owner) that builds no first-party physics runtime.
167
+ */
168
+ function readRapierHandles(owner) {
169
+ const { physics, rapierWorld } = owner;
170
+ if (!physics || !rapierWorld)
171
+ return null;
172
+ return { physics, rapierWorld };
173
+ }
174
+ /**
175
+ * P3 — what `SimClock.disposeAfter` actually does. `core/sim-clock.ts` knows
176
+ * only *when*; this is the *what*, and it lives here because it is the runtime
177
+ * that knows about Rapier and shared geometry.
178
+ *
179
+ * The order mirrors `adapter/setup-three-root-adapter.ts`'s world teardown for
180
+ * ONE subtree. Within the physics step,
181
+ * `rapierWorld.removeRigidBody(body)` comes BEFORE `physics.remove(node)`: the
182
+ * registry's `remove()` only drops index entries, so reversing the two leaks
183
+ * the Rapier body.
184
+ *
185
+ * `owners` is every world that could own part of the subtree — `createGame`
186
+ * passes all of its first-party three roots as their `GameContext`, a bare
187
+ * adapter mount passes its own ctx. `physics.get` is a no-op for a node the
188
+ * owner does not own, so offering the subtree to each is safe.
189
+ *
190
+ * Safe on an object already removed or already disposed: `removeFromParent` on
191
+ * a parentless `Object3D` is a no-op and three's `dispose()` calls are
192
+ * idempotent.
193
+ */
194
+ export function disposeDebrisSubtree(obj, owners) {
195
+ const nodes = [];
196
+ obj.traverse((node) => nodes.push(node));
197
+ for (const owner of owners) {
198
+ const rapier = readRapierHandles(owner);
199
+ if (!rapier)
200
+ continue;
201
+ for (const node of nodes) {
202
+ const refs = rapier.physics.get(node);
203
+ if (!refs)
204
+ continue;
205
+ rapier.rapierWorld.removeRigidBody(refs.body);
206
+ rapier.physics.remove(node);
207
+ }
208
+ }
209
+ obj.removeFromParent();
210
+ for (const node of nodes) {
211
+ // Duck-typed rather than `instanceof THREE.Mesh` so this module keeps its
212
+ // TYPE-ONLY three import (it is surface-neutral — it also hosts pixi
213
+ // roots). Covers Points/Line/Sprite debris too, which a Mesh check would
214
+ // silently leak.
215
+ const drawable = node;
216
+ if (!hasUserData(node, '__sharedGeometry'))
217
+ drawable.geometry?.dispose();
218
+ const material = drawable.material;
219
+ if (Array.isArray(material)) {
220
+ for (const m of material)
221
+ m.dispose();
222
+ }
223
+ else {
224
+ material?.dispose();
225
+ }
226
+ }
227
+ }
228
+ /**
229
+ * The {@link DebrisOwner}s one registered world contributes: its `GameContext`
230
+ * when the mount is first-party, else nothing. Written as a standalone function
231
+ * so `createGame`'s clock disposer is one line and the "which mounts count"
232
+ * rule has exactly one home.
233
+ */
234
+ function debrisOwnersOf(world) {
235
+ return isFirstPartyMounted(world.mounted) ? [world.mounted.ctx] : [];
236
+ }
237
+ function describeMismatch(handle, world) {
238
+ return (`Game.${handle}: default world "${world.id}" (kind: ${world.kind}) is not a first-party ` +
239
+ "mount — hoisting these to true Game ownership is a later slice's work; available only " +
240
+ 'via a first-party default world today');
241
+ }
242
+ /**
243
+ * Construct the (host-internal) Game shell. Callers: `createGameRuntime`
244
+ * builds this BEFORE mounting its one adapter, then registers the default
245
+ * three world once mount resolves (see `registerThreeRoot` in
246
+ * `create-runtime.ts`).
247
+ */
248
+ export function createGame(opts) {
249
+ const roots = [];
250
+ const profiler = createPerformanceProfiler();
251
+ const systems = createSystemRunner(profiler.systemObserver, 'game');
252
+ const input = new InputManager();
253
+ const stateBridge = createStateBridge();
254
+ // D15 (T-D15.1) — the game-scoped seeded-random surface every world's
255
+ // `ctx.random` aliases (see `setup-three-root-adapter.ts`'s `ctx.random =
256
+ // ...`, wired the same way `ctx.debug` is just below). Constructed
257
+ // unconditionally (cheap — a handful of closures) regardless of whether
258
+ // this project ever declares `determinism.seededRandom`; only the BOOT
259
+ // SEED and the enforcement (the burn-down scan, the trap right below) are
260
+ // conditional on that declaration.
261
+ const seededRandom = createSeededRandom(opts.seed ?? DEFAULT_SEEDED_RANDOM_SEED);
262
+ // D15 (T-D15.3) — the dev-mode `Math.random` phase trap. Constructed
263
+ // unconditionally too (disabled by default: `rngTrapEnabled` starts
264
+ // `false`, so `runFrameImpl`'s enable/disable calls below are no-ops) —
265
+ // the manifest-aware boot path flips `rngTrapControl.setEnabled(true)`
266
+ // once it resolves `manifest.determinism?.seededRandom` (mirrors
267
+ // `debugRegistry.setRoomDeclared` being flipped post-hoc from the same
268
+ // boot path for the very same "only the caller who read the manifest
269
+ // knows" reason).
270
+ const rngTrap = createGameplayRngTrap();
271
+ let rngTrapEnabled = false;
272
+ const rngTrapControl = {
273
+ setEnabled(enabled) {
274
+ rngTrapEnabled = enabled;
275
+ },
276
+ get enabled() {
277
+ return rngTrapEnabled;
278
+ },
279
+ };
280
+ // `tick` counts completed fixed substeps, `simT` accumulates their `dt` —
281
+ // both game-scoped, bumped ONLY where `stateBridge.bump()` is (guarded by
282
+ // `advanced`, `runFrame`'s tail below), so a paused/frozen frame never
283
+ // advances either. The debug registry reads them via suppliers (not by
284
+ // capturing the numbers now) so its built-in `time` provider always sees
285
+ // the CURRENT values.
286
+ let tick = 0;
287
+ let simT = 0;
288
+ // WO-P13 — the display-rate half. `renderAlpha` is the last alpha
289
+ // `runRenderFrameImpl` presented at (0 until a host drives one); the set is
290
+ // the `RenderStepped`-shaped registry `Game.onRenderStep` feeds. Deliberately
291
+ // NOT beside `tick`/`simT` in meaning: neither of these ever advances sim
292
+ // state, which is exactly what makes running them at the monitor's rate safe.
293
+ let renderAlpha = 0;
294
+ const renderStepCallbacks = new Set();
295
+ // P3 — the ONE sim clock this Game owns, declared beside the accumulator it
296
+ // is bound to (`flush(simT)` runs at the tail of `runFrameImpl`, inside the
297
+ // same `advanced` guard as the two bumps above, so a paused/frozen frame
298
+ // fires no timers). Every world's `ctx.clock` is THIS instance, reached the
299
+ // same way `ctx.random`/`ctx.debug` reach their game-scoped singletons —
300
+ // `getSimClock(host.game)` over the WeakMap filed below.
301
+ //
302
+ // The disposer is supplied HERE rather than inside the clock because
303
+ // `core/sim-clock.ts` deliberately knows nothing about Rapier or shared
304
+ // geometry. It mirrors `setup-three-root-adapter.ts`'s world teardown
305
+ // ordering for ONE subtree.
306
+ const simClock = createSimClock({
307
+ // Every mount that could own part of the subtree is offered it:
308
+ // `physics.get` is a no-op for a node the mount does not own, so this is
309
+ // correct with several three roots on one Game and needs no ownership
310
+ // bookkeeping.
311
+ dispose: (obj) => disposeDebrisSubtree(obj, roots.flatMap(debrisOwnersOf)),
312
+ });
313
+ const debugRegistry = createDebugRegistry({
314
+ getTick: () => tick,
315
+ getSimT: () => simT,
316
+ // D15/T-D15.5 — "the manifest's first/default world" for the debug
317
+ // registry's world-addressed input-target compatibility surface. Every
318
+ // first-party root now registers the SAME game-owned InputManager, but the
319
+ // stable default id still keeps explicit/implicit debug routing coherent.
320
+ // Use the SAME "first three world, else first world" rule
321
+ // `requireDefaultRoot` (declared just below — safe: this closure is
322
+ // only ever CALLED later, once at least one world has mounted) already
323
+ // defines for `Game.defaultRoot`.
324
+ getDefaultRootId: () => (roots.length > 0 ? requireDefaultRoot().id : null),
325
+ // Issue #175 — the built-in `time` provider's `loopLiveness` field reads
326
+ // the REAL loop, not any UI-level play-state store: `opts.loop` is the
327
+ // SAME `GameLoop` this `Game`'s own `.loop` field exposes, so this
328
+ // registry can never disagree with `game.loop.liveness` about whether
329
+ // the loop is actually ticking.
330
+ getLoopLiveness: () => opts.loop.liveness,
331
+ });
332
+ let inputFrameActive = false;
333
+ systems.add('input', () => {
334
+ if (inputFrameActive)
335
+ input.poll(debugRegistry.getGameTick());
336
+ }, { name: 'input.poll' });
337
+ input.setDebugEmit((event, detail) => debugRegistry.forRoot('(game)').emit(event, detail));
338
+ let inputMapPath = null;
339
+ let inputMapLoad = null;
340
+ function requireDefaultRoot() {
341
+ if (roots.length === 0) {
342
+ throw new Error('Game.defaultRoot: no roots registered yet');
343
+ }
344
+ return roots.find((w) => w.kind === 'three') ?? roots[0];
345
+ }
346
+ function requireFirstPartyCtx(handle) {
347
+ const world = requireDefaultRoot();
348
+ if (!isFirstPartyMounted(world.mounted)) {
349
+ throw new Error(describeMismatch(handle, world));
350
+ }
351
+ return world.mounted.ctx;
352
+ }
353
+ // --- Game.systemAdapters aggregation (§7.1-3, probe1) --------------------
354
+ // Warn-once-per-colliding-key state, scoped to this Game instance (a fresh
355
+ // Game gets a fresh warn history) — deliberately NOT reset by anything
356
+ // short of a new `createGame` call, matching `reportedGateShortfalls`
357
+ // above's "once per game instance" idiom.
358
+ const warnedSystemAdapterKeys = new Set();
359
+ const systemAdapterListeners = new Set();
360
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one cohesive merge-with-collision-report walk (per-world × per-key); splitting the collision-warn branch out would obscure that it's part of the same pass, not reduce real complexity
361
+ function computeSystemAdapters() {
362
+ // Debug is game-scoped: React hooks, probes, and the built-in time
363
+ // provider all register with the one registry created above, regardless
364
+ // of whether any mounted root happens to expose a `systems` object. A
365
+ // root may still publish this SAME adapter (the first-party Three/Pixi
366
+ // mounts do); the reference-equality branch below treats that as the
367
+ // intentional shared registration it is.
368
+ const result = { debug: debugRegistry.adapter };
369
+ const ownerRootId = new Map([['debug', '(game)']]);
370
+ for (const world of roots) {
371
+ const adapters = world.mounted.systems;
372
+ if (!adapters)
373
+ continue;
374
+ for (const key of Object.keys(adapters)) {
375
+ if (adapters[key] === undefined)
376
+ continue;
377
+ const existingOwner = ownerRootId.get(key);
378
+ if (existingOwner !== undefined) {
379
+ // Reference-equality short-circuit: two roots sharing the ONE
380
+ // game-scoped debug registry's adapter (T1.1) both expose the SAME
381
+ // object under `systems.debug` — that is by design, not a
382
+ // collision, so it must never warn.
383
+ if (result[key] === adapters[key])
384
+ continue;
385
+ if (!warnedSystemAdapterKeys.has(key)) {
386
+ warnedSystemAdapterKeys.add(key);
387
+ // biome-ignore lint/suspicious/noConsole: structured, greppable — mirrors this file's own reportGateShortfallOnce's deliberate direct console.warn just above
388
+ console.warn(`[game] systemAdapters: "${key}" is registered by both world "${existingOwner}" and ` +
389
+ `world "${world.id}" — the FIRST registration ("${existingOwner}") wins; the later ` +
390
+ 'one is shadowed (game-scoped system adapters).');
391
+ }
392
+ continue;
393
+ }
394
+ // biome-ignore lint/suspicious/noExplicitAny: SystemAdapters is a plain optional-field record; the per-key copy is correct by construction (same key on both sides), just not expressible without a cast
395
+ result[key] = adapters[key];
396
+ ownerRootId.set(key, world.id);
397
+ }
398
+ }
399
+ return result;
400
+ }
401
+ // --- D10/T7.6 play-state (Game.play) -------------------------------------
402
+ let paused = false;
403
+ // Dedupe loop-gate/audio-gate shortfall warnings to ONCE per (world, kind)
404
+ // — `pause()` fans out over every world every call; without this a
405
+ // multi-second play session would re-log the same "can't gate" shortfall
406
+ // every time the user hits Pause.
407
+ const reportedGateShortfalls = new Set();
408
+ function reportGateShortfallOnce(kind, worldId, reason) {
409
+ const key = `${kind}:${worldId}`;
410
+ if (reportedGateShortfalls.has(key))
411
+ return;
412
+ reportedGateShortfalls.add(key);
413
+ const message = kind === 'loop'
414
+ ? formatLoopGateMessage({ worldId, reason })
415
+ : formatAudioGateMessage({ worldId, reason });
416
+ // Other native `console.warn`/`console.error` call sites in this file are
417
+ // unsuppressed and already counted in the lint baseline (see `runFrame`'s
418
+ // impl below); this one is a NEW site, so it's suppressed to keep this
419
+ // task's diff at zero NEW warnings (same reasoning as `achieved-tier.ts`'s
420
+ // identical suppression).
421
+ // biome-ignore lint/suspicious/noConsole: see comment above
422
+ console.warn(message);
423
+ }
424
+ /** Fan out a loop-gate (self-driven roots) + audio-gate call over every
425
+ * `pausable` world, reporting honestly (once) wherever the capability is
426
+ * absent — shared by `pause()`/`resume()` below (same fan-out, opposite
427
+ * boolean). */
428
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: fans out TWO independent capability gates (loop, audio) with the same "call it, else report once" shape per world — splitting the two gates into separate loops would duplicate the fan-out, not reduce real complexity
429
+ function setRootGates(next) {
430
+ for (const world of roots) {
431
+ if (!world.pausable)
432
+ continue; // pausable:false roots are untouched by design
433
+ if (world.mounted.drivesOwnLoop) {
434
+ if (world.mounted.setPaused) {
435
+ world.mounted.setPaused(next);
436
+ }
437
+ else if (next) {
438
+ reportGateShortfallOnce('loop', world.id, 'self-driven world, adapter declares no setPaused capability');
439
+ }
440
+ }
441
+ const audio = world.mounted.systems?.audio;
442
+ if (audio) {
443
+ audio.setMuted(next);
444
+ }
445
+ else if (next) {
446
+ reportGateShortfallOnce('audio', world.id, 'no SystemAdapters.audio on this world');
447
+ }
448
+ }
449
+ }
450
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: the frame algorithm (now with D10's per-world pause gate + the onlyFrozen step()-only mode) is one cohesive nested loop over phases/roots — splitting it would obscure the ordering contract documented on GameInternal.runFrame
451
+ function runFrameImpl(dt, frameOpts) {
452
+ // D15 (T-D15.3) — brackets the ENTIRE frame body (every phase, every
453
+ // world, both the `onlyFrozen` and normal branches below converge on the
454
+ // single `profiler.endFrame()` at the tail) with zero reordering of the
455
+ // phase algorithm itself — a no-op pair of calls while
456
+ // `rngTrapEnabled` is false (the common case: most projects never
457
+ // declare `determinism.seededRandom`).
458
+ if (rngTrapEnabled)
459
+ rngTrap.enable();
460
+ profiler.beginFrame();
461
+ const ignorePause = frameOpts?.ignorePause ?? false;
462
+ // D15/T-D15.4 + WO-P13: the render-phase skip. Two callers set it, for the
463
+ // same reason — this substep is not the thing that paints. `runTicks`'s
464
+ // `render: 'none'|'last'` fast-forward sets it on every tick it doesn't
465
+ // want to paint (see `runTicks`'s doc comment on `GameInternal`), and a
466
+ // DISPLAY-RATE host sets it on every substep because `runRenderFrame`
467
+ // paints once per real frame instead. Unlike `onlyFrozen` below it is no
468
+ // longer internal-only: it is part of the public `GameInternal.runFrame`
469
+ // signature, since an external host is now one of those two callers.
470
+ const skipRenderPhases = frameOpts?.skipRenderPhases ?? false;
471
+ // `onlyFrozen` is internal-only (not part of the public `GameInternal.runFrame`
472
+ // signature — no external caller sets it) — `Game.play.step()` below is the
473
+ // one and only caller. It ticks EXACTLY the currently-frozen set (host-driven,
474
+ // `pausable`, and `paused`) through every phase + `endFrame`, with the REAL
475
+ // `dt` (not the render-phase's forced `0`), and touches no other world at all
476
+ // — §7.1-2 fix, probe4: the previous `ignorePause` full-frame re-run ticked
477
+ // EVERY host-driven world, double-ticking every already-running
478
+ // `pausable: false` world (D10's ordinary loop never stops ticking them).
479
+ const onlyFrozen = frameOpts?.onlyFrozen ?? false;
480
+ // Checklist item 7: snapshot the world count ONCE at entry and iterate
481
+ // by index in both loops below. A world registered mid-frame (e.g. from
482
+ // a game-scoped system's side effect) joins at the NEXT `runFrame` call,
483
+ // not this one — `for (const world of roots)` would otherwise pick up
484
+ // a world pushed during this very frame. This also drops the two
485
+ // per-phase `for...of` iterator allocations.
486
+ const n = roots.length;
487
+ inputFrameActive = false;
488
+ for (let i = 0; i < n; i++) {
489
+ const world = roots[i];
490
+ if (onlyFrozen) {
491
+ if (paused && world.pausable && !world.mounted.drivesOwnLoop) {
492
+ inputFrameActive = true;
493
+ break;
494
+ }
495
+ }
496
+ else if (ignorePause ||
497
+ (world.mounted.drivesOwnLoop
498
+ ? !paused || !world.pausable || !world.mounted.setPaused
499
+ : !paused || !world.pausable)) {
500
+ inputFrameActive = true;
501
+ break;
502
+ }
503
+ }
504
+ // §7.1-11 fix (probe5): `stateBridge.bump()` must fire iff at least one
505
+ // world actually advanced this call — not unconditionally. `advanced`
506
+ // covers rule (a) below (host-driven roots this call actually ticked);
507
+ // rule (b): a self-driven world that is RUNNING — which is every
508
+ // self-driven world while not paused, and, while paused, the ones the
509
+ // gate can't reach (`pausable: false`, or no `setPaused` capability) —
510
+ // checked once, up front, over the same `roots` array (no extra
511
+ // allocation, matching every other loop here). Rule (b) is skipped in
512
+ // `onlyFrozen` mode: a `step()` call bumps iff it ticked a frozen world
513
+ // — self-driven notifications belong to the loop's own `runFrame`s.
514
+ let advanced = false;
515
+ if (!onlyFrozen) {
516
+ for (let i = 0; i < n; i++) {
517
+ const world = roots[i];
518
+ if (!world.mounted.drivesOwnLoop)
519
+ continue;
520
+ if (!paused || !world.pausable || !world.mounted.setPaused) {
521
+ advanced = true;
522
+ break;
523
+ }
524
+ }
525
+ }
526
+ if (onlyFrozen) {
527
+ // step(): the frozen set is host-driven + pausable + currently paused.
528
+ // While NOT paused this set is empty by construction, so this whole
529
+ // branch is a natural no-op — `Game.play.step()`'s "no-op while
530
+ // running" behavior falls straight out of this, no separate guard
531
+ // needed.
532
+ for (const phase of PHASE_ORDER) {
533
+ profiler.beginPhase();
534
+ // Game-owned systems follow the same one-run-per-phase contract as a
535
+ // normal frame whenever Step advances at least one frozen world.
536
+ // In particular, the game-owned InputManager must poll before those
537
+ // worlds read actions; its matching endFrame remains at the shared
538
+ // frame tail below.
539
+ if (inputFrameActive)
540
+ systems.runPhase(phase, dt);
541
+ for (let i = 0; i < n; i++) {
542
+ const world = roots[i];
543
+ if (world.mounted.drivesOwnLoop)
544
+ continue;
545
+ if (!(paused && world.pausable))
546
+ continue; // only the frozen set
547
+ try {
548
+ world.frame?.runPhase(phase, dt);
549
+ }
550
+ catch (err) {
551
+ console.error(`[game] world "${world.id}" (kind: ${world.kind}) runPhase("${phase}") threw ` +
552
+ '(step()):', err);
553
+ }
554
+ }
555
+ profiler.endPhase(phase);
556
+ }
557
+ for (let i = 0; i < n; i++) {
558
+ const world = roots[i];
559
+ if (world.mounted.drivesOwnLoop)
560
+ continue;
561
+ if (!(paused && world.pausable))
562
+ continue;
563
+ advanced = true;
564
+ if (world.frame) {
565
+ try {
566
+ world.frame.endFrame?.();
567
+ }
568
+ catch (err) {
569
+ console.error(`[game] world "${world.id}" (kind: ${world.kind}) endFrame() threw (step()):`, err);
570
+ }
571
+ }
572
+ else {
573
+ try {
574
+ world.mounted.update?.(dt);
575
+ }
576
+ catch (err) {
577
+ console.error(`[game] world "${world.id}" (kind: ${world.kind}) update() threw (step()):`, err);
578
+ }
579
+ }
580
+ }
581
+ }
582
+ else {
583
+ for (const phase of PHASE_ORDER) {
584
+ profiler.beginPhase();
585
+ // D15/T-D15.4: `runTicks`'s fast-forward skip — `preRender`/`render`
586
+ // are the ONLY phases ever skipped this way (every earlier gameplay
587
+ // phase, and `endFrame` below, always run) — see `skipRenderPhases`'s
588
+ // declaration above and `runTicks`'s doc comment on `GameInternal`.
589
+ const skipThisPhase = skipRenderPhases && (phase === SystemPhase.PRE_RENDER || phase === SystemPhase.RENDER);
590
+ if (!skipThisPhase) {
591
+ systems.runPhase(phase, dt);
592
+ for (let i = 0; i < n; i++) {
593
+ const world = roots[i];
594
+ if (world.mounted.drivesOwnLoop)
595
+ continue;
596
+ // D10/T7.6: a `pausable` world under an active (non-ignored) pause
597
+ // skips every phase except `render` — its render still runs, every
598
+ // substep, but with `dt` forced to `0` (deterministic: no
599
+ // time-based render effect silently keeps animating a "frozen"
600
+ // scene). A `pausable: false` world (or any world while
601
+ // `ignorePause`) is unaffected.
602
+ const frozen = !ignorePause && paused && world.pausable;
603
+ if (frozen && phase !== SystemPhase.RENDER)
604
+ continue;
605
+ const phaseDt = frozen ? 0 : dt;
606
+ // Checklist item 2: isolate each world's per-phase work — one
607
+ // world's `runPhase` throwing must not starve sibling roots still
608
+ // due this phase, nor abort the frame. Mirrors `runOne`'s style in
609
+ // `core/system-runner.ts` (loud console.error, never swallowed).
610
+ try {
611
+ world.frame?.runPhase(phase, phaseDt);
612
+ }
613
+ catch (err) {
614
+ console.error(`[game] world "${world.id}" (kind: ${world.kind}) runPhase("${phase}") threw:`, err);
615
+ }
616
+ }
617
+ }
618
+ profiler.endPhase(phase);
619
+ }
620
+ for (let i = 0; i < n; i++) {
621
+ const world = roots[i];
622
+ if (world.mounted.drivesOwnLoop)
623
+ continue;
624
+ // A fully-frozen world gets no `endFrame`/opaque-`update` call either —
625
+ // there is nothing to "end the frame" of when nothing ran this substep.
626
+ const frozen = !ignorePause && paused && world.pausable;
627
+ if (frozen)
628
+ continue;
629
+ advanced = true;
630
+ if (world.frame) {
631
+ try {
632
+ world.frame.endFrame?.();
633
+ }
634
+ catch (err) {
635
+ console.error(`[game] world "${world.id}" (kind: ${world.kind}) endFrame() threw:`, err);
636
+ }
637
+ }
638
+ else {
639
+ // Checklist item 2: same isolation for the opaque-world fallback —
640
+ // one foreign mount's `update` throwing must not starve its
641
+ // siblings' `endFrame`/`update` calls this same loop.
642
+ try {
643
+ world.mounted.update?.(dt);
644
+ }
645
+ catch (err) {
646
+ console.error(`[game] world "${world.id}" (kind: ${world.kind}) update() threw:`, err);
647
+ }
648
+ }
649
+ }
650
+ }
651
+ // T7.4 slice 1: bump + notify LAST, after
652
+ // every phase of every world and every world's endFrame/update above —
653
+ // subscribers must only ever observe post-frame state. Bumped at most
654
+ // once per completed `runFrame`/`step()` call, never per phase/world —
655
+ // and, per §7.1-11's fix, only when `advanced` (see above) is true: a
656
+ // fully-gated paused game produces no notifications at all.
657
+ if (inputFrameActive)
658
+ input.endFrame();
659
+ if (advanced) {
660
+ stateBridge.bump();
661
+ tick++;
662
+ simT += dt;
663
+ // P3 — sim timers fire IMMEDIATELY after the bump, inside this same
664
+ // `advanced` guard: a paused/frozen world advances no sim time, so it
665
+ // must fire no timers either, and `Game.play.step()` (which reaches this
666
+ // block with `advanced` set by the frozen-set loop above) fires exactly
667
+ // the timers that ONE substep makes due. One flush === one completed
668
+ // substep, which is what lets `SimClock.tickNow()` simply count flushes
669
+ // and always agree with `tick`.
670
+ simClock.flush(simT);
671
+ }
672
+ profiler.endFrame();
673
+ if (rngTrapEnabled)
674
+ rngTrap.disable();
675
+ }
676
+ /**
677
+ * WO-P13 — one display frame's PRESENTATION pass. See
678
+ * `GameInternal.runRenderFrame`'s doc comment for the contract; this is the
679
+ * `preRender`+`render` slice of `runFrameImpl`'s phase loop, lifted out and
680
+ * driven by the loop's own per-real-frame callback instead of by the substep
681
+ * loop.
682
+ *
683
+ * Three deliberate parallels with `runFrameImpl`, so the two passes cannot
684
+ * drift into different rules for the same situation:
685
+ * - game-scoped systems run before per-world hooks, per phase;
686
+ * - one world's throw is logged and isolated, never allowed to starve a
687
+ * sibling still due this phase;
688
+ * - the RNG trap brackets the whole pass, so `Math.random` called from a
689
+ * render-phase system is still caught by D15's dev-mode trap — it was,
690
+ * back when this pass lived inside `runFrame`, and moving code must not
691
+ * quietly move it out from under a guard.
692
+ *
693
+ * Profiler note (a real, accepted consequence): the profiler's "frame" has
694
+ * always meant "one `runFrame` call", so a display frame now produces the
695
+ * substep records it always did PLUS one record for this pass, carrying the
696
+ * `preRender`/`render` spans and the renderer counters. Re-modelling the
697
+ * profiler's frame boundary around the display frame is a separate change to
698
+ * a debugging surface, not part of flipping the loop.
699
+ */
700
+ function runRenderFrameImpl(alpha, displayDt) {
701
+ renderAlpha = alpha;
702
+ if (rngTrapEnabled)
703
+ rngTrap.enable();
704
+ profiler.beginFrame();
705
+ // `RenderStepped` first: a camera adjusted here is drawn by THIS frame's
706
+ // render phase, not next frame's. Iterated over a snapshot so a callback
707
+ // that unsubscribes itself (or registers another) cannot mutate the set
708
+ // mid-iteration.
709
+ if (renderStepCallbacks.size > 0) {
710
+ for (const fn of [...renderStepCallbacks]) {
711
+ try {
712
+ fn(alpha, displayDt);
713
+ }
714
+ catch (err) {
715
+ console.error('[game] an onRenderStep callback threw:', err);
716
+ }
717
+ }
718
+ }
719
+ const n = roots.length;
720
+ for (const phase of DISPLAY_RATE_PHASES) {
721
+ profiler.beginPhase();
722
+ systems.runPhase(phase, displayDt);
723
+ for (let i = 0; i < n; i++) {
724
+ const world = roots[i];
725
+ if (world.mounted.drivesOwnLoop)
726
+ continue;
727
+ // D10/T7.6, carried over verbatim in substance: a frozen world still
728
+ // renders (a paused viewport stays painted) but with `dt` forced to
729
+ // `0`, and skips every other phase.
730
+ const frozen = paused && world.pausable;
731
+ if (frozen && phase !== SystemPhase.RENDER)
732
+ continue;
733
+ try {
734
+ world.frame?.runPhase(phase, frozen ? 0 : displayDt);
735
+ }
736
+ catch (err) {
737
+ console.error(`[game] world "${world.id}" (kind: ${world.kind}) runPhase("${phase}") threw ` +
738
+ '(display frame):', err);
739
+ }
740
+ }
741
+ profiler.endPhase(phase);
742
+ }
743
+ profiler.endFrame();
744
+ if (rngTrapEnabled)
745
+ rngTrap.disable();
746
+ }
747
+ const gameInternal = {
748
+ loop: opts.loop,
749
+ assets: opts.assets,
750
+ playtest: opts.playtest ?? null,
751
+ profiler,
752
+ systems,
753
+ get roots() {
754
+ return roots;
755
+ },
756
+ world(id) {
757
+ return roots.find((w) => w.id === id) ?? null;
758
+ },
759
+ get defaultRoot() {
760
+ return requireDefaultRoot();
761
+ },
762
+ get systemAdapters() {
763
+ return computeSystemAdapters();
764
+ },
765
+ subscribeSystemAdapters(listener) {
766
+ systemAdapterListeners.add(listener);
767
+ return () => systemAdapterListeners.delete(listener);
768
+ },
769
+ notifySystemAdaptersChanged() {
770
+ for (const listener of systemAdapterListeners)
771
+ listener();
772
+ },
773
+ input,
774
+ get audio() {
775
+ return requireFirstPartyCtx('audio').audio;
776
+ },
777
+ state: stateBridge,
778
+ play: {
779
+ get paused() {
780
+ return paused;
781
+ },
782
+ pause() {
783
+ if (paused)
784
+ return;
785
+ paused = true;
786
+ setRootGates(true);
787
+ },
788
+ resume() {
789
+ if (!paused)
790
+ return;
791
+ paused = false;
792
+ setRootGates(false);
793
+ },
794
+ step(dt = 1 / 60) {
795
+ // Self-driven pausable roots advance via their adapter's `step()`
796
+ // capability — but only the ones that are actually FROZEN: the game
797
+ // must be paused, and the world must have been gate-able in the
798
+ // first place (`setPaused` present — a world the gate couldn't reach
799
+ // never stopped, so "stepping" it would double-tick a still-running
800
+ // loop, the same §7.1-2 class as the host-driven fix below). While
801
+ // not paused, nothing here runs — step() is a whole-call no-op.
802
+ if (paused) {
803
+ for (const world of roots) {
804
+ if (!world.pausable || !world.mounted.drivesOwnLoop)
805
+ continue;
806
+ if (!world.mounted.setPaused)
807
+ continue; // never gated — still running
808
+ if (world.mounted.step) {
809
+ world.mounted.step();
810
+ }
811
+ else {
812
+ reportGateShortfallOnce('loop', world.id, 'self-driven world, adapter declares no step capability');
813
+ }
814
+ }
815
+ }
816
+ // §7.1-2 fix (probe4): tick EXACTLY the frozen (host-driven, pausable,
817
+ // paused) set — not `{ ignorePause: true }`, which re-ran a FULL
818
+ // extra frame for every host-driven world (including already-running
819
+ // `pausable: false` ones — a double-tick, since D10's ordinary loop
820
+ // never stops ticking them). See `runFrameImpl`'s `onlyFrozen` mode.
821
+ runFrameImpl(dt, { onlyFrozen: true });
822
+ },
823
+ },
824
+ registerRoot(world) {
825
+ if (roots.some((w) => w.id === world.id)) {
826
+ throw new Error(`Game.registerRoot: duplicate world id "${world.id}"`);
827
+ }
828
+ // Checklist item 6: the SAME `mounted` object registered under two
829
+ // world ids would be double-ticked by `runFrame` (its `frame.runPhase`/
830
+ // `endFrame` called once per registration) and double-dispose-wrapped
831
+ // (`createRootInstance` wraps `mounted.dispose` in place — a second
832
+ // wrap would flip `disposed` and call through on ITS OWN wrapped
833
+ // `originalDispose`, which is harmless today only by accident of
834
+ // `SetupThreeRootAdapter.dispose` being idempotent; a foreign adapter
835
+ // has no such guarantee). Reject it outright instead.
836
+ if (roots.some((w) => w.mounted === world.mounted)) {
837
+ throw new Error(`Game.registerRoot: world "${world.id}" shares its \`mounted\` object with an ` +
838
+ `already-registered world ("${roots.find((w) => w.mounted === world.mounted).id}") — ` +
839
+ 'the same mount cannot be registered twice.');
840
+ }
841
+ roots.push(world);
842
+ gameInternal.notifySystemAdaptersChanged();
843
+ // Notify state-bridge subscribers THE INSTANT the world list changes —
844
+ // not just at the next completed `runFrame`. Root-cause fix for the
845
+ // "roots: " (empty) hang in `36-r3f-first-party.spec.ts` on CI (gate
846
+ // 4): `mountAllRootSpecs` (`create-runtime.ts`) mounts roots
847
+ // SEQUENTIALLY, and a react world's `adapter.mount()`
848
+ // (`resolveDefaultReactAdapter`/`mountOneReactRoot`) renders its tree —
849
+ // synchronously in some React builds, but React 19's concurrent
850
+ // renderer does NOT guarantee a synchronous first commit (see
851
+ // `r3f-adapter.tsx`'s own doc comment on `onCreated`) — BEFORE the
852
+ // caller calls `registerRoot` for that very world. A `useWorldState`
853
+ // selector reading `g.roots` can therefore render for the first time
854
+ // while `roots` is still missing entries that register moments later.
855
+ // Previously the ONLY way such a subscriber ever saw the corrected
856
+ // value was the state bridge's next `bump()`, which fires exclusively
857
+ // from `GameInternal.runFrame`'s tail — i.e. only once the host's loop
858
+ // has actually ticked (`started = true; loop.start()` in
859
+ // `create-runtime.ts`, itself gated on EVERY world finishing its
860
+ // mount). On a slow/contended host (CI's 4-core SwiftShader runners)
861
+ // that first tick can be delayed well past a test's assertion window,
862
+ // or — if the tab is ever backgrounded — not fire at all for a long
863
+ // stretch; the subscriber's cached snapshot then sits on its stale
864
+ // (possibly fully empty) first render for that whole time, matching the
865
+ // observed CI symptom exactly ("the HUD element IS mounted; the roots
866
+ // list is EMPTY"). Proven red-then-green by
867
+ // `packages/engine/test/state-bridge.test.ts`'s "registerRoot notifies
868
+ // subscribers immediately" case: a subscriber registered before a
869
+ // world, with `runFrame` NEVER called, only saw the update after this
870
+ // fix. `bump()` also advances `frameVersion` (the only invalidation key
871
+ // `createFrameSelectorCache`/`useWorldState` understand — see
872
+ // `frame-selector-cache.ts`), so this doubles as "frameVersion is
873
+ // bumped once per completed runFrame OR once per world registered",
874
+ // documented on `GameStateBridge.frameVersion` below.
875
+ stateBridge.bump();
876
+ // T7.4 slice 2: a NON-first-party mount
877
+ // (an ingested/foreign world — first-party mounts are exempt, they're
878
+ // observed via `Game.state`/`useWorldState` instead) with no `observe`
879
+ // has no state bridge at all — react HUDs cannot subscribe to it, and
880
+ // silently returning `undefined` forever would hide that. Report ONCE
881
+ // per mount, at registration time, matching this file's existing
882
+ // `[game] world "<id>" (kind: <kind>) ...` console idiom (see
883
+ // `runFrame` below).
884
+ //
885
+ // §7.1-15: a `kind: 'dom'` world is ALSO exempt — it has no `observe`
886
+ // BY DESIGN (D8: a react world's own mounted tree
887
+ // reads state via `Game.state`/`useWorldState`, the SAME first-party
888
+ // bridge a three/canvas world's HUD uses, never `RootStateObserver`
889
+ // — that hook is scoped to the ingested/foreign-world case). Without
890
+ // this exemption every production react world logged a false-positive
891
+ // "no state bridge" warning at registration (probe: every real react
892
+ // world mount), eroding the signal for a genuinely un-observable
893
+ // ingested world.
894
+ // A HOST-DRIVEN mount (`drivesOwnLoop: false`, notably the R3F adapter)
895
+ // is observable through `Game.state` even though it does not carry the
896
+ // classic scene adapter's `firstParty` brand: it ticks inside
897
+ // `runFrame`, so the frame-versioned bridge covers it. Requiring a
898
+ // foreign-world `observe` bridge there produces a false warning while
899
+ // the canonical bridge is already live.
900
+ if (!isFirstPartyMounted(world.mounted) &&
901
+ world.mounted.drivesOwnLoop &&
902
+ world.kind !== 'dom' &&
903
+ !world.mounted.observe) {
904
+ console.warn(`[game] world "${world.id}" (kind: ${world.kind}, adapter: "${world.adapter.id}"): ` +
905
+ 'no state bridge — this mounted game has no `observe` (RootStateObserver); ' +
906
+ 'react HUDs/useRootObservation cannot subscribe to its state.');
907
+ }
908
+ },
909
+ runFrame(dt, frameOpts) {
910
+ runFrameImpl(dt, frameOpts);
911
+ },
912
+ runRenderFrame(alpha, displayDt) {
913
+ runRenderFrameImpl(alpha, displayDt);
914
+ },
915
+ get renderAlpha() {
916
+ return renderAlpha;
917
+ },
918
+ onRenderStep(fn, stepOpts) {
919
+ const signal = stepOpts?.signal;
920
+ // Already-aborted is a no-op registration, never a throw — the same
921
+ // `AbortSignal` contract `SimClock.after` honors.
922
+ if (signal?.aborted)
923
+ return () => { };
924
+ renderStepCallbacks.add(fn);
925
+ let detachAbort = null;
926
+ const unsubscribe = () => {
927
+ renderStepCallbacks.delete(fn);
928
+ detachAbort?.();
929
+ detachAbort = null;
930
+ };
931
+ if (signal) {
932
+ signal.addEventListener('abort', unsubscribe, { once: true });
933
+ detachAbort = () => signal.removeEventListener('abort', unsubscribe);
934
+ }
935
+ return unsubscribe;
936
+ },
937
+ runTicks(n, ticksOpts) {
938
+ if (!Number.isInteger(n) || n < 0) {
939
+ throw new RangeError(`Game.runTicks: n must be a non-negative integer, got ${n}`);
940
+ }
941
+ // Refuse while paused (D15 §2.b): stepping the frozen set is
942
+ // `Game.play.step()`'s contract, not this one's — see `runTicks`'s doc
943
+ // comment on `GameInternal` above.
944
+ if (paused) {
945
+ throw new DebugError('RUN_TICKS_PAUSED', 'Game.runTicks: refused — Game.play.paused is true; stepping the frozen set is ' +
946
+ "Game.play.step()'s contract, not runTicks'");
947
+ }
948
+ const render = ticksOpts?.render ?? 'last';
949
+ const fixedDt = opts.loop.fixedDt;
950
+ for (let i = 0; i < n; i++) {
951
+ const isFinalTick = i === n - 1;
952
+ const skipRenderPhases = render === 'all' ? false : render === 'none' ? true : !isFinalTick;
953
+ runFrameImpl(fixedDt, { skipRenderPhases });
954
+ }
955
+ },
956
+ loadInputMap(path) {
957
+ if (inputMapPath && inputMapPath !== path) {
958
+ throw new Error(`Game.loadInputMap: input map is already "${inputMapPath}"; root requested "${path}". ` +
959
+ 'Input is game-owned, so roots cannot load competing maps.');
960
+ }
961
+ inputMapPath = path;
962
+ inputMapLoad ??= input.loadMap(path);
963
+ return inputMapLoad;
964
+ },
965
+ dispose() {
966
+ // P3 — the sim clock is GAME-scoped, so this is the only correct place to
967
+ // dispose it: `create-runtime.ts`'s `fullCleanup` calls us after EVERY
968
+ // root's `mounted.dispose()`, whereas a per-root teardown may be ending
969
+ // just one sub-session while sibling roots keep running (see the
970
+ // disposed-world guard in `runFrame`). It used to be disposed from
971
+ // `setup-three-root-adapter.ts`'s teardown, which meant disposing one of
972
+ // two three roots froze `now()` for the whole Game, rejected the other
973
+ // world's pending `delay`s and turned its `after()` calls into silent
974
+ // no-ops. Note the asymmetry that gives the bug away: `seededRandom` and
975
+ // `debugRegistry` are game-scoped too, and per-root teardown has never
976
+ // destroyed either — only ever `strip(this.id)`, its own slice.
977
+ simClock.dispose();
978
+ input.dispose();
979
+ debugRegistry.strip();
980
+ },
981
+ };
982
+ // Filed AFTER the shell exists (the WeakMap keys on the Game object
983
+ // itself) so `getDebugRegistry(game)` — the react hooks' and any later
984
+ // consumer's reach-in — works from the moment `createGame` returns.
985
+ registerDebugRegistry(gameInternal, debugRegistry);
986
+ // D15/T-D15.4: wire the run-ticks target the instant the Game shell exists
987
+ // (unlike `setVirtualInputTarget`, which waits for a per-world mount, a
988
+ // Game's own `runTicks` needs nothing else) — this is what makes
989
+ // `window.__vgai.runTicks` (`debug-bridge.ts`) and the editor relay's
990
+ // `run-ticks` case reach the SAME implementation `game.runTicks` above is.
991
+ debugRegistry.setRunTicksTarget({ runTicks: gameInternal.runTicks });
992
+ // D15 (T-D15.1/.3) — same "file after the shell exists" ordering as the
993
+ // debug registry above: `getSeededRandom(game)`/`getGameplayRngTrapControl
994
+ // (game)` (per-world `ctx.random` wiring, and the manifest-aware boot
995
+ // path's post-hoc `setEnabled` call) both work from the moment
996
+ // `createGame` returns.
997
+ registerSeededRandom(gameInternal, seededRandom);
998
+ registerGameplayRngTrapControl(gameInternal, rngTrapControl);
999
+ // P3 — same "file after the shell exists" ordering: `getSimClock(game)` is
1000
+ // how every world's mount resolves `ctx.clock` to THIS game's one clock.
1001
+ registerSimClock(gameInternal, simClock);
1002
+ return gameInternal;
1003
+ }