@jgengine/core 0.18.1 → 0.21.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 (404) hide show
  1. package/CHANGELOG.md +504 -7
  2. package/dist/ai/decisionGraph.d.ts +50 -4
  3. package/dist/ai/decisionGraph.js +159 -23
  4. package/dist/ai/flock.d.ts +1 -1
  5. package/dist/ai/flock.js +7 -4
  6. package/dist/ai/groupAssist.d.ts +10 -0
  7. package/dist/ai/groupAssist.js +11 -0
  8. package/dist/ai/pursuit.d.ts +1 -1
  9. package/dist/ai/pursuit.js +1 -1
  10. package/dist/ai/threat.d.ts +8 -0
  11. package/dist/ai/threat.js +10 -0
  12. package/dist/anim/animGraph.d.ts +43 -1
  13. package/dist/anim/animGraph.js +326 -10
  14. package/dist/anim/boneTexture.d.ts +55 -0
  15. package/dist/anim/boneTexture.js +43 -0
  16. package/dist/anim/footPlacement.d.ts +57 -0
  17. package/dist/anim/footPlacement.js +67 -0
  18. package/dist/anim/locomotionGraph.d.ts +18 -1
  19. package/dist/anim/locomotionGraph.js +42 -3
  20. package/dist/audio/audioFalloff.d.ts +6 -0
  21. package/dist/audio/doppler.d.ts +21 -0
  22. package/dist/audio/doppler.js +32 -0
  23. package/dist/audio/engineLayers.d.ts +83 -0
  24. package/dist/audio/engineLayers.js +115 -0
  25. package/dist/audio/voiceAllocator.d.ts +46 -0
  26. package/dist/audio/voiceAllocator.js +76 -0
  27. package/dist/combat/abilityKit.d.ts +20 -0
  28. package/dist/combat/abilityKit.js +41 -4
  29. package/dist/combat/death.d.ts +11 -3
  30. package/dist/combat/death.js +3 -3
  31. package/dist/combat/magazine.d.ts +9 -0
  32. package/dist/combat/magazine.js +24 -2
  33. package/dist/combat/projectiles.d.ts +94 -2
  34. package/dist/combat/projectiles.js +282 -61
  35. package/dist/combat/regenShield.d.ts +14 -0
  36. package/dist/combat/regenShield.js +23 -7
  37. package/dist/combat/shotOrigin.d.ts +6 -0
  38. package/dist/combat/shotOrigin.js +6 -0
  39. package/dist/combat/weaponFeedback.d.ts +36 -0
  40. package/dist/combat/weaponFeedback.js +42 -0
  41. package/dist/combat/weaponHandling.d.ts +161 -0
  42. package/dist/combat/weaponHandling.js +202 -0
  43. package/dist/combat/weaponPresentation.d.ts +115 -0
  44. package/dist/combat/weaponPresentation.js +96 -0
  45. package/dist/crafting/recipeCatalog.d.ts +16 -0
  46. package/dist/crafting/recipeCatalog.js +55 -0
  47. package/dist/devtools/warnOnce.d.ts +8 -0
  48. package/dist/devtools/warnOnce.js +18 -0
  49. package/dist/economy/auctionBook.d.ts +1 -1
  50. package/dist/economy/auctionBook.js +11 -1
  51. package/dist/economy/listingBook.d.ts +1 -1
  52. package/dist/economy/listingBook.js +11 -1
  53. package/dist/economy/resourceLedger.js +3 -3
  54. package/dist/economy/shopStock.js +7 -6
  55. package/dist/economy/techTree.js +7 -6
  56. package/dist/economy/unlockPoints.js +3 -6
  57. package/dist/editor/commandApply.js +89 -16
  58. package/dist/editor/commands.d.ts +38 -1
  59. package/dist/editor/commands.js +320 -3
  60. package/dist/editor/creatorStorage.d.ts +59 -0
  61. package/dist/editor/creatorStorage.js +164 -0
  62. package/dist/editor/document.d.ts +4 -0
  63. package/dist/editor/document.js +119 -18
  64. package/dist/editor/environment.js +8 -0
  65. package/dist/editor/index.d.ts +2 -0
  66. package/dist/editor/liveSync.d.ts +4 -1
  67. package/dist/editor/liveSync.js +26 -10
  68. package/dist/editor/materialAuthoring.d.ts +14 -0
  69. package/dist/editor/materialAuthoring.js +38 -0
  70. package/dist/editor/modelNodeVisibility.d.ts +11 -0
  71. package/dist/editor/modelNodeVisibility.js +29 -0
  72. package/dist/editor/movementCatalog.d.ts +90 -0
  73. package/dist/editor/movementCatalog.js +144 -0
  74. package/dist/editor/simulation.d.ts +60 -0
  75. package/dist/editor/simulation.js +202 -0
  76. package/dist/editor/staticPrefab.d.ts +17 -0
  77. package/dist/editor/staticPrefab.js +51 -0
  78. package/dist/editor/types.d.ts +15 -0
  79. package/dist/faction/reputation.d.ts +6 -0
  80. package/dist/faction/reputation.js +11 -0
  81. package/dist/game/cameraConfig.d.ts +54 -2
  82. package/dist/game/clipRoles.d.ts +2 -1
  83. package/dist/game/clipRoles.js +7 -3
  84. package/dist/game/contentValidation.d.ts +69 -0
  85. package/dist/game/contentValidation.js +118 -0
  86. package/dist/game/controlGate.d.ts +8 -0
  87. package/dist/game/controlGate.js +43 -6
  88. package/dist/game/defineGame.d.ts +32 -3
  89. package/dist/game/dialogueGraph.d.ts +20 -0
  90. package/dist/game/dialogueGraph.js +107 -0
  91. package/dist/game/events.d.ts +11 -0
  92. package/dist/game/keyValueStore.d.ts +17 -4
  93. package/dist/game/keyValueStore.js +35 -23
  94. package/dist/game/lootModifiers.d.ts +17 -0
  95. package/dist/game/lootModifiers.js +26 -0
  96. package/dist/game/lootTable.d.ts +4 -0
  97. package/dist/game/lootTable.js +16 -0
  98. package/dist/game/narrativeAuthoring.d.ts +67 -0
  99. package/dist/game/narrativeAuthoring.js +122 -0
  100. package/dist/game/playableGame.d.ts +134 -35
  101. package/dist/game/quest.d.ts +15 -0
  102. package/dist/game/quest.js +209 -63
  103. package/dist/game/questCatalog.d.ts +50 -0
  104. package/dist/game/questCatalog.js +205 -0
  105. package/dist/game/saveStore.d.ts +6 -5
  106. package/dist/game/saveStore.js +57 -57
  107. package/dist/game/systemRuntime.d.ts +2 -2
  108. package/dist/game/systemRuntime.js +84 -33
  109. package/dist/game/toasts.d.ts +8 -0
  110. package/dist/game/toasts.js +7 -0
  111. package/dist/game/vfxInstance.d.ts +9 -0
  112. package/dist/game/vfxInstance.js +12 -1
  113. package/dist/game/viewports.d.ts +43 -0
  114. package/dist/game/viewports.js +46 -0
  115. package/dist/game/worldItem.d.ts +5 -1
  116. package/dist/game/worldItem.js +0 -1
  117. package/dist/gameplay.d.ts +3 -0
  118. package/dist/gameplay.js +3 -0
  119. package/dist/input/actionBindings.d.ts +15 -1
  120. package/dist/input/actionBindings.js +14 -3
  121. package/dist/input/actionContexts.d.ts +25 -1
  122. package/dist/input/actionContexts.js +82 -25
  123. package/dist/input/axisInput.d.ts +4 -0
  124. package/dist/input/axisInput.js +4 -0
  125. package/dist/input/axisShaper.d.ts +73 -0
  126. package/dist/input/axisShaper.js +105 -0
  127. package/dist/input/gamepadModel.d.ts +36 -2
  128. package/dist/input/gamepadModel.js +69 -30
  129. package/dist/input/haptics.d.ts +50 -0
  130. package/dist/input/haptics.js +74 -0
  131. package/dist/input/inputBuffer.d.ts +40 -0
  132. package/dist/input/inputBuffer.js +59 -0
  133. package/dist/input/lookChannel.d.ts +10 -0
  134. package/dist/input/lookChannel.js +6 -0
  135. package/dist/input/pointer.d.ts +2 -0
  136. package/dist/input/pointer.js +4 -0
  137. package/dist/interaction/promptRegistry.d.ts +5 -4
  138. package/dist/interaction/promptRegistry.js +8 -2
  139. package/dist/inventory/inventoryModel.js +9 -6
  140. package/dist/inventory/shapedGrid.d.ts +48 -9
  141. package/dist/inventory/shapedGrid.js +69 -10
  142. package/dist/item/affix.d.ts +6 -0
  143. package/dist/item/affix.js +6 -0
  144. package/dist/item/durability.js +4 -0
  145. package/dist/item/itemInstanceRegistry.d.ts +32 -1
  146. package/dist/item/itemInstanceRegistry.js +111 -7
  147. package/dist/item/itemgen.d.ts +1 -1
  148. package/dist/item/itemgen.js +1 -1
  149. package/dist/item/modularItem.js +7 -1
  150. package/dist/material/appearanceSignals.d.ts +53 -0
  151. package/dist/material/appearanceSignals.js +47 -0
  152. package/dist/material/hairCards.d.ts +47 -0
  153. package/dist/material/hairCards.js +136 -0
  154. package/dist/material/materialAsset.d.ts +162 -0
  155. package/dist/material/materialAsset.js +382 -0
  156. package/dist/meta/changelog.d.ts +1 -1
  157. package/dist/meta/changelog.js +444 -1
  158. package/dist/movement/characterController.d.ts +5 -2
  159. package/dist/movement/characterController.js +50 -23
  160. package/dist/movement/freeFlight.d.ts +2 -2
  161. package/dist/movement/freeFlight.js +3 -3
  162. package/dist/movement/glideModel.d.ts +3 -0
  163. package/dist/movement/glideModel.js +6 -0
  164. package/dist/movement/grappleSwing.d.ts +2 -0
  165. package/dist/movement/grappleSwing.js +5 -1
  166. package/dist/movement/movementModel.d.ts +63 -1
  167. package/dist/movement/movementModel.js +136 -26
  168. package/dist/movement/movementProbe.d.ts +51 -0
  169. package/dist/movement/movementProbe.js +119 -0
  170. package/dist/movement/playerMovement.d.ts +48 -2
  171. package/dist/movement/playerMovement.js +493 -87
  172. package/dist/movement/poseState.d.ts +2 -0
  173. package/dist/movement/poseState.js +29 -1
  174. package/dist/movement/solidObstacles.d.ts +11 -1
  175. package/dist/movement/solidObstacles.js +48 -3
  176. package/dist/movement/terrainGrade.d.ts +13 -0
  177. package/dist/movement/terrainGrade.js +33 -0
  178. package/dist/movement/voxelController.d.ts +2 -2
  179. package/dist/movement/voxelController.js +10 -10
  180. package/dist/nav/navFromEnvironment.d.ts +9 -3
  181. package/dist/nav/navFromEnvironment.js +18 -12
  182. package/dist/nav/navGrid.d.ts +19 -1
  183. package/dist/nav/navGrid.js +26 -10
  184. package/dist/nav/navMesh.d.ts +43 -2
  185. package/dist/nav/navMesh.js +534 -128
  186. package/dist/nav/solidRoute.d.ts +50 -0
  187. package/dist/nav/solidRoute.js +144 -0
  188. package/dist/physics/aircraftDynamics.d.ts +390 -0
  189. package/dist/physics/aircraftDynamics.js +704 -0
  190. package/dist/physics/ballisticSweep.d.ts +10 -3
  191. package/dist/physics/ballisticSweep.js +53 -29
  192. package/dist/physics/boatDynamics.d.ts +116 -0
  193. package/dist/physics/boatDynamics.js +160 -0
  194. package/dist/physics/drivableVehicle.d.ts +22 -8
  195. package/dist/physics/drivableVehicle.js +4 -2
  196. package/dist/physics/flightDynamics.d.ts +21 -4
  197. package/dist/physics/flightDynamics.js +28 -5
  198. package/dist/physics/forceVolume.d.ts +41 -0
  199. package/dist/physics/forceVolume.js +88 -0
  200. package/dist/physics/handlingProbe.d.ts +249 -0
  201. package/dist/physics/handlingProbe.js +606 -0
  202. package/dist/physics/kinematicVehicle.d.ts +40 -5
  203. package/dist/physics/kinematicVehicle.js +17 -2
  204. package/dist/physics/physicsBackend.d.ts +12 -0
  205. package/dist/physics/physicsBackendConformance.js +34 -0
  206. package/dist/physics/physicsWorldBackend.d.ts +1 -0
  207. package/dist/physics/physicsWorldBackend.js +52 -3
  208. package/dist/physics/spatialGrid.d.ts +1 -1
  209. package/dist/physics/spatialGrid.js +8 -9
  210. package/dist/physics/vehicleBackendLink.d.ts +49 -0
  211. package/dist/physics/vehicleBackendLink.js +102 -0
  212. package/dist/physics/vehicleDynamics.d.ts +375 -0
  213. package/dist/physics/vehicleDynamics.js +737 -0
  214. package/dist/physics/vehicleObstacles.d.ts +3 -2
  215. package/dist/physics/vehicleObstacles.js +29 -5
  216. package/dist/physics/worldColliders.d.ts +1 -1
  217. package/dist/physics/worldColliders.js +35 -3
  218. package/dist/puzzle/cellGrid.d.ts +15 -9
  219. package/dist/puzzle/cellGrid.js +13 -9
  220. package/dist/render/lookPreset.d.ts +9 -2
  221. package/dist/render/lookPreset.js +29 -2
  222. package/dist/render/postProcessing.d.ts +30 -0
  223. package/dist/render/postProcessing.js +1 -1
  224. package/dist/runtime/cameraDirector.d.ts +54 -3
  225. package/dist/runtime/cameraDirector.js +187 -0
  226. package/dist/runtime/commandDispatch.d.ts +14 -0
  227. package/dist/runtime/commandDispatch.js +19 -0
  228. package/dist/runtime/commandInput.d.ts +37 -0
  229. package/dist/runtime/commandInput.js +58 -0
  230. package/dist/runtime/commandRunner.d.ts +27 -0
  231. package/dist/runtime/commandRunner.js +31 -3
  232. package/dist/runtime/context/combat.d.ts +12 -4
  233. package/dist/runtime/context/combat.js +179 -11
  234. package/dist/runtime/context/deathLoot.d.ts +11 -8
  235. package/dist/runtime/context/deathLoot.js +17 -16
  236. package/dist/runtime/context/scene.d.ts +2 -0
  237. package/dist/runtime/context/scene.js +13 -21
  238. package/dist/runtime/descriptors/baseline.js +34 -3
  239. package/dist/runtime/descriptors/features.d.ts +2 -1
  240. package/dist/runtime/descriptors/features.js +18 -0
  241. package/dist/runtime/gameContext.js +86 -13
  242. package/dist/runtime/gameContextTypes.d.ts +33 -3
  243. package/dist/runtime/gameRuntime.d.ts +3 -1
  244. package/dist/runtime/gameRuntime.js +4 -0
  245. package/dist/runtime/headlessRunner.d.ts +3 -1
  246. package/dist/runtime/headlessRunner.js +11 -11
  247. package/dist/runtime/hostedGameRunner.d.ts +24 -1
  248. package/dist/runtime/hostedGameRunner.js +156 -10
  249. package/dist/runtime/hostedWorldSession.d.ts +17 -5
  250. package/dist/runtime/hostedWorldSession.js +154 -16
  251. package/dist/runtime/hostedWorldStore.d.ts +2 -2
  252. package/dist/runtime/inputRecorder.d.ts +1 -1
  253. package/dist/runtime/inputRecorder.js +21 -1
  254. package/dist/runtime/inputSnapshot.d.ts +22 -4
  255. package/dist/runtime/inputSnapshot.js +41 -3
  256. package/dist/runtime/localPlayers.d.ts +73 -0
  257. package/dist/runtime/localPlayers.js +119 -0
  258. package/dist/runtime/runtimeSave.d.ts +10 -6
  259. package/dist/runtime/runtimeSave.js +11 -5
  260. package/dist/runtime/simContext.d.ts +8 -3
  261. package/dist/runtime/simContext.js +7 -3
  262. package/dist/runtime/simSnapshot.js +2 -0
  263. package/dist/runtime/transport.d.ts +9 -0
  264. package/dist/runtime/transport.js +7 -1
  265. package/dist/runtime/worldReplication.d.ts +2 -0
  266. package/dist/runtime/worldReplication.js +1 -1
  267. package/dist/scene/assetCatalog.d.ts +2 -0
  268. package/dist/scene/behaviorRuntime.d.ts +38 -7
  269. package/dist/scene/behaviorRuntime.js +293 -30
  270. package/dist/scene/behaviors.d.ts +35 -2
  271. package/dist/scene/behaviors.js +29 -0
  272. package/dist/scene/entityStore.d.ts +14 -3
  273. package/dist/scene/entityStore.js +45 -9
  274. package/dist/scene/mount.d.ts +9 -0
  275. package/dist/scene/mount.js +35 -0
  276. package/dist/scene/possession.d.ts +2 -0
  277. package/dist/scene/possession.js +21 -1
  278. package/dist/scene/sceneKinds.d.ts +9 -0
  279. package/dist/scene/sceneRaycast.d.ts +7 -0
  280. package/dist/scene/sceneRaycast.js +55 -0
  281. package/dist/scene/spatial.d.ts +20 -1
  282. package/dist/scene/spatial.js +94 -2
  283. package/dist/scene/vehicleSeat.d.ts +41 -3
  284. package/dist/scene/vehicleSeat.js +94 -3
  285. package/dist/sensor/concealment.d.ts +6 -0
  286. package/dist/sensor/concealment.js +8 -0
  287. package/dist/sensor/freezeMonitor.d.ts +6 -0
  288. package/dist/sensor/freezeMonitor.js +8 -0
  289. package/dist/sensor/perception.d.ts +11 -1
  290. package/dist/sensor/perception.js +57 -10
  291. package/dist/sensor/recordingBuffer.d.ts +3 -0
  292. package/dist/sensor/recordingBuffer.js +8 -0
  293. package/dist/session/roundState.d.ts +4 -0
  294. package/dist/session/roundState.js +11 -0
  295. package/dist/settings/frameRateLimit.d.ts +16 -0
  296. package/dist/settings/frameRateLimit.js +37 -0
  297. package/dist/settings/graphicsProfile.d.ts +2 -0
  298. package/dist/settings/graphicsProfile.js +12 -0
  299. package/dist/settings/graphicsSettings.d.ts +40 -0
  300. package/dist/settings/graphicsSettings.js +81 -0
  301. package/dist/settings/settingsModel.d.ts +9 -1
  302. package/dist/settings/settingsModel.js +10 -0
  303. package/dist/stats/accumulatorMeter.d.ts +10 -0
  304. package/dist/stats/accumulatorMeter.js +6 -0
  305. package/dist/stats/eventMeter.d.ts +10 -3
  306. package/dist/stats/eventMeter.js +6 -3
  307. package/dist/store/defineStore.d.ts +3 -1
  308. package/dist/store/defineStore.js +3 -1
  309. package/dist/store/observableKeyedStore.d.ts +1 -1
  310. package/dist/store/observableKeyedStore.js +8 -6
  311. package/dist/time/simClock.d.ts +6 -2
  312. package/dist/time/simClock.js +5 -0
  313. package/dist/vfx/feedbackMixer.d.ts +61 -0
  314. package/dist/vfx/feedbackMixer.js +106 -0
  315. package/dist/vfx/particleDirector.d.ts +58 -6
  316. package/dist/vfx/particleDirector.js +113 -22
  317. package/dist/vfx/particles.d.ts +68 -5
  318. package/dist/vfx/particles.js +284 -32
  319. package/dist/visibility/assetStreaming.d.ts +21 -3
  320. package/dist/visibility/assetStreaming.js +108 -24
  321. package/dist/visibility/settings.d.ts +8 -2
  322. package/dist/visibility/settings.js +3 -0
  323. package/dist/visibility/simulationCulling.d.ts +8 -1
  324. package/dist/visibility/simulationCulling.js +7 -0
  325. package/dist/visibility/spatialIndex.d.ts +21 -0
  326. package/dist/visibility/spatialIndex.js +16 -0
  327. package/dist/visibility/visibilitySystem.js +1 -0
  328. package/dist/work/expedition.d.ts +110 -0
  329. package/dist/work/expedition.js +195 -0
  330. package/dist/work/staffedStation.d.ts +29 -0
  331. package/dist/work/staffedStation.js +27 -0
  332. package/dist/world/authoredAnimation.d.ts +38 -0
  333. package/dist/world/authoredAnimation.js +99 -0
  334. package/dist/world/authoredObjects.d.ts +24 -4
  335. package/dist/world/authoredObjects.js +43 -16
  336. package/dist/world/authoredSimulation.d.ts +74 -0
  337. package/dist/world/authoredSimulation.js +302 -0
  338. package/dist/world/authoredSolids.d.ts +46 -0
  339. package/dist/world/authoredSolids.js +75 -0
  340. package/dist/world/authoredWeather.d.ts +120 -0
  341. package/dist/world/authoredWeather.js +314 -0
  342. package/dist/world/buildingGenerator.js +2 -2
  343. package/dist/world/buildingKit.d.ts +11 -4
  344. package/dist/world/buildingKit.js +21 -8
  345. package/dist/world/buildings.d.ts +34 -2
  346. package/dist/world/buildings.js +11 -1
  347. package/dist/world/cityKind.d.ts +7 -0
  348. package/dist/world/cityKind.js +52 -1
  349. package/dist/world/environmentForces.d.ts +21 -0
  350. package/dist/world/environmentForces.js +42 -0
  351. package/dist/world/features.d.ts +32 -5
  352. package/dist/world/features.js +6 -0
  353. package/dist/world/footprintGrid.d.ts +62 -0
  354. package/dist/world/footprintGrid.js +217 -0
  355. package/dist/world/interiors.d.ts +1 -0
  356. package/dist/world/interiors.js +5 -3
  357. package/dist/world/lod.d.ts +6 -0
  358. package/dist/world/lod.js +8 -0
  359. package/dist/world/pathTerrain.d.ts +1 -1
  360. package/dist/world/terraform.d.ts +9 -1
  361. package/dist/world/terraform.js +53 -18
  362. package/dist/world/walls.d.ts +7 -0
  363. package/dist/world/walls.js +7 -0
  364. package/dist/world/weather.d.ts +17 -0
  365. package/dist/world/weather.js +116 -20
  366. package/dist/world/worldSolids.d.ts +95 -0
  367. package/dist/world/worldSolids.js +351 -0
  368. package/dist/world.d.ts +6 -6
  369. package/dist/world.js +5 -5
  370. package/package.json +1 -1
  371. package/skills/ce-handoff/SKILL.md +2 -2
  372. package/skills/game-design/SKILL.md +2 -2
  373. package/skills/jgengine/SKILL.md +28 -6
  374. package/skills/jgengine/api.md +139 -99
  375. package/skills/jgengine/capabilities.md +28 -0
  376. package/skills/jgengine/recipes/minimal-game.md +10 -2
  377. package/skills/jgengine-combat/SKILL.md +13 -2
  378. package/skills/jgengine-combat/api.md +150 -103
  379. package/skills/jgengine-combat/capabilities.md +29 -1
  380. package/skills/jgengine-combat/recipes/portable-runtime-state.md +60 -6
  381. package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +37 -3
  382. package/skills/jgengine-combat/recipes/weapon-feel.md +50 -0
  383. package/skills/jgengine-combat/reference.md +24 -2
  384. package/skills/jgengine-gameplay/SKILL.md +63 -0
  385. package/skills/jgengine-gameplay/api.md +583 -463
  386. package/skills/jgengine-gameplay/capabilities.md +92 -0
  387. package/skills/jgengine-gameplay/recipes/device-save-status.md +46 -0
  388. package/skills/jgengine-gameplay/recipes/expeditions.md +108 -0
  389. package/skills/jgengine-gameplay/recipes/narrative-authoring.md +76 -0
  390. package/skills/jgengine-gameplay/recipes/paged-item-storage.md +101 -0
  391. package/skills/jgengine-gameplay/recipes/quest-authoring.md +63 -0
  392. package/skills/jgengine-gameplay/recipes/shaped-inventory.md +50 -0
  393. package/skills/jgengine-gameplay/reference-content-validation.md +74 -0
  394. package/skills/jgengine-gameplay/reference-systems.md +123 -0
  395. package/skills/jgengine-world/SKILL.md +25 -4
  396. package/skills/jgengine-world/api.md +1045 -655
  397. package/skills/jgengine-world/capabilities.md +246 -9
  398. package/skills/jgengine-world/recipes/character-animation.md +62 -0
  399. package/skills/jgengine-world/recipes/controls.md +48 -0
  400. package/skills/jgengine-world/recipes/flight-feel.md +76 -0
  401. package/skills/jgengine-world/recipes/npc-senses.md +55 -0
  402. package/skills/jgengine-world/recipes/vehicle-feel.md +87 -0
  403. package/skills/jgengine-world/reference.md +180 -4
  404. package/skills/jgengine-world/visibility.md +139 -0
