@vgai/engine 0.3.0 → 0.4.0-canary.20260715.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 (559) hide show
  1. package/package.json +8 -2
  2. package/schemas/engine-api.json +124 -0
  3. package/schemas/engine-api.md +53 -0
  4. package/schemas/engine-capabilities.json +124 -0
  5. package/schemas/inputmap.schema.json +314 -0
  6. package/schemas/mat.schema.json +286 -0
  7. package/schemas/prefab.schema.json +10148 -0
  8. package/schemas/scn2d.schema.json +475 -0
  9. package/schemas/vgai-game.schema.json +383 -0
  10. package/schemas/vscn.schema.json +11007 -0
  11. package/src/adapter/{world-kind.ts → adapter-surface.ts} +6 -6
  12. package/src/adapter/authoring.ts +67 -0
  13. package/src/adapter/game-adapter.ts +8 -8
  14. package/src/adapter/host-context.ts +2 -4
  15. package/src/adapter/index.ts +3 -1
  16. package/src/adapter/system-adapter.ts +73 -0
  17. package/src/adapter/vgai-scene-game-adapter.ts +239 -180
  18. package/src/animation/xstate-animation-binding.ts +300 -262
  19. package/src/animation/xstate-animation-meta.ts +50 -13
  20. package/src/character/cloth-sim.ts +533 -0
  21. package/src/character/spring-chain.ts +307 -0
  22. package/src/core/game-loop.ts +41 -3
  23. package/src/core/seeded-random.ts +161 -0
  24. package/src/core/system-runner.ts +20 -3
  25. package/src/core/types.ts +37 -0
  26. package/src/data/data-asset.ts +1 -1
  27. package/src/dev/performance-profiler.ts +213 -0
  28. package/src/dev/webgl-gpu-timer.ts +53 -0
  29. package/src/ecs/component-manager.ts +45 -12
  30. package/src/ecs/game-component.ts +95 -11
  31. package/src/humanoid/bake.operation.ts +326 -0
  32. package/src/humanoid/body.ts +663 -0
  33. package/src/humanoid/clips.ts +149 -0
  34. package/src/humanoid/compose.ts +209 -0
  35. package/src/humanoid/generate.ts +189 -0
  36. package/src/humanoid/index.ts +36 -0
  37. package/src/humanoid/schema.ts +108 -0
  38. package/src/humanoid/skeleton.ts +345 -0
  39. package/src/index.ts +5 -5
  40. package/src/input/input-manager.ts +701 -22
  41. package/src/input/input-types.ts +5 -2
  42. package/src/manifest/index.ts +5 -5
  43. package/src/manifest/load.ts +125 -72
  44. package/src/manifest/schema.ts +362 -255
  45. package/src/react/game-state.tsx +135 -32
  46. package/src/react/root-adapter.tsx +49 -0
  47. package/src/react/unmanaged-root-detector.ts +66 -0
  48. package/src/react/use-data.ts +12 -2
  49. package/src/runtime/create-runtime.ts +100 -282
  50. package/src/runtime/debug-bridge.ts +483 -0
  51. package/src/runtime/debug-registry.ts +856 -0
  52. package/src/runtime/game.ts +342 -93
  53. package/src/runtime/gameplay-rng-trap.ts +134 -0
  54. package/src/runtime/input-router.ts +7 -7
  55. package/src/runtime/mount-game.ts +40 -38
  56. package/src/runtime/mount-manifest.ts +169 -37
  57. package/src/runtime/render-control.ts +13 -13
  58. package/src/runtime/render-seed.ts +1 -1
  59. package/src/runtime/state-bridge.ts +24 -10
  60. package/src/runtime/types.ts +110 -28
  61. package/src/scene/asset-registry.ts +22 -0
  62. package/src/scene/component-registry.ts +14 -3
  63. package/src/scene/defaults.ts +1 -0
  64. package/src/scene/light-camera-factory.ts +11 -3
  65. package/src/scene/scene-apply.ts +55 -4
  66. package/src/scene/scene-loader.ts +10 -27
  67. package/src/scene/scene-types.ts +0 -1
  68. package/src/scene/schema/index.ts +0 -34
  69. package/src/scene/schema/light.ts +16 -1
  70. package/src/scene/schema/material.ts +96 -91
  71. package/src/scene/schema/scene-file.ts +1 -7
  72. package/src/scene/user-data.ts +14 -1
  73. package/src/setup/setup-renderer.ts +1 -1
  74. package/src/tools/define-tool.ts +50 -11
  75. package/src/world2d/authoring-2d.ts +17 -1
  76. package/src/world2d/collision-2d.ts +1 -1
  77. package/src/world2d/pixi-game-adapter.ts +19 -17
  78. package/src/world2d/scene2d-loader.ts +1 -0
  79. package/src/world2d/types.ts +8 -2
  80. package/dist/adapter/authoring.d.ts +0 -404
  81. package/dist/adapter/authoring.d.ts.map +0 -1
  82. package/dist/adapter/authoring.js +0 -22
  83. package/dist/adapter/colyseus-networking-adapter.d.ts +0 -43
  84. package/dist/adapter/colyseus-networking-adapter.d.ts.map +0 -1
  85. package/dist/adapter/colyseus-networking-adapter.js +0 -38
  86. package/dist/adapter/first-party-systems.d.ts +0 -38
  87. package/dist/adapter/first-party-systems.d.ts.map +0 -1
  88. package/dist/adapter/first-party-systems.js +0 -77
  89. package/dist/adapter/game-adapter.d.ts +0 -133
  90. package/dist/adapter/game-adapter.d.ts.map +0 -1
  91. package/dist/adapter/game-adapter.js +0 -10
  92. package/dist/adapter/host-context.d.ts +0 -73
  93. package/dist/adapter/host-context.d.ts.map +0 -1
  94. package/dist/adapter/host-context.js +0 -16
  95. package/dist/adapter/index.d.ts +0 -22
  96. package/dist/adapter/index.d.ts.map +0 -1
  97. package/dist/adapter/index.js +0 -15
  98. package/dist/adapter/ingest/game-contract.d.ts +0 -53
  99. package/dist/adapter/ingest/game-contract.d.ts.map +0 -1
  100. package/dist/adapter/ingest/game-contract.js +0 -30
  101. package/dist/adapter/ingest/overlay-applier.d.ts +0 -118
  102. package/dist/adapter/ingest/overlay-applier.d.ts.map +0 -1
  103. package/dist/adapter/ingest/overlay-applier.js +0 -132
  104. package/dist/adapter/ingest/overlay-apply.d.ts +0 -60
  105. package/dist/adapter/ingest/overlay-apply.d.ts.map +0 -1
  106. package/dist/adapter/ingest/overlay-apply.js +0 -101
  107. package/dist/adapter/ingest/overlay-file.d.ts +0 -79
  108. package/dist/adapter/ingest/overlay-file.d.ts.map +0 -1
  109. package/dist/adapter/ingest/overlay-file.js +0 -66
  110. package/dist/adapter/ingest/overlay-report.d.ts +0 -113
  111. package/dist/adapter/ingest/overlay-report.d.ts.map +0 -1
  112. package/dist/adapter/ingest/overlay-report.js +0 -103
  113. package/dist/adapter/ingest/scene-capture.d.ts +0 -97
  114. package/dist/adapter/ingest/scene-capture.d.ts.map +0 -1
  115. package/dist/adapter/ingest/scene-capture.js +0 -248
  116. package/dist/adapter/ingest/upstream-pin.d.ts +0 -47
  117. package/dist/adapter/ingest/upstream-pin.d.ts.map +0 -1
  118. package/dist/adapter/ingest/upstream-pin.js +0 -50
  119. package/dist/adapter/loop-gate-report.d.ts +0 -34
  120. package/dist/adapter/loop-gate-report.d.ts.map +0 -1
  121. package/dist/adapter/loop-gate-report.js +0 -37
  122. package/dist/adapter/rapier-physics-adapter.d.ts +0 -12
  123. package/dist/adapter/rapier-physics-adapter.d.ts.map +0 -1
  124. package/dist/adapter/rapier-physics-adapter.js +0 -46
  125. package/dist/adapter/system-adapter.d.ts +0 -133
  126. package/dist/adapter/system-adapter.d.ts.map +0 -1
  127. package/dist/adapter/system-adapter.js +0 -13
  128. package/dist/adapter/transform.d.ts +0 -18
  129. package/dist/adapter/transform.d.ts.map +0 -1
  130. package/dist/adapter/transform.js +0 -1
  131. package/dist/adapter/vgai-scene-game-adapter.d.ts +0 -116
  132. package/dist/adapter/vgai-scene-game-adapter.d.ts.map +0 -1
  133. package/dist/adapter/vgai-scene-game-adapter.js +0 -714
  134. package/dist/adapter/world-kind.d.ts +0 -30
  135. package/dist/adapter/world-kind.d.ts.map +0 -1
  136. package/dist/adapter/world-kind.js +0 -18
  137. package/dist/ai/navigation.d.ts +0 -64
  138. package/dist/ai/navigation.d.ts.map +0 -1
  139. package/dist/ai/navigation.js +0 -133
  140. package/dist/animation/anim-graph-types.d.ts +0 -25
  141. package/dist/animation/anim-graph-types.d.ts.map +0 -1
  142. package/dist/animation/anim-graph-types.js +0 -1
  143. package/dist/animation/animation-clock.d.ts +0 -213
  144. package/dist/animation/animation-clock.d.ts.map +0 -1
  145. package/dist/animation/animation-clock.js +0 -301
  146. package/dist/animation/blend-node.d.ts +0 -22
  147. package/dist/animation/blend-node.d.ts.map +0 -1
  148. package/dist/animation/blend-node.js +0 -97
  149. package/dist/animation/camera-ownership.d.ts +0 -250
  150. package/dist/animation/camera-ownership.d.ts.map +0 -1
  151. package/dist/animation/camera-ownership.js +0 -169
  152. package/dist/animation/cinematic-cues.d.ts +0 -200
  153. package/dist/animation/cinematic-cues.d.ts.map +0 -1
  154. package/dist/animation/cinematic-cues.js +0 -213
  155. package/dist/animation/clip-map.d.ts +0 -12
  156. package/dist/animation/clip-map.d.ts.map +0 -1
  157. package/dist/animation/clip-map.js +0 -37
  158. package/dist/animation/gsap-registration.d.ts +0 -119
  159. package/dist/animation/gsap-registration.d.ts.map +0 -1
  160. package/dist/animation/gsap-registration.js +0 -92
  161. package/dist/animation/theatre-clock-binding.d.ts +0 -89
  162. package/dist/animation/theatre-clock-binding.d.ts.map +0 -1
  163. package/dist/animation/theatre-clock-binding.js +0 -29
  164. package/dist/animation/theatre-director.d.ts +0 -242
  165. package/dist/animation/theatre-director.d.ts.map +0 -1
  166. package/dist/animation/theatre-director.js +0 -235
  167. package/dist/animation/theatre-object-binding.d.ts +0 -305
  168. package/dist/animation/theatre-object-binding.d.ts.map +0 -1
  169. package/dist/animation/theatre-object-binding.js +0 -351
  170. package/dist/animation/xstate-animation-binding.d.ts +0 -111
  171. package/dist/animation/xstate-animation-binding.d.ts.map +0 -1
  172. package/dist/animation/xstate-animation-binding.js +0 -307
  173. package/dist/animation/xstate-animation-meta.d.ts +0 -228
  174. package/dist/animation/xstate-animation-meta.d.ts.map +0 -1
  175. package/dist/animation/xstate-animation-meta.js +0 -221
  176. package/dist/assets.d.ts +0 -26
  177. package/dist/assets.d.ts.map +0 -1
  178. package/dist/assets.js +0 -55
  179. package/dist/audio/index.d.ts +0 -4
  180. package/dist/audio/index.d.ts.map +0 -1
  181. package/dist/audio/index.js +0 -24
  182. package/dist/audio/tone-clock-binding.d.ts +0 -75
  183. package/dist/audio/tone-clock-binding.d.ts.map +0 -1
  184. package/dist/audio/tone-clock-binding.js +0 -29
  185. package/dist/audio/tone-context.d.ts +0 -114
  186. package/dist/audio/tone-context.d.ts.map +0 -1
  187. package/dist/audio/tone-context.js +0 -39
  188. package/dist/audio/tone-offline-render.d.ts +0 -90
  189. package/dist/audio/tone-offline-render.d.ts.map +0 -1
  190. package/dist/audio/tone-offline-render.js +0 -56
  191. package/dist/audio/wav-encode.d.ts +0 -38
  192. package/dist/audio/wav-encode.d.ts.map +0 -1
  193. package/dist/audio/wav-encode.js +0 -97
  194. package/dist/core/game-loop.d.ts +0 -21
  195. package/dist/core/game-loop.d.ts.map +0 -1
  196. package/dist/core/game-loop.js +0 -130
  197. package/dist/core/system-runner.d.ts +0 -120
  198. package/dist/core/system-runner.d.ts.map +0 -1
  199. package/dist/core/system-runner.js +0 -286
  200. package/dist/core/types.d.ts +0 -58
  201. package/dist/core/types.d.ts.map +0 -1
  202. package/dist/core/types.js +0 -25
  203. package/dist/data/data-asset.d.ts +0 -101
  204. package/dist/data/data-asset.d.ts.map +0 -1
  205. package/dist/data/data-asset.js +0 -122
  206. package/dist/data/data-check-core.d.ts +0 -110
  207. package/dist/data/data-check-core.d.ts.map +0 -1
  208. package/dist/data/data-check-core.js +0 -178
  209. package/dist/data/data-ref.d.ts +0 -79
  210. package/dist/data/data-ref.d.ts.map +0 -1
  211. package/dist/data/data-ref.js +0 -130
  212. package/dist/data/vite-plugin-data.d.ts +0 -79
  213. package/dist/data/vite-plugin-data.d.ts.map +0 -1
  214. package/dist/data/vite-plugin-data.js +0 -217
  215. package/dist/dev/console-bridge.d.ts +0 -22
  216. package/dist/dev/console-bridge.d.ts.map +0 -1
  217. package/dist/dev/console-bridge.js +0 -71
  218. package/dist/dev/debug-draw.d.ts +0 -24
  219. package/dist/dev/debug-draw.d.ts.map +0 -1
  220. package/dist/dev/debug-draw.js +0 -73
  221. package/dist/dev/logger.d.ts +0 -30
  222. package/dist/dev/logger.d.ts.map +0 -1
  223. package/dist/dev/logger.js +0 -90
  224. package/dist/ecs/component-manager.d.ts +0 -189
  225. package/dist/ecs/component-manager.d.ts.map +0 -1
  226. package/dist/ecs/component-manager.js +0 -676
  227. package/dist/ecs/game-component.d.ts +0 -97
  228. package/dist/ecs/game-component.d.ts.map +0 -1
  229. package/dist/ecs/game-component.js +0 -80
  230. package/dist/ecs/hmr-swap-report.d.ts +0 -53
  231. package/dist/ecs/hmr-swap-report.d.ts.map +0 -1
  232. package/dist/ecs/hmr-swap-report.js +0 -55
  233. package/dist/index.d.ts +0 -37
  234. package/dist/index.d.ts.map +0 -1
  235. package/dist/index.js +0 -30
  236. package/dist/input/input-manager.d.ts +0 -596
  237. package/dist/input/input-manager.d.ts.map +0 -1
  238. package/dist/input/input-manager.js +0 -1564
  239. package/dist/input/input-types.d.ts +0 -171
  240. package/dist/input/input-types.d.ts.map +0 -1
  241. package/dist/input/input-types.js +0 -1
  242. package/dist/input/prompt-labels.d.ts +0 -21
  243. package/dist/input/prompt-labels.d.ts.map +0 -1
  244. package/dist/input/prompt-labels.js +0 -121
  245. package/dist/input/rebind-controller.d.ts +0 -59
  246. package/dist/input/rebind-controller.d.ts.map +0 -1
  247. package/dist/input/rebind-controller.js +0 -85
  248. package/dist/input/schema.d.ts +0 -16
  249. package/dist/input/schema.d.ts.map +0 -1
  250. package/dist/input/schema.js +0 -228
  251. package/dist/loader.d.ts +0 -43
  252. package/dist/loader.d.ts.map +0 -1
  253. package/dist/loader.js +0 -59
  254. package/dist/manifest/index.d.ts +0 -5
  255. package/dist/manifest/index.d.ts.map +0 -1
  256. package/dist/manifest/index.js +0 -8
  257. package/dist/manifest/load-file.d.ts +0 -4
  258. package/dist/manifest/load-file.d.ts.map +0 -1
  259. package/dist/manifest/load-file.js +0 -14
  260. package/dist/manifest/load.d.ts +0 -117
  261. package/dist/manifest/load.d.ts.map +0 -1
  262. package/dist/manifest/load.js +0 -255
  263. package/dist/manifest/schema.d.ts +0 -173
  264. package/dist/manifest/schema.d.ts.map +0 -1
  265. package/dist/manifest/schema.js +0 -302
  266. package/dist/physics/collision-system.d.ts +0 -41
  267. package/dist/physics/collision-system.d.ts.map +0 -1
  268. package/dist/physics/collision-system.js +0 -64
  269. package/dist/physics/physics-registry.d.ts +0 -45
  270. package/dist/physics/physics-registry.d.ts.map +0 -1
  271. package/dist/physics/physics-registry.js +0 -65
  272. package/dist/physics/transform-writer.d.ts +0 -16
  273. package/dist/physics/transform-writer.d.ts.map +0 -1
  274. package/dist/physics/transform-writer.js +0 -38
  275. package/dist/physics/trigger-dispatch.d.ts +0 -24
  276. package/dist/physics/trigger-dispatch.d.ts.map +0 -1
  277. package/dist/physics/trigger-dispatch.js +0 -80
  278. package/dist/react/game-state.d.ts +0 -105
  279. package/dist/react/game-state.d.ts.map +0 -1
  280. package/dist/react/game-state.js +0 -144
  281. package/dist/react/use-data.d.ts +0 -60
  282. package/dist/react/use-data.d.ts.map +0 -1
  283. package/dist/react/use-data.js +0 -96
  284. package/dist/react/use-selection.d.ts +0 -75
  285. package/dist/react/use-selection.d.ts.map +0 -1
  286. package/dist/react/use-selection.js +0 -90
  287. package/dist/render/auto-batcher.d.ts +0 -34
  288. package/dist/render/auto-batcher.d.ts.map +0 -1
  289. package/dist/render/auto-batcher.js +0 -140
  290. package/dist/render/render-batch-system.d.ts +0 -33
  291. package/dist/render/render-batch-system.d.ts.map +0 -1
  292. package/dist/render/render-batch-system.js +0 -230
  293. package/dist/render/render-features.d.ts +0 -47
  294. package/dist/render/render-features.d.ts.map +0 -1
  295. package/dist/render/render-features.js +0 -105
  296. package/dist/render/render-settings.d.ts +0 -36
  297. package/dist/render/render-settings.d.ts.map +0 -1
  298. package/dist/render/render-settings.js +0 -51
  299. package/dist/runtime/create-runtime.d.ts +0 -280
  300. package/dist/runtime/create-runtime.d.ts.map +0 -1
  301. package/dist/runtime/create-runtime.js +0 -795
  302. package/dist/runtime/frame-selector-cache.d.ts +0 -41
  303. package/dist/runtime/frame-selector-cache.d.ts.map +0 -1
  304. package/dist/runtime/frame-selector-cache.js +0 -65
  305. package/dist/runtime/game.d.ts +0 -433
  306. package/dist/runtime/game.d.ts.map +0 -1
  307. package/dist/runtime/game.js +0 -569
  308. package/dist/runtime/input-router.d.ts +0 -78
  309. package/dist/runtime/input-router.d.ts.map +0 -1
  310. package/dist/runtime/input-router.js +0 -171
  311. package/dist/runtime/mount-game.d.ts +0 -129
  312. package/dist/runtime/mount-game.d.ts.map +0 -1
  313. package/dist/runtime/mount-game.js +0 -152
  314. package/dist/runtime/mount-manifest.d.ts +0 -119
  315. package/dist/runtime/mount-manifest.d.ts.map +0 -1
  316. package/dist/runtime/mount-manifest.js +0 -204
  317. package/dist/runtime/render-audio-control.d.ts +0 -110
  318. package/dist/runtime/render-audio-control.d.ts.map +0 -1
  319. package/dist/runtime/render-audio-control.js +0 -111
  320. package/dist/runtime/render-control.d.ts +0 -291
  321. package/dist/runtime/render-control.d.ts.map +0 -1
  322. package/dist/runtime/render-control.js +0 -264
  323. package/dist/runtime/render-seed.d.ts +0 -57
  324. package/dist/runtime/render-seed.d.ts.map +0 -1
  325. package/dist/runtime/render-seed.js +0 -74
  326. package/dist/runtime/scene-ui-bridge.d.ts +0 -69
  327. package/dist/runtime/scene-ui-bridge.d.ts.map +0 -1
  328. package/dist/runtime/scene-ui-bridge.js +0 -23
  329. package/dist/runtime/scene-ui-data.d.ts +0 -13
  330. package/dist/runtime/scene-ui-data.d.ts.map +0 -1
  331. package/dist/runtime/scene-ui-data.js +0 -92
  332. package/dist/runtime/state-bridge.d.ts +0 -47
  333. package/dist/runtime/state-bridge.d.ts.map +0 -1
  334. package/dist/runtime/state-bridge.js +0 -53
  335. package/dist/runtime/types.d.ts +0 -179
  336. package/dist/runtime/types.d.ts.map +0 -1
  337. package/dist/runtime/types.js +0 -1
  338. package/dist/scene/asset-loaders.d.ts +0 -79
  339. package/dist/scene/asset-loaders.d.ts.map +0 -1
  340. package/dist/scene/asset-loaders.js +0 -141
  341. package/dist/scene/asset-paths.d.ts +0 -11
  342. package/dist/scene/asset-paths.d.ts.map +0 -1
  343. package/dist/scene/asset-paths.js +0 -108
  344. package/dist/scene/asset-ref-check.d.ts +0 -88
  345. package/dist/scene/asset-ref-check.d.ts.map +0 -1
  346. package/dist/scene/asset-ref-check.js +0 -214
  347. package/dist/scene/asset-registry.d.ts +0 -30
  348. package/dist/scene/asset-registry.d.ts.map +0 -1
  349. package/dist/scene/asset-registry.js +0 -50
  350. package/dist/scene/collider-dimensions.d.ts +0 -63
  351. package/dist/scene/collider-dimensions.d.ts.map +0 -1
  352. package/dist/scene/collider-dimensions.js +0 -98
  353. package/dist/scene/component-registry.d.ts +0 -17
  354. package/dist/scene/component-registry.d.ts.map +0 -1
  355. package/dist/scene/component-registry.js +0 -28
  356. package/dist/scene/defaults.d.ts +0 -252
  357. package/dist/scene/defaults.d.ts.map +0 -1
  358. package/dist/scene/defaults.js +0 -151
  359. package/dist/scene/geometries/index.d.ts +0 -8
  360. package/dist/scene/geometries/index.d.ts.map +0 -1
  361. package/dist/scene/geometries/index.js +0 -7
  362. package/dist/scene/geometries/terrain.d.ts +0 -2
  363. package/dist/scene/geometries/terrain.d.ts.map +0 -1
  364. package/dist/scene/geometries/terrain.js +0 -37
  365. package/dist/scene/geometry-registry.d.ts +0 -24
  366. package/dist/scene/geometry-registry.d.ts.map +0 -1
  367. package/dist/scene/geometry-registry.js +0 -20
  368. package/dist/scene/instance-registry.d.ts +0 -36
  369. package/dist/scene/instance-registry.d.ts.map +0 -1
  370. package/dist/scene/instance-registry.js +0 -55
  371. package/dist/scene/instancers/grid.d.ts +0 -2
  372. package/dist/scene/instancers/grid.d.ts.map +0 -1
  373. package/dist/scene/instancers/grid.js +0 -36
  374. package/dist/scene/instancers/index.d.ts +0 -8
  375. package/dist/scene/instancers/index.d.ts.map +0 -1
  376. package/dist/scene/instancers/index.js +0 -7
  377. package/dist/scene/light-camera-factory.d.ts +0 -23
  378. package/dist/scene/light-camera-factory.d.ts.map +0 -1
  379. package/dist/scene/light-camera-factory.js +0 -68
  380. package/dist/scene/material-factory.d.ts +0 -17
  381. package/dist/scene/material-factory.d.ts.map +0 -1
  382. package/dist/scene/material-factory.js +0 -197
  383. package/dist/scene/material-registry.d.ts +0 -40
  384. package/dist/scene/material-registry.d.ts.map +0 -1
  385. package/dist/scene/material-registry.js +0 -44
  386. package/dist/scene/materials/index.d.ts +0 -8
  387. package/dist/scene/materials/index.d.ts.map +0 -1
  388. package/dist/scene/materials/index.js +0 -7
  389. package/dist/scene/materials/water.d.ts +0 -2
  390. package/dist/scene/materials/water.d.ts.map +0 -1
  391. package/dist/scene/materials/water.js +0 -54
  392. package/dist/scene/parse.d.ts +0 -23
  393. package/dist/scene/parse.d.ts.map +0 -1
  394. package/dist/scene/parse.js +0 -179
  395. package/dist/scene/particles-factory.d.ts +0 -33
  396. package/dist/scene/particles-factory.d.ts.map +0 -1
  397. package/dist/scene/particles-factory.js +0 -282
  398. package/dist/scene/scene-apply.d.ts +0 -109
  399. package/dist/scene/scene-apply.d.ts.map +0 -1
  400. package/dist/scene/scene-apply.js +0 -326
  401. package/dist/scene/scene-diff-schema.d.ts +0 -285
  402. package/dist/scene/scene-diff-schema.d.ts.map +0 -1
  403. package/dist/scene/scene-diff-schema.js +0 -92
  404. package/dist/scene/scene-diff-types.d.ts +0 -40
  405. package/dist/scene/scene-diff-types.d.ts.map +0 -1
  406. package/dist/scene/scene-diff-types.js +0 -14
  407. package/dist/scene/scene-loader.d.ts +0 -177
  408. package/dist/scene/scene-loader.d.ts.map +0 -1
  409. package/dist/scene/scene-loader.js +0 -1213
  410. package/dist/scene/scene-query.d.ts +0 -11
  411. package/dist/scene/scene-query.d.ts.map +0 -1
  412. package/dist/scene/scene-query.js +0 -63
  413. package/dist/scene/scene-types.d.ts +0 -10
  414. package/dist/scene/scene-types.d.ts.map +0 -1
  415. package/dist/scene/scene-types.js +0 -8
  416. package/dist/scene/scene-version.d.ts +0 -26
  417. package/dist/scene/scene-version.d.ts.map +0 -1
  418. package/dist/scene/scene-version.js +0 -34
  419. package/dist/scene/schema/animation.d.ts +0 -8
  420. package/dist/scene/schema/animation.d.ts.map +0 -1
  421. package/dist/scene/schema/animation.js +0 -42
  422. package/dist/scene/schema/audio.d.ts +0 -13
  423. package/dist/scene/schema/audio.d.ts.map +0 -1
  424. package/dist/scene/schema/audio.js +0 -22
  425. package/dist/scene/schema/camera.d.ts +0 -28
  426. package/dist/scene/schema/camera.d.ts.map +0 -1
  427. package/dist/scene/schema/camera.js +0 -17
  428. package/dist/scene/schema/collider.d.ts +0 -40
  429. package/dist/scene/schema/collider.d.ts.map +0 -1
  430. package/dist/scene/schema/collider.js +0 -55
  431. package/dist/scene/schema/entity-ref.d.ts +0 -31
  432. package/dist/scene/schema/entity-ref.d.ts.map +0 -1
  433. package/dist/scene/schema/entity-ref.js +0 -61
  434. package/dist/scene/schema/entity.d.ts +0 -1036
  435. package/dist/scene/schema/entity.d.ts.map +0 -1
  436. package/dist/scene/schema/entity.js +0 -153
  437. package/dist/scene/schema/environment.d.ts +0 -425
  438. package/dist/scene/schema/environment.d.ts.map +0 -1
  439. package/dist/scene/schema/environment.js +0 -357
  440. package/dist/scene/schema/index.d.ts +0 -39
  441. package/dist/scene/schema/index.d.ts.map +0 -1
  442. package/dist/scene/schema/index.js +0 -20
  443. package/dist/scene/schema/instances.d.ts +0 -14
  444. package/dist/scene/schema/instances.d.ts.map +0 -1
  445. package/dist/scene/schema/instances.js +0 -28
  446. package/dist/scene/schema/joint.d.ts +0 -23
  447. package/dist/scene/schema/joint.d.ts.map +0 -1
  448. package/dist/scene/schema/joint.js +0 -23
  449. package/dist/scene/schema/light.d.ts +0 -38
  450. package/dist/scene/schema/light.d.ts.map +0 -1
  451. package/dist/scene/schema/light.js +0 -26
  452. package/dist/scene/schema/material.d.ts +0 -199
  453. package/dist/scene/schema/material.d.ts.map +0 -1
  454. package/dist/scene/schema/material.js +0 -99
  455. package/dist/scene/schema/mesh.d.ts +0 -48
  456. package/dist/scene/schema/mesh.d.ts.map +0 -1
  457. package/dist/scene/schema/mesh.js +0 -82
  458. package/dist/scene/schema/particles.d.ts +0 -2178
  459. package/dist/scene/schema/particles.d.ts.map +0 -1
  460. package/dist/scene/schema/particles.js +0 -321
  461. package/dist/scene/schema/physics.d.ts +0 -56
  462. package/dist/scene/schema/physics.d.ts.map +0 -1
  463. package/dist/scene/schema/physics.js +0 -37
  464. package/dist/scene/schema/scene-file.d.ts +0 -304
  465. package/dist/scene/schema/scene-file.d.ts.map +0 -1
  466. package/dist/scene/schema/scene-file.js +0 -262
  467. package/dist/scene/schema/shadow.d.ts +0 -28
  468. package/dist/scene/schema/shadow.d.ts.map +0 -1
  469. package/dist/scene/schema/shadow.js +0 -20
  470. package/dist/scene/schema/spline.d.ts +0 -25
  471. package/dist/scene/schema/spline.d.ts.map +0 -1
  472. package/dist/scene/schema/spline.js +0 -15
  473. package/dist/scene/schema/tuples.d.ts +0 -12
  474. package/dist/scene/schema/tuples.d.ts.map +0 -1
  475. package/dist/scene/schema/tuples.js +0 -14
  476. package/dist/scene/schema/ui.d.ts +0 -880
  477. package/dist/scene/schema/ui.d.ts.map +0 -1
  478. package/dist/scene/schema/ui.js +0 -506
  479. package/dist/scene/user-data.d.ts +0 -171
  480. package/dist/scene/user-data.d.ts.map +0 -1
  481. package/dist/scene/user-data.js +0 -137
  482. package/dist/setup/setup-audio.d.ts +0 -20
  483. package/dist/setup/setup-audio.d.ts.map +0 -1
  484. package/dist/setup/setup-audio.js +0 -41
  485. package/dist/setup/setup-particles.d.ts +0 -17
  486. package/dist/setup/setup-particles.d.ts.map +0 -1
  487. package/dist/setup/setup-particles.js +0 -16
  488. package/dist/setup/setup-physics.d.ts +0 -25
  489. package/dist/setup/setup-physics.d.ts.map +0 -1
  490. package/dist/setup/setup-physics.js +0 -51
  491. package/dist/setup/setup-renderer.d.ts +0 -69
  492. package/dist/setup/setup-renderer.d.ts.map +0 -1
  493. package/dist/setup/setup-renderer.js +0 -363
  494. package/dist/tools/define-tool.d.ts +0 -105
  495. package/dist/tools/define-tool.d.ts.map +0 -1
  496. package/dist/tools/define-tool.js +0 -82
  497. package/dist/world2d/authoring-2d.d.ts +0 -76
  498. package/dist/world2d/authoring-2d.d.ts.map +0 -1
  499. package/dist/world2d/authoring-2d.js +0 -176
  500. package/dist/world2d/capture-to-scene2d.d.ts +0 -5
  501. package/dist/world2d/capture-to-scene2d.d.ts.map +0 -1
  502. package/dist/world2d/capture-to-scene2d.js +0 -49
  503. package/dist/world2d/collision-2d.d.ts +0 -52
  504. package/dist/world2d/collision-2d.d.ts.map +0 -1
  505. package/dist/world2d/collision-2d.js +0 -70
  506. package/dist/world2d/components-2d.d.ts +0 -47
  507. package/dist/world2d/components-2d.d.ts.map +0 -1
  508. package/dist/world2d/components-2d.js +0 -77
  509. package/dist/world2d/index.d.ts +0 -34
  510. package/dist/world2d/index.d.ts.map +0 -1
  511. package/dist/world2d/index.js +0 -32
  512. package/dist/world2d/ingest-iframe-2d.d.ts +0 -82
  513. package/dist/world2d/ingest-iframe-2d.d.ts.map +0 -1
  514. package/dist/world2d/ingest-iframe-2d.js +0 -166
  515. package/dist/world2d/ingest2d.d.ts +0 -97
  516. package/dist/world2d/ingest2d.d.ts.map +0 -1
  517. package/dist/world2d/ingest2d.js +0 -40
  518. package/dist/world2d/physics2d-registry.d.ts +0 -24
  519. package/dist/world2d/physics2d-registry.d.ts.map +0 -1
  520. package/dist/world2d/physics2d-registry.js +0 -38
  521. package/dist/world2d/pixi-game-adapter.d.ts +0 -107
  522. package/dist/world2d/pixi-game-adapter.d.ts.map +0 -1
  523. package/dist/world2d/pixi-game-adapter.js +0 -211
  524. package/dist/world2d/pixi-surface.d.ts +0 -39
  525. package/dist/world2d/pixi-surface.d.ts.map +0 -1
  526. package/dist/world2d/pixi-surface.js +0 -47
  527. package/dist/world2d/scene-capture-2d.d.ts +0 -43
  528. package/dist/world2d/scene-capture-2d.d.ts.map +0 -1
  529. package/dist/world2d/scene-capture-2d.js +0 -92
  530. package/dist/world2d/scene2d-loader.d.ts +0 -27
  531. package/dist/world2d/scene2d-loader.d.ts.map +0 -1
  532. package/dist/world2d/scene2d-loader.js +0 -274
  533. package/dist/world2d/schema/entity2d.d.ts +0 -102
  534. package/dist/world2d/schema/entity2d.d.ts.map +0 -1
  535. package/dist/world2d/schema/entity2d.js +0 -119
  536. package/dist/world2d/schema/physics2d.d.ts +0 -46
  537. package/dist/world2d/schema/physics2d.d.ts.map +0 -1
  538. package/dist/world2d/schema/physics2d.js +0 -45
  539. package/dist/world2d/schema/sprite.d.ts +0 -42
  540. package/dist/world2d/schema/sprite.d.ts.map +0 -1
  541. package/dist/world2d/schema/sprite.js +0 -63
  542. package/dist/world2d/schema/tilemap.d.ts +0 -15
  543. package/dist/world2d/schema/tilemap.d.ts.map +0 -1
  544. package/dist/world2d/schema/tilemap.js +0 -19
  545. package/dist/world2d/schema/tuples2d.d.ts +0 -20
  546. package/dist/world2d/schema/tuples2d.d.ts.map +0 -1
  547. package/dist/world2d/schema/tuples2d.js +0 -19
  548. package/dist/world2d/system-adapters-2d.d.ts +0 -16
  549. package/dist/world2d/system-adapters-2d.d.ts.map +0 -1
  550. package/dist/world2d/system-adapters-2d.js +0 -37
  551. package/dist/world2d/transform-writer-2d.d.ts +0 -16
  552. package/dist/world2d/transform-writer-2d.d.ts.map +0 -1
  553. package/dist/world2d/transform-writer-2d.js +0 -22
  554. package/dist/world2d/types.d.ts +0 -53
  555. package/dist/world2d/types.d.ts.map +0 -1
  556. package/dist/world2d/types.js +0 -1
  557. package/src/runtime/scene-ui-bridge.ts +0 -86
  558. package/src/runtime/scene-ui-data.ts +0 -119
  559. package/src/scene/schema/ui.ts +0 -602
