@vgai/engine 0.5.0 → 0.5.2

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