@vgai/engine 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (541) hide show
  1. package/README.md +3 -1
  2. package/dist/adapter/authoring.d.ts +404 -0
  3. package/dist/adapter/authoring.d.ts.map +1 -0
  4. package/dist/adapter/authoring.js +22 -0
  5. package/dist/adapter/colyseus-networking-adapter.d.ts +43 -0
  6. package/dist/adapter/colyseus-networking-adapter.d.ts.map +1 -0
  7. package/dist/adapter/colyseus-networking-adapter.js +38 -0
  8. package/dist/adapter/first-party-systems.d.ts +38 -0
  9. package/dist/adapter/first-party-systems.d.ts.map +1 -0
  10. package/dist/adapter/first-party-systems.js +77 -0
  11. package/dist/adapter/game-adapter.d.ts +133 -0
  12. package/dist/adapter/game-adapter.d.ts.map +1 -0
  13. package/dist/adapter/game-adapter.js +10 -0
  14. package/dist/adapter/host-context.d.ts +73 -0
  15. package/dist/adapter/host-context.d.ts.map +1 -0
  16. package/dist/adapter/host-context.js +16 -0
  17. package/dist/adapter/index.d.ts +22 -0
  18. package/dist/adapter/index.d.ts.map +1 -0
  19. package/dist/adapter/index.js +15 -0
  20. package/dist/adapter/ingest/game-contract.d.ts +53 -0
  21. package/dist/adapter/ingest/game-contract.d.ts.map +1 -0
  22. package/dist/adapter/ingest/game-contract.js +30 -0
  23. package/dist/adapter/ingest/overlay-applier.d.ts +118 -0
  24. package/dist/adapter/ingest/overlay-applier.d.ts.map +1 -0
  25. package/dist/adapter/ingest/overlay-applier.js +132 -0
  26. package/dist/adapter/ingest/overlay-apply.d.ts +60 -0
  27. package/dist/adapter/ingest/overlay-apply.d.ts.map +1 -0
  28. package/dist/adapter/ingest/overlay-apply.js +101 -0
  29. package/dist/adapter/ingest/overlay-file.d.ts +79 -0
  30. package/dist/adapter/ingest/overlay-file.d.ts.map +1 -0
  31. package/dist/adapter/ingest/overlay-file.js +66 -0
  32. package/dist/adapter/ingest/overlay-report.d.ts +113 -0
  33. package/dist/adapter/ingest/overlay-report.d.ts.map +1 -0
  34. package/dist/adapter/ingest/overlay-report.js +103 -0
  35. package/dist/adapter/ingest/scene-capture.d.ts +97 -0
  36. package/dist/adapter/ingest/scene-capture.d.ts.map +1 -0
  37. package/dist/adapter/ingest/scene-capture.js +248 -0
  38. package/dist/adapter/ingest/upstream-pin.d.ts +47 -0
  39. package/dist/adapter/ingest/upstream-pin.d.ts.map +1 -0
  40. package/dist/adapter/ingest/upstream-pin.js +50 -0
  41. package/dist/adapter/loop-gate-report.d.ts +34 -0
  42. package/dist/adapter/loop-gate-report.d.ts.map +1 -0
  43. package/dist/adapter/loop-gate-report.js +37 -0
  44. package/dist/adapter/rapier-physics-adapter.d.ts +12 -0
  45. package/dist/adapter/rapier-physics-adapter.d.ts.map +1 -0
  46. package/dist/adapter/rapier-physics-adapter.js +46 -0
  47. package/dist/adapter/system-adapter.d.ts +133 -0
  48. package/dist/adapter/system-adapter.d.ts.map +1 -0
  49. package/dist/adapter/system-adapter.js +13 -0
  50. package/dist/adapter/transform.d.ts +18 -0
  51. package/dist/adapter/transform.d.ts.map +1 -0
  52. package/dist/adapter/transform.js +1 -0
  53. package/dist/adapter/vgai-scene-game-adapter.d.ts +116 -0
  54. package/dist/adapter/vgai-scene-game-adapter.d.ts.map +1 -0
  55. package/dist/adapter/vgai-scene-game-adapter.js +714 -0
  56. package/dist/adapter/world-kind.d.ts +30 -0
  57. package/dist/adapter/world-kind.d.ts.map +1 -0
  58. package/dist/adapter/world-kind.js +18 -0
  59. package/dist/ai/navigation.d.ts +64 -0
  60. package/dist/ai/navigation.d.ts.map +1 -0
  61. package/dist/ai/navigation.js +133 -0
  62. package/dist/animation/anim-graph-types.d.ts +25 -0
  63. package/dist/animation/anim-graph-types.d.ts.map +1 -0
  64. package/dist/animation/anim-graph-types.js +1 -0
  65. package/dist/animation/animation-clock.d.ts +213 -0
  66. package/dist/animation/animation-clock.d.ts.map +1 -0
  67. package/dist/animation/animation-clock.js +301 -0
  68. package/dist/animation/blend-node.d.ts +22 -0
  69. package/dist/animation/blend-node.d.ts.map +1 -0
  70. package/dist/animation/blend-node.js +97 -0
  71. package/dist/animation/camera-ownership.d.ts +250 -0
  72. package/dist/animation/camera-ownership.d.ts.map +1 -0
  73. package/dist/animation/camera-ownership.js +169 -0
  74. package/dist/animation/cinematic-cues.d.ts +200 -0
  75. package/dist/animation/cinematic-cues.d.ts.map +1 -0
  76. package/dist/animation/cinematic-cues.js +213 -0
  77. package/dist/animation/clip-map.d.ts +12 -0
  78. package/dist/animation/clip-map.d.ts.map +1 -0
  79. package/dist/animation/clip-map.js +37 -0
  80. package/dist/animation/gsap-registration.d.ts +119 -0
  81. package/dist/animation/gsap-registration.d.ts.map +1 -0
  82. package/dist/animation/gsap-registration.js +92 -0
  83. package/dist/animation/theatre-clock-binding.d.ts +89 -0
  84. package/dist/animation/theatre-clock-binding.d.ts.map +1 -0
  85. package/dist/animation/theatre-clock-binding.js +29 -0
  86. package/dist/animation/theatre-director.d.ts +242 -0
  87. package/dist/animation/theatre-director.d.ts.map +1 -0
  88. package/dist/animation/theatre-director.js +235 -0
  89. package/dist/animation/theatre-object-binding.d.ts +305 -0
  90. package/dist/animation/theatre-object-binding.d.ts.map +1 -0
  91. package/dist/animation/theatre-object-binding.js +351 -0
  92. package/dist/animation/xstate-animation-binding.d.ts +111 -0
  93. package/dist/animation/xstate-animation-binding.d.ts.map +1 -0
  94. package/dist/animation/xstate-animation-binding.js +307 -0
  95. package/dist/animation/xstate-animation-meta.d.ts +228 -0
  96. package/dist/animation/xstate-animation-meta.d.ts.map +1 -0
  97. package/dist/animation/xstate-animation-meta.js +221 -0
  98. package/dist/assets.d.ts +26 -0
  99. package/dist/assets.d.ts.map +1 -0
  100. package/dist/assets.js +55 -0
  101. package/dist/audio/index.d.ts +4 -0
  102. package/dist/audio/index.d.ts.map +1 -0
  103. package/dist/audio/index.js +24 -0
  104. package/dist/audio/tone-clock-binding.d.ts +75 -0
  105. package/dist/audio/tone-clock-binding.d.ts.map +1 -0
  106. package/dist/audio/tone-clock-binding.js +29 -0
  107. package/dist/audio/tone-context.d.ts +114 -0
  108. package/dist/audio/tone-context.d.ts.map +1 -0
  109. package/dist/audio/tone-context.js +39 -0
  110. package/dist/audio/tone-offline-render.d.ts +90 -0
  111. package/dist/audio/tone-offline-render.d.ts.map +1 -0
  112. package/dist/audio/tone-offline-render.js +56 -0
  113. package/dist/audio/wav-encode.d.ts +38 -0
  114. package/dist/audio/wav-encode.d.ts.map +1 -0
  115. package/dist/audio/wav-encode.js +97 -0
  116. package/dist/core/game-loop.d.ts +21 -0
  117. package/dist/core/game-loop.d.ts.map +1 -0
  118. package/dist/core/game-loop.js +130 -0
  119. package/dist/core/system-runner.d.ts +120 -0
  120. package/dist/core/system-runner.d.ts.map +1 -0
  121. package/dist/core/system-runner.js +286 -0
  122. package/dist/core/types.d.ts +58 -0
  123. package/dist/core/types.d.ts.map +1 -0
  124. package/dist/core/types.js +25 -0
  125. package/dist/data/data-asset.d.ts +101 -0
  126. package/dist/data/data-asset.d.ts.map +1 -0
  127. package/dist/data/data-asset.js +122 -0
  128. package/dist/data/data-check-core.d.ts +110 -0
  129. package/dist/data/data-check-core.d.ts.map +1 -0
  130. package/dist/data/data-check-core.js +178 -0
  131. package/dist/data/data-ref.d.ts +79 -0
  132. package/dist/data/data-ref.d.ts.map +1 -0
  133. package/dist/data/data-ref.js +130 -0
  134. package/dist/data/vite-plugin-data.d.ts +79 -0
  135. package/dist/data/vite-plugin-data.d.ts.map +1 -0
  136. package/dist/data/vite-plugin-data.js +217 -0
  137. package/dist/dev/console-bridge.d.ts +22 -0
  138. package/dist/dev/console-bridge.d.ts.map +1 -0
  139. package/dist/dev/console-bridge.js +71 -0
  140. package/dist/dev/debug-draw.d.ts +24 -0
  141. package/dist/dev/debug-draw.d.ts.map +1 -0
  142. package/dist/dev/debug-draw.js +73 -0
  143. package/dist/dev/logger.d.ts +30 -0
  144. package/dist/dev/logger.d.ts.map +1 -0
  145. package/dist/dev/logger.js +90 -0
  146. package/dist/ecs/component-manager.d.ts +189 -0
  147. package/dist/ecs/component-manager.d.ts.map +1 -0
  148. package/dist/ecs/component-manager.js +676 -0
  149. package/dist/ecs/game-component.d.ts +97 -0
  150. package/dist/ecs/game-component.d.ts.map +1 -0
  151. package/dist/ecs/game-component.js +80 -0
  152. package/dist/ecs/hmr-swap-report.d.ts +53 -0
  153. package/dist/ecs/hmr-swap-report.d.ts.map +1 -0
  154. package/dist/ecs/hmr-swap-report.js +55 -0
  155. package/dist/index.d.ts +37 -0
  156. package/dist/index.d.ts.map +1 -0
  157. package/dist/index.js +30 -0
  158. package/dist/input/input-manager.d.ts +596 -0
  159. package/dist/input/input-manager.d.ts.map +1 -0
  160. package/dist/input/input-manager.js +1564 -0
  161. package/dist/input/input-types.d.ts +171 -0
  162. package/dist/input/input-types.d.ts.map +1 -0
  163. package/dist/input/input-types.js +1 -0
  164. package/dist/input/prompt-labels.d.ts +21 -0
  165. package/dist/input/prompt-labels.d.ts.map +1 -0
  166. package/dist/input/prompt-labels.js +121 -0
  167. package/dist/input/rebind-controller.d.ts +59 -0
  168. package/dist/input/rebind-controller.d.ts.map +1 -0
  169. package/dist/input/rebind-controller.js +85 -0
  170. package/dist/input/schema.d.ts +16 -0
  171. package/dist/input/schema.d.ts.map +1 -0
  172. package/dist/input/schema.js +228 -0
  173. package/dist/loader.d.ts +43 -0
  174. package/dist/loader.d.ts.map +1 -0
  175. package/dist/loader.js +59 -0
  176. package/dist/manifest/index.d.ts +5 -0
  177. package/dist/manifest/index.d.ts.map +1 -0
  178. package/dist/manifest/index.js +8 -0
  179. package/dist/manifest/load-file.d.ts +4 -0
  180. package/dist/manifest/load-file.d.ts.map +1 -0
  181. package/dist/manifest/load-file.js +14 -0
  182. package/dist/manifest/load.d.ts +117 -0
  183. package/dist/manifest/load.d.ts.map +1 -0
  184. package/dist/manifest/load.js +255 -0
  185. package/dist/manifest/schema.d.ts +173 -0
  186. package/dist/manifest/schema.d.ts.map +1 -0
  187. package/dist/manifest/schema.js +302 -0
  188. package/dist/physics/collision-system.d.ts +41 -0
  189. package/dist/physics/collision-system.d.ts.map +1 -0
  190. package/dist/physics/collision-system.js +64 -0
  191. package/dist/physics/physics-registry.d.ts +45 -0
  192. package/dist/physics/physics-registry.d.ts.map +1 -0
  193. package/dist/physics/physics-registry.js +65 -0
  194. package/dist/physics/transform-writer.d.ts +16 -0
  195. package/dist/physics/transform-writer.d.ts.map +1 -0
  196. package/dist/physics/transform-writer.js +38 -0
  197. package/dist/physics/trigger-dispatch.d.ts +24 -0
  198. package/dist/physics/trigger-dispatch.d.ts.map +1 -0
  199. package/dist/physics/trigger-dispatch.js +80 -0
  200. package/dist/react/game-state.d.ts +105 -0
  201. package/dist/react/game-state.d.ts.map +1 -0
  202. package/dist/react/game-state.js +144 -0
  203. package/dist/react/use-data.d.ts +60 -0
  204. package/dist/react/use-data.d.ts.map +1 -0
  205. package/dist/react/use-data.js +96 -0
  206. package/dist/react/use-selection.d.ts +75 -0
  207. package/dist/react/use-selection.d.ts.map +1 -0
  208. package/dist/react/use-selection.js +90 -0
  209. package/dist/render/auto-batcher.d.ts +34 -0
  210. package/dist/render/auto-batcher.d.ts.map +1 -0
  211. package/dist/render/auto-batcher.js +140 -0
  212. package/dist/render/render-batch-system.d.ts +33 -0
  213. package/dist/render/render-batch-system.d.ts.map +1 -0
  214. package/dist/render/render-batch-system.js +230 -0
  215. package/dist/render/render-features.d.ts +47 -0
  216. package/dist/render/render-features.d.ts.map +1 -0
  217. package/dist/render/render-features.js +105 -0
  218. package/dist/render/render-settings.d.ts +36 -0
  219. package/dist/render/render-settings.d.ts.map +1 -0
  220. package/dist/render/render-settings.js +51 -0
  221. package/dist/runtime/create-runtime.d.ts +280 -0
  222. package/dist/runtime/create-runtime.d.ts.map +1 -0
  223. package/dist/runtime/create-runtime.js +795 -0
  224. package/dist/runtime/frame-selector-cache.d.ts +41 -0
  225. package/dist/runtime/frame-selector-cache.d.ts.map +1 -0
  226. package/dist/runtime/frame-selector-cache.js +65 -0
  227. package/dist/runtime/game.d.ts +433 -0
  228. package/dist/runtime/game.d.ts.map +1 -0
  229. package/dist/runtime/game.js +569 -0
  230. package/dist/runtime/input-router.d.ts +78 -0
  231. package/dist/runtime/input-router.d.ts.map +1 -0
  232. package/dist/runtime/input-router.js +171 -0
  233. package/dist/runtime/mount-game.d.ts +129 -0
  234. package/dist/runtime/mount-game.d.ts.map +1 -0
  235. package/dist/runtime/mount-game.js +152 -0
  236. package/dist/runtime/mount-manifest.d.ts +119 -0
  237. package/dist/runtime/mount-manifest.d.ts.map +1 -0
  238. package/dist/runtime/mount-manifest.js +204 -0
  239. package/dist/runtime/render-audio-control.d.ts +110 -0
  240. package/dist/runtime/render-audio-control.d.ts.map +1 -0
  241. package/dist/runtime/render-audio-control.js +111 -0
  242. package/dist/runtime/render-control.d.ts +291 -0
  243. package/dist/runtime/render-control.d.ts.map +1 -0
  244. package/dist/runtime/render-control.js +264 -0
  245. package/dist/runtime/render-seed.d.ts +57 -0
  246. package/dist/runtime/render-seed.d.ts.map +1 -0
  247. package/dist/runtime/render-seed.js +74 -0
  248. package/dist/runtime/scene-ui-bridge.d.ts +69 -0
  249. package/dist/runtime/scene-ui-bridge.d.ts.map +1 -0
  250. package/dist/runtime/scene-ui-bridge.js +23 -0
  251. package/dist/runtime/scene-ui-data.d.ts +13 -0
  252. package/dist/runtime/scene-ui-data.d.ts.map +1 -0
  253. package/dist/runtime/scene-ui-data.js +92 -0
  254. package/dist/runtime/state-bridge.d.ts +47 -0
  255. package/dist/runtime/state-bridge.d.ts.map +1 -0
  256. package/dist/runtime/state-bridge.js +53 -0
  257. package/dist/runtime/types.d.ts +179 -0
  258. package/dist/runtime/types.d.ts.map +1 -0
  259. package/dist/runtime/types.js +1 -0
  260. package/dist/scene/asset-loaders.d.ts +79 -0
  261. package/dist/scene/asset-loaders.d.ts.map +1 -0
  262. package/dist/scene/asset-loaders.js +141 -0
  263. package/dist/scene/asset-paths.d.ts +11 -0
  264. package/dist/scene/asset-paths.d.ts.map +1 -0
  265. package/dist/scene/asset-paths.js +108 -0
  266. package/dist/scene/asset-ref-check.d.ts +88 -0
  267. package/dist/scene/asset-ref-check.d.ts.map +1 -0
  268. package/dist/scene/asset-ref-check.js +214 -0
  269. package/dist/scene/asset-registry.d.ts +30 -0
  270. package/dist/scene/asset-registry.d.ts.map +1 -0
  271. package/dist/scene/asset-registry.js +50 -0
  272. package/dist/scene/collider-dimensions.d.ts +63 -0
  273. package/dist/scene/collider-dimensions.d.ts.map +1 -0
  274. package/dist/scene/collider-dimensions.js +98 -0
  275. package/dist/scene/component-registry.d.ts +17 -0
  276. package/dist/scene/component-registry.d.ts.map +1 -0
  277. package/dist/scene/component-registry.js +28 -0
  278. package/dist/scene/defaults.d.ts +252 -0
  279. package/dist/scene/defaults.d.ts.map +1 -0
  280. package/dist/scene/defaults.js +151 -0
  281. package/dist/scene/geometries/index.d.ts +8 -0
  282. package/dist/scene/geometries/index.d.ts.map +1 -0
  283. package/dist/scene/geometries/index.js +7 -0
  284. package/dist/scene/geometries/terrain.d.ts +2 -0
  285. package/dist/scene/geometries/terrain.d.ts.map +1 -0
  286. package/dist/scene/geometries/terrain.js +37 -0
  287. package/dist/scene/geometry-registry.d.ts +24 -0
  288. package/dist/scene/geometry-registry.d.ts.map +1 -0
  289. package/dist/scene/geometry-registry.js +20 -0
  290. package/dist/scene/instance-registry.d.ts +36 -0
  291. package/dist/scene/instance-registry.d.ts.map +1 -0
  292. package/dist/scene/instance-registry.js +55 -0
  293. package/dist/scene/instancers/grid.d.ts +2 -0
  294. package/dist/scene/instancers/grid.d.ts.map +1 -0
  295. package/dist/scene/instancers/grid.js +36 -0
  296. package/dist/scene/instancers/index.d.ts +8 -0
  297. package/dist/scene/instancers/index.d.ts.map +1 -0
  298. package/dist/scene/instancers/index.js +7 -0
  299. package/dist/scene/light-camera-factory.d.ts +23 -0
  300. package/dist/scene/light-camera-factory.d.ts.map +1 -0
  301. package/dist/scene/light-camera-factory.js +68 -0
  302. package/dist/scene/material-factory.d.ts +17 -0
  303. package/dist/scene/material-factory.d.ts.map +1 -0
  304. package/dist/scene/material-factory.js +197 -0
  305. package/dist/scene/material-registry.d.ts +40 -0
  306. package/dist/scene/material-registry.d.ts.map +1 -0
  307. package/dist/scene/material-registry.js +44 -0
  308. package/dist/scene/materials/index.d.ts +8 -0
  309. package/dist/scene/materials/index.d.ts.map +1 -0
  310. package/dist/scene/materials/index.js +7 -0
  311. package/dist/scene/materials/water.d.ts +2 -0
  312. package/dist/scene/materials/water.d.ts.map +1 -0
  313. package/dist/scene/materials/water.js +54 -0
  314. package/dist/scene/parse.d.ts +23 -0
  315. package/dist/scene/parse.d.ts.map +1 -0
  316. package/dist/scene/parse.js +179 -0
  317. package/dist/scene/particles-factory.d.ts +33 -0
  318. package/dist/scene/particles-factory.d.ts.map +1 -0
  319. package/dist/scene/particles-factory.js +282 -0
  320. package/dist/scene/scene-apply.d.ts +109 -0
  321. package/dist/scene/scene-apply.d.ts.map +1 -0
  322. package/dist/scene/scene-apply.js +326 -0
  323. package/dist/scene/scene-diff-schema.d.ts +285 -0
  324. package/dist/scene/scene-diff-schema.d.ts.map +1 -0
  325. package/dist/scene/scene-diff-schema.js +92 -0
  326. package/dist/scene/scene-diff-types.d.ts +40 -0
  327. package/dist/scene/scene-diff-types.d.ts.map +1 -0
  328. package/dist/scene/scene-diff-types.js +14 -0
  329. package/dist/scene/scene-loader.d.ts +177 -0
  330. package/dist/scene/scene-loader.d.ts.map +1 -0
  331. package/dist/scene/scene-loader.js +1213 -0
  332. package/dist/scene/scene-query.d.ts +11 -0
  333. package/dist/scene/scene-query.d.ts.map +1 -0
  334. package/dist/scene/scene-query.js +63 -0
  335. package/dist/scene/scene-types.d.ts +10 -0
  336. package/dist/scene/scene-types.d.ts.map +1 -0
  337. package/dist/scene/scene-types.js +8 -0
  338. package/dist/scene/scene-version.d.ts +26 -0
  339. package/dist/scene/scene-version.d.ts.map +1 -0
  340. package/dist/scene/scene-version.js +34 -0
  341. package/dist/scene/schema/animation.d.ts +8 -0
  342. package/dist/scene/schema/animation.d.ts.map +1 -0
  343. package/dist/scene/schema/animation.js +42 -0
  344. package/dist/scene/schema/audio.d.ts +13 -0
  345. package/dist/scene/schema/audio.d.ts.map +1 -0
  346. package/dist/scene/schema/audio.js +22 -0
  347. package/dist/scene/schema/camera.d.ts +28 -0
  348. package/dist/scene/schema/camera.d.ts.map +1 -0
  349. package/dist/scene/schema/camera.js +17 -0
  350. package/dist/scene/schema/collider.d.ts +40 -0
  351. package/dist/scene/schema/collider.d.ts.map +1 -0
  352. package/dist/scene/schema/collider.js +55 -0
  353. package/dist/scene/schema/entity-ref.d.ts +31 -0
  354. package/dist/scene/schema/entity-ref.d.ts.map +1 -0
  355. package/dist/scene/schema/entity-ref.js +61 -0
  356. package/dist/scene/schema/entity.d.ts +1036 -0
  357. package/dist/scene/schema/entity.d.ts.map +1 -0
  358. package/dist/scene/schema/entity.js +153 -0
  359. package/dist/scene/schema/environment.d.ts +425 -0
  360. package/dist/scene/schema/environment.d.ts.map +1 -0
  361. package/dist/scene/schema/environment.js +357 -0
  362. package/dist/scene/schema/index.d.ts +39 -0
  363. package/dist/scene/schema/index.d.ts.map +1 -0
  364. package/dist/scene/schema/index.js +20 -0
  365. package/dist/scene/schema/instances.d.ts +14 -0
  366. package/dist/scene/schema/instances.d.ts.map +1 -0
  367. package/dist/scene/schema/instances.js +28 -0
  368. package/dist/scene/schema/joint.d.ts +23 -0
  369. package/dist/scene/schema/joint.d.ts.map +1 -0
  370. package/dist/scene/schema/joint.js +23 -0
  371. package/dist/scene/schema/light.d.ts +38 -0
  372. package/dist/scene/schema/light.d.ts.map +1 -0
  373. package/dist/scene/schema/light.js +26 -0
  374. package/dist/scene/schema/material.d.ts +199 -0
  375. package/dist/scene/schema/material.d.ts.map +1 -0
  376. package/dist/scene/schema/material.js +99 -0
  377. package/dist/scene/schema/mesh.d.ts +48 -0
  378. package/dist/scene/schema/mesh.d.ts.map +1 -0
  379. package/dist/scene/schema/mesh.js +82 -0
  380. package/dist/scene/schema/particles.d.ts +2178 -0
  381. package/dist/scene/schema/particles.d.ts.map +1 -0
  382. package/dist/scene/schema/particles.js +321 -0
  383. package/dist/scene/schema/physics.d.ts +56 -0
  384. package/dist/scene/schema/physics.d.ts.map +1 -0
  385. package/dist/scene/schema/physics.js +37 -0
  386. package/dist/scene/schema/scene-file.d.ts +304 -0
  387. package/dist/scene/schema/scene-file.d.ts.map +1 -0
  388. package/dist/scene/schema/scene-file.js +262 -0
  389. package/dist/scene/schema/shadow.d.ts +28 -0
  390. package/dist/scene/schema/shadow.d.ts.map +1 -0
  391. package/dist/scene/schema/shadow.js +20 -0
  392. package/dist/scene/schema/spline.d.ts +25 -0
  393. package/dist/scene/schema/spline.d.ts.map +1 -0
  394. package/dist/scene/schema/spline.js +15 -0
  395. package/dist/scene/schema/tuples.d.ts +12 -0
  396. package/dist/scene/schema/tuples.d.ts.map +1 -0
  397. package/dist/scene/schema/tuples.js +14 -0
  398. package/dist/scene/schema/ui.d.ts +880 -0
  399. package/dist/scene/schema/ui.d.ts.map +1 -0
  400. package/dist/scene/schema/ui.js +506 -0
  401. package/dist/scene/user-data.d.ts +171 -0
  402. package/dist/scene/user-data.d.ts.map +1 -0
  403. package/dist/scene/user-data.js +137 -0
  404. package/dist/setup/setup-audio.d.ts +20 -0
  405. package/dist/setup/setup-audio.d.ts.map +1 -0
  406. package/dist/setup/setup-audio.js +41 -0
  407. package/dist/setup/setup-particles.d.ts +17 -0
  408. package/dist/setup/setup-particles.d.ts.map +1 -0
  409. package/dist/setup/setup-particles.js +16 -0
  410. package/dist/setup/setup-physics.d.ts +25 -0
  411. package/dist/setup/setup-physics.d.ts.map +1 -0
  412. package/dist/setup/setup-physics.js +51 -0
  413. package/dist/setup/setup-renderer.d.ts +69 -0
  414. package/dist/setup/setup-renderer.d.ts.map +1 -0
  415. package/dist/setup/setup-renderer.js +363 -0
  416. package/dist/tools/define-tool.d.ts +105 -0
  417. package/dist/tools/define-tool.d.ts.map +1 -0
  418. package/dist/tools/define-tool.js +82 -0
  419. package/dist/world2d/authoring-2d.d.ts +76 -0
  420. package/dist/world2d/authoring-2d.d.ts.map +1 -0
  421. package/dist/world2d/authoring-2d.js +176 -0
  422. package/dist/world2d/capture-to-scene2d.d.ts +5 -0
  423. package/dist/world2d/capture-to-scene2d.d.ts.map +1 -0
  424. package/dist/world2d/capture-to-scene2d.js +49 -0
  425. package/dist/world2d/collision-2d.d.ts +52 -0
  426. package/dist/world2d/collision-2d.d.ts.map +1 -0
  427. package/dist/world2d/collision-2d.js +70 -0
  428. package/dist/world2d/components-2d.d.ts +47 -0
  429. package/dist/world2d/components-2d.d.ts.map +1 -0
  430. package/dist/world2d/components-2d.js +77 -0
  431. package/dist/world2d/index.d.ts +34 -0
  432. package/dist/world2d/index.d.ts.map +1 -0
  433. package/dist/world2d/index.js +32 -0
  434. package/dist/world2d/ingest-iframe-2d.d.ts +82 -0
  435. package/dist/world2d/ingest-iframe-2d.d.ts.map +1 -0
  436. package/dist/world2d/ingest-iframe-2d.js +166 -0
  437. package/dist/world2d/ingest2d.d.ts +97 -0
  438. package/dist/world2d/ingest2d.d.ts.map +1 -0
  439. package/dist/world2d/ingest2d.js +40 -0
  440. package/dist/world2d/physics2d-registry.d.ts +24 -0
  441. package/dist/world2d/physics2d-registry.d.ts.map +1 -0
  442. package/dist/world2d/physics2d-registry.js +38 -0
  443. package/dist/world2d/pixi-game-adapter.d.ts +107 -0
  444. package/dist/world2d/pixi-game-adapter.d.ts.map +1 -0
  445. package/dist/world2d/pixi-game-adapter.js +211 -0
  446. package/dist/world2d/pixi-surface.d.ts +39 -0
  447. package/dist/world2d/pixi-surface.d.ts.map +1 -0
  448. package/dist/world2d/pixi-surface.js +47 -0
  449. package/dist/world2d/scene-capture-2d.d.ts +43 -0
  450. package/dist/world2d/scene-capture-2d.d.ts.map +1 -0
  451. package/dist/world2d/scene-capture-2d.js +92 -0
  452. package/dist/world2d/scene2d-loader.d.ts +27 -0
  453. package/dist/world2d/scene2d-loader.d.ts.map +1 -0
  454. package/dist/world2d/scene2d-loader.js +274 -0
  455. package/dist/world2d/schema/entity2d.d.ts +102 -0
  456. package/dist/world2d/schema/entity2d.d.ts.map +1 -0
  457. package/dist/world2d/schema/entity2d.js +119 -0
  458. package/dist/world2d/schema/physics2d.d.ts +46 -0
  459. package/dist/world2d/schema/physics2d.d.ts.map +1 -0
  460. package/dist/world2d/schema/physics2d.js +45 -0
  461. package/dist/world2d/schema/sprite.d.ts +42 -0
  462. package/dist/world2d/schema/sprite.d.ts.map +1 -0
  463. package/dist/world2d/schema/sprite.js +63 -0
  464. package/dist/world2d/schema/tilemap.d.ts +15 -0
  465. package/dist/world2d/schema/tilemap.d.ts.map +1 -0
  466. package/dist/world2d/schema/tilemap.js +19 -0
  467. package/dist/world2d/schema/tuples2d.d.ts +20 -0
  468. package/dist/world2d/schema/tuples2d.d.ts.map +1 -0
  469. package/dist/world2d/schema/tuples2d.js +19 -0
  470. package/dist/world2d/system-adapters-2d.d.ts +16 -0
  471. package/dist/world2d/system-adapters-2d.d.ts.map +1 -0
  472. package/dist/world2d/system-adapters-2d.js +37 -0
  473. package/dist/world2d/transform-writer-2d.d.ts +16 -0
  474. package/dist/world2d/transform-writer-2d.d.ts.map +1 -0
  475. package/dist/world2d/transform-writer-2d.js +22 -0
  476. package/dist/world2d/types.d.ts +53 -0
  477. package/dist/world2d/types.d.ts.map +1 -0
  478. package/dist/world2d/types.js +1 -0
  479. package/package.json +18 -4
  480. package/src/adapter/authoring.ts +10 -0
  481. package/src/adapter/first-party-systems.ts +23 -34
  482. package/src/adapter/index.ts +1 -3
  483. package/src/adapter/system-adapter.ts +15 -22
  484. package/src/adapter/vgai-scene-game-adapter.ts +5 -14
  485. package/src/animation/anim-graph-types.ts +12 -43
  486. package/src/animation/animation-clock.ts +479 -0
  487. package/src/animation/camera-ownership.ts +467 -0
  488. package/src/animation/cinematic-cues.ts +451 -0
  489. package/src/animation/clip-map.ts +41 -0
  490. package/src/animation/gsap-registration.ts +184 -0
  491. package/src/animation/theatre-clock-binding.ts +111 -0
  492. package/src/animation/theatre-director.ts +347 -0
  493. package/src/animation/theatre-object-binding.ts +661 -0
  494. package/src/animation/xstate-animation-binding.ts +398 -0
  495. package/src/animation/xstate-animation-meta.ts +282 -0
  496. package/src/audio/index.ts +39 -7
  497. package/src/audio/tone-clock-binding.ts +98 -0
  498. package/src/audio/tone-context.ts +129 -0
  499. package/src/audio/tone-offline-render.ts +167 -0
  500. package/src/audio/wav-encode.ts +119 -0
  501. package/src/core/game-loop.ts +17 -0
  502. package/src/core/types.ts +13 -0
  503. package/src/data/data-asset.ts +167 -0
  504. package/src/data/data-check-core.ts +242 -0
  505. package/src/data/data-ref.ts +145 -0
  506. package/src/data/vite-plugin-data.ts +290 -0
  507. package/src/index.ts +48 -0
  508. package/src/input/input-manager.ts +1206 -32
  509. package/src/input/input-types.ts +155 -3
  510. package/src/input/prompt-labels.ts +122 -0
  511. package/src/input/rebind-controller.ts +105 -0
  512. package/src/input/schema.ts +206 -52
  513. package/src/react/use-data.ts +114 -0
  514. package/src/react/use-selection.tsx +135 -0
  515. package/src/runtime/create-runtime.ts +30 -9
  516. package/src/runtime/render-audio-control.ts +168 -0
  517. package/src/runtime/render-control.ts +522 -0
  518. package/src/runtime/render-seed.ts +79 -0
  519. package/src/runtime/types.ts +0 -5
  520. package/src/scene/asset-loaders.ts +10 -36
  521. package/src/scene/asset-paths.ts +0 -2
  522. package/src/scene/asset-ref-check.ts +248 -0
  523. package/src/scene/parse.ts +133 -0
  524. package/src/scene/scene-loader.ts +81 -96
  525. package/src/scene/schema/animation.ts +30 -79
  526. package/src/scene/schema/entity.ts +20 -0
  527. package/src/scene/schema/index.ts +2 -12
  528. package/src/scene/user-data.ts +8 -9
  529. package/src/setup/setup-renderer.ts +9 -2
  530. package/src/tools/define-tool.ts +152 -0
  531. package/src/animation/anim-graph.ts +0 -406
  532. package/src/animation/anim-system.ts +0 -28
  533. package/src/animation/property-track.ts +0 -178
  534. package/src/animation/schema.ts +0 -204
  535. package/src/audio/ambient.ts +0 -300
  536. package/src/audio/impacts.ts +0 -212
  537. package/src/audio/movement.ts +0 -140
  538. package/src/audio/musical.ts +0 -200
  539. package/src/audio/ui-sounds.ts +0 -171
  540. package/src/audio/vehicle.ts +0 -235
  541. package/src/audio/weapons.ts +0 -152