@@ -1,3 +1,4 @@
1
+ import { resolveOneShotClip } from "../game/modelAnimation.js";
1
2
  const DEFAULT_FADE = 0.2;
2
3
  /** Entity blackboard key the shell reads extra graph parameters from: `ctx.scene.entity.blackboard.set(id, ANIM_PARAMS_KEY, { aiming: true })`. */
3
4
  export const ANIM_PARAMS_KEY = "anim.params";
@@ -126,7 +127,7 @@ function clipTimeFor(time, duration, loop) {
126
127
  *
127
128
  * @capability anim-graph data-first animation state machine with blend trees, crossfades, layers, and clip events
128
129
  */
129
- export function createAnimGraphRuntime(initial) {
130
+ export function createAnimGraphRuntime(initial, options = {}) {
130
131
  let graph = initial;
131
132
  let triggers = new Set();
132
133
  let layers = {};
@@ -136,6 +137,14 @@ export function createAnimGraphRuntime(initial) {
136
137
  layers[layer.id] = { current: layer.entry, time: 0, transition: null };
137
138
  }
138
139
  reset();
140
+ function chooseClip(state, def) {
141
+ if (def.kind === "clip" && def.variants !== undefined && def.variants.length > 0) {
142
+ state.selectedClip = resolveOneShotClip({ choice: def.variants }, "choice", options.rng?.() ?? 0) ?? def.clip;
143
+ }
144
+ else {
145
+ delete state.selectedClip;
146
+ }
147
+ }
139
148
  function pickTransition(layer, state, params, normalized) {
140
149
  for (const transition of layer.transitions) {
141
150
  if (transition.from !== "*" && transition.from !== state.current)
@@ -162,8 +171,7 @@ export function createAnimGraphRuntime(initial) {
162
171
  continue;
163
172
  const at = event.atSec;
164
173
  if (loop) {
165
- const wrapped = after < before;
166
- const hit = wrapped ? at > before || at <= after : at > before && at <= after;
174
+ const hit = at >= 0 && at <= duration && Math.floor((after - at) / duration) > Math.floor((before - at) / duration);
167
175
  if (hit)
168
176
  out.push({ name: event.name, clip });
169
177
  }
@@ -197,6 +205,7 @@ export function createAnimGraphRuntime(initial) {
197
205
  advance(dt, params, clips) {
198
206
  const output = { clips: [], events: [] };
199
207
  const rootDelta = [0, 0, 0];
208
+ let rootMotion = false;
200
209
  for (const layer of graph.layers) {
201
210
  const state = layers[layer.id];
202
211
  if (state === undefined)
@@ -204,16 +213,24 @@ export function createAnimGraphRuntime(initial) {
204
213
  const def = layer.states[state.current];
205
214
  if (def === undefined)
206
215
  continue;
216
+ if (def.kind === "clip" && def.variants !== undefined && state.selectedClip === undefined)
217
+ chooseClip(state, def);
207
218
  const layerWeight = layer.weight ?? 1;
219
+ if (def.rootMotion === true && layerWeight > 0)
220
+ rootMotion = true;
208
221
  const before = state.time;
209
222
  state.time += dt * stateSpeed(def);
210
- const weights = stateClipWeights(def, params);
223
+ const weights = def.kind === "clip" && state.selectedClip !== undefined && def.variants?.includes(state.selectedClip)
224
+ ? { [state.selectedClip]: 1 }
225
+ : stateClipWeights(def, params);
211
226
  const duration = stateDuration(weights, clips);
212
227
  const loop = stateLoops(def);
213
228
  const normalized = duration > 0 ? (loop ? (state.time % duration) / duration : Math.min(1, state.time / duration)) : 1;
214
229
  for (const clip of Object.keys(weights)) {
230
+ if (!(weights[clip] * layerWeight > 0))
231
+ continue;
215
232
  const duration = clipDuration(clips[clip]);
216
- collectEvents(output.events, clip, clipTimeFor(before, duration, loop), clipTimeFor(state.time, duration, loop), duration, loop);
233
+ collectEvents(output.events, clip, before, state.time, duration, loop);
217
234
  if (def.rootMotion) {
218
235
  const info = clips[clip];
219
236
  if (typeof info !== "number" && info?.rootTrack !== undefined) {
@@ -232,7 +249,13 @@ export function createAnimGraphRuntime(initial) {
232
249
  const times = {};
233
250
  for (const [clip, w] of Object.entries(transition.fromWeights)) {
234
251
  merged[clip] = (merged[clip] ?? 0) + w * (1 - t);
235
- times[clip] = transition.fromTimes[clip] ?? 0;
252
+ const playback = transition.fromPlayback?.[clip];
253
+ if (w * (1 - t) * layerWeight > 0 && playback?.rootMotion === true)
254
+ rootMotion = true;
255
+ const time = transition.fromTimes[clip] ?? 0;
256
+ const nextTime = playback === undefined ? time : clipTimeFor(time + dt * playback.speed, clipDuration(clips[clip]), playback.loop);
257
+ transition.fromTimes[clip] = nextTime;
258
+ times[clip] = nextTime;
236
259
  }
237
260
  for (const [clip, w] of Object.entries(weights)) {
238
261
  merged[clip] = (merged[clip] ?? 0) + w * t;
@@ -254,33 +277,326 @@ export function createAnimGraphRuntime(initial) {
254
277
  }
255
278
  }
256
279
  const next = pickTransition(layer, state, params, normalized);
257
- if (next !== null && next.to !== state.current) {
280
+ if (next !== null) {
258
281
  const fromWeights = {};
259
282
  const fromTimes = {};
283
+ const fromPlayback = {};
260
284
  for (const entry of output.clips) {
261
285
  if (entry.layer !== layer.id)
262
286
  continue;
263
287
  fromWeights[entry.clip] = (fromWeights[entry.clip] ?? 0) + entry.weight / (layerWeight || 1);
264
288
  fromTimes[entry.clip] = entry.time;
289
+ const previous = transition?.fromPlayback?.[entry.clip];
290
+ const outgoingRoot = previous?.rootMotion === true && transition !== null &&
291
+ transition.duration > 0 && transition.elapsed < transition.duration &&
292
+ (transition.fromWeights[entry.clip] ?? 0) > 0;
293
+ const playback = weights[entry.clip] === undefined && previous !== undefined
294
+ ? previous : { speed: stateSpeed(def), loop };
295
+ // Same-clip states share one clock, but an interrupted fade must retain either
296
+ // influencing source's in-place policy until that source leaves the pose.
297
+ fromPlayback[entry.clip] = { ...playback, ...(def.rootMotion === true || outgoingRoot ? { rootMotion: true } : {}) };
265
298
  }
266
299
  state.current = next.to;
267
300
  state.time = 0;
301
+ const target = layer.states[next.to];
302
+ if (target !== undefined)
303
+ chooseClip(state, target);
268
304
  state.transition = {
269
305
  to: next.to,
270
306
  elapsed: 0,
271
307
  duration: next.duration ?? DEFAULT_FADE,
272
308
  fromWeights,
273
309
  fromTimes,
310
+ fromPlayback,
274
311
  };
275
312
  }
276
- else if (next !== null && next.to === state.current) {
277
- state.time = 0;
278
- }
279
313
  }
280
314
  triggers.clear();
281
315
  if (rootDelta[0] !== 0 || rootDelta[1] !== 0 || rootDelta[2] !== 0)
282
316
  output.rootDelta = rootDelta;
317
+ if (rootMotion)
318
+ output.rootMotion = true;
283
319
  return output;
284
320
  },
285
321
  };
286
322
  }
323
+ const COMPARES = [">", "<", ">=", "<=", "==", "!="];
324
+ function record(value) {
325
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
326
+ }
327
+ function optionalNumber(value) {
328
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
329
+ }
330
+ function parseState(value) {
331
+ const raw = record(value);
332
+ if (raw === null)
333
+ return null;
334
+ const extra = {
335
+ ...(optionalNumber(raw.speed) === undefined ? {} : { speed: raw.speed }),
336
+ ...(typeof raw.loop === "boolean" ? { loop: raw.loop } : {}),
337
+ ...(typeof raw.rootMotion === "boolean" ? { rootMotion: raw.rootMotion } : {}),
338
+ };
339
+ if (raw.kind === "clip" && typeof raw.clip === "string") {
340
+ const variants = Array.isArray(raw.variants) ? raw.variants.filter((clip) => typeof clip === "string" && clip.length > 0) : undefined;
341
+ return { kind: "clip", clip: raw.clip, ...(variants === undefined || variants.length === 0 ? {} : { variants }), ...extra };
342
+ }
343
+ if (!Array.isArray(raw.points))
344
+ return null;
345
+ if (raw.kind === "blend1D" && typeof raw.param === "string") {
346
+ const points = raw.points.flatMap((point) => {
347
+ const entry = record(point);
348
+ return entry !== null && typeof entry.clip === "string" && optionalNumber(entry.at) !== undefined ? [{ at: entry.at, clip: entry.clip }] : [];
349
+ });
350
+ return { kind: "blend1D", param: raw.param, points, ...extra };
351
+ }
352
+ if (raw.kind === "blend2D" && Array.isArray(raw.params) && raw.params.length === 2 && raw.params.every((param) => typeof param === "string")) {
353
+ const points = raw.points.flatMap((point) => {
354
+ const entry = record(point);
355
+ const at = entry?.at;
356
+ return entry !== null && typeof entry.clip === "string" && Array.isArray(at) && at.length === 2 && at.every((n) => optionalNumber(n) !== undefined)
357
+ ? [{ at: [at[0], at[1]], clip: entry.clip }]
358
+ : [];
359
+ });
360
+ return { kind: "blend2D", params: [raw.params[0], raw.params[1]], points, ...extra };
361
+ }
362
+ return null;
363
+ }
364
+ function parseTransition(value, states) {
365
+ const raw = record(value);
366
+ if (raw === null || typeof raw.from !== "string" || typeof raw.to !== "string")
367
+ return null;
368
+ if (states[raw.to] === undefined || (raw.from !== "*" && states[raw.from] === undefined))
369
+ return null;
370
+ const when = Array.isArray(raw.when)
371
+ ? raw.when.flatMap((condition) => {
372
+ const entry = record(condition);
373
+ return entry !== null &&
374
+ typeof entry.param === "string" &&
375
+ COMPARES.includes(entry.op) &&
376
+ (typeof entry.value === "number" || typeof entry.value === "boolean")
377
+ ? [{ param: entry.param, op: entry.op, value: entry.value }]
378
+ : [];
379
+ })
380
+ : undefined;
381
+ return {
382
+ from: raw.from,
383
+ to: raw.to,
384
+ ...(when === undefined || when.length === 0 ? {} : { when }),
385
+ ...(typeof raw.trigger === "string" ? { trigger: raw.trigger } : {}),
386
+ ...(optionalNumber(raw.duration) === undefined ? {} : { duration: Math.max(0, raw.duration) }),
387
+ ...(optionalNumber(raw.exitTime) === undefined ? {} : { exitTime: raw.exitTime }),
388
+ };
389
+ }
390
+ /**
391
+ * Validates untrusted JSON (a saved scene document, a network payload) as an {@link AnimGraph}.
392
+ * Malformed states, transitions to unknown states, and bad conditions are dropped; a layer whose
393
+ * entry state is missing is dropped; the result is `undefined` when no layer survives.
394
+ *
395
+ * @capability anim-graph load an animation graph from saved JSON without trusting its shape
396
+ */
397
+ export function parseAnimGraph(value) {
398
+ const raw = record(value);
399
+ if (raw === null || !Array.isArray(raw.layers))
400
+ return undefined;
401
+ const layers = [];
402
+ const seen = new Set();
403
+ for (const candidate of raw.layers) {
404
+ const layer = record(candidate);
405
+ if (layer === null || typeof layer.id !== "string" || seen.has(layer.id) || typeof layer.entry !== "string")
406
+ continue;
407
+ const rawStates = record(layer.states);
408
+ if (rawStates === null)
409
+ continue;
410
+ const states = {};
411
+ for (const [name, state] of Object.entries(rawStates)) {
412
+ const parsed = parseState(state);
413
+ if (parsed !== null)
414
+ states[name] = parsed;
415
+ }
416
+ if (states[layer.entry] === undefined)
417
+ continue;
418
+ const transitions = Array.isArray(layer.transitions)
419
+ ? layer.transitions.flatMap((transition) => {
420
+ const parsed = parseTransition(transition, states);
421
+ return parsed === null ? [] : [parsed];
422
+ })
423
+ : [];
424
+ const mask = Array.isArray(layer.mask) ? layer.mask.filter((prefix) => typeof prefix === "string") : undefined;
425
+ seen.add(layer.id);
426
+ layers.push({
427
+ id: layer.id,
428
+ entry: layer.entry,
429
+ states,
430
+ transitions,
431
+ ...(mask === undefined ? {} : { mask }),
432
+ ...(typeof layer.additive === "boolean" ? { additive: layer.additive } : {}),
433
+ ...(optionalNumber(layer.weight) === undefined ? {} : { weight: Math.min(1, Math.max(0, layer.weight)) }),
434
+ });
435
+ }
436
+ if (layers.length === 0)
437
+ return undefined;
438
+ const events = Array.isArray(raw.events)
439
+ ? raw.events.flatMap((event) => {
440
+ const entry = record(event);
441
+ return entry !== null && typeof entry.clip === "string" && typeof entry.name === "string" && optionalNumber(entry.atSec) !== undefined
442
+ ? [{ clip: entry.clip, atSec: entry.atSec, name: entry.name }]
443
+ : [];
444
+ })
445
+ : [];
446
+ return { layers, ...(events.length === 0 ? {} : { events }) };
447
+ }
448
+ /**
449
+ * Validate an authored graph without dropping broken combat states or weakening transition conditions.
450
+ * Known malformed fields and dangling references reject the whole graph with relative repair locations;
451
+ * valid data, including authored object/array order, is returned unchanged. Unknown extension fields are
452
+ * retained. Clip availability belongs to the imported-rig diagnostics. Use {@link parseAnimGraph} when
453
+ * permissive repair is intentional.
454
+ * @capability anim-graph validate authored animation graphs with located repair diagnostics without partial fallback
455
+ */
456
+ export function validateAnimGraph(value) {
457
+ const diagnostics = [];
458
+ const issue = (path, message, repair) => { diagnostics.push({ path, message, repair }); };
459
+ const field = (path, key) => path === "" ? key : /^[A-Za-z_$][\w$]*$/.test(key) ? `${path}.${key}` : `${path}[${JSON.stringify(key)}]`;
460
+ const object = (candidate, path) => {
461
+ const raw = record(candidate);
462
+ if (raw === null)
463
+ issue(path, "Expected an animation graph object.", "Supply an object with the supported fields.");
464
+ return raw;
465
+ };
466
+ const text = (candidate, path, empty = false) => {
467
+ if (typeof candidate === "string" && (empty || candidate.trim().length > 0))
468
+ return true;
469
+ issue(path, "Expected a nonempty string.", "Set an explicit name from this graph or imported rig.");
470
+ return false;
471
+ };
472
+ const number = (candidate, path, min = -Infinity, max = Infinity) => {
473
+ if (typeof candidate === "number" && Number.isFinite(candidate) && candidate >= min && candidate <= max)
474
+ return true;
475
+ issue(path, `Expected a finite number${min === -Infinity && max === Infinity ? "" : max === Infinity ? ` at least ${min}` : ` between ${min} and ${max}`}.`, "Set a finite value in the supported range.");
476
+ return false;
477
+ };
478
+ const boolean = (candidate, path) => {
479
+ if (typeof candidate !== "boolean")
480
+ issue(path, "Expected a boolean.", "Set true or false, or omit the optional field.");
481
+ };
482
+ const array = (candidate, path) => {
483
+ if (Array.isArray(candidate))
484
+ return candidate;
485
+ issue(path, "Expected an array.", "Supply an array of supported entries.");
486
+ return null;
487
+ };
488
+ const tuple = (candidate, path, numeric) => {
489
+ const entries = array(candidate, path);
490
+ if (entries === null)
491
+ return;
492
+ if (entries.length !== 2)
493
+ issue(path, "Expected exactly two entries.", "Supply the two blend axes in authored order.");
494
+ entries.forEach((entry, index) => numeric ? number(entry, `${path}[${index}]`) : text(entry, `${path}[${index}]`));
495
+ };
496
+ const state = (candidate, path) => {
497
+ const raw = object(candidate, path);
498
+ if (raw === null)
499
+ return;
500
+ if (!["clip", "blend1D", "blend2D"].includes(raw.kind)) {
501
+ issue(`${path}.kind`, "Unknown animation state kind.", "Choose clip, blend1D or blend2D.");
502
+ return;
503
+ }
504
+ if (raw.speed !== undefined)
505
+ number(raw.speed, `${path}.speed`);
506
+ for (const key of ["loop", "rootMotion"])
507
+ if (raw[key] !== undefined)
508
+ boolean(raw[key], `${path}.${key}`);
509
+ const allowed = raw.kind === "clip" ? ["clip", "variants"] : raw.kind === "blend1D" ? ["param", "points"] : ["params", "points"];
510
+ for (const key of ["clip", "variants", "param", "params", "points"])
511
+ if (raw[key] !== undefined && !allowed.includes(key)) {
512
+ issue(`${path}.${key}`, `Field ${key} is not used by a ${raw.kind} state.`, "Remove the field or choose the matching state kind.");
513
+ }
514
+ if (raw.kind === "clip") {
515
+ text(raw.clip, `${path}.clip`);
516
+ if (raw.variants !== undefined)
517
+ array(raw.variants, `${path}.variants`)?.forEach((clip, index) => text(clip, `${path}.variants[${index}]`));
518
+ return;
519
+ }
520
+ if (raw.kind === "blend1D")
521
+ text(raw.param, `${path}.param`);
522
+ else
523
+ tuple(raw.params, `${path}.params`, false);
524
+ array(raw.points, `${path}.points`)?.forEach((candidate, index) => {
525
+ const pointPath = `${path}.points[${index}]`, point = object(candidate, pointPath);
526
+ if (point === null)
527
+ return;
528
+ text(point.clip, `${pointPath}.clip`);
529
+ if (raw.kind === "blend1D")
530
+ number(point.at, `${pointPath}.at`);
531
+ else
532
+ tuple(point.at, `${pointPath}.at`, true);
533
+ });
534
+ };
535
+ const raw = object(value, "");
536
+ if (raw === null)
537
+ return { graph: undefined, diagnostics };
538
+ const layers = array(raw.layers, "layers"), seen = new Set();
539
+ if (layers?.length === 0)
540
+ issue("layers", "An animation graph needs at least one layer.", "Add a layer with an entry state.");
541
+ layers?.forEach((candidate, index) => {
542
+ const path = `layers[${index}]`, layer = object(candidate, path);
543
+ if (layer === null)
544
+ return;
545
+ if (text(layer.id, `${path}.id`)) {
546
+ if (seen.has(layer.id))
547
+ issue(`${path}.id`, "Layer id is duplicated.", "Give each layer a distinct id.");
548
+ seen.add(layer.id);
549
+ }
550
+ const validEntry = text(layer.entry, `${path}.entry`), states = object(layer.states, `${path}.states`);
551
+ if (states !== null) {
552
+ for (const [name, candidate] of Object.entries(states)) {
553
+ text(name, field(`${path}.states`, name));
554
+ state(candidate, field(`${path}.states`, name));
555
+ }
556
+ if (validEntry && !Object.hasOwn(states, layer.entry))
557
+ issue(`${path}.entry`, "Entry state does not exist in this layer.", "Choose a state declared in this layer.");
558
+ }
559
+ if (layer.weight !== undefined)
560
+ number(layer.weight, `${path}.weight`, 0, 1);
561
+ if (layer.additive !== undefined)
562
+ boolean(layer.additive, `${path}.additive`);
563
+ if (layer.mask !== undefined)
564
+ array(layer.mask, `${path}.mask`)?.forEach((prefix, i) => text(prefix, `${path}.mask[${i}]`, true));
565
+ array(layer.transitions, `${path}.transitions`)?.forEach((candidate, i) => {
566
+ const edgePath = `${path}.transitions[${i}]`, edge = object(candidate, edgePath);
567
+ if (edge === null)
568
+ return;
569
+ for (const key of ["from", "to"])
570
+ if (text(edge[key], `${edgePath}.${key}`) && states !== null && !(key === "from" && edge[key] === "*") && !Object.hasOwn(states, edge[key])) {
571
+ issue(`${edgePath}.${key}`, "Transition references an unknown state.", "Choose a state declared in this layer; only from may use *.");
572
+ }
573
+ if (edge.trigger !== undefined)
574
+ text(edge.trigger, `${edgePath}.trigger`);
575
+ if (edge.duration !== undefined)
576
+ number(edge.duration, `${edgePath}.duration`, 0);
577
+ if (edge.exitTime !== undefined)
578
+ number(edge.exitTime, `${edgePath}.exitTime`);
579
+ if (edge.when !== undefined)
580
+ array(edge.when, `${edgePath}.when`)?.forEach((candidate, i) => {
581
+ const conditionPath = `${edgePath}.when[${i}]`, condition = object(candidate, conditionPath);
582
+ if (condition === null)
583
+ return;
584
+ text(condition.param, `${conditionPath}.param`);
585
+ if (!COMPARES.includes(condition.op))
586
+ issue(`${conditionPath}.op`, "Unknown comparison operator.", "Choose >, <, >=, <=, == or !=.");
587
+ if (typeof condition.value !== "boolean")
588
+ number(condition.value, `${conditionPath}.value`);
589
+ });
590
+ });
591
+ });
592
+ if (raw.events !== undefined)
593
+ array(raw.events, "events")?.forEach((candidate, index) => {
594
+ const path = `events[${index}]`, event = object(candidate, path);
595
+ if (event === null)
596
+ return;
597
+ text(event.clip, `${path}.clip`);
598
+ text(event.name, `${path}.name`);
599
+ number(event.atSec, `${path}.atSec`, 0);
600
+ });
601
+ return { graph: diagnostics.length === 0 ? value : undefined, diagnostics };
602
+ }
@@ -0,0 +1,55 @@
1
+ /** A clip to bake: its name and length in seconds. */
2
+ export interface BoneTextureClipInput {
3
+ name: string;
4
+ duration: number;
5
+ }
6
+ /** One baked clip's rows in a {@link BoneTextureLayout}. */
7
+ export interface BoneTextureClip {
8
+ name: string;
9
+ /** First texture row of the clip. */
10
+ startFrame: number;
11
+ /** Rows baked for the clip, first and last pose included. */
12
+ frameCount: number;
13
+ duration: number;
14
+ }
15
+ /**
16
+ * Where every bone matrix of every baked frame lives in a vertex-animation texture: one row per
17
+ * frame, four RGBA texels (matrix columns) per bone. Plain data, so a baked crowd can be cached,
18
+ * shipped or checked in a test without a renderer.
19
+ */
20
+ export interface BoneTextureLayout {
21
+ boneCount: number;
22
+ fps: number;
23
+ width: number;
24
+ height: number;
25
+ clips: BoneTextureClip[];
26
+ }
27
+ /** Inputs for {@link planBoneTexture}. */
28
+ export interface BoneTextureInput {
29
+ boneCount: number;
30
+ clips: readonly BoneTextureClipInput[];
31
+ /** Baked samples per second. Default 30. */
32
+ fps?: number;
33
+ /** Largest texture side the target GPU accepts. Default 4096. */
34
+ maxTextureSize?: number;
35
+ }
36
+ /**
37
+ * Plans a bone-matrix texture for a rig's clips. Throws when the rig or the clip set does not fit
38
+ * `maxTextureSize`; lower `fps` or bake fewer clips.
39
+ *
40
+ * @capability animation bake many clips of a rig into one bone-matrix texture for instanced crowds
41
+ */
42
+ export declare function planBoneTexture(input: BoneTextureInput): BoneTextureLayout;
43
+ /** Two texture rows around a playback time and the blend between them. */
44
+ export interface BoneTextureSample {
45
+ rowA: number;
46
+ rowB: number;
47
+ t: number;
48
+ }
49
+ /**
50
+ * Finds the baked rows around `time` in a clip, wrapping when `loop` and holding the last pose
51
+ * otherwise. This is the CPU mirror of the crowd shader's lookup.
52
+ *
53
+ * @capability animation look up the baked frames and blend for a crowd member's playback time
54
+ */
55
+ export declare function sampleBoneTexture(clip: BoneTextureClip, fps: number, time: number, loop: boolean, out?: BoneTextureSample): BoneTextureSample;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Plans a bone-matrix texture for a rig's clips. Throws when the rig or the clip set does not fit
3
+ * `maxTextureSize`; lower `fps` or bake fewer clips.
4
+ *
5
+ * @capability animation bake many clips of a rig into one bone-matrix texture for instanced crowds
6
+ */
7
+ export function planBoneTexture(input) {
8
+ const fps = input.fps ?? 30;
9
+ const maxSize = input.maxTextureSize ?? 4096;
10
+ if (!(fps > 0))
11
+ throw new Error(`planBoneTexture: fps must be positive, got ${fps}`);
12
+ const width = Math.max(1, input.boneCount) * 4;
13
+ if (width > maxSize)
14
+ throw new Error(`planBoneTexture: ${input.boneCount} bones need ${width} texels per row, over the ${maxSize} limit`);
15
+ const clips = [];
16
+ let row = 0;
17
+ for (const clip of input.clips) {
18
+ const duration = Math.max(0, clip.duration);
19
+ const frameCount = Math.max(2, Math.ceil(duration * fps) + 1);
20
+ clips.push({ name: clip.name, startFrame: row, frameCount, duration });
21
+ row += frameCount;
22
+ }
23
+ if (row > maxSize)
24
+ throw new Error(`planBoneTexture: ${row} frames exceed the ${maxSize}-row limit; lower fps or bake fewer clips`);
25
+ return { boneCount: input.boneCount, fps, width, height: Math.max(1, row), clips };
26
+ }
27
+ /**
28
+ * Finds the baked rows around `time` in a clip, wrapping when `loop` and holding the last pose
29
+ * otherwise. This is the CPU mirror of the crowd shader's lookup.
30
+ *
31
+ * @capability animation look up the baked frames and blend for a crowd member's playback time
32
+ */
33
+ export function sampleBoneTexture(clip, fps, time, loop, out) {
34
+ const result = out ?? { rowA: 0, rowB: 0, t: 0 };
35
+ const span = clip.frameCount - 1;
36
+ const raw = time * fps;
37
+ const frame = loop ? ((raw % span) + span) % span : Math.min(Math.max(raw, 0), span);
38
+ const a = Math.floor(frame);
39
+ result.rowA = clip.startFrame + a;
40
+ result.rowB = clip.startFrame + Math.min(a + 1, span);
41
+ result.t = frame - a;
42
+ return result;
43
+ }
@@ -0,0 +1,57 @@
1
+ /** One foot's animated ankle height and the ground found under it, both in world units. */
2
+ export interface FootGroundSample {
3
+ /** Animated ankle (foot bone) height this frame, before any correction. */
4
+ ankleY: number;
5
+ /** Ground height under the foot, or `null` when the probe found nothing. */
6
+ groundY: number | null;
7
+ }
8
+ /** Inputs for {@link placeFeet}. All heights are world units. */
9
+ export interface FootPlacementInput {
10
+ feet: readonly FootGroundSample[];
11
+ /** Height of the model origin, which the clips were authored to stand on. */
12
+ originY: number;
13
+ /** Ankle height above the sole in the rig's rest pose; the sole never ends below the ground. */
14
+ ankleHeight: number;
15
+ /** Largest correction applied to a foot or the pelvis, up or down. */
16
+ maxAdjust: number;
17
+ /** A lowest sole this far above its ground reads as airborne. */
18
+ airborneGap: number;
19
+ }
20
+ /** Result of {@link placeFeet}: where each ankle should go and how far the pelvis drops. */
21
+ export interface FootPlacement {
22
+ /** Vertical pelvis shift, `<= 0`, so the lowest planted foot can reach its ground. */
23
+ pelvisOffset: number;
24
+ /** Target ankle height per foot, or `null` when that foot has no ground or no correction. */
25
+ ankleTargets: (number | null)[];
26
+ /** `false` while the feet are clear of the ground, so callers fade the correction out. */
27
+ grounded: boolean;
28
+ }
29
+ /**
30
+ * Resolves foot placement for a rig whose clips were authored on flat ground at its origin.
31
+ * Each foot keeps its animated lift and moves by its ground's height above or below the origin,
32
+ * the sole is never left under the ground, and the pelvis drops by the deepest reach so both legs
33
+ * stay within length. Pure and allocation-light; the renderer applies the targets with
34
+ * {@link solveTwoBone}.
35
+ *
36
+ * @capability animation ground rigged feet on slopes and steps without hiding the swing leg
37
+ */
38
+ export declare function placeFeet(input: FootPlacementInput, out?: FootPlacement): FootPlacement;
39
+ /** A bone as {@link inferLegChains} sees it: its name and its parent's name. */
40
+ export interface RigBone {
41
+ name: string;
42
+ parent: string | null;
43
+ }
44
+ /** Thigh → shin → foot bone names for one leg. */
45
+ export interface LegChain {
46
+ root: string;
47
+ mid: string;
48
+ tip: string;
49
+ }
50
+ /**
51
+ * Finds leg chains on a humanoid rig by bone name: a thigh-like bone (`UpLeg`, `UpperLeg`, `Thigh`)
52
+ * with its first child and grandchild as shin and foot. Control, IK-target and twist bones are
53
+ * skipped. Returns chains in rig order; an empty array means the rig has no recognizable legs.
54
+ *
55
+ * @capability animation find thigh, shin and foot bones on a humanoid rig for foot IK
56
+ */
57
+ export declare function inferLegChains(bones: readonly RigBone[]): LegChain[];
@@ -0,0 +1,67 @@
1
+ function clamp(value, limit) {
2
+ return Math.max(-limit, Math.min(limit, value));
3
+ }
4
+ /**
5
+ * Resolves foot placement for a rig whose clips were authored on flat ground at its origin.
6
+ * Each foot keeps its animated lift and moves by its ground's height above or below the origin,
7
+ * the sole is never left under the ground, and the pelvis drops by the deepest reach so both legs
8
+ * stay within length. Pure and allocation-light; the renderer applies the targets with
9
+ * {@link solveTwoBone}.
10
+ *
11
+ * @capability animation ground rigged feet on slopes and steps without hiding the swing leg
12
+ */
13
+ export function placeFeet(input, out) {
14
+ const result = out ?? { pelvisOffset: 0, ankleTargets: [], grounded: false };
15
+ result.ankleTargets.length = input.feet.length;
16
+ const limit = Math.max(0, input.maxAdjust);
17
+ let lowestGap = Infinity;
18
+ let pelvis = 0;
19
+ for (let i = 0; i < input.feet.length; i += 1) {
20
+ const foot = input.feet[i];
21
+ if (foot.groundY === null) {
22
+ result.ankleTargets[i] = null;
23
+ continue;
24
+ }
25
+ lowestGap = Math.min(lowestGap, foot.ankleY - input.ankleHeight - foot.groundY);
26
+ const followed = foot.ankleY + clamp(foot.groundY - input.originY, limit);
27
+ const target = Math.min(foot.ankleY + limit, Math.max(followed, foot.groundY + input.ankleHeight));
28
+ result.ankleTargets[i] = target;
29
+ pelvis = Math.min(pelvis, target - foot.ankleY);
30
+ }
31
+ result.grounded = lowestGap <= input.airborneGap;
32
+ result.pelvisOffset = result.grounded ? Math.max(-limit, pelvis) : 0;
33
+ return result;
34
+ }
35
+ const THIGH = /(upleg|upperleg|thigh|femur)/;
36
+ const HELPER = /(ik|control|ctrl|twist|pole|target|roll)/;
37
+ /**
38
+ * Finds leg chains on a humanoid rig by bone name: a thigh-like bone (`UpLeg`, `UpperLeg`, `Thigh`)
39
+ * with its first child and grandchild as shin and foot. Control, IK-target and twist bones are
40
+ * skipped. Returns chains in rig order; an empty array means the rig has no recognizable legs.
41
+ *
42
+ * @capability animation find thigh, shin and foot bones on a humanoid rig for foot IK
43
+ */
44
+ export function inferLegChains(bones) {
45
+ const children = new Map();
46
+ for (const bone of bones) {
47
+ if (bone.parent === null)
48
+ continue;
49
+ const list = children.get(bone.parent);
50
+ if (list === undefined)
51
+ children.set(bone.parent, [bone.name]);
52
+ else
53
+ list.push(bone.name);
54
+ }
55
+ const firstChild = (name) => children.get(name)?.find((child) => !HELPER.test(child.toLowerCase()));
56
+ const chains = [];
57
+ for (const bone of bones) {
58
+ const key = bone.name.toLowerCase().replace(/[^a-z0-9]/g, "");
59
+ if (!THIGH.test(key) || HELPER.test(key))
60
+ continue;
61
+ const mid = firstChild(bone.name);
62
+ const tip = mid === undefined ? undefined : firstChild(mid);
63
+ if (mid !== undefined && tip !== undefined)
64
+ chains.push({ root: bone.name, mid, tip });
65
+ }
66
+ return chains;
67
+ }