@@ -1,8 +1,11 @@
1
1
  import * as THREE from 'three';
2
2
  import type { Actor, AnyActor, AnyStateMachine } from 'xstate';
3
+ import type { createSystemRunner } from '../core/system-runner';
4
+ import { deleteUserData, getUserData, setUserData } from '../scene/user-data';
3
5
  import type { BlendTreeDef } from './anim-graph-types';
4
6
  import { evaluateBlendTree } from './blend-node';
5
7
  import {
8
+ type AnimationBoneMask,
6
9
  type AnimationMetaStateNodeLike,
7
10
  clipNamesOf,
8
11
  collectMachineAnimationMeta,
@@ -11,69 +14,7 @@ import {
11
14
  type StateAnimationMeta,
12
15
  } from './xstate-animation-meta';
13
16
 
14
- /**
15
- * E2 — bind a native XState actor to a native `THREE.AnimationMixer` (spec
16
- * §11 E2, §5.6, §3.3).
17
- *
18
- * A native XState actor drives native `THREE.AnimationAction`s: entering a
19
- * state activates its `meta.animation` clip(s); leaving it deactivates them.
20
- * There is no VGAI-specific trigger vocabulary — callers drive the machine
21
- * with ordinary `actor.send(event)` (or the actor is externally owned and
22
- * this binding merely observes it); XState alone decides which state is
23
- * active.
24
- *
25
- * ## How enter/exit crossfades use NATIVE Three primitives (not a
26
- * hand-rolled weight scheduler)
27
- *
28
- * The former `AnimGraph` runtime (removed by E5) pre-played every action at
29
- * weight 0 and re-set every action's weight explicitly every frame,
30
- * BYPASSING `AnimationAction.fadeIn`/`fadeOut`/`crossFadeTo` (to dodge
31
- * multi-layer weight-summing bugs it hit historically).
32
- * E2 deliberately does the opposite, per the spec AC ("use
33
- * AnimationAction.crossFadeTo / fadeIn / fadeOut per the metadata"): the
34
- * ENTER/EXIT edge of an XState state transition is represented by one real
35
- * Three fade call, and `mixer.update(dt)` — the same call that advances clip
36
- * time — is what advances the fade too. No parallel elapsed-time clock
37
- * drives "is the fade done" for a plain (non-blend-tree) state; Three's own
38
- * scheduler is authoritative end to end.
39
- *
40
- * A **blend-tree** state is, at the instant of entry, represented by ONE
41
- * clip — its heaviest-weighted child at that instant (the same "representative"
42
- * duration/action heuristic the removed `AnimGraph.LayerRuntime` used) — which
43
- * receives the native crossFadeTo/fadeIn. Sibling
44
- * children stay silent (weight 0) until the crossfade window elapses, at
45
- * which point the FULL live blend-tree distribution (`evaluateBlendTree`,
46
- * reused verbatim from `blend-node.ts` — no reimplementation) takes over via
47
- * `setEffectiveWeight` every tick, recomputed from the actor's live
48
- * context/selector value. This is a documented simplification (Three's
49
- * `crossFadeTo` is strictly 1-to-1; there is no native N-ary blend-tree
50
- * crossfade), the same spirit as `blend-node.ts`'s own documented 2D
51
- * "smooth approximation, not Delaunay/barycentric" caveat. The crossfade
52
- * window itself is tracked as a small elapsed-seconds counter, decremented
53
- * by the SAME `dt` passed to `mixer.update(dt)` — it never drifts from
54
- * Three's own fade schedule because both are driven by the identical `dt`
55
- * stream, so it is bookkeeping for "when to hand off from native fade to
56
- * live blend", not a second competing scheduler.
57
- *
58
- * ## Mixer advancement uses the canonical engine ANIMATION phase
59
- *
60
- * `tick(dt)` has exactly the engine's `SystemFn` shape (`core/types.ts`:
61
- * `(dt: number) => void`) — a caller registers this binding on the engine's
62
- * one real fixed-step scheduler at the ANIMATION phase, exactly as any other
63
- * per-frame system: `ctx.systems.add(SystemPhase.ANIMATION, binding.tick)`
64
- * (or, when the binding is owned by a `GameComponent` that also owns the
65
- * actor — the §5.6 pattern the examples use — the component simply calls
66
- * `binding.tick(dt)` from its own `update(dt)`). Either way mixer advancement
67
- * rides that one scheduler; no second scheduler is invented here. (This is
68
- * the same ANIMATION-phase slot the removed `AnimGraph` runtime's global
69
- * `animationSystem` occupied before E5 — see
70
- * docs/AI-NATIVE-AUTHORING-IMPLEMENTATION-SPEC.md §3.3/§11 E5. The D1
71
- * `AnimationClock` — `animation-clock.ts` — is a distinct, sibling concern: a
72
- * per-cinematic/Theatre-timeline seek clock for scrubbing/export, not wired
73
- * into ordinary gameplay ticking today.)
74
- */
75
-
76
- /** Thrown for a binding-time problem: a missing clip, or a target-less bind. */
17
+ /** A binding-time problem such as a missing clip, bone, or ambiguous layer. */
77
18
  export class AnimationBindingError extends Error {
78
19
  constructor(
79
20
  message: string,
@@ -85,77 +26,161 @@ export class AnimationBindingError extends Error {
85
26
  }
86
27
 
87
28
  export interface XStateAnimationBindingOptions {
29
+ /** Select live numeric/boolean blend-tree parameters from actor context. */
30
+ selectParameters?: (context: unknown) => Record<string, number | boolean>;
31
+ /** Root whose named Bone/Object3D hierarchy is used by `boneMask`. Defaults to mixer root. */
32
+ root?: THREE.Object3D;
33
+ /** Entity that exposes this binding to editor/runtime inspection. Defaults to `root`. */
34
+ owner?: THREE.Object3D;
88
35
  /**
89
- * Reads the blend-tree parameter map from the actor's live context.
90
- * Defaults to treating `context` itself as `Record<string, number | boolean>`
91
- * (matches the §5.6 example: `context: { speed: number; grounded: boolean }`
92
- * fed straight into a blend tree keyed by `parameter: 'speed'`). Override
93
- * when a machine's blend-tree parameters need deriving from context via a
94
- * selector rather than reading top-level fields directly.
36
+ * Engine runner used to register `tick` in the canonical animation phase.
37
+ * Pass `ctx.systems`; disposal removes the callback automatically. When
38
+ * omitted, the caller owns ticking (useful for standalone Three tests).
95
39
  */
96
- selectParameters?: (context: unknown) => Record<string, number | boolean>;
40
+ systems?: Pick<ReturnType<typeof createSystemRunner>, 'add' | 'remove'>;
41
+ }
42
+
43
+ export interface XStateAnimationLayerState {
44
+ readonly layer: string;
45
+ readonly stateId: string;
46
+ readonly clips: readonly string[];
47
+ readonly weight: number;
48
+ readonly blendMode: 'override' | 'additive';
49
+ readonly boneMask?: AnimationBoneMask;
97
50
  }
98
51
 
99
52
  export interface XStateAnimationBinding {
100
- /**
101
- * Advance the bound mixer (and any live blend-tree weights) by `dt`
102
- * seconds. Register on the engine's `'animation'` phase — see the module
103
- * doc above. A no-op after `dispose()`.
104
- */
53
+ /** Advance native actions/mixer. Register this in the engine animation phase. */
105
54
  tick: (dt: number) => void;
106
- /** The bound actor, for convenience (`binding.actor.send({...})`). */
107
55
  readonly actor: AnyActor;
108
- /**
109
- * Stops observing the actor (does NOT stop the actor itself — the actor's
110
- * lifecycle is the caller's, this binding only ever subscribed to it) and
111
- * uncaches every `AnimationAction` this binding created from the mixer, so
112
- * a dispose+rebind cycle (e.g. hot-reload, despawn/respawn) does not leak
113
- * mixer-cached actions. Idempotent; `tick()` is a no-op after this.
114
- */
56
+ /** Current native composition, useful to the editor and game diagnostics. */
57
+ getActiveLayers: () => readonly XStateAnimationLayerState[];
58
+ /** Unsubscribe, stop/uncache actions, and remove owner inspection data. Idempotent. */
115
59
  dispose: () => void;
116
60
  }
117
61
 
62
+ let inspectionVersion = 0;
63
+ const inspectionListeners = new Set<() => void>();
64
+
65
+ /** React/useSyncExternalStore-compatible lifecycle signal for live editor inspection. */
66
+ export function subscribeXStateAnimationBindings(listener: () => void): () => void {
67
+ inspectionListeners.add(listener);
68
+ return () => inspectionListeners.delete(listener);
69
+ }
70
+
71
+ /** Monotonic snapshot changed whenever a binding is attached or disposed. */
72
+ export function getXStateAnimationBindingsVersion(): number {
73
+ return inspectionVersion;
74
+ }
75
+
76
+ function notifyInspectionLifecycle(): void {
77
+ inspectionVersion++;
78
+ for (const listener of inspectionListeners) listener();
79
+ }
80
+
118
81
  interface WeightedClip {
119
82
  clip: string;
83
+ actionKey: string;
120
84
  weight: number;
121
85
  }
122
86
 
123
87
  interface ActiveState {
88
+ layer: string;
124
89
  stateId: string;
125
90
  meta: StateAnimationMeta;
126
- /** The action(s) this state's `meta` refers to, one weighted entry per clip. */
127
91
  natural: WeightedClip[];
128
- /** The single clip carrying the native crossFadeTo/fadeIn schedule. */
129
- dominantClip: string;
130
- /** Seconds remaining in the enter crossfade; 0 once settled (or if duration was 0). */
92
+ dominantActionKey: string;
131
93
  crossfadeRemaining: number;
132
94
  }
133
95
 
134
- function dominantOf(weights: WeightedClip[]): WeightedClip {
135
- return weights.reduce((a, b) => (b.weight > a.weight ? b : a), weights[0]!);
96
+ interface CachedAction {
97
+ action: THREE.AnimationAction;
98
+ clip: THREE.AnimationClip;
136
99
  }
137
100
 
138
- function naturalWeightsOf(meta: StateAnimationMeta): WeightedClip[] {
139
- if (isBlendTreeAnimationMeta(meta)) {
140
- // AnimationBlendTree (xstate-animation-meta.ts) is structurally
141
- // identical to BlendTreeDef (anim-graph-types.ts) by design — reuse
142
- // evaluateBlendTree verbatim, no conversion/reimplementation.
143
- return [];
101
+ const layerOf = (meta: StateAnimationMeta): string => meta.layer ?? 'base';
102
+ const layerWeightOf = (meta: StateAnimationMeta): number => meta.weight ?? 1;
103
+ const blendModeOf = (meta: StateAnimationMeta): 'override' | 'additive' =>
104
+ meta.blendMode ?? 'override';
105
+
106
+ function targetNameOf(track: THREE.KeyframeTrack): string {
107
+ try {
108
+ return THREE.PropertyBinding.parseTrackName(track.name).nodeName ?? '';
109
+ } catch {
110
+ const dot = track.name.indexOf('.');
111
+ return dot < 0 ? track.name : track.name.slice(0, dot);
144
112
  }
145
- return [{ clip: meta.clip, weight: 1 }];
113
+ }
114
+
115
+ function subtreeNames(root: THREE.Object3D, requested: readonly string[], trackNames: Set<string>) {
116
+ const names = new Set<string>();
117
+ const missing: string[] = [];
118
+ for (const requestedName of requested) {
119
+ let found = false;
120
+ root.traverse((object) => {
121
+ if (object.name !== requestedName) return;
122
+ found = true;
123
+ object.traverse((descendant) => {
124
+ if (descendant.name) names.add(descendant.name);
125
+ });
126
+ });
127
+ // Some exporters target a named node not retained as a discoverable Bone.
128
+ // Exact track-target matches remain useful, but cannot imply descendants.
129
+ if (trackNames.has(requestedName)) {
130
+ found = true;
131
+ names.add(requestedName);
132
+ }
133
+ if (!found) missing.push(requestedName);
134
+ }
135
+ return { names, missing };
136
+ }
137
+
138
+ function maskedClip(
139
+ source: THREE.AnimationClip,
140
+ mask: AnimationBoneMask,
141
+ root: THREE.Object3D,
142
+ stateId: string,
143
+ ): THREE.AnimationClip {
144
+ const trackNames = new Set(source.tracks.map(targetNameOf));
145
+ const included = mask.include ? subtreeNames(root, mask.include, trackNames) : undefined;
146
+ const excluded = mask.exclude ? subtreeNames(root, mask.exclude, trackNames) : undefined;
147
+ const missing = [...(included?.missing ?? []), ...(excluded?.missing ?? [])];
148
+ if (missing.length) {
149
+ throw new AnimationBindingError(
150
+ `[bindXStateAnimation] state "${stateId}" boneMask names unknown bones/objects: ${missing.join(', ')}`,
151
+ stateId,
152
+ );
153
+ }
154
+ const tracks = source.tracks.filter((track) => {
155
+ const name = targetNameOf(track);
156
+ return (!included || included.names.has(name)) && !excluded?.names.has(name);
157
+ });
158
+ if (tracks.length === 0) {
159
+ throw new AnimationBindingError(
160
+ `[bindXStateAnimation] state "${stateId}" boneMask removes every track from clip "${source.name}"`,
161
+ stateId,
162
+ );
163
+ }
164
+ return new THREE.AnimationClip(
165
+ source.name,
166
+ source.duration,
167
+ tracks.map((track) => track.clone()),
168
+ );
169
+ }
170
+
171
+ function dominantOf(weights: WeightedClip[]): WeightedClip {
172
+ return weights.reduce((a, b) => (b.weight > a.weight ? b : a), weights[0]!);
146
173
  }
147
174
 
148
175
  /**
149
- * Bind an XState actor to a `THREE.AnimationMixer`.
150
- *
151
- * Validates EVERY state's `meta.animation` (via `collectMachineAnimationMeta`,
152
- * E1) and resolves every referenced clip name against `clips` up front, at
153
- * bind time — before the actor enters any of those states — so an agent
154
- * authoring a machine with a typo'd clip name gets an actionable error
155
- * immediately, not only once gameplay happens to reach that state.
176
+ * Bind a native XState actor to native Three actions.
156
177
  *
157
- * @throws {AnimationBindingError} if a state's meta references a clip name
158
- * not present in `clips`, naming both the state and the clip.
178
+ * One active animated state is allowed per named layer. Parallel XState
179
+ * regions therefore compose lower-body locomotion, upper-body weapon states,
180
+ * facial animation, and other independent layers without introducing a
181
+ * second transition language. Bone masks clone/filter native clip tracks;
182
+ * additive layers use `AnimationUtils.makeClipAdditive` and Three's additive
183
+ * blend mode. Single unlayered machines retain the original clip/action path.
159
184
  */
160
185
  export function bindXStateAnimation(
161
186
  actor: Actor<AnyStateMachine>,
@@ -163,12 +188,18 @@ export function bindXStateAnimation(
163
188
  clips: Map<string, THREE.AnimationClip>,
164
189
  options: XStateAnimationBindingOptions = {},
165
190
  ): XStateAnimationBinding {
166
- const root = actor.logic.root as unknown as AnimationMetaStateNodeLike;
167
- const entries: MachineAnimationMetaEntry[] = collectMachineAnimationMeta(root);
168
- const metaByStateId = new Map(entries.map((e) => [e.stateId, e.meta]));
191
+ const mixerRoot = mixer.getRoot();
192
+ const root = options.root ?? (mixerRoot instanceof THREE.Object3D ? mixerRoot : undefined);
193
+ if (!root) {
194
+ throw new AnimationBindingError(
195
+ '[bindXStateAnimation] a THREE.Object3D `root` option is required when the mixer uses AnimationObjectGroup',
196
+ );
197
+ }
198
+ const owner = options.owner ?? root;
199
+ const machineRoot = actor.logic.root as unknown as AnimationMetaStateNodeLike;
200
+ const entries: MachineAnimationMetaEntry[] = collectMachineAnimationMeta(machineRoot);
201
+ const metaByStateId = new Map(entries.map((entry) => [entry.stateId, entry.meta]));
169
202
 
170
- // Resolve every referenced clip up front — actionable, named errors before
171
- // any state is ever entered.
172
203
  for (const entry of entries) {
173
204
  for (const clipName of clipNamesOf(entry.meta)) {
174
205
  if (!clips.has(clipName)) {
@@ -181,25 +212,58 @@ export function bindXStateAnimation(
181
212
  }
182
213
  }
183
214
 
184
- const actionCache = new Map<string, THREE.AnimationAction>();
185
- function ensureAction(clipName: string, loop: boolean, speed: number): THREE.AnimationAction {
186
- let action = actionCache.get(clipName);
187
- if (!action) {
188
- const clip = clips.get(clipName);
189
- if (!clip) {
190
- // Unreachable given the up-front resolution above; kept as a named,
191
- // defensive error rather than a silent `mixer.clipAction(undefined)`.
192
- throw new AnimationBindingError(`[bindXStateAnimation] clip "${clipName}" not found`);
215
+ // Different layers need distinct AnimationActions even when they use the
216
+ // same source clip. A plain base state keeps the original clip identity for
217
+ // backwards-compatible mixer.existingAction(sourceClip) behavior.
218
+ const preparedClips = new Map<string, THREE.AnimationClip>();
219
+ function actionKey(stateId: string, meta: StateAnimationMeta, clipName: string): string {
220
+ const transformed =
221
+ layerOf(meta) !== 'base' || Boolean(meta.boneMask) || blendModeOf(meta) === 'additive';
222
+ return transformed ? `${stateId}\u0000${clipName}` : clipName;
223
+ }
224
+ for (const { stateId, meta } of entries) {
225
+ for (const clipName of clipNamesOf(meta)) {
226
+ const key = actionKey(stateId, meta, clipName);
227
+ if (preparedClips.has(key)) continue;
228
+ const source = clips.get(clipName)!;
229
+ let prepared = meta.boneMask ? maskedClip(source, meta.boneMask, root, stateId) : source;
230
+ if (prepared !== source || key !== clipName) {
231
+ if (prepared === source) prepared = source.clone();
232
+ prepared.name = `${source.name}@${layerOf(meta)}:${stateId}`;
193
233
  }
194
- action = mixer.clipAction(clip);
234
+ if (blendModeOf(meta) === 'additive') {
235
+ THREE.AnimationUtils.makeClipAdditive(prepared);
236
+ prepared.blendMode = THREE.AdditiveAnimationBlendMode;
237
+ } else {
238
+ prepared.blendMode = THREE.NormalAnimationBlendMode;
239
+ }
240
+ preparedClips.set(key, prepared);
241
+ }
242
+ }
243
+
244
+ const actionCache = new Map<string, CachedAction>();
245
+ function ensureAction(
246
+ key: string,
247
+ loop: boolean,
248
+ speed: number,
249
+ weight: number,
250
+ ): THREE.AnimationAction {
251
+ let cached = actionCache.get(key);
252
+ if (!cached) {
253
+ const clip = preparedClips.get(key);
254
+ if (!clip)
255
+ throw new AnimationBindingError(`[bindXStateAnimation] prepared clip "${key}" not found`);
256
+ const action = mixer.clipAction(clip);
195
257
  action.setEffectiveWeight(0);
196
- action.play(); // must be "active" for mixer.update() to tick it at all (Three only advances active actions)
197
- actionCache.set(clipName, action);
258
+ action.play();
259
+ cached = { action, clip };
260
+ actionCache.set(key, cached);
198
261
  }
199
- action.setLoop(loop ? THREE.LoopRepeat : THREE.LoopOnce, Infinity);
200
- action.clampWhenFinished = !loop;
201
- action.timeScale = speed;
202
- return action;
262
+ cached.action.setLoop(loop ? THREE.LoopRepeat : THREE.LoopOnce, Infinity);
263
+ cached.action.clampWhenFinished = !loop;
264
+ cached.action.timeScale = speed;
265
+ cached.action.weight = weight;
266
+ return cached.action;
203
267
  }
204
268
 
205
269
  function readParameters(context: unknown): Map<string, number | boolean> {
@@ -209,190 +273,164 @@ export function bindXStateAnimation(
209
273
  return new Map(Object.entries(raw ?? {}));
210
274
  }
211
275
 
212
- let active: ActiveState | null = null;
276
+ function weightsOf(stateId: string, meta: StateAnimationMeta, context: unknown): WeightedClip[] {
277
+ const weights = isBlendTreeAnimationMeta(meta)
278
+ ? evaluateBlendTree(meta.blendTree as unknown as BlendTreeDef, readParameters(context))
279
+ : [{ clip: meta.clip, weight: 1 }];
280
+ return weights.map(({ clip, weight }) => ({
281
+ clip,
282
+ weight,
283
+ actionKey: actionKey(stateId, meta, clip),
284
+ }));
285
+ }
286
+
287
+ const activeLayers = new Map<string, ActiveState>();
213
288
  let disposed = false;
214
289
 
215
- // Known limitation (shared with plain Three.js `crossFadeTo` usage generally):
216
- // `AnimationAction.fadeOut(duration)` always schedules a fixed 1→0 ramp, not
217
- // "current weight → 0" — interrupting an action that is itself still
218
- // mid-fade-in (e.g. a state transitions again before its own crossfade
219
- // settles) can produce a brief visible pop back toward full weight before
220
- // fading out. This is a native Three scheduling property, not something
221
- // this binding's bookkeeping can smooth over without re-implementing
222
- // Three's own weight scheduler; same accepted-approximation spirit as
223
- // `blend-node.ts`'s documented 2D IDW caveat.
224
290
  function silence(weights: WeightedClip[], except: string | undefined, duration: number): void {
225
- for (const { clip: clipName } of weights) {
226
- if (clipName === except) continue;
227
- const action = actionCache.get(clipName);
291
+ for (const weighted of weights) {
292
+ if (weighted.actionKey === except) continue;
293
+ const action = actionCache.get(weighted.actionKey)?.action;
228
294
  if (!action) continue;
229
295
  if (duration > 0) action.fadeOut(duration);
230
296
  else action.setEffectiveWeight(0);
231
297
  }
232
298
  }
233
299
 
234
- /** Ensure every clip in `weights` has an action; reset the dominant one to weight 1 (base
235
- * multiplier for the fade schedule about to be applied) and every sibling to silent (0). */
236
- function prepareStateActions(
237
- weights: WeightedClip[],
238
- dominantClip: string,
239
- loop: boolean,
240
- speed: number,
241
- ): THREE.AnimationAction {
242
- for (const { clip: clipName } of weights) ensureAction(clipName, loop, speed);
243
-
244
- const dominantAction = actionCache.get(dominantClip)!;
245
- dominantAction.reset();
246
- // `setEffectiveWeight(1)` (not just `.reset()`) is required here: reset() does not touch the
247
- // base `.weight` multiplier a scheduled fade scales against, and this action may previously
248
- // have been a silenced blend-tree sibling left at `.weight === 0` — left alone, that would mute
249
- // the crossFadeTo/fadeIn schedule applied below to a permanent 0.
250
- dominantAction.setEffectiveWeight(1);
251
-
252
- // Non-dominant children of THIS state start silent; they pick up their
253
- // live share once the crossfade window elapses (see `tick`).
254
- for (const { clip: clipName } of weights) {
255
- if (clipName === dominantClip) continue;
256
- const action = actionCache.get(clipName)!;
257
- action.reset();
258
- action.setEffectiveWeight(0);
259
- }
260
- return dominantAction;
261
- }
262
-
263
- /** Apply the enter-transition schedule: native crossFadeTo (from a previous dominant), a plain
264
- * fadeIn (first-ever activation with a duration), or an immediate hard cut (duration 0). */
265
- function activateTransition(
266
- dominantAction: THREE.AnimationAction,
267
- outgoing: ActiveState | null,
268
- duration: number,
269
- warp: boolean,
270
- ): void {
271
- if (!outgoing) {
272
- if (duration > 0) dominantAction.fadeIn(duration);
273
- else dominantAction.setEffectiveWeight(1);
274
- return;
275
- }
276
- // Fade out every non-dominant sibling of the OUTGOING state — a plain
277
- // (non-blend) outgoing state has none.
278
- silence(outgoing.natural, outgoing.dominantClip, duration);
279
- const previousDominant = actionCache.get(outgoing.dominantClip);
280
- if (previousDominant && duration > 0) {
281
- previousDominant.crossFadeTo(dominantAction, duration, warp);
282
- return;
300
+ function applyLiveBlendWeights(state: ActiveState, context: unknown): void {
301
+ if (!isBlendTreeAnimationMeta(state.meta)) return;
302
+ state.natural = weightsOf(state.stateId, state.meta, context);
303
+ const layerWeight = layerWeightOf(state.meta);
304
+ for (const weighted of state.natural) {
305
+ actionCache.get(weighted.actionKey)?.action.setEffectiveWeight(weighted.weight * layerWeight);
283
306
  }
284
- previousDominant?.setEffectiveWeight(0);
285
- dominantAction.setEffectiveWeight(1);
286
307
  }
287
308
 
288
309
  function enterState(stateId: string, meta: StateAnimationMeta, context: unknown): void {
289
- const isBlend = isBlendTreeAnimationMeta(meta);
290
- const weights = isBlend
291
- ? evaluateBlendTree(meta.blendTree as unknown as BlendTreeDef, readParameters(context))
292
- : naturalWeightsOf(meta);
310
+ const layer = layerOf(meta);
311
+ const outgoing = activeLayers.get(layer) ?? null;
312
+ const weights = weightsOf(stateId, meta, context);
293
313
  const dominant = dominantOf(weights);
294
- const loop = isBlend ? true : meta.loop;
295
- const speed = isBlend ? 1 : meta.speed;
314
+ const loop = isBlendTreeAnimationMeta(meta) ? true : meta.loop;
315
+ const speed = isBlendTreeAnimationMeta(meta) ? 1 : meta.speed;
316
+ const layerWeight = layerWeightOf(meta);
317
+
318
+ for (const weighted of weights) ensureAction(weighted.actionKey, loop, speed, layerWeight);
319
+ const dominantAction = actionCache.get(dominant.actionKey)!.action;
320
+ dominantAction.reset().setEffectiveWeight(layerWeight);
321
+ for (const weighted of weights) {
322
+ if (weighted.actionKey === dominant.actionKey) continue;
323
+ actionCache.get(weighted.actionKey)!.action.reset().setEffectiveWeight(0);
324
+ }
296
325
 
297
- const dominantAction = prepareStateActions(weights, dominant.clip, loop, speed);
298
326
  const duration = meta.crossfade?.duration ?? 0;
299
327
  const warp = meta.crossfade?.warp ?? false;
300
- activateTransition(dominantAction, active, duration, warp);
328
+ if (!outgoing) {
329
+ if (duration > 0) dominantAction.fadeIn(duration);
330
+ } else {
331
+ silence(outgoing.natural, outgoing.dominantActionKey, duration);
332
+ const previous = actionCache.get(outgoing.dominantActionKey)?.action;
333
+ if (previous && duration > 0) previous.crossFadeTo(dominantAction, duration, warp);
334
+ else {
335
+ previous?.setEffectiveWeight(0);
336
+ dominantAction.setEffectiveWeight(layerWeight);
337
+ }
338
+ }
301
339
 
302
- active = {
340
+ const active: ActiveState = {
341
+ layer,
303
342
  stateId,
304
343
  meta,
305
344
  natural: weights,
306
- dominantClip: dominant.clip,
345
+ dominantActionKey: dominant.actionKey,
307
346
  crossfadeRemaining: duration,
308
347
  };
348
+ activeLayers.set(layer, active);
349
+ if (isBlendTreeAnimationMeta(meta) && duration <= 0) applyLiveBlendWeights(active, context);
350
+ }
309
351
 
310
- // Zero-duration blend-tree entry: no crossfade window to wait out —
311
- // apply the full live distribution immediately instead of leaving
312
- // siblings silent for one extra tick.
313
- if (isBlend && duration <= 0) {
314
- applyLiveBlendWeights(active, context);
315
- }
352
+ function activeStateIds(snapshot: { getMeta: () => Record<string, unknown> }): string[] {
353
+ return Object.keys(snapshot.getMeta()).filter((id) => metaByStateId.has(id));
316
354
  }
317
355
 
318
- function applyLiveBlendWeights(state: ActiveState, context: unknown): void {
319
- if (!isBlendTreeAnimationMeta(state.meta)) return;
320
- const params = readParameters(context);
321
- const weights = evaluateBlendTree(state.meta.blendTree as unknown as BlendTreeDef, params);
322
- state.natural = weights;
323
- for (const { clip: clipName, weight } of weights) {
324
- actionCache.get(clipName)?.setEffectiveWeight(weight);
356
+ function reconcile(snapshot: { getMeta: () => Record<string, unknown>; context: unknown }): void {
357
+ const nextByLayer = new Map<string, string>();
358
+ for (const stateId of activeStateIds(snapshot)) {
359
+ const meta = metaByStateId.get(stateId)!;
360
+ const layer = layerOf(meta);
361
+ const previous = nextByLayer.get(layer);
362
+ if (previous) {
363
+ throw new AnimationBindingError(
364
+ `[bindXStateAnimation] animated states "${previous}" and "${stateId}" are both active ` +
365
+ `on layer "${layer}". Give parallel regions distinct meta.animation.layer names.`,
366
+ stateId,
367
+ );
368
+ }
369
+ nextByLayer.set(layer, stateId);
325
370
  }
326
- }
327
371
 
328
- const initialSnapshot = actor.getSnapshot();
329
- const initialStateId = leafStateIdOf(initialSnapshot, metaByStateId);
330
- if (initialStateId) {
331
- enterState(initialStateId, metaByStateId.get(initialStateId)!, initialSnapshot.context);
372
+ for (const [layer, active] of activeLayers) {
373
+ if (nextByLayer.has(layer)) continue;
374
+ silence(active.natural, undefined, 0);
375
+ activeLayers.delete(layer);
376
+ }
377
+ for (const [layer, stateId] of nextByLayer) {
378
+ if (activeLayers.get(layer)?.stateId === stateId) continue;
379
+ enterState(stateId, metaByStateId.get(stateId)!, snapshot.context);
380
+ }
332
381
  }
333
382
 
383
+ const initialSnapshot = actor.getSnapshot();
384
+ reconcile(initialSnapshot);
334
385
  const subscription = actor.subscribe((snapshot) => {
335
- if (disposed) return;
336
- const stateId = leafStateIdOf(snapshot, metaByStateId);
337
- const meta = stateId ? metaByStateId.get(stateId) : undefined;
338
- if (meta && stateId && stateId !== active?.stateId) {
339
- enterState(stateId, meta, snapshot.context);
340
- }
386
+ if (!disposed) reconcile(snapshot);
341
387
  });
342
388
 
343
389
  function tick(dt: number): void {
344
390
  if (disposed) return;
345
- if (active && active.crossfadeRemaining > 0) {
346
- active.crossfadeRemaining = Math.max(0, active.crossfadeRemaining - dt);
347
- if (active.crossfadeRemaining <= 0 && isBlendTreeAnimationMeta(active.meta)) {
348
- applyLiveBlendWeights(active, actor.getSnapshot().context);
391
+ const context = actor.getSnapshot().context;
392
+ for (const active of activeLayers.values()) {
393
+ if (active.crossfadeRemaining > 0) {
394
+ active.crossfadeRemaining = Math.max(0, active.crossfadeRemaining - dt);
395
+ if (active.crossfadeRemaining <= 0) applyLiveBlendWeights(active, context);
396
+ } else {
397
+ applyLiveBlendWeights(active, context);
349
398
  }
350
- } else if (active && isBlendTreeAnimationMeta(active.meta)) {
351
- applyLiveBlendWeights(active, actor.getSnapshot().context);
352
399
  }
353
400
  mixer.update(dt);
354
401
  }
355
402
 
356
- return {
403
+ const binding: XStateAnimationBinding = {
357
404
  tick,
358
405
  actor,
406
+ getActiveLayers: () =>
407
+ [...activeLayers.values()].map((active) => ({
408
+ layer: active.layer,
409
+ stateId: active.stateId,
410
+ clips: active.natural.map((weighted) => weighted.clip),
411
+ weight: layerWeightOf(active.meta),
412
+ blendMode: blendModeOf(active.meta),
413
+ ...(active.meta.boneMask ? { boneMask: active.meta.boneMask } : {}),
414
+ })),
359
415
  dispose(): void {
360
416
  if (disposed) return;
361
417
  disposed = true;
418
+ options.systems?.remove('animation', tick);
362
419
  subscription.unsubscribe();
363
- for (const [clipName, action] of actionCache) {
420
+ for (const { action, clip } of actionCache.values()) {
364
421
  action.stop();
365
- mixer.uncacheAction(action.getClip(), action.getRoot());
366
- actionCache.delete(clipName);
422
+ mixer.uncacheAction(clip, action.getRoot());
423
+ }
424
+ actionCache.clear();
425
+ activeLayers.clear();
426
+ if (getUserData(owner, '_xstateAnimation') === binding) {
427
+ deleteUserData(owner, '_xstateAnimation');
428
+ notifyInspectionLifecycle();
367
429
  }
368
- active = null;
369
430
  },
370
431
  };
371
- }
372
-
373
- /**
374
- * The full state id (e.g. `"character-animation.locomotion"`) of the single
375
- * currently-active leaf that declares `meta.animation`, per `snapshot.value`.
376
- * XState's `snapshot.getMeta()` is keyed by EVERY active state node's id
377
- * (including ancestors with no meta of their own); this narrows to whichever
378
- * one is actually described in `metaByStateId`. Machines with more than one
379
- * simultaneously-active animated leaf (parallel regions each declaring
380
- * `meta.animation`) are out of scope for this binding — v1 assumes exactly
381
- * one active animation-bearing state at a time, matching the single-mixer
382
- * §5.6 example; a machine that violates this throws rather than silently
383
- * picking one.
384
- */
385
- function leafStateIdOf(
386
- snapshot: { getMeta: () => Record<string, unknown> },
387
- metaByStateId: Map<string, StateAnimationMeta>,
388
- ): string | undefined {
389
- const activeIds = Object.keys(snapshot.getMeta()).filter((id) => metaByStateId.has(id));
390
- if (activeIds.length > 1) {
391
- throw new AnimationBindingError(
392
- `[bindXStateAnimation] more than one animated state is simultaneously active ` +
393
- `(${activeIds.join(', ')}) — parallel-region animation composition is not supported by this ` +
394
- 'binding; exactly one active meta.animation state is required.',
395
- );
396
- }
397
- return activeIds[0];
432
+ setUserData(owner, '_xstateAnimation', binding);
433
+ notifyInspectionLifecycle();
434
+ options.systems?.add('animation', tick);
435
+ return binding;
398
436
  }