@@ -0,0 +1,1213 @@
1
+ import * as THREE from 'three';
2
+ import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
3
+ import { initNavigation, NavMeshManager } from '../ai/navigation';
4
+ import { buildClipMap } from '../animation/clip-map';
5
+ import { log } from '../dev/logger';
6
+ import { loadingManager, resolveUrl } from '../loader';
7
+ import { resolveRenderSettings } from '../render/render-settings';
8
+ import { assetCacheSizes, clearAssetCaches as clearSharedAssetCaches, loadGLTF, resolveGltfNode, } from './asset-loaders';
9
+ import { getAssetMeta, loadRegistry } from './asset-registry';
10
+ import { computeColliderWorldDimensions, computeColliderWorldOffset } from './collider-dimensions';
11
+ import { applyComponents } from './component-registry';
12
+ import { DEFAULTS } from './defaults';
13
+ import './geometries';
14
+ import './instancers';
15
+ import { buildInstancedMesh, buildInstancedMeshFromTuples } from './instance-registry';
16
+ import { createCamera, createLight } from './light-camera-factory';
17
+ import { createGeometry, createMaterial } from './material-factory';
18
+ import './materials';
19
+ import { parseInstancesFile, parseMaterialFile, parsePrefabFile, parseSceneFile } from './parse';
20
+ import { createParticleSystemFromData } from './particles-factory';
21
+ import { mergePrefabInstance, } from './scene-types';
22
+ import { isEntityRefSchema } from './schema/entity-ref';
23
+ import { getUserData, setUserData } from './user-data';
24
+ // GLTF / texture load+cache logic lives in the shared ./asset-loaders module
25
+ // (A2) so the editor and runtime share one cache + one clone-safety contract.
26
+ // Only the IBL/skybox env-map cache stays here — it needs a WebGLRenderer
27
+ // (PMREM), which the shared module deliberately doesn't depend on.
28
+ const envTextureCache = new Map();
29
+ let pmremGenerator = null;
30
+ /**
31
+ * Clear module-level asset caches (textures, GLTF scenes, IBL env maps). Call
32
+ * on full runtime teardown to release cached GPU resources and avoid leaking
33
+ * across editor Play sessions. Delegates the GLTF/texture caches to
34
+ * {@link clearSharedAssetCaches} and additionally frees the local
35
+ * IBL env-map cache + PMREM generator.
36
+ *
37
+ * Lifetime / P1.8: these caches are intentionally module-level and persist
38
+ * ACROSS hot-reloads / scene switches (they are NOT cleared on every
39
+ * loadSceneFromData). This is deliberate: (1) it makes repeated Play→Stop→Play
40
+ * cheap, and (2) it underpins the `__sharedGeometry` contract from P0.2 —
41
+ * GLTF clones share the cached source geometry, so clearing mid-session would
42
+ * orphan live clones. The caches are keyed by URL, so loading the SAME scene N
43
+ * times is bounded (one entry per distinct asset URL, not per load). They are
44
+ * only released here, on full runtime teardown (`fullCleanup`).
45
+ */
46
+ export function clearAssetCaches() {
47
+ clearSharedAssetCaches();
48
+ envTextureCache.clear();
49
+ if (pmremGenerator) {
50
+ pmremGenerator.dispose();
51
+ pmremGenerator = null;
52
+ }
53
+ }
54
+ /**
55
+ * F7 — per-frame LOD tick. THREE.LOD only swaps its active level when
56
+ * `.update(camera)` runs, so every `THREE.LOD` in the scene graph (built by
57
+ * the `mesh.lod` spawn branch above) needs this called once a frame. Mirrors
58
+ * `tickCustomMaterials`'s traverse-and-tick shape; wired into the runtime's
59
+ * `preRender` system in `vgai-scene-game-adapter.ts`.
60
+ */
61
+ export function updateSceneLODs(scene, camera) {
62
+ scene.traverse((o) => {
63
+ if (o.isLOD)
64
+ o.update(camera);
65
+ });
66
+ }
67
+ /**
68
+ * Test-only: current sizes of the module-level asset caches. Used by the P1.8
69
+ * headless test to assert caches stay bounded (one entry per distinct asset URL)
70
+ * across repeated loads of the same scene, rather than growing per load.
71
+ */
72
+ export function __assetCacheSizesForTest() {
73
+ const sizes = assetCacheSizes();
74
+ return {
75
+ textures: sizes.textures,
76
+ gltf: sizes.gltf,
77
+ env: envTextureCache.size,
78
+ };
79
+ }
80
+ /**
81
+ * Load an equirectangular HDR/EXR/image as a PMREM-processed IBL environment
82
+ * map and assign it to `scene.background` + `scene.environment`. Mirrors the
83
+ * editor's scene-sync `applyEnvironment`/`loadEnvTexture` so the runtime renders
84
+ * the same skybox the editor previews. Requires a WebGLRenderer for PMREM; in
85
+ * headless (no renderer) this is skipped and no placeholder is set.
86
+ */
87
+ function applySkybox(skybox, env, ctx) {
88
+ const renderer = ctx.renderer;
89
+ if (!renderer)
90
+ return;
91
+ const cached = envTextureCache.get(skybox);
92
+ if (cached) {
93
+ ctx.scene.background = cached;
94
+ ctx.scene.environment = cached;
95
+ ctx.scene.environmentIntensity = env.envMapIntensity ?? DEFAULTS.environment.envMapIntensity;
96
+ return;
97
+ }
98
+ loadEnvTexture(skybox, renderer)
99
+ .then((texture) => {
100
+ if (!texture)
101
+ return;
102
+ envTextureCache.set(skybox, texture);
103
+ ctx.scene.background = texture;
104
+ ctx.scene.environment = texture;
105
+ ctx.scene.environmentIntensity = env.envMapIntensity ?? DEFAULTS.environment.envMapIntensity;
106
+ })
107
+ .catch(() => {
108
+ // HDRI load failed — leave background unset rather than show a wrong sky.
109
+ });
110
+ }
111
+ async function loadEnvTexture(path, renderer) {
112
+ try {
113
+ if (!pmremGenerator) {
114
+ pmremGenerator = new THREE.PMREMGenerator(renderer);
115
+ pmremGenerator.compileEquirectangularShader();
116
+ }
117
+ const ext = path.split('.').pop()?.toLowerCase();
118
+ const rawTexture = ext === 'hdr'
119
+ ? await new RGBELoader(loadingManager).loadAsync(resolveUrl(path))
120
+ : await new THREE.TextureLoader(loadingManager).loadAsync(resolveUrl(path));
121
+ rawTexture.mapping = THREE.EquirectangularReflectionMapping;
122
+ const envMap = pmremGenerator.fromEquirectangular(rawTexture).texture;
123
+ rawTexture.dispose();
124
+ return envMap;
125
+ }
126
+ catch {
127
+ return null;
128
+ }
129
+ }
130
+ /** Build a Three.js CatmullRomCurve3 from a spline descriptor (≥2 points). */
131
+ function createCurveFromSpline(spline) {
132
+ if (spline.points.length < 2)
133
+ return null;
134
+ const points = spline.points.map((p) => new THREE.Vector3(p[0], p[1], p[2]));
135
+ return new THREE.CatmullRomCurve3(points, spline.closed ?? false, spline.curveType, spline.tension ?? 0.5);
136
+ }
137
+ /**
138
+ * Loads a .vscn.json file and instantiates all entities.
139
+ *
140
+ * Creates Three.js objects and Rapier bodies. The Object3D IS the entity —
141
+ * there is no separate ECS id. Uses raw Three.js/Rapier calls — no abstraction.
142
+ *
143
+ * Returns a SceneInstance that must be ticked each frame (for GLTF animations).
144
+ */
145
+ export async function loadScene(url, ctx) {
146
+ const res = await fetch(resolveUrl(url));
147
+ // Checklist item 9 (audit residual — the editor-side equivalent was fixed
148
+ // under T1.12): a 404/500 response body is typically HTML, not JSON — feed
149
+ // it straight to `res.json()` and the failure surfaces as an opaque
150
+ // "Unexpected token '<'" JSON parse error instead of naming the URL/status
151
+ // that actually failed. Throw loudly here instead.
152
+ if (!res.ok) {
153
+ throw new Error(`Failed to load scene: ${url} (${res.status} ${res.statusText})`);
154
+ }
155
+ const data = parseSceneFile(await res.json(), url);
156
+ return instantiateScene(data, ctx, url);
157
+ }
158
+ /**
159
+ * Instantiate a scene from an already-parsed SceneFile.
160
+ *
161
+ * Same as loadScene but skips the fetch — useful when the scene data is
162
+ * already in memory (e.g. serialized from the editor for play-mode preview).
163
+ */
164
+ export async function loadSceneFromData(data, ctx) {
165
+ return instantiateScene(data, ctx);
166
+ }
167
+ const ROOT_ANCESTOR_CONTEXT = { worldMatrix: new THREE.Matrix4() };
168
+ /** Human-readable entity identifier for error messages (loud-failure naming). */
169
+ function entityLabel(def) {
170
+ return def.id ? `${def.name} (id: "${def.id}")` : def.name;
171
+ }
172
+ // ---------------------------------------------------------------------------
173
+ // T3.9 — bounded-concurrency asset prefetch.
174
+ //
175
+ // Child/asset fetches (prefab files, materials, GLTF models) used to only
176
+ // ever happen inline, one at a time, inside the fully-sequential
177
+ // `spawnEntity` recursion — fine at ~20 assets, product-breaking at hundreds
178
+ // (every fetch's network round-trip is paid serially). Rather than restructure
179
+ // the deterministic sequential spawn walk itself (which authored array order,
180
+ // tickFns/mixers order, and GameComponent registration order all depend on),
181
+ // this prefetch pass walks the WHOLE entity tree up front and warms the
182
+ // existing URL-keyed caches (asset-loaders.ts's module-level GLTF cache, and
183
+ // this module's ctx-level prefab/material file caches) with a
184
+ // small concurrency limiter. The sequential spawn pass below is completely
185
+ // unchanged — its `await`s just resolve instantly against a warm cache, so
186
+ // wall-clock time is bounded by ceil(assetCount / limit) round-trips instead
187
+ // of assetCount. Prefetching is a pure optimization: any error here is
188
+ // swallowed and re-surfaced normally (with a real error) by the sequential
189
+ // pass that actually consumes the data.
190
+ // ---------------------------------------------------------------------------
191
+ const PREFETCH_CONCURRENCY = 8;
192
+ /**
193
+ * Tiny concurrency-limited task runner — no new dependency. Runs `tasks` with
194
+ * at most `limit` in flight at once. Tasks may push MORE tasks onto the same
195
+ * array while running (each worker re-checks `tasks.length` every loop), which
196
+ * is how nested prefab expansion gets picked up without a second pass.
197
+ */
198
+ async function runLimited(tasks, limit) {
199
+ let next = 0;
200
+ async function worker() {
201
+ for (;;) {
202
+ const i = next++;
203
+ if (i >= tasks.length)
204
+ return;
205
+ try {
206
+ await tasks[i]();
207
+ }
208
+ catch {
209
+ // Swallow — prefetching is an optimization; the real (sequential)
210
+ // spawn pass re-fetches and surfaces genuine load errors normally.
211
+ }
212
+ }
213
+ }
214
+ const workerCount = Math.min(limit, tasks.length);
215
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
216
+ }
217
+ /**
218
+ * Recursively collect asset-prefetch tasks for an entity subtree (T3.9).
219
+ * Best-effort: walks JSON-prefab expansion (fetch the `.prefab.json`, merge,
220
+ * recurse into the merged root) so a prefab's OWN nested asset refs get
221
+ * warmed too. Any inaccuracy here (e.g. a merge that would actually throw)
222
+ * only costs a bit of parallelism, never correctness — errors are swallowed
223
+ * by `runLimited` and the real spawn pass is unaffected.
224
+ */
225
+ function collectPrefetchTasks(def, ctx, tasks, seenPrefabUrls) {
226
+ if (def.mesh?.type === 'gltf' && def.mesh.src) {
227
+ const src = def.mesh.src;
228
+ tasks.push(() => loadGLTF(src));
229
+ }
230
+ if (def.materialRef) {
231
+ const ref = def.materialRef;
232
+ tasks.push(() => loadMaterialFile(ref, ctx));
233
+ }
234
+ if (def.prefab && !ctx.prefabs?.has(def.prefab) && !seenPrefabUrls.has(def.prefab)) {
235
+ const prefabUrl = def.prefab;
236
+ seenPrefabUrls.add(prefabUrl);
237
+ tasks.push(async () => {
238
+ const prefabDef = await loadPrefabFile(prefabUrl, ctx);
239
+ const merged = mergePrefabInstance(prefabDef.root, def);
240
+ collectPrefetchTasks(merged, ctx, tasks, seenPrefabUrls);
241
+ });
242
+ }
243
+ if (def.children) {
244
+ for (const child of def.children)
245
+ collectPrefetchTasks(child, ctx, tasks, seenPrefabUrls);
246
+ }
247
+ }
248
+ async function instantiateScene(data, ctx, url) {
249
+ const tickFns = [];
250
+ const mixers = [];
251
+ // Ensure asset registry is loaded before spawning entities
252
+ await loadRegistry();
253
+ // T3.9 — warm asset caches for the whole tree with bounded concurrency
254
+ // before the deterministic sequential spawn pass below.
255
+ const prefetchTasks = [];
256
+ const seenPrefabUrls = new Set();
257
+ for (const entityDef of data.entities) {
258
+ collectPrefetchTasks(entityDef, ctx, prefetchTasks, seenPrefabUrls);
259
+ }
260
+ await runLimited(prefetchTasks, PREFETCH_CONCURRENCY);
261
+ // F7 — populate the scene-level render-feature scope once, before any entity
262
+ // spawns, so the mesh.lod master-gate check (resolveRenderSettings) below has
263
+ // it available regardless of whether `data.environment` is set.
264
+ if (data.environment?.rendering) {
265
+ ctx.sceneRendering = data.environment.rendering;
266
+ }
267
+ // Environment
268
+ if (data.environment) {
269
+ const env = data.environment;
270
+ if (env.ambient) {
271
+ const light = new THREE.AmbientLight(env.ambient.color, env.ambient.intensity);
272
+ ctx.scene.add(light);
273
+ }
274
+ if (env.fog) {
275
+ if (env.fog.type === 'exponential') {
276
+ ctx.scene.fog = new THREE.FogExp2(env.fog.color, env.fog.density ?? DEFAULTS.fog.exponential.density);
277
+ }
278
+ else {
279
+ ctx.scene.fog = new THREE.Fog(env.fog.color, env.fog.near ?? DEFAULTS.fog.linear.near, env.fog.far ?? DEFAULTS.fog.linear.far);
280
+ }
281
+ }
282
+ if (env.background) {
283
+ ctx.scene.background = new THREE.Color(env.background);
284
+ }
285
+ if (env.skybox) {
286
+ // Load the real HDRI as a PMREM IBL env map (background + environment),
287
+ // mirroring the editor. Async + fire-and-forget; no placeholder color.
288
+ applySkybox(env.skybox, env, ctx);
289
+ }
290
+ }
291
+ // Entities — pass 1: spawn all, build entity ID → Object3D mapping.
292
+ // spawnEntity recurses into children itself and registers every id
293
+ // (including nested children) in idMap via the idMap parameter.
294
+ const idMap = new Map();
295
+ for (const entityDef of data.entities) {
296
+ const obj = await spawnEntity(entityDef, ctx, tickFns, mixers, idMap);
297
+ ctx.scene.add(obj);
298
+ }
299
+ // Pass 1.5: animated attachments must resolve only after every entity and
300
+ // GLTF skeleton has spawned. Reparenting preserves the authored transform
301
+ // as the local socket offset; gameplay components remain on the same
302
+ // Object3D and therefore require no mirror or adapter entity.
303
+ resolveBoneAttachments(data.entities, idMap);
304
+ // Pass 2: create joints between physics bodies
305
+ createSceneJoints(data.entities, idMap, ctx);
306
+ // Pass 2.5 (F6): resolve EntityRefSchema component fields (string id ->
307
+ // live Object3D) — must run after every entity id is in idMap, and before
308
+ // initAll() so components observe the resolved Object3D in init().
309
+ if (ctx.componentManager) {
310
+ resolveEntityRefs(data.entities, idMap, ctx.componentManager);
311
+ }
312
+ // Pass 3: initialize GameComponent instances (now that all entities + joints exist)
313
+ if (ctx.componentManager) {
314
+ await ctx.componentManager.initAll();
315
+ }
316
+ // Try to load pre-baked navmesh from sibling .navmesh file
317
+ let navMesh;
318
+ if (url) {
319
+ const navmeshUrl = url.replace('.vscn.json', '.navmesh');
320
+ try {
321
+ const navRes = await fetch(resolveUrl(navmeshUrl));
322
+ if (navRes.ok) {
323
+ await initNavigation();
324
+ const buffer = await navRes.arrayBuffer();
325
+ navMesh = new NavMeshManager();
326
+ navMesh.loadFromData(new Uint8Array(buffer));
327
+ }
328
+ }
329
+ catch {
330
+ // No pre-baked navmesh — continue without one
331
+ }
332
+ }
333
+ // Collect cameras defined in scene entities
334
+ const cameras = [];
335
+ ctx.scene.traverse((obj) => {
336
+ const cam = getUserData(obj, '_camera');
337
+ if (cam)
338
+ cameras.push(cam);
339
+ });
340
+ return {
341
+ navMesh,
342
+ cameras,
343
+ environment: data.environment,
344
+ ui: data.ui,
345
+ entities: idMap,
346
+ update(dt) {
347
+ for (const fn of tickFns)
348
+ fn(dt);
349
+ },
350
+ dispose() {
351
+ for (const mixer of mixers)
352
+ mixer.stopAllAction();
353
+ tickFns.length = 0;
354
+ mixers.length = 0;
355
+ },
356
+ };
357
+ }
358
+ async function spawnEntity(def, ctx, tickFns, mixers, idMap, ancestorCtx = ROOT_ANCESTOR_CONTEXT, audioTracker) {
359
+ // Check for factory-function prefabs (code prefabs registered at runtime)
360
+ if (def.prefab && ctx.prefabs?.has(def.prefab)) {
361
+ // R1c — the factory function receives NO ancestor context (no world
362
+ // matrix, no moving-ancestor info), unlike the JSON-prefab/primitive path
363
+ // below which composes it for physics spawn. If this code-prefab is
364
+ // nested under a transformed ancestor, its own transform/physics are
365
+ // built by the factory as if it were at the scene root — they will NOT
366
+ // reflect the ancestor's position/rotation/scale. Warn loudly (once per
367
+ // entity, naturally once per scene load since each entity is visited once)
368
+ // rather than silently producing a wrong-looking result.
369
+ if (!ancestorCtx.worldMatrix.equals(ROOT_ANCESTOR_CONTEXT.worldMatrix)) {
370
+ log.scene.warn(`Code-prefab entity "${entityLabel(def)}" (factory "${def.prefab}") is nested under an ` +
371
+ 'ancestor with a non-identity world transform. The runtime ctx.prefabs factory API ' +
372
+ "doesn't yet receive ancestor context, so this entity's own transform/physics are " +
373
+ "built as if it were at the scene root — they will NOT compose the ancestor's " +
374
+ 'position/rotation/scale. Move this entity to the scene root, or wait for ancestor-' +
375
+ 'context support in the code-prefab factory API.', { entityId: def.id });
376
+ }
377
+ const object3d = ctx.prefabs.get(def.prefab)(ctx, def);
378
+ // Same entity-metadata contract as the main path below (see the comment
379
+ // there): `entity` userData is written unconditionally so queries find
380
+ // code-prefab instances too; only `entityId`/idMap stay id-gated.
381
+ object3d.name = def.name;
382
+ setUserData(object3d, 'entity', def);
383
+ if (def.id) {
384
+ setUserData(object3d, 'entityId', def.id);
385
+ if (idMap)
386
+ idMap.set(def.id, object3d);
387
+ }
388
+ // Apply JSON components on top of prefab defaults
389
+ if (def.components && ctx.componentRegistry && ctx.componentManager) {
390
+ applyComponents(ctx.componentRegistry, object3d, def.components, ctx.componentManager);
391
+ }
392
+ return object3d;
393
+ }
394
+ // Check for JSON prefab files (.prefab.json)
395
+ if (def.prefab) {
396
+ const prefabDef = await loadPrefabFile(def.prefab, ctx);
397
+ const merged = mergePrefabInstance(prefabDef.root, def);
398
+ return spawnEntity(merged, ctx, tickFns, mixers, idMap, ancestorCtx, audioTracker);
399
+ }
400
+ // Transform
401
+ const pos = def.transform?.position ?? DEFAULTS.transform.position;
402
+ const rot = def.transform?.rotation ?? DEFAULTS.transform.rotation;
403
+ const scl = def.transform?.scale ?? DEFAULTS.transform.scale;
404
+ // T1.1 — compose this entity's WORLD transform (parent chain × own local
405
+ // transform) for physics spawn purposes. Cheap (one 4x4 multiply) and done
406
+ // unconditionally so children can thread it further down regardless of
407
+ // whether THIS entity has physics.
408
+ const localMatrix = new THREE.Matrix4().compose(new THREE.Vector3(pos[0], pos[1], pos[2]), new THREE.Quaternion(rot[0], rot[1], rot[2], rot[3]), new THREE.Vector3(scl[0], scl[1], scl[2]));
409
+ const worldMatrix = ancestorCtx.worldMatrix.clone().multiply(localMatrix);
410
+ // Create Three.js object — the Object3D IS the entity.
411
+ let object3d = new THREE.Group();
412
+ if (def.mesh) {
413
+ if (def.mesh.type === 'gltf' && def.mesh.src) {
414
+ // Load GLTF model
415
+ const { scene: gltfScene, animations } = await loadGLTF(def.mesh.src);
416
+ if (def.shadow?.enabled) {
417
+ gltfScene.traverse((child) => {
418
+ if (child instanceof THREE.Mesh) {
419
+ child.castShadow = true;
420
+ child.receiveShadow = true;
421
+ }
422
+ });
423
+ }
424
+ // F4 — mesh.node: resolve a single sub-node to instantiate as this
425
+ // entity's geometry instead of the whole file (shared geometry, no
426
+ // copy — see resolveGltfNode). Must run before import correction below
427
+ // so the correction applies to whatever we actually wrap.
428
+ let content = gltfScene;
429
+ if (def.mesh.node) {
430
+ content = resolveGltfNode(gltfScene, def.mesh.node, def.mesh.src);
431
+ // wrapper.add() below detaches `content` from the glTF hierarchy.
432
+ // Reset its local transform to identity — the entity's own transform
433
+ // (applied to `wrapper`) is the sole positioner; the node's offset
434
+ // within the source file's hierarchy is not carried over (this
435
+ // instantiates the node "as this entity's geometry", not as a
436
+ // placed sub-scene).
437
+ content.position.set(0, 0, 0);
438
+ content.rotation.set(0, 0, 0);
439
+ content.scale.set(1, 1, 1);
440
+ }
441
+ else if (def.mesh.lod?.length) {
442
+ // F7 — mesh.lod: multi-level distance-keyed glTF sub-node swap, gated
443
+ // by the environment.rendering.lod master toggle (per-entity cascade
444
+ // via resolveRenderSettings — the first real reader of that toggle).
445
+ // Mutually exclusive with `node`/`instances`/`instancer` (schema refine).
446
+ const lodOn = resolveRenderSettings(ctx.sceneRendering, def.render)['lod'] !== false;
447
+ const levels = [...def.mesh.lod].sort((a, b) => a.distance - b.distance);
448
+ if (lodOn) {
449
+ const lod = new THREE.LOD();
450
+ for (const level of levels) {
451
+ const n = resolveGltfNode(gltfScene, level.node, def.mesh.src);
452
+ n.position.set(0, 0, 0);
453
+ n.rotation.set(0, 0, 0);
454
+ n.scale.set(1, 1, 1);
455
+ lod.addLevel(n, level.distance);
456
+ }
457
+ content = lod;
458
+ }
459
+ else {
460
+ // Master gate off — degrade to the closest (highest-detail) level
461
+ // only, no THREE.LOD.
462
+ const closest = levels[0];
463
+ const n = resolveGltfNode(gltfScene, closest.node, def.mesh.src);
464
+ n.position.set(0, 0, 0);
465
+ n.rotation.set(0, 0, 0);
466
+ n.scale.set(1, 1, 1);
467
+ content = n;
468
+ }
469
+ }
470
+ // Apply import correction from asset registry
471
+ const meta = getAssetMeta(def.mesh.src);
472
+ if (meta?.importCorrection) {
473
+ const ic = meta.importCorrection;
474
+ if (ic.scale !== undefined) {
475
+ content.scale.multiplyScalar(ic.scale);
476
+ }
477
+ if (ic.rotation) {
478
+ content.rotation.set(THREE.MathUtils.degToRad(ic.rotation[0]), THREE.MathUtils.degToRad(ic.rotation[1]), THREE.MathUtils.degToRad(ic.rotation[2]));
479
+ }
480
+ }
481
+ // Wrap in a group so entity transform and import correction are separated.
482
+ // `content` carries import correction; wrapper carries entity transform.
483
+ const wrapper = new THREE.Group();
484
+ wrapper.add(content);
485
+ object3d = wrapper;
486
+ // Set up animation if defined. Always build the mixer + clip map (and
487
+ // expose both via userData) whenever the entity has clips — not just
488
+ // when `autoplay` is set — so a GameComponent (e.g. an XState-driven
489
+ // character controller, E5's replacement for the removed AnimGraph) can
490
+ // pick up `_animMixer`/`_animClips` in its own `init()` and drive the
491
+ // SAME mixer via `bindXStateAnimation` (xstate-animation-binding.ts).
492
+ // Only `autoplay` self-ticks here (`tickFns`) — a component driving its
493
+ // own binding calls `mixer.update(dt)` itself (inside the binding's
494
+ // `tick`), so auto-ticking unconditionally would double-advance it.
495
+ if (def.animation && animations.length > 0) {
496
+ const clipMap = buildClipMap(animations, def.animation.clipAliases);
497
+ const mixer = new THREE.AnimationMixer(gltfScene);
498
+ setUserData(object3d, '_animMixer', mixer);
499
+ setUserData(object3d, '_animClips', clipMap);
500
+ setUserData(object3d, '_availableClips', [...clipMap.keys()]);
501
+ mixers.push(mixer);
502
+ if (def.animation.autoplay) {
503
+ // Simple clip autoplay
504
+ const clip = clipMap.get(def.animation.autoplay);
505
+ if (clip) {
506
+ const action = mixer.clipAction(clip);
507
+ const loop = def.animation.loop ?? DEFAULTS.animation.loop;
508
+ action.setLoop(loop ? THREE.LoopRepeat : THREE.LoopOnce, Infinity);
509
+ action.clampWhenFinished = !loop;
510
+ action.play();
511
+ tickFns.push((dt) => mixer.update(dt));
512
+ }
513
+ }
514
+ }
515
+ }
516
+ else {
517
+ const geometry = createGeometry(def.mesh);
518
+ const resolvedMat = await resolveMaterial(def, ctx);
519
+ const material = createMaterial(resolvedMat);
520
+ let mesh;
521
+ if (def.mesh.instances) {
522
+ // F3 — declarative/authored instancing: fetch+parse the referenced
523
+ // .instances.json and build one InstancedMesh from its tuples.
524
+ const tuples = await loadInstancesFile(def.mesh.instances, ctx);
525
+ mesh = buildInstancedMeshFromTuples(geometry, material, tuples);
526
+ }
527
+ else if (def.mesh.instancer) {
528
+ mesh = buildInstancedMesh(geometry, material, def.mesh.instancer, def.mesh.instancerParams ?? {});
529
+ }
530
+ else {
531
+ mesh = new THREE.Mesh(geometry, material);
532
+ }
533
+ if (def.shadow?.enabled) {
534
+ mesh.castShadow = true;
535
+ mesh.receiveShadow = true;
536
+ }
537
+ object3d = mesh;
538
+ }
539
+ }
540
+ if (def.light) {
541
+ const light = createLight(def.light);
542
+ if (def.shadow?.enabled && 'shadow' in light) {
543
+ const shadowLight = light;
544
+ shadowLight.castShadow = true;
545
+ // Default to DEFAULTS.shadow.mapSize (1024) rather than Three's 512 so the
546
+ // runtime matches the documented single-source default.
547
+ shadowLight.shadow.mapSize.setScalar(def.shadow.mapSize ?? DEFAULTS.shadow.mapSize);
548
+ if (def.shadow.camera) {
549
+ const cam = def.shadow.camera;
550
+ if (cam.left !== undefined)
551
+ shadowLight.shadow.camera.left = cam.left;
552
+ if (cam.right !== undefined)
553
+ shadowLight.shadow.camera.right = cam.right;
554
+ if (cam.top !== undefined)
555
+ shadowLight.shadow.camera.top = cam.top;
556
+ if (cam.bottom !== undefined)
557
+ shadowLight.shadow.camera.bottom = cam.bottom;
558
+ }
559
+ if (def.shadow.radius !== undefined) {
560
+ shadowLight.shadow.radius = def.shadow.radius;
561
+ }
562
+ if (def.shadow.bias !== undefined) {
563
+ shadowLight.shadow.bias = def.shadow.bias;
564
+ }
565
+ }
566
+ object3d = light;
567
+ }
568
+ if (def.camera) {
569
+ const camera = createCamera(def.camera);
570
+ // Wrap in a group so entity transform and camera are separated (same pattern as editor)
571
+ const wrapper = new THREE.Group();
572
+ wrapper.add(camera);
573
+ setUserData(wrapper, '_camera', camera);
574
+ object3d = wrapper;
575
+ }
576
+ // Audio source
577
+ if (def.audio && ctx.audioListener) {
578
+ const audioDef = def.audio;
579
+ const dAudio = DEFAULTS.audio;
580
+ const spatial = audioDef.spatial ?? dAudio.spatial;
581
+ const audioObj = spatial
582
+ ? new THREE.PositionalAudio(ctx.audioListener)
583
+ : new THREE.Audio(ctx.audioListener);
584
+ audioObj.setVolume(audioDef.volume ?? dAudio.volume);
585
+ audioObj.setLoop(audioDef.loop ?? dAudio.loop);
586
+ if (spatial && audioObj instanceof THREE.PositionalAudio) {
587
+ audioObj.setRefDistance(audioDef.refDistance ?? dAudio.refDistance);
588
+ audioObj.setRolloffFactor(audioDef.rolloffFactor ?? dAudio.rolloffFactor);
589
+ audioObj.setMaxDistance(audioDef.maxDistance ?? dAudio.maxDistance);
590
+ }
591
+ // R1c — track every THREE.Audio this spawn creates (when the caller
592
+ // supplied a tracker; only spawnPrefab's dispose() does) so dispose() can
593
+ // stop() it, and guard the async load callback so a still-in-flight
594
+ // AudioLoader request can't setBuffer/play on an already-disposed audio
595
+ // object (the callback captures `audioObj`, which outlives dispose() —
596
+ // Three.js gives no other hook to cancel an in-flight AudioLoader request).
597
+ audioTracker?.audios.push(audioObj);
598
+ if (audioDef.src) {
599
+ const loader = new THREE.AudioLoader(loadingManager);
600
+ loader.load(audioDef.src, (buffer) => {
601
+ if (audioTracker?.disposed.value)
602
+ return;
603
+ audioObj.setBuffer(buffer);
604
+ if (audioDef.autoplay ?? dAudio.autoplay)
605
+ audioObj.play();
606
+ });
607
+ }
608
+ object3d.add(audioObj);
609
+ }
610
+ // Particles — instantiate the three.quarks system and register it with the
611
+ // runtime BatchedRenderer (mirrors the editor entity-factory + scene-sync).
612
+ if (def.particles) {
613
+ try {
614
+ const { emitter, system } = createParticleSystemFromData(def.particles);
615
+ object3d.add(emitter);
616
+ setUserData(object3d, '_particleSystem', system);
617
+ if (ctx.particleRenderer)
618
+ ctx.particleRenderer.addSystem(system);
619
+ }
620
+ catch (err) {
621
+ log.scene.warn(`Failed to create particle system for entity "${def.name}"`, {
622
+ entityId: def.id,
623
+ error: String(err),
624
+ });
625
+ }
626
+ }
627
+ // Spline — build a CatmullRomCurve3 and stash it for gameplay code (path
628
+ // following, etc.). The editor renders an interactive gizmo for splines; at
629
+ // runtime the curve itself is the load-bearing artifact.
630
+ if (def.spline) {
631
+ const curve = createCurveFromSpline(def.spline);
632
+ if (curve)
633
+ setUserData(object3d, 'splineCurve', curve);
634
+ }
635
+ // Tag navigation role for runtime navmesh collection
636
+ if (def.navigation?.role) {
637
+ setUserData(object3d, 'navRole', def.navigation.role);
638
+ }
639
+ // Tag pivot for runtime code that needs to rotate/scale around it
640
+ if (def.pivot) {
641
+ setUserData(object3d, 'pivot', def.pivot);
642
+ }
643
+ // Tag entity metadata (used by editor hierarchy/inspector when viewing game
644
+ // scene, and by gameplay queries like queryByComponent/queryByTag which read
645
+ // component/tag names off `userData.entity`). Write `entity` unconditionally
646
+ // — NOT only when `def.id` is set: id-less prefab instances (e.g. the RTS
647
+ // units) still attach components (below), so gating entity metadata on `id`
648
+ // made those components undiscoverable by name.
649
+ //
650
+ // The scene schema declares `id` optional with "auto-generated if omitted",
651
+ // so `entityId` is unconditional too (auto-generating when absent) rather
652
+ // than skipped: without this, hand-authored scenes (which rarely carry ids)
653
+ // load into an EMPTY play-mode hierarchy even though they render fine. This
654
+ // mirrors the editor store, which fills missing ids on load
655
+ // (`if (!e.id) e.id = crypto.randomUUID()`). Populating idMap for every
656
+ // entity is safe — it's only read by joint-target resolution keyed on an
657
+ // authored `joint.target` id, which can't collide with a random UUID.
658
+ object3d.name = def.name;
659
+ if (!def.id)
660
+ def.id = crypto.randomUUID();
661
+ setUserData(object3d, 'entity', def);
662
+ setUserData(object3d, 'entityId', def.id);
663
+ if (idMap)
664
+ idMap.set(def.id, object3d);
665
+ // Apply transform
666
+ object3d.position.set(pos[0], pos[1], pos[2]);
667
+ object3d.quaternion.set(rot[0], rot[1], rot[2], rot[3]);
668
+ object3d.scale.set(scl[0], scl[1], scl[2]);
669
+ // Visibility (mirrors editor: visible unless explicitly false)
670
+ object3d.visible = def.visible ?? DEFAULTS.entity.visible;
671
+ // Physics — stores {body, collider} in the physics registry keyed by object3d.
672
+ // T1.1: the body is created at the COMPOSED WORLD transform (ancestorCtx's
673
+ // world matrix × this entity's own local transform), not the raw authored
674
+ // local transform — Rapier knows nothing about the Three.js parent chain.
675
+ if (def.physics) {
676
+ // R1c: this used to only throw for a "dynamic" child body — but a
677
+ // "fixed"/"kinematic" child nested under a moving (dynamic/kinematic)
678
+ // ancestor is equally unsupported: Rapier has no parent/child body
679
+ // relationship at all, so ANY nested body (whatever its own type) gets its
680
+ // world transform baked in ONCE at spawn time and then never follows the
681
+ // ancestor's body again — the transform writer only pins the child's
682
+ // VISUAL Object3D to its OWN (stale, non-moving) body pose, so both the
683
+ // mesh and collider silently fall behind the moving ancestor.
684
+ if (ancestorCtx.movingAncestor) {
685
+ throw new Error(`Entity "${entityLabel(def)}" has a "${def.physics.bodyType}" physics body nested under ` +
686
+ `"${ancestorCtx.movingAncestor.name}", which itself has a "${ancestorCtx.movingAncestor.bodyType}" ` +
687
+ `physics body. Rapier has no parent/child body relationship, so a moving ` +
688
+ `(dynamic/kinematic) ancestor cannot carry ANY nested physics body with it, regardless of ` +
689
+ `the nested body's own type — the child body's world position would immediately drift out ` +
690
+ `of sync with its visual parent. This configuration is unsupported: move ` +
691
+ `"${entityLabel(def)}" out from under "${ancestorCtx.movingAncestor.name}", or make the ` +
692
+ `ancestor's body "fixed" (a fixed/static ancestor's transform is fine to compose).`);
693
+ }
694
+ const worldPos = new THREE.Vector3();
695
+ const worldQuat = new THREE.Quaternion();
696
+ const worldScale = new THREE.Vector3();
697
+ worldMatrix.decompose(worldPos, worldQuat, worldScale);
698
+ createPhysicsBody(def.physics, [worldPos.x, worldPos.y, worldPos.z], [worldQuat.x, worldQuat.y, worldQuat.z, worldQuat.w], ctx, object3d, [worldScale.x, worldScale.y, worldScale.z], entityLabel(def));
699
+ }
700
+ // Apply custom GameComponents from scene data
701
+ if (def.components && ctx.componentRegistry && ctx.componentManager) {
702
+ applyComponents(ctx.componentRegistry, object3d, def.components, ctx.componentManager);
703
+ }
704
+ // Children — parent each child Object3D directly under this one. Threads the
705
+ // composed world matrix + nearest moving-ancestor (T1.1) down: a
706
+ // dynamic/kinematic body ON THIS entity becomes the nearest moving ancestor
707
+ // for its own children (overriding any further-up ancestor, since it's the
708
+ // closer/more relevant one to report); otherwise the existing ancestor
709
+ // context is passed through unchanged.
710
+ if (def.children) {
711
+ const childAncestorCtx = {
712
+ worldMatrix,
713
+ ...(def.physics &&
714
+ (def.physics.bodyType === 'dynamic' || def.physics.bodyType === 'kinematic')
715
+ ? { movingAncestor: { name: entityLabel(def), bodyType: def.physics.bodyType } }
716
+ : ancestorCtx.movingAncestor
717
+ ? { movingAncestor: ancestorCtx.movingAncestor }
718
+ : {}),
719
+ };
720
+ for (const childDef of def.children) {
721
+ const childObj = await spawnEntity(childDef, ctx, tickFns, mixers, idMap, childAncestorCtx, audioTracker);
722
+ object3d.add(childObj);
723
+ }
724
+ }
725
+ return object3d;
726
+ }
727
+ /** Find the first mesh geometry in an Object3D subtree (for trimesh colliders). */
728
+ function findMeshGeometry(object3d) {
729
+ let geometry = null;
730
+ object3d.traverse((child) => {
731
+ if (!geometry && child instanceof THREE.Mesh && child.geometry) {
732
+ geometry = child.geometry;
733
+ }
734
+ });
735
+ return geometry;
736
+ }
737
+ function createPhysicsBody(physics, worldPos, worldRot, ctx, object3d, worldScale, label) {
738
+ const rapier = ctx.rapier;
739
+ // Create rigid body
740
+ let bodyDesc;
741
+ switch (physics.bodyType) {
742
+ case 'fixed':
743
+ bodyDesc = rapier.RigidBodyDesc.fixed();
744
+ break;
745
+ case 'kinematic':
746
+ bodyDesc = rapier.RigidBodyDesc.kinematicPositionBased();
747
+ break;
748
+ default:
749
+ bodyDesc = rapier.RigidBodyDesc.dynamic();
750
+ break;
751
+ }
752
+ bodyDesc.setTranslation(worldPos[0], worldPos[1], worldPos[2]);
753
+ bodyDesc.setRotation({ x: worldRot[0], y: worldRot[1], z: worldRot[2], w: worldRot[3] });
754
+ const body = ctx.rapierWorld.createRigidBody(bodyDesc);
755
+ // Create collider. T1.2: dimensions are baked from the entity's composed
756
+ // WORLD scale (self × ancestors) — Rapier colliders have no notion of a
757
+ // parent Object3D's scale, so this is what keeps the physical shape
758
+ // matching whatever's visually on screen (which scales for free via the
759
+ // normal Three.js hierarchy). See collider-dimensions.ts (shared with the
760
+ // editor's collider gizmo) for the exact per-type math + round-collider
761
+ // non-uniform-scale error.
762
+ let colliderDesc;
763
+ const col = physics.collider;
764
+ switch (col.type) {
765
+ case 'cuboid': {
766
+ const dims = computeColliderWorldDimensions(col, worldScale, label);
767
+ const he = dims.halfExtents;
768
+ colliderDesc = rapier.ColliderDesc.cuboid(he[0], he[1], he[2]);
769
+ break;
770
+ }
771
+ case 'ball': {
772
+ const dims = computeColliderWorldDimensions(col, worldScale, label);
773
+ colliderDesc = rapier.ColliderDesc.ball(dims.radius);
774
+ break;
775
+ }
776
+ case 'capsule': {
777
+ const dims = computeColliderWorldDimensions(col, worldScale, label);
778
+ colliderDesc = rapier.ColliderDesc.capsule(dims.halfHeight, dims.radius);
779
+ break;
780
+ }
781
+ case 'trimesh': {
782
+ // Build a triangle-mesh collider from the entity's own mesh geometry.
783
+ // NEVER silently fall back to a cuboid — wrong physics with no warning is
784
+ // a classic AI trap. If geometry isn't available we throw loudly.
785
+ const geometry = findMeshGeometry(object3d);
786
+ if (!geometry) {
787
+ throw new Error(`Entity "${object3d.name}" declares a "trimesh" collider but has no mesh geometry to build it from. ` +
788
+ `Trimesh colliders require a mesh (primitive or GLTF) on the same entity.`);
789
+ }
790
+ const posAttr = geometry.getAttribute('position');
791
+ if (!posAttr) {
792
+ throw new Error(`Entity "${object3d.name}" declares a "trimesh" collider but its mesh geometry has no position attribute.`);
793
+ }
794
+ const vertices = new Float32Array(posAttr.array);
795
+ // T1.2: pre-scale vertices per-axis by the composed world scale — the
796
+ // geometry's raw vertex data is in local (unscaled) space.
797
+ for (let i = 0; i < vertices.length; i += 3) {
798
+ vertices[i] *= worldScale[0];
799
+ vertices[i + 1] *= worldScale[1];
800
+ vertices[i + 2] *= worldScale[2];
801
+ }
802
+ let indices;
803
+ if (geometry.index) {
804
+ indices = new Uint32Array(geometry.index.array);
805
+ }
806
+ else {
807
+ // Non-indexed geometry: every 3 consecutive vertices form a triangle.
808
+ indices = new Uint32Array(posAttr.count);
809
+ for (let i = 0; i < posAttr.count; i++)
810
+ indices[i] = i;
811
+ }
812
+ colliderDesc = rapier.ColliderDesc.trimesh(vertices, indices);
813
+ break;
814
+ }
815
+ default:
816
+ colliderDesc = rapier.ColliderDesc.cuboid(0.5, 0.5, 0.5);
817
+ }
818
+ // Mass precedence: an explicit `physics.mass` always wins. When mass is set we
819
+ // skip setDensity entirely, otherwise Rapier's later setDensity would recompute
820
+ // (and silently defeat) the requested mass.
821
+ if (physics.mass !== undefined) {
822
+ colliderDesc.setMass(physics.mass);
823
+ }
824
+ if (col.friction !== undefined) {
825
+ colliderDesc.setFriction(col.friction);
826
+ }
827
+ if (col.restitution !== undefined) {
828
+ colliderDesc.setRestitution(col.restitution);
829
+ }
830
+ if (col.density !== undefined && physics.mass === undefined) {
831
+ colliderDesc.setDensity(col.density);
832
+ }
833
+ // T1.2/R1c: the offset is a physics-engine translation with no notion of the
834
+ // parent Object3D's scale (same reasoning as the dimensions above) — scale it
835
+ // by the entity's composed world scale, or it silently drifts off its visual
836
+ // anchor point under any non-1 scale.
837
+ const worldOffset = computeColliderWorldOffset(col.offset, worldScale);
838
+ if (worldOffset) {
839
+ colliderDesc.setTranslation(worldOffset[0], worldOffset[1], worldOffset[2]);
840
+ }
841
+ if (col.isSensor) {
842
+ colliderDesc.setSensor(true);
843
+ }
844
+ // All scene-loaded colliders opt into collision events — sensors need them for
845
+ // onTriggerEnter/onTriggerExit, and solid colliders need them for onCollision
846
+ // (matches the world2d loader's unconditional setActiveEvents; see T1.3).
847
+ colliderDesc.setActiveEvents(rapier.ActiveEvents.COLLISION_EVENTS);
848
+ if (col.collisionGroups !== undefined || col.collisionFilter !== undefined) {
849
+ const groups = col.collisionGroups ?? 0xffff;
850
+ const filter = col.collisionFilter ?? 0xffff;
851
+ colliderDesc.setCollisionGroups((groups << 16) | filter);
852
+ }
853
+ const collider = ctx.rapierWorld.createCollider(colliderDesc, body);
854
+ // Register in the physics registry: object3d → {body, collider} and the
855
+ // colliderHandle → object3d reverse index (used for collision/trigger dispatch).
856
+ ctx.physics.add(object3d, body, collider);
857
+ }
858
+ /** Walk all entities in a scene tree recursively. */
859
+ function walkEntities(entities, fn) {
860
+ for (const e of entities) {
861
+ fn(e);
862
+ if (e.children)
863
+ walkEntities(e.children, fn);
864
+ }
865
+ }
866
+ /** Reparent scene-authored entities to named bones/sockets in loaded GLTF skeletons. */
867
+ function resolveBoneAttachments(entities, idMap) {
868
+ walkEntities(entities, (def) => {
869
+ const attachment = def.boneAttachment;
870
+ if (!attachment || !def.id)
871
+ return;
872
+ const object = idMap.get(def.id);
873
+ if (!object)
874
+ return;
875
+ const target = idMap.get(attachment.target);
876
+ if (!target) {
877
+ throw new Error(`Bone attachment target "${attachment.target}" not found for entity "${entityLabel(def)}".`);
878
+ }
879
+ if (target === object) {
880
+ throw new Error(`Entity "${entityLabel(def)}" cannot attach to its own skeleton.`);
881
+ }
882
+ const socket = target.getObjectByName(attachment.bone);
883
+ if (!socket) {
884
+ const available = [];
885
+ target.traverse((node) => {
886
+ if (node.name && node instanceof THREE.Bone) {
887
+ available.push(node.name);
888
+ }
889
+ });
890
+ const suffix = available.length > 0 ? ` Available bones: ${available.sort().join(', ')}` : '';
891
+ throw new Error(`Bone/socket "${attachment.bone}" was not found on target "${attachment.target}" for entity "${entityLabel(def)}".${suffix}`);
892
+ }
893
+ socket.add(object);
894
+ });
895
+ }
896
+ /** Create Rapier joints for all entities that define them. */
897
+ function createSceneJoints(entities, idMap, ctx) {
898
+ const rapier = ctx.rapier;
899
+ walkEntities(entities, (def) => {
900
+ if (!def.joints || !def.id)
901
+ return;
902
+ const objA = idMap.get(def.id);
903
+ if (!objA)
904
+ return;
905
+ const bodyA = ctx.physics.get(objA)?.body;
906
+ if (!bodyA)
907
+ return;
908
+ for (const joint of def.joints) {
909
+ const targetObj = idMap.get(joint.target);
910
+ if (!targetObj) {
911
+ log.scene.warn(`Joint target "${joint.target}" not found for entity "${def.name}"`, {
912
+ entityId: def.id,
913
+ });
914
+ continue;
915
+ }
916
+ const bodyB = ctx.physics.get(targetObj)?.body;
917
+ if (!bodyB)
918
+ continue;
919
+ const a1 = joint.anchor ?? [0, 0, 0];
920
+ const a2 = joint.targetAnchor ?? [0, 0, 0];
921
+ const anchor1 = { x: a1[0], y: a1[1], z: a1[2] };
922
+ const anchor2 = { x: a2[0], y: a2[1], z: a2[2] };
923
+ let params;
924
+ switch (joint.type) {
925
+ case 'fixed': {
926
+ const frame1 = { x: 0, y: 0, z: 0, w: 1 };
927
+ const frame2 = { x: 0, y: 0, z: 0, w: 1 };
928
+ params = rapier.JointData.fixed(anchor1, frame1, anchor2, frame2);
929
+ break;
930
+ }
931
+ case 'revolute': {
932
+ const ax = joint.axis ?? [0, 1, 0];
933
+ params = rapier.JointData.revolute(anchor1, anchor2, { x: ax[0], y: ax[1], z: ax[2] });
934
+ if (joint.limits) {
935
+ const deg2rad = Math.PI / 180;
936
+ const lp = params;
937
+ lp.limitsEnabled = true;
938
+ lp.limits = [joint.limits.min * deg2rad, joint.limits.max * deg2rad];
939
+ }
940
+ break;
941
+ }
942
+ case 'prismatic': {
943
+ const ax = joint.axis ?? [0, 1, 0];
944
+ params = rapier.JointData.prismatic(anchor1, anchor2, { x: ax[0], y: ax[1], z: ax[2] });
945
+ if (joint.limits) {
946
+ const lp = params;
947
+ lp.limitsEnabled = true;
948
+ lp.limits = [joint.limits.min, joint.limits.max];
949
+ }
950
+ break;
951
+ }
952
+ case 'spherical':
953
+ params = rapier.JointData.spherical(anchor1, anchor2);
954
+ break;
955
+ case 'spring': {
956
+ const rest = joint.restLength ?? 1;
957
+ const stiff = joint.stiffness ?? 10;
958
+ const damp = joint.damping ?? 1;
959
+ params = rapier.JointData.spring(rest, stiff, damp, anchor1, anchor2);
960
+ break;
961
+ }
962
+ default:
963
+ continue;
964
+ }
965
+ ctx.rapierWorld.createImpulseJoint(params, bodyA, bodyB, true);
966
+ }
967
+ });
968
+ }
969
+ /**
970
+ * F6 (`docs/VSCN-STRUCTURAL-GAPS-DESIGN.md`) — resolve `EntityRefSchema`
971
+ * component fields from the authored string id to the live `Object3D`.
972
+ *
973
+ * Modeled directly on `createSceneJoints` above (same walk, same `idMap`,
974
+ * same "missing target" shape) — the difference is WHAT gets resolved
975
+ * (a component field, discovered via the component's own static schema)
976
+ * and HOW a miss is handled: a joint with a missing target logs a warning
977
+ * and skips it; a missing entity-ref id is a loud load error (throw) per
978
+ * the design doc's AC, because a component that expects a live `Object3D`
979
+ * at `init()` cannot safely run with a dangling string in that field.
980
+ *
981
+ * Must run AFTER every entity is spawned (idMap is complete — components are
982
+ * attached during spawn too, so by this point every instance's ref field
983
+ * still holds its raw authored string id) and BEFORE
984
+ * `componentManager.initAll()`, so `init()` observes the resolved
985
+ * `Object3D`, not the string. Resolves through the SAME `idMap`
986
+ * `createSceneJoints` uses — no new registry (G3).
987
+ */
988
+ export function resolveEntityRefs(entities, idMap, manager) {
989
+ walkEntities(entities, (def) => {
990
+ if (!def.id || !def.components)
991
+ return;
992
+ const node = idMap.get(def.id);
993
+ if (!node)
994
+ return;
995
+ for (const inst of manager.getComponents(node)) {
996
+ const schema = inst.constructor.schema;
997
+ if (!schema)
998
+ continue;
999
+ for (const [field, fieldSchema] of Object.entries(schema.shape)) {
1000
+ if (!isEntityRefSchema(fieldSchema))
1001
+ continue;
1002
+ const record = inst;
1003
+ const value = record[field];
1004
+ // Optional ref, not authored on this instance — nothing to resolve.
1005
+ if (value === undefined)
1006
+ continue;
1007
+ if (typeof value !== 'string' || value.length === 0)
1008
+ continue;
1009
+ const resolved = idMap.get(value);
1010
+ if (!resolved) {
1011
+ throw new Error(`Entity ref "${field}" on component "${inst.constructor.name}" (entity ` +
1012
+ `"${entityLabel(def)}") points at missing entity id "${value}".`);
1013
+ }
1014
+ record[field] = resolved;
1015
+ }
1016
+ }
1017
+ });
1018
+ }
1019
+ /** Fetch and cache a .mat.json file. Validated via Zod (T4.6) — a malformed material asset
1020
+ * throws a `SceneParseError` naming the file, not a deep TypeError once the bad data reaches
1021
+ * material-factory.ts. */
1022
+ async function loadMaterialFile(url, ctx) {
1023
+ if (!ctx.materialFileCache)
1024
+ ctx.materialFileCache = new Map();
1025
+ const cached = ctx.materialFileCache.get(url);
1026
+ if (cached)
1027
+ return cached;
1028
+ const res = await fetch(resolveUrl(url));
1029
+ if (!res.ok)
1030
+ throw new Error(`Failed to load material: ${url} (${res.status})`);
1031
+ const data = parseMaterialFile(await res.json(), url);
1032
+ ctx.materialFileCache.set(url, data);
1033
+ return data;
1034
+ }
1035
+ /** Fetch and cache a .instances.json file (F3). Validated via Zod — a malformed instance-
1036
+ * transform asset throws a `SceneParseError` naming the file. */
1037
+ async function loadInstancesFile(url, ctx) {
1038
+ if (!ctx.instancesFileCache)
1039
+ ctx.instancesFileCache = new Map();
1040
+ const cached = ctx.instancesFileCache.get(url);
1041
+ if (cached)
1042
+ return cached;
1043
+ const res = await fetch(resolveUrl(url));
1044
+ if (!res.ok)
1045
+ throw new Error(`Failed to load instances: ${url} (${res.status})`);
1046
+ const data = parseInstancesFile(await res.json(), url);
1047
+ ctx.instancesFileCache.set(url, data);
1048
+ return data;
1049
+ }
1050
+ /** Resolve material for an entity — load from .mat.json ref or use inline material. */
1051
+ async function resolveMaterial(def, ctx) {
1052
+ if (def.materialRef) {
1053
+ try {
1054
+ const file = await loadMaterialFile(def.materialRef, ctx);
1055
+ return file.material;
1056
+ }
1057
+ catch (err) {
1058
+ log.scene.warn(`Failed to load material ref: ${def.materialRef}`, {
1059
+ entityId: def.id,
1060
+ error: String(err),
1061
+ });
1062
+ return def.material;
1063
+ }
1064
+ }
1065
+ return def.material;
1066
+ }
1067
+ /** Fetch and cache a .prefab.json file. Validated via Zod (T4.6) — a malformed prefab file
1068
+ * throws a `SceneParseError` naming the file, not a deep TypeError once the bad data reaches
1069
+ * spawnEntity. */
1070
+ async function loadPrefabFile(url, ctx) {
1071
+ if (!ctx.prefabFileCache)
1072
+ ctx.prefabFileCache = new Map();
1073
+ const cached = ctx.prefabFileCache.get(url);
1074
+ if (cached)
1075
+ return cached;
1076
+ const res = await fetch(resolveUrl(url));
1077
+ if (!res.ok)
1078
+ throw new Error(`Failed to load prefab: ${url} (${res.status})`);
1079
+ const data = parsePrefabFile(await res.json(), url);
1080
+ ctx.prefabFileCache.set(url, data);
1081
+ return data;
1082
+ }
1083
+ /**
1084
+ * Spawn a prefab instance at runtime from gameplay code.
1085
+ *
1086
+ * Returns a {@link SpawnedPrefab}. The previous version passed throwaway
1087
+ * tickFns/mixers arrays, so a runtime-spawned prefab's `animation.autoplay`
1088
+ * never advanced. Now the tick functions are captured and exposed via
1089
+ * `update()`, consistent with how loadScene's SceneInstance.update drives
1090
+ * animations.
1091
+ *
1092
+ * Usage:
1093
+ * const crate = await spawnPrefab('/data/prefabs/crate.prefab.json', ctx, {
1094
+ * transform: { position: [x, y, z] },
1095
+ * components: { Health: { current: 50 } },
1096
+ * });
1097
+ * // each frame (only needed for autoplay clips):
1098
+ * crate.update(dt);
1099
+ */
1100
+ export async function spawnPrefab(prefabUrl, ctx, overrides) {
1101
+ const instance = {
1102
+ name: 'Instance',
1103
+ prefab: prefabUrl,
1104
+ ...overrides,
1105
+ };
1106
+ const tickFns = [];
1107
+ const mixers = [];
1108
+ const audioTracker = { audios: [], disposed: { value: false } };
1109
+ const object3D = await spawnEntity(instance, ctx, tickFns, mixers, undefined, ROOT_ANCESTOR_CONTEXT, audioTracker);
1110
+ ctx.scene.add(object3D);
1111
+ // Run init() on any GameComponents the prefab attached. attach() only queues
1112
+ // them in pendingInit + the per-phase tick list; without this, update() fires
1113
+ // every frame while init() never runs (mirrors loadScene's pass-3 initAll).
1114
+ if (ctx.componentManager)
1115
+ await ctx.componentManager.initAll();
1116
+ return {
1117
+ object3D,
1118
+ update(dt) {
1119
+ for (const fn of tickFns)
1120
+ fn(dt);
1121
+ },
1122
+ // ---------------------------------------------------------------------
1123
+ // R1c CLASS INVENTORY — everything spawnEntity can register into a
1124
+ // `ctx` map/system/registry for a spawned subtree, and what dispose()
1125
+ // below does about each one. (This is the acceptance criterion for the
1126
+ // "spawnPrefab().dispose() completeness" task — keep it in sync with
1127
+ // spawnEntity whenever a new registration point is added.)
1128
+ //
1129
+ // - ctx.physics (Object3D -> {body, collider}) DISPOSED — root AND
1130
+ // every nested descendant's Rapier body is removed from the world
1131
+ // AND the registry (pre-existing, T1.4).
1132
+ // - ctx.componentManager (GameComponent instances) DISPOSED —
1133
+ // detach() runs on root + every descendant, firing each component's
1134
+ // own dispose() and stopping its tick (pre-existing, T1.4).
1135
+ // - mixers (THREE.AnimationMixer, simple `animation.autoplay` clips)
1136
+ // DISPOSED — stopAllAction() on every mixer this spawn created
1137
+ // (pre-existing).
1138
+ // - tickFns (standalone `animation.autoplay` mixer tick closures)
1139
+ // DISPOSED — array cleared so update() becomes a no-op (pre-existing).
1140
+ // - ctx.particleRenderer (three.quarks BatchedRenderer, when provided)
1141
+ // NEWLY-DISPOSED — every particle system tagged via the
1142
+ // `_particleSystem` userData key (mirrors editor scene-sync's own
1143
+ // registration convention) anywhere in the subtree is removed with
1144
+ // `deleteSystem` (probe1: previously kept rendering/simulating after
1145
+ // the visual object was gone).
1146
+ // - THREE.Audio/PositionalAudio objects (added as children of entities
1147
+ // with `def.audio`) NEWLY-DISPOSED — every audio object this spawn
1148
+ // created is `stop()`-ed, AND the `disposed` flag is set so a
1149
+ // still-in-flight `AudioLoader.load()` callback (which captures the
1150
+ // audio object and can resolve after dispose()) skips setBuffer/play
1151
+ // instead of reviving already-torn-down audio.
1152
+ // - `entity`/`entityId`/`navRole`/`pivot`/`splineCurve`/`_camera`
1153
+ // userData tags DELIBERATELY NOT DISPOSED — these are plain data
1154
+ // tags on Object3Ds that get garbage-collected with the subtree once
1155
+ // `ctx.scene.remove(object3D)` drops the last reference; there is no
1156
+ // separate ctx-level registry entry to leak.
1157
+ // - GLTF/texture asset caches (asset-loaders.ts, module-level)
1158
+ // DELIBERATELY NOT DISPOSED — these are shared, URL-keyed caches
1159
+ // across the whole runtime session (P1.8 contract: repeated
1160
+ // spawns/loads of the same asset must NOT re-fetch or re-parse), not
1161
+ // per-spawn state; they're only released by `clearAssetCaches()` on
1162
+ // full runtime teardown.
1163
+ // ---------------------------------------------------------------------
1164
+ dispose() {
1165
+ // Detach components on the root AND every descendant (a prefab tree can
1166
+ // attach components anywhere, not just at the root) — fires each
1167
+ // instance's dispose() + stops it ticking.
1168
+ object3D.traverse((node) => {
1169
+ ctx.componentManager?.detach(node);
1170
+ });
1171
+ // Remove any Rapier bodies this prefab tree created (root or nested) from
1172
+ // both the physics world and the physics registry — otherwise the body
1173
+ // keeps simulating/colliding after the visual object is gone (orphan body).
1174
+ object3D.traverse((node) => {
1175
+ const refs = ctx.physics.get(node);
1176
+ if (!refs)
1177
+ return;
1178
+ ctx.rapierWorld.removeRigidBody(refs.body);
1179
+ ctx.physics.remove(node);
1180
+ });
1181
+ // Unregister any particle systems this subtree registered with the
1182
+ // shared BatchedRenderer — otherwise they keep simulating/rendering
1183
+ // after the visual object is gone. `_particleSystem` is the same
1184
+ // userData tag the editor's scene-sync uses for the same purpose.
1185
+ if (ctx.particleRenderer) {
1186
+ const particleRenderer = ctx.particleRenderer;
1187
+ object3D.traverse((node) => {
1188
+ const system = getUserData(node, '_particleSystem');
1189
+ if (!system)
1190
+ return;
1191
+ try {
1192
+ particleRenderer.deleteSystem(system);
1193
+ }
1194
+ catch {
1195
+ // Already removed — nothing left to do.
1196
+ }
1197
+ });
1198
+ }
1199
+ // Stop every THREE.Audio/PositionalAudio object this spawn created, and
1200
+ // flag `disposed` so an AudioLoader request still in flight (its
1201
+ // callback captured `audioObj`, which outlives this dispose() call)
1202
+ // skips setBuffer/play on audio that's already torn down.
1203
+ audioTracker.disposed.value = true;
1204
+ for (const audio of audioTracker.audios)
1205
+ audio.stop();
1206
+ ctx.scene.remove(object3D);
1207
+ for (const mixer of mixers)
1208
+ mixer.stopAllAction();
1209
+ tickFns.length = 0;
1210
+ mixers.length = 0;
1211
+ },
1212
+ };
1213
+ }