@bitefoundry/retrieviewer 0.0.0-stage → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (297) hide show
  1. package/ASSET-PROVENANCE.md +43 -0
  2. package/ASSETS-LICENSE.md +58 -0
  3. package/ATTRIBUTION.md +54 -0
  4. package/EMBEDDING.md +96 -0
  5. package/INSTALLING.md +126 -0
  6. package/LICENSE +16 -0
  7. package/LICENSE-CODE +21 -0
  8. package/PLUGIN.md +110 -0
  9. package/PUPPY-MEMORY.md +57 -0
  10. package/PUPPY-Q5-COMPARISON.md +73 -0
  11. package/README.md +86 -2
  12. package/RELEASING.md +70 -0
  13. package/THIRD-PARTY-NOTICES.md +17 -0
  14. package/TRADEMARKS.md +21 -0
  15. package/WINDOWS-COMPANION.md +113 -0
  16. package/braille.mjs +45 -0
  17. package/examples/bridge.py +62 -0
  18. package/examples/embed.mjs +15 -0
  19. package/examples/live.mjs +40 -0
  20. package/examples/plugin.mjs +21 -0
  21. package/integrations/opencode/runtime.mjs +195 -0
  22. package/integrations/opencode/tui.tsx +59 -0
  23. package/integrations/windows-overlay/Overlay.cs +259 -0
  24. package/integrations/windows-overlay/desktop.mjs +87 -0
  25. package/integrations/windows-overlay/protocol.mjs +39 -0
  26. package/integrations/windows-overlay/worker.mjs +60 -0
  27. package/locomotion-core.mjs +21 -0
  28. package/locomotion-puppy.animal +0 -0
  29. package/package.json +131 -4
  30. package/pet-q5-manifest.json +118 -0
  31. package/puppy-clean.mjs +51 -0
  32. package/puppy-companion-interactions-v3-q5.bin.gz +0 -0
  33. package/puppy-companion-interactions-v3.json +1 -0
  34. package/puppy-companion-life.mjs +182 -0
  35. package/puppy-companion-objects.mjs +30 -0
  36. package/puppy-companion-sprites.mjs +14 -0
  37. package/puppy-continuous.mjs +57 -0
  38. package/puppy-face.mjs +46 -0
  39. package/puppy-frame-predictive.mjs +147 -0
  40. package/puppy-frame-store.mjs +108 -0
  41. package/puppy-mouth-anchor.json +1 -0
  42. package/puppy-mouth-anchor.mjs +33 -0
  43. package/puppy-paper-actions-v9-q5.bin.gz +0 -0
  44. package/puppy-paper-actions-v9.json +1 -0
  45. package/puppy-paper-actions.mjs +48 -0
  46. package/puppy-paper-approach-q5.bin.gz +0 -0
  47. package/puppy-paper-approach.json +1 -0
  48. package/puppy-paper-area.mjs +15 -0
  49. package/puppy-paper-arrival-seat-q5.bin.gz +0 -0
  50. package/puppy-paper-arrival-seat.json +1 -0
  51. package/puppy-paper-behaviors-q5.bin.gz +0 -0
  52. package/puppy-paper-behaviors.json +1 -0
  53. package/puppy-paper-behaviors.mjs +8 -0
  54. package/puppy-paper-contact.mjs +103 -0
  55. package/puppy-paper-director.mjs +21 -0
  56. package/puppy-paper-paw-transport.mjs +76 -0
  57. package/puppy-paper-rear-gallop-q5.bin.gz +0 -0
  58. package/puppy-paper-rear-gallop.json +1 -0
  59. package/puppy-paper-seat-q5.bin.gz +0 -0
  60. package/puppy-paper-seat.json +14 -0
  61. package/puppy-paper-seat.mjs +13 -0
  62. package/puppy-paper-trick-timing.mjs +58 -0
  63. package/puppy-paper-world.mjs +108 -0
  64. package/puppy-paper.mjs +271 -0
  65. package/puppy-stable-meta.json +1606 -0
  66. package/puppy-stable.mjs +72 -0
  67. package/puppy-stride-meta.json +343 -0
  68. package/puppy-stride-tweens-q5.bin.gz +0 -0
  69. package/puppy-zoomies.mjs +27 -0
  70. package/puppy-zoomies30.mjs +12 -0
  71. package/retrieviewer-cli.mjs +94 -0
  72. package/retrieviewer-config.mjs +49 -0
  73. package/retrieviewer-install.mjs +157 -0
  74. package/retrieviewer-plugin.d.mts +44 -0
  75. package/retrieviewer-plugin.mjs +97 -0
  76. package/retrieviewer-stdio.mjs +38 -0
  77. package/retrieviewer.d.mts +85 -0
  78. package/retrieviewer.mjs +142 -0
  79. package/vendor/procedural-animals/LICENSE +21 -0
  80. package/vendor/procedural-animals/package.json +40 -0
  81. package/vendor/procedural-animals/src/animal.js +146 -0
  82. package/vendor/procedural-animals/src/bake.js +51 -0
  83. package/vendor/procedural-animals/src/build.js +81 -0
  84. package/vendor/procedural-animals/src/core/build/coatKit.js +121 -0
  85. package/vendor/procedural-animals/src/core/build/featherCards.js +189 -0
  86. package/vendor/procedural-animals/src/core/build/finEdges.js +85 -0
  87. package/vendor/procedural-animals/src/core/build/furSlope.js +115 -0
  88. package/vendor/procedural-animals/src/core/build/mesh.js +67 -0
  89. package/vendor/procedural-animals/src/core/build/pipeline.js +272 -0
  90. package/vendor/procedural-animals/src/core/build/seams.js +183 -0
  91. package/vendor/procedural-animals/src/core/build/warp.js +112 -0
  92. package/vendor/procedural-animals/src/core/build/weights.js +393 -0
  93. package/vendor/procedural-animals/src/core/math/vec.js +81 -0
  94. package/vendor/procedural-animals/src/core/motion/actions.js +800 -0
  95. package/vendor/procedural-animals/src/core/motion/bird.js +3063 -0
  96. package/vendor/procedural-animals/src/core/motion/birdActions.js +1059 -0
  97. package/vendor/procedural-animals/src/core/motion/common.js +158 -0
  98. package/vendor/procedural-animals/src/core/motion/index.js +22 -0
  99. package/vendor/procedural-animals/src/core/motion/quadruped.js +2875 -0
  100. package/vendor/procedural-animals/src/core/motion/snake.js +1594 -0
  101. package/vendor/procedural-animals/src/core/motion/spider.js +1810 -0
  102. package/vendor/procedural-animals/src/core/motion/swimmer.js +1047 -0
  103. package/vendor/procedural-animals/src/core/motion/swimmerActions.js +613 -0
  104. package/vendor/procedural-animals/src/core/render/animalObject.js +262 -0
  105. package/vendor/procedural-animals/src/core/render/coatMaterial.js +1301 -0
  106. package/vendor/procedural-animals/src/core/render/dqs.js +159 -0
  107. package/vendor/procedural-animals/src/core/render/eyes.js +676 -0
  108. package/vendor/procedural-animals/src/core/render/membranes.js +77 -0
  109. package/vendor/procedural-animals/src/core/render/skeleton.js +41 -0
  110. package/vendor/procedural-animals/src/core/rig/bird.js +327 -0
  111. package/vendor/procedural-animals/src/core/rig/quadruped.js +76 -0
  112. package/vendor/procedural-animals/src/core/rig/rig.js +79 -0
  113. package/vendor/procedural-animals/src/core/rig/snake.js +76 -0
  114. package/vendor/procedural-animals/src/core/rig/spider.js +110 -0
  115. package/vendor/procedural-animals/src/core/rig/swimmer.js +181 -0
  116. package/vendor/procedural-animals/src/core/sdf/eyeSocket.js +53 -0
  117. package/vendor/procedural-animals/src/core/sdf/mesher.js +238 -0
  118. package/vendor/procedural-animals/src/core/sdf/sdf.js +263 -0
  119. package/vendor/procedural-animals/src/index.d.ts +98 -0
  120. package/vendor/procedural-animals/src/index.js +29 -0
  121. package/vendor/procedural-animals/src/registry.js +48 -0
  122. package/vendor/procedural-animals/src/species/bear/behaviour.js +463 -0
  123. package/vendor/procedural-animals/src/species/bear/claws.js +147 -0
  124. package/vendor/procedural-animals/src/species/bear/coat.js +726 -0
  125. package/vendor/procedural-animals/src/species/bear/index.js +139 -0
  126. package/vendor/procedural-animals/src/species/bear/motion.js +112 -0
  127. package/vendor/procedural-animals/src/species/bear/regions.js +78 -0
  128. package/vendor/procedural-animals/src/species/bear/rig.js +112 -0
  129. package/vendor/procedural-animals/src/species/bear/sculpt.js +272 -0
  130. package/vendor/procedural-animals/src/species/boar/behaviour.js +335 -0
  131. package/vendor/procedural-animals/src/species/boar/coat.js +372 -0
  132. package/vendor/procedural-animals/src/species/boar/index.js +110 -0
  133. package/vendor/procedural-animals/src/species/boar/motion.js +85 -0
  134. package/vendor/procedural-animals/src/species/boar/regions.js +88 -0
  135. package/vendor/procedural-animals/src/species/boar/rig.js +111 -0
  136. package/vendor/procedural-animals/src/species/boar/sculpt.js +285 -0
  137. package/vendor/procedural-animals/src/species/cat/behaviour.js +559 -0
  138. package/vendor/procedural-animals/src/species/cat/coat.js +841 -0
  139. package/vendor/procedural-animals/src/species/cat/index.js +211 -0
  140. package/vendor/procedural-animals/src/species/cat/motion.js +101 -0
  141. package/vendor/procedural-animals/src/species/cat/regions.js +82 -0
  142. package/vendor/procedural-animals/src/species/cat/rig.js +100 -0
  143. package/vendor/procedural-animals/src/species/cat/sculpt.js +266 -0
  144. package/vendor/procedural-animals/src/species/cat/whiskers.js +110 -0
  145. package/vendor/procedural-animals/src/species/cheetah/coat.js +427 -0
  146. package/vendor/procedural-animals/src/species/cheetah/index.js +51 -0
  147. package/vendor/procedural-animals/src/species/cheetah/joints.js +3 -0
  148. package/vendor/procedural-animals/src/species/cheetah/motion.js +77 -0
  149. package/vendor/procedural-animals/src/species/cheetah/regions.js +95 -0
  150. package/vendor/procedural-animals/src/species/cheetah/rig.js +66 -0
  151. package/vendor/procedural-animals/src/species/cheetah/sculpt.js +158 -0
  152. package/vendor/procedural-animals/src/species/chicken/coat.js +313 -0
  153. package/vendor/procedural-animals/src/species/chicken/coverts.js +320 -0
  154. package/vendor/procedural-animals/src/species/chicken/index.js +110 -0
  155. package/vendor/procedural-animals/src/species/chicken/motion.js +177 -0
  156. package/vendor/procedural-animals/src/species/chicken/regions.js +66 -0
  157. package/vendor/procedural-animals/src/species/chicken/rig.js +144 -0
  158. package/vendor/procedural-animals/src/species/chicken/sculpt.js +389 -0
  159. package/vendor/procedural-animals/src/species/chicken/wingLook.js +66 -0
  160. package/vendor/procedural-animals/src/species/chicken/wingStack.js +124 -0
  161. package/vendor/procedural-animals/src/species/cow/behaviour.js +521 -0
  162. package/vendor/procedural-animals/src/species/cow/coat.js +465 -0
  163. package/vendor/procedural-animals/src/species/cow/index.js +118 -0
  164. package/vendor/procedural-animals/src/species/cow/motion.js +81 -0
  165. package/vendor/procedural-animals/src/species/cow/regions.js +98 -0
  166. package/vendor/procedural-animals/src/species/cow/rig.js +116 -0
  167. package/vendor/procedural-animals/src/species/cow/sculpt.js +339 -0
  168. package/vendor/procedural-animals/src/species/crow/coat.js +145 -0
  169. package/vendor/procedural-animals/src/species/crow/index.js +70 -0
  170. package/vendor/procedural-animals/src/species/crow/joints.js +3 -0
  171. package/vendor/procedural-animals/src/species/crow/motion.js +101 -0
  172. package/vendor/procedural-animals/src/species/crow/regions.js +50 -0
  173. package/vendor/procedural-animals/src/species/crow/rig.js +83 -0
  174. package/vendor/procedural-animals/src/species/crow/sculpt.js +118 -0
  175. package/vendor/procedural-animals/src/species/deer/behaviour.js +513 -0
  176. package/vendor/procedural-animals/src/species/deer/coat.js +391 -0
  177. package/vendor/procedural-animals/src/species/deer/index.js +106 -0
  178. package/vendor/procedural-animals/src/species/deer/motion.js +97 -0
  179. package/vendor/procedural-animals/src/species/deer/regions.js +60 -0
  180. package/vendor/procedural-animals/src/species/deer/rig.js +124 -0
  181. package/vendor/procedural-animals/src/species/deer/sculpt.js +297 -0
  182. package/vendor/procedural-animals/src/species/dog/behaviour.js +521 -0
  183. package/vendor/procedural-animals/src/species/dog/coat.js +495 -0
  184. package/vendor/procedural-animals/src/species/dog/index.js +144 -0
  185. package/vendor/procedural-animals/src/species/dog/motion.js +118 -0
  186. package/vendor/procedural-animals/src/species/dog/regions.js +61 -0
  187. package/vendor/procedural-animals/src/species/dog/rig.js +129 -0
  188. package/vendor/procedural-animals/src/species/dog/sculpt.js +246 -0
  189. package/vendor/procedural-animals/src/species/eagle/coat.js +250 -0
  190. package/vendor/procedural-animals/src/species/eagle/index.js +76 -0
  191. package/vendor/procedural-animals/src/species/eagle/joints.js +3 -0
  192. package/vendor/procedural-animals/src/species/eagle/motion.js +137 -0
  193. package/vendor/procedural-animals/src/species/eagle/regions.js +57 -0
  194. package/vendor/procedural-animals/src/species/eagle/rig.js +107 -0
  195. package/vendor/procedural-animals/src/species/eagle/sculpt.js +137 -0
  196. package/vendor/procedural-animals/src/species/fish/coat.js +271 -0
  197. package/vendor/procedural-animals/src/species/fish/index.js +88 -0
  198. package/vendor/procedural-animals/src/species/fish/motion.js +92 -0
  199. package/vendor/procedural-animals/src/species/fish/regions.js +27 -0
  200. package/vendor/procedural-animals/src/species/fish/rig.js +143 -0
  201. package/vendor/procedural-animals/src/species/fish/sculpt.js +214 -0
  202. package/vendor/procedural-animals/src/species/fish/variants.js +126 -0
  203. package/vendor/procedural-animals/src/species/fox/behaviour.js +314 -0
  204. package/vendor/procedural-animals/src/species/fox/coat.js +444 -0
  205. package/vendor/procedural-animals/src/species/fox/index.js +120 -0
  206. package/vendor/procedural-animals/src/species/fox/motion.js +81 -0
  207. package/vendor/procedural-animals/src/species/fox/regions.js +59 -0
  208. package/vendor/procedural-animals/src/species/fox/rig.js +91 -0
  209. package/vendor/procedural-animals/src/species/fox/sculpt.js +202 -0
  210. package/vendor/procedural-animals/src/species/frog/behaviour.js +590 -0
  211. package/vendor/procedural-animals/src/species/frog/coat.js +305 -0
  212. package/vendor/procedural-animals/src/species/frog/index.js +97 -0
  213. package/vendor/procedural-animals/src/species/frog/motion.js +69 -0
  214. package/vendor/procedural-animals/src/species/frog/regions.js +46 -0
  215. package/vendor/procedural-animals/src/species/frog/rig.js +156 -0
  216. package/vendor/procedural-animals/src/species/frog/sculpt.js +240 -0
  217. package/vendor/procedural-animals/src/species/goat/behaviour.js +509 -0
  218. package/vendor/procedural-animals/src/species/goat/coat.js +590 -0
  219. package/vendor/procedural-animals/src/species/goat/index.js +149 -0
  220. package/vendor/procedural-animals/src/species/goat/motion.js +95 -0
  221. package/vendor/procedural-animals/src/species/goat/regions.js +98 -0
  222. package/vendor/procedural-animals/src/species/goat/rig.js +181 -0
  223. package/vendor/procedural-animals/src/species/goat/sculpt.js +547 -0
  224. package/vendor/procedural-animals/src/species/horse/coat.js +361 -0
  225. package/vendor/procedural-animals/src/species/horse/coatSeam.js +122 -0
  226. package/vendor/procedural-animals/src/species/horse/index.js +80 -0
  227. package/vendor/procedural-animals/src/species/horse/motion.js +245 -0
  228. package/vendor/procedural-animals/src/species/horse/regions.js +60 -0
  229. package/vendor/procedural-animals/src/species/horse/rig.js +117 -0
  230. package/vendor/procedural-animals/src/species/horse/sculpt.js +316 -0
  231. package/vendor/procedural-animals/src/species/horse/seamWeights.js +272 -0
  232. package/vendor/procedural-animals/src/species/lion/behaviour.js +509 -0
  233. package/vendor/procedural-animals/src/species/lion/coat.js +1016 -0
  234. package/vendor/procedural-animals/src/species/lion/index.js +111 -0
  235. package/vendor/procedural-animals/src/species/lion/motion.js +87 -0
  236. package/vendor/procedural-animals/src/species/lion/regions.js +106 -0
  237. package/vendor/procedural-animals/src/species/lion/rig.js +110 -0
  238. package/vendor/procedural-animals/src/species/lion/sculpt.js +384 -0
  239. package/vendor/procedural-animals/src/species/lion/whiskers.js +132 -0
  240. package/vendor/procedural-animals/src/species/pig/behaviour.js +575 -0
  241. package/vendor/procedural-animals/src/species/pig/coat.js +730 -0
  242. package/vendor/procedural-animals/src/species/pig/ears.js +242 -0
  243. package/vendor/procedural-animals/src/species/pig/index.js +161 -0
  244. package/vendor/procedural-animals/src/species/pig/motion.js +105 -0
  245. package/vendor/procedural-animals/src/species/pig/regions.js +100 -0
  246. package/vendor/procedural-animals/src/species/pig/rig.js +152 -0
  247. package/vendor/procedural-animals/src/species/pig/sculpt.js +382 -0
  248. package/vendor/procedural-animals/src/species/rabbit/behaviour.js +605 -0
  249. package/vendor/procedural-animals/src/species/rabbit/coat.js +285 -0
  250. package/vendor/procedural-animals/src/species/rabbit/index.js +85 -0
  251. package/vendor/procedural-animals/src/species/rabbit/motion.js +85 -0
  252. package/vendor/procedural-animals/src/species/rabbit/regions.js +48 -0
  253. package/vendor/procedural-animals/src/species/rabbit/rig.js +99 -0
  254. package/vendor/procedural-animals/src/species/rabbit/sculpt.js +148 -0
  255. package/vendor/procedural-animals/src/species/rat/behaviour.js +539 -0
  256. package/vendor/procedural-animals/src/species/rat/coat.js +341 -0
  257. package/vendor/procedural-animals/src/species/rat/index.js +111 -0
  258. package/vendor/procedural-animals/src/species/rat/motion.js +90 -0
  259. package/vendor/procedural-animals/src/species/rat/regions.js +92 -0
  260. package/vendor/procedural-animals/src/species/rat/rig.js +112 -0
  261. package/vendor/procedural-animals/src/species/rat/sculpt.js +163 -0
  262. package/vendor/procedural-animals/src/species/rat/whiskers.js +119 -0
  263. package/vendor/procedural-animals/src/species/shark/coat.js +383 -0
  264. package/vendor/procedural-animals/src/species/shark/index.js +104 -0
  265. package/vendor/procedural-animals/src/species/shark/motion.js +69 -0
  266. package/vendor/procedural-animals/src/species/shark/regions.js +34 -0
  267. package/vendor/procedural-animals/src/species/shark/rig.js +194 -0
  268. package/vendor/procedural-animals/src/species/shark/sculpt.js +343 -0
  269. package/vendor/procedural-animals/src/species/shark/variants.js +105 -0
  270. package/vendor/procedural-animals/src/species/sheep/behaviour.js +522 -0
  271. package/vendor/procedural-animals/src/species/sheep/coat.js +529 -0
  272. package/vendor/procedural-animals/src/species/sheep/index.js +152 -0
  273. package/vendor/procedural-animals/src/species/sheep/motion.js +86 -0
  274. package/vendor/procedural-animals/src/species/sheep/neck.js +41 -0
  275. package/vendor/procedural-animals/src/species/sheep/regions.js +62 -0
  276. package/vendor/procedural-animals/src/species/sheep/rig.js +163 -0
  277. package/vendor/procedural-animals/src/species/sheep/sculpt.js +542 -0
  278. package/vendor/procedural-animals/src/species/snake/coat.js +315 -0
  279. package/vendor/procedural-animals/src/species/snake/index.js +73 -0
  280. package/vendor/procedural-animals/src/species/snake/motion.js +51 -0
  281. package/vendor/procedural-animals/src/species/snake/regions.js +51 -0
  282. package/vendor/procedural-animals/src/species/snake/rig.js +108 -0
  283. package/vendor/procedural-animals/src/species/snake/sculpt.js +122 -0
  284. package/vendor/procedural-animals/src/species/spider/coat.js +279 -0
  285. package/vendor/procedural-animals/src/species/spider/index.js +78 -0
  286. package/vendor/procedural-animals/src/species/spider/motion.js +72 -0
  287. package/vendor/procedural-animals/src/species/spider/regions.js +87 -0
  288. package/vendor/procedural-animals/src/species/spider/rig.js +118 -0
  289. package/vendor/procedural-animals/src/species/spider/sculpt.js +182 -0
  290. package/vendor/procedural-animals/src/species/wolf/behaviour.js +160 -0
  291. package/vendor/procedural-animals/src/species/wolf/coat.js +749 -0
  292. package/vendor/procedural-animals/src/species/wolf/index.js +107 -0
  293. package/vendor/procedural-animals/src/species/wolf/motion.js +80 -0
  294. package/vendor/procedural-animals/src/species/wolf/regions.js +57 -0
  295. package/vendor/procedural-animals/src/species/wolf/rig.js +87 -0
  296. package/vendor/procedural-animals/src/species/wolf/sculpt.js +339 -0
  297. package/vendor/procedural-animals/src/worker.js +29 -0
@@ -0,0 +1,1810 @@
1
+ // Spider motion engine: eight legs of seven segments, pedipalps, chelicerae with fangs and a hanging
2
+ // abdomen, on uneven ground. Drives the bones of core/rig/spider.js.
3
+ //
4
+ // Locomotion: a phase-offset gait table (alternating tetrapod: L1 R2 L3 R4 against R1 L2 R3 L4, with
5
+ // the metachronal lag inside each tetrapod) blended by speed. Stance claws stay planted where they
6
+ // touched down; swing claws travel on a "ballistic" path (short speed ramps, straight cruise, a
7
+ // rounded apex) whose height clears the terrain sampled along the way, so legs step over bumps.
8
+ // Standing still, legs that end up away from their neutral foothold re-step one tetrapod at a time;
9
+ // turning on the spot runs the gait from the yaw rate. The body height, pitch and roll follow a
10
+ // least-squares plane through the terrain under the eight neutral footholds (springs), and the
11
+ // sternum / abdomen never touch a bump. The abdomen hangs from the pedicel on a spring (lags in turns
12
+ // and speed changes) and breathes (bone scale).
13
+ //
14
+ // Leg solver (body frame, per frame, continuous): the tarsus lies along the leg's azimuth at its
15
+ // bind slope (steeper while the "heel" lifts), which gives the metatarsus-tarsus joint; the coxa
16
+ // yaws part of the way toward it and the trochanter the rest; in the vertical leg plane the
17
+ // metatarsus takes a preferred slope that flattens as the leg stretches, and femur + (patella +
18
+ // tibia, a rigid pair) solve as a two-link chain, knee up, with a soft reach limit. Every bone is
19
+ // written as a rotation from its bind frame: body rotation x yaw about the body's up axis x pitch
20
+ // about the bind leg-plane normal, so the bone roll never flips. Action poses (raised threat legs,
21
+ // held prey, the death curl, the in-air tuck) are blended in the same joint-angle space.
22
+ //
23
+ // Actions: idle (breathing, leg and palp twitches, looking around), jump (a pounce: crouch, hydraulic
24
+ // push-off, front legs raised to grab, landing), sit / lie / sleep (crouch low with the legs drawn
25
+ // in), eat (chelicerae working, palps manipulating, prey held by legs I), drink (mouthparts down),
26
+ // attack (threat pose: front legs raised, fangs out; then a lunge and attackHit), hit (flinch and
27
+ // scuttle back), death (hydraulic leg collapse: the legs curl under the body and it settles on its
28
+ // back or side on whatever ground is there), stand.
29
+ //
30
+ // Lessons from the cheetah engine kept here: continuous solves (no state jumps between frames),
31
+ // soft reach limits, motion from phase not events, springs for secondary motion, zero-dt safety,
32
+ // no allocations in the hot path.
33
+ //
34
+ // ------------------------------------------------------------------------------------------------
35
+ // species.motion schema (all lengths in metres at the reference size; per-variant overrides in
36
+ // `variants: { name: { ...same fields } }`, chosen by params.variant)
37
+ // ------------------------------------------------------------------------------------------------
38
+ // maxSpeed m/s, accel / decel m/s^2, turnRate rad/s, latAccelMax m/s^2 (cornering: wider arcs at speed)
39
+ // gears { name: speed } named speeds (walk, run, sprint)
40
+ // gaits (req.) [{ name, v, f, D, off: [L1 L2 L3 L4 R1 R2 R3 R4], lift, bob, sway }] sorted by v.
41
+ // v: speed of the row, f: stride Hz, D: duty factor, off: touchdown phase per leg, lift:
42
+ // swing height (m), bob: body bounce (m, twice per stride), sway: body yaw sway (rad).
43
+ // stance { reach: [I, II, III, IV] neutral claw distance from the coxa as a fraction of the
44
+ // bind pose, height: stance height factor, width: lateral spread factor }
45
+ // feet { heelLift: tarsus heel lift at push-off (deg) }
46
+ // abdomen { stiffness: spring rate (1/s), lag: how far it swings in accelerations (rad per
47
+ // m/s^2 x s), droop: pitch it sags by when relaxed (rad) }
48
+ // breath { rate Hz, amp: abdomen scale amplitude }
49
+ // chelicerae { fang: 'labidognath' (fangs close sideways: wolf spiders) | 'orthognath' (fangs
50
+ // strike down: tarantulas), fangOpen: rad, spread: rad, pitch: rad }
51
+ // palps { twitch 0..1, tap 0..1 } idle { twitch, raiseFront (rad), look (rad) }
52
+ // look { yaw, pitch }: how far the body turns / rears toward a look target (rad)
53
+ // actions { jump: { height, distance, crouch (s) }, attack: { rear (rad), lunge (m),
54
+ // threat (s) }, sleep: { tuck 0..1 } }
55
+ // ------------------------------------------------------------------------------------------------
56
+ import * as THREE from 'three';
57
+ import { TAU, clamp, lerp, smooth, smin, wrap01, d01, angDiff, Spring, prng, envelope, tv } from './common.js';
58
+ import { LEG_KEYS, legBones, legJoints, palpBones, palpJoints } from '../rig/spider.js';
59
+
60
+ const DEG = Math.PI / 180;
61
+ // swing shaping (exported for tuning tools)
62
+ export const TUNE = { bf: 1.15, bp: 0.25, bm: 0.2, ramp: 0.22, path: 'joint' };
63
+ const G_ACC = 9.81;
64
+ const PUSH = 0.07; // jump push-off duration (s)
65
+ const COAST = 1 / 50; // constant-velocity moments between push, flight and landing (s)
66
+ const Y_AXIS = Object.freeze(new THREE.Vector3(0, 1, 0));
67
+ const X_AXIS = Object.freeze(new THREE.Vector3(1, 0, 0));
68
+ const Z_AXIS = Object.freeze(new THREE.Vector3(0, 0, 1));
69
+ const ONE = Object.freeze(new THREE.Vector3(1, 1, 1));
70
+ const SET_A = [1, 0, 1, 0, 0, 1, 0, 1]; // tetrapod A: L1 R2 L3 R4
71
+ // action leg poses: bends (deg) of patella, tibia, metatarsus, tarsus relative to the previous segment
72
+ const P_AIR_F = [-40, -25, -30, -20], P_AIR_H = [-15, -12, -10, -8];
73
+ const P_RAISE = [-18, -12, -14, -6], P_HOLD = [-78, -30, -52, -28], P_CURL = [-120, -65, -55, -45], P_TUCK = [-105, -35, -115, -40];
74
+ const STAG = [null, null, null, [0.2, 0.85], [0.1, 0.7], [0, 0.55], [0, 0.45]];
75
+ const softMin = (x, lim, w) => (x < lim - w ? x : lim - w * Math.exp((lim - w - x) / w)); // smooth clamp from above
76
+ const expTo = (x, target, dt, tau) => x + (target - x) * (1 - Math.exp(-dt / tau));
77
+
78
+ // ------------------------------------------------------------------------------------------------
79
+ // configuration
80
+ function mergeSpec(base, over) {
81
+ if (!over) return base;
82
+ const out = { ...base };
83
+ for (const [k, v] of Object.entries(over)) {
84
+ if (v && typeof v === 'object' && !Array.isArray(v) && base[k] && typeof base[k] === 'object' && !Array.isArray(base[k])) out[k] = mergeSpec(base[k], v);
85
+ else out[k] = v;
86
+ }
87
+ return out;
88
+ }
89
+
90
+ function resolveConfig(species, data, skeleton) {
91
+ const base = species.motion || {};
92
+ const variant = data.params?.variant;
93
+ const spec = mergeSpec(base, base.variants && variant ? base.variants[variant] : null);
94
+ const J = {};
95
+ for (const [k, v] of Object.entries(data.joints)) J[k] = new THREE.Vector3(v[0], v[1], v[2]);
96
+ const s = data.params?.size || 1, sq = Math.sqrt(s);
97
+ const boneIndex = Object.fromEntries(data.bones.map((b, i) => [b.name, i]));
98
+ const bindQ = skeleton.bind.map((m) => new THREE.Quaternion().setFromRotationMatrix(m));
99
+ // body root: centre of the coxa bases (sternum level), on the midline
100
+ const root = new THREE.Vector3();
101
+ for (const key of LEG_KEYS) root.add(J['coxa' + key[1] + key[0]]);
102
+ root.multiplyScalar(1 / 8); root.x = 0;
103
+ const loc = (v) => v.clone().sub(root);
104
+ const cfg = {
105
+ spec, s, sq, J, root, boneIndex, bindQ,
106
+ rootH: root.y,
107
+ maxSpeed: (spec.maxSpeed ?? 0.3) * sq,
108
+ accel: (spec.accel ?? 2) * sq, decel: (spec.decel ?? 3) * sq,
109
+ turnRate: (spec.turnRate ?? 4) / sq,
110
+ latAccelMax: (spec.latAccelMax ?? 0.4) * sq,
111
+ gears: Object.fromEntries(Object.entries(spec.gears || { walk: 0.05, run: 0.15 }).map(([k, v]) => [k, v * sq])),
112
+ gaits: (spec.gaits || []).map((g) => ({ lift: 0.003, bob: 0, sway: 0, ...g })),
113
+ stance: { reach: [0.9, 0.93, 0.93, 0.9], height: 1, width: 1, ...(spec.stance || {}) },
114
+ heelLift: (spec.feet?.heelLift ?? 12) * DEG,
115
+ abd: { stiffness: 16, lag: 0.12, droop: 0.05, ...(spec.abdomen || {}) },
116
+ breath: { rate: 0.5, amp: 0.015, ...(spec.breath || {}) },
117
+ chel: { fang: 'labidognath', fangOpen: 1.2, spread: 0.3, pitch: 0.45, ...(spec.chelicerae || {}) },
118
+ palps: { twitch: 1, tap: 0.5, ...(spec.palps || {}) },
119
+ idle: { twitch: 1, raiseFront: 0.1, look: 0.3, ...(spec.idle || {}) },
120
+ look: { yaw: 0.45, pitch: 0.3, ...(spec.look || {}) },
121
+ actions: spec.actions || {},
122
+ };
123
+ if (!cfg.gaits.length) throw new Error('procedural-animals spider: species.motion.gaits is required');
124
+ // ---- legs
125
+ cfg.legs = LEG_KEYS.map((key, idx) => {
126
+ const S = key[0], i = +key[1], side = S === 'L' ? 1 : -1;
127
+ const jn = legJoints(i, S), bn = legBones(i, S);
128
+ const P = jn.map((n) => loc(J[n]));
129
+ const len = [], az = [], el = [];
130
+ for (let k = 0; k < 7; k++) {
131
+ const d = P[k + 1].clone().sub(P[k]);
132
+ len.push(d.length());
133
+ d.normalize();
134
+ az.push(Math.atan2(d.x * side, d.z));
135
+ el.push(Math.asin(clamp(d.y, -1, 1)));
136
+ }
137
+ const phi0 = az[2];
138
+ const u0 = new THREE.Vector3(Math.sin(phi0) * side, 0, Math.cos(phi0));
139
+ const n0 = new THREE.Vector3().crossVectors(u0, Y_AXIS).normalize();
140
+ // patella + tibia as one rigid link from the knee to the tibia end
141
+ const pt = P[5].clone().sub(P[3]);
142
+ const ptH = pt.x * u0.x + pt.z * u0.z;
143
+ const ept0 = Math.atan2(pt.y, ptH);
144
+ const L = {
145
+ key, idx, i, S, side, front: i <= 2,
146
+ bones: bn.map((b) => cfg.boneIndex[b]), bindQ: bn.map((b) => bindQ[cfg.boneIndex[b]]),
147
+ P0: P, len, az0: az, el0: el, phi0, u0, n0,
148
+ Lpt: Math.hypot(ptH, pt.y), dp: el[3] - ept0, dt: el[4] - ept0,
149
+ total: len.reduce((a, b) => a + b, 0),
150
+ tipY: J[jn[7]].y, // claw tip height above the ground at bind
151
+ set: SET_A[idx],
152
+ };
153
+ L.reach2 = L.len[2] + L.Lpt;
154
+ const F0 = P[2], A0 = P[6];
155
+ L.D0 = Math.hypot((A0.x - F0.x) * u0.x + (A0.z - F0.z) * u0.z, A0.y - F0.y);
156
+ // neutral foothold (body frame, horizontal): the bind claw pulled toward the coxa
157
+ const rk = cfg.stance.reach[i - 1] ?? 0.9;
158
+ L.neutral = new THREE.Vector3(P[0].x + (P[7].x - P[0].x) * rk * cfg.stance.width, 0, P[0].z + (P[7].z - P[0].z) * rk);
159
+ // workspace: horizontal distance of the claw from the coxa base (too close folds the leg over
160
+ // itself, too far cannot be reached)
161
+ L.rN = Math.hypot(L.neutral.x - P[0].x, L.neutral.z - P[0].z);
162
+ L.rMin = 0.64 * L.rN;
163
+ L.rMax = Math.min(1.18 * L.rN, 0.95 * (L.total - len[0] - len[1]));
164
+ L.azN = Math.atan2((L.neutral.x - P[0].x) * side, L.neutral.z - P[0].z);
165
+ L.azMax = 0.7;
166
+ return L;
167
+ });
168
+ cfg.legLen = cfg.legs.reduce((a, L) => a + L.total, 0) / 8;
169
+ // least-squares plane through the neutral footholds: rows of (X^T X)^-1 X^T for y = a + b x + c z
170
+ {
171
+ const X = cfg.legs.map((L) => [1, L.neutral.x, L.neutral.z]);
172
+ const A = [[0, 0, 0], [0, 0, 0], [0, 0, 0]];
173
+ for (const r of X) for (let a = 0; a < 3; a++) for (let b = 0; b < 3; b++) A[a][b] += r[a] * r[b];
174
+ const inv = new THREE.Matrix3().set(...A[0], ...A[1], ...A[2]).invert().elements; // column-major
175
+ const Ai = (a, b) => inv[b * 3 + a];
176
+ cfg.fitW = [0, 1, 2].map((a) => X.map((r) => Ai(a, 0) * r[0] + Ai(a, 1) * r[1] + Ai(a, 2) * r[2]));
177
+ }
178
+ cfg.turnR = cfg.legs.reduce((a, L) => a + Math.hypot(L.neutral.x, L.neutral.z), 0) / 8;
179
+ // ---- palps
180
+ cfg.palpDefs = ['L', 'R'].map((S) => {
181
+ const side = S === 'L' ? 1 : -1, jn = palpJoints(S), bn = palpBones(S);
182
+ const P = jn.map((n) => loc(J[n]));
183
+ const len = [], az = [], el = [];
184
+ for (let k = 0; k < 6; k++) {
185
+ const d = P[k + 1].clone().sub(P[k]);
186
+ len.push(d.length()); d.normalize();
187
+ az.push(Math.atan2(d.x * side, d.z)); el.push(Math.asin(clamp(d.y, -1, 1)));
188
+ }
189
+ const phi0 = az[2];
190
+ const u0 = new THREE.Vector3(Math.sin(phi0) * side, 0, Math.cos(phi0));
191
+ return { S, side, bones: bn.map((b) => cfg.boneIndex[b]), bindQ: bn.map((b) => bindQ[cfg.boneIndex[b]]), P0: P, len, az0: az, el0: el, phi0, n0: new THREE.Vector3().crossVectors(u0, Y_AXIS).normalize(), tipR: 0.0004 * s };
192
+ });
193
+ // ---- chelicerae and fangs
194
+ cfg.chels = ['L', 'R'].map((S) => {
195
+ const side = S === 'L' ? 1 : -1;
196
+ const base = loc(J['cheBase' + S]), tip = loc(J['cheTip' + S]), ft = loc(J['fangTip' + S]);
197
+ const fangVec = ft.clone().sub(tip);
198
+ // fang hinge: labidognath fangs swing sideways (about the forward axis), orthognath fangs swing
199
+ // down and forward (about the lateral axis); the sign is chosen so that opening moves the tip down
200
+ const axis = cfg.chel.fang === 'orthognath' ? new THREE.Vector3(1, 0, 0) : new THREE.Vector3(0, 0, 1);
201
+ const test = fangVec.clone().applyAxisAngle(axis, 0.2);
202
+ if (test.y > fangVec.y) axis.negate();
203
+ return { S, side, base, tip, fangVec, fangAxis: axis, bc: cfg.boneIndex['chelicera' + S], bf: cfg.boneIndex['fang' + S] };
204
+ });
205
+ cfg.ax = {};
206
+ for (const n of ['front', 'ceph', 'pedA', 'pedB', 'abdEnd', 'spinTip']) cfg.ax[n] = loc(J[n]);
207
+ cfg.bone = {};
208
+ for (const n of ['prosoma', 'head', 'pedicel', 'abdomen', 'spinnerets']) cfg.bone[n] = cfg.boneIndex[n] ?? -1;
209
+ // support points for ground contact of the body: extreme vertices of the bind mesh in 26
210
+ // directions, for the parts rigid with the prosoma and with the abdomen
211
+ const abdC = cfg.ax.pedB.clone().lerp(cfg.ax.abdEnd, 0.5);
212
+ cfg.abdC = abdC; cfg.abdL = cfg.ax.pedB.distanceTo(cfg.ax.abdEnd) * 0.5;
213
+ cfg.abdR = cfg.abdL * 0.62;
214
+ {
215
+ const dirs = [];
216
+ for (let x = -1; x <= 1; x++) for (let y = -1; y <= 1; y++) for (let z = -1; z <= 1; z++) if (x || y || z) dirs.push(new THREE.Vector3(x, y, z).normalize());
217
+ const grp = data.bones.map((b) => (b.name === 'prosoma' || b.name === 'head' ? 0 : b.name === 'abdomen' || b.name === 'spinnerets' ? 1 : -1));
218
+ const best = [dirs.map(() => ({ d: -1e9, p: null })), dirs.map(() => ({ d: -1e9, p: null }))];
219
+ const pos = data.pos, si = data.skinIndex, sw = data.skinWeight;
220
+ const c0 = [cfg.ax.pedA.clone().lerp(cfg.ax.front, 0.5), abdC];
221
+ for (let v = 0; v < data.nV; v++) {
222
+ if (sw[v * 4] < 0.9) continue;
223
+ const gI = grp[si[v * 4]];
224
+ if (gI < 0) continue;
225
+ const x = pos[v * 3] - root.x, y = pos[v * 3 + 1] - root.y, z = pos[v * 3 + 2] - root.z;
226
+ const c = c0[gI];
227
+ for (let k = 0; k < dirs.length; k++) {
228
+ const dd = (x - c.x) * dirs[k].x + (y - c.y) * dirs[k].y + (z - c.z) * dirs[k].z;
229
+ if (dd > best[gI][k].d) best[gI][k] = { d: dd, p: [x, y, z] };
230
+ }
231
+ }
232
+ cfg.supPro = best[0].filter((b) => b.p).map((b) => new THREE.Vector3(...b.p));
233
+ cfg.supAbd = best[1].filter((b) => b.p).map((b) => new THREE.Vector3(...b.p).sub(cfg.ax.pedB));
234
+ // the abdomen's lowest bind point (for resting it on the ground)
235
+ let lo = null;
236
+ for (const p of cfg.supAbd) if (!lo || p.y < lo.y) lo = p;
237
+ cfg.abdLow = lo || new THREE.Vector3(0, -cfg.abdR, -cfg.abdL);
238
+ }
239
+ cfg.bodyLen = cfg.ax.front.distanceTo(cfg.ax.spinTip);
240
+ // ---- death curl, per leg: every joint flexed ventrally so the leg folds under the sternum and no
241
+ // joint sticks out beyond the carapace top or the body's flanks (a dead spider on its back or side
242
+ // rests on its body, never propped on its knees). Solved once here in joint space (femur
243
+ // elevation + four flexions) by coordinate descent.
244
+ {
245
+ let yTop = -1e9, yBot = 1e9, halfW = 0;
246
+ for (const p of cfg.supPro) { yTop = Math.max(yTop, p.y); yBot = Math.min(yBot, p.y); halfW = Math.max(halfW, Math.abs(p.x)); }
247
+ for (const p of cfg.supAbd) halfW = Math.max(halfW, Math.abs(p.x + cfg.ax.pedB.x));
248
+ cfg.bodyTop = yTop; cfg.bodyHalfW = halfW;
249
+ // two curls per leg: 'back' (a loose fist over the sternum: on its back, or the upper legs on its
250
+ // side) and 'side' (the legs underneath when it lies on its side: folded straight down so no
251
+ // joint reaches beyond the flank it rests on)
252
+ const solve = (L, mode) => {
253
+ const m = 1.6 * L.tipY, s = L.side, side = mode === 'side', up = mode === 'up';
254
+ const out = [Math.sin(L.phi0) * s, Math.cos(L.phi0)];
255
+ const tgt = side ? [s * 0.35 * halfW, yBot - 0.15 * L.total, L.P0[0].z * 0.7]
256
+ : up ? [L.P0[0].x + out[0] * 0.28 * L.total, yBot + L.tipY, L.P0[0].z + out[1] * 0.28 * L.total]
257
+ : [s * 0.12 * halfW, yBot - 0.1 * L.total, L.P0[0].z * 0.5];
258
+ const deep = yBot - 0.3 * L.total;
259
+ const jp = [];
260
+ const fk = (x) => {
261
+ // x = [femur, b1, b2, b3, b4, coxa] (rad); leg plane at phi0
262
+ let px = L.P0[0].x, py = L.P0[0].y, pz = L.P0[0].z;
263
+ const segs = [[L.az0[0], x[5]], [L.phi0, x[5]]];
264
+ let e = x[0];
265
+ segs.push([L.phi0, e]);
266
+ for (let k = 1; k < 5; k++) { e += x[k]; segs.push([L.phi0, e]); }
267
+ jp.length = 0;
268
+ for (let k = 0; k < 7; k++) {
269
+ const [a, el] = segs[k], ce = Math.cos(el);
270
+ px += L.len[k] * Math.sin(a) * ce * s; py += L.len[k] * Math.sin(el); pz += L.len[k] * Math.cos(a) * ce;
271
+ jp.push(px, py, pz);
272
+ }
273
+ };
274
+ const cost = (x) => {
275
+ fk(x);
276
+ const n = jp.length;
277
+ let c = ((jp[n - 3] - tgt[0]) ** 2 + (jp[n - 2] - tgt[1]) ** 2 + (jp[n - 1] - tgt[2]) ** 2) / (L.total * L.total);
278
+ for (let k = 0; k < 7; k++) {
279
+ const x_ = jp[k * 3], y_ = jp[k * 3 + 1];
280
+ const over = up ? Math.max(0, yBot + m - y_) : Math.max(0, y_ - (yTop - m)) + (side ? Math.max(0, Math.abs(x_) - (halfW - m)) : Math.max(0, deep - y_));
281
+ c += 400 * (over / L.total) ** 2;
282
+ }
283
+ // even flexions (a curl, not one sharp fold), within anatomical limits
284
+ const mean = (x[1] + x[2] + x[3] + x[4]) / 4;
285
+ for (let k = 1; k < 5; k++) c += 0.02 * (x[k] - mean) ** 2 + 50 * Math.max(0, x[k] + 0.1) ** 2 + 50 * Math.max(0, -x[k] - 2.6) ** 2;
286
+ c += 50 * Math.max(0, x[0] - (up ? 1.3 : 0.2)) ** 2 + 50 * Math.max(0, -x[0] - 1.6) ** 2 + (side ? 0 : up ? 0.05 * (x[0] - 0.8) ** 2 : 0.05 * (x[0] + 0.8) ** 2);
287
+ // the coxa and trochanter may swing down (ventrally) in their sockets, not up
288
+ c += 50 * Math.max(0, x[5] - L.el0[0]) ** 2 + 50 * Math.max(0, -x[5] - 1.4) ** 2 + 0.05 * (x[5] - L.el0[0]) ** 2;
289
+ return c;
290
+ };
291
+ const x = up ? [0.8, -1.6, -0.6, -0.6, -0.4, L.el0[0]] : [-1.0, -1.4, -0.8, -0.8, -0.6, L.el0[0]];
292
+ let best = cost(x), step = 0.4;
293
+ for (let it = 0; it < 400 && step > 1e-3; it++) {
294
+ let improved = false;
295
+ for (let k = 0; k < 6; k++) for (const d of [step, -step]) {
296
+ x[k] += d;
297
+ const c = cost(x);
298
+ if (c < best) { best = c; improved = true; } else x[k] -= d;
299
+ }
300
+ if (!improved) step *= 0.5;
301
+ }
302
+ return { F: x[0], C: x[5], B: [x[1], x[2], x[3], x[4]] };
303
+ };
304
+ for (const L of cfg.legs) { L.curlBack = solve(L, 'back'); L.curlSide = solve(L, 'side'); L.curlUp = solve(L, 'up'); }
305
+ }
306
+ // mouth: between the chelicera tips
307
+ cfg.mouth = cfg.chels[0].tip.clone().add(cfg.chels[1].tip).multiplyScalar(0.5);
308
+ return cfg;
309
+ }
310
+
311
+ // ------------------------------------------------------------------------------------------------
312
+ // swing path profiles (see the header): horizontal progress with short linear speed ramps; vertical
313
+ // hump: constant climb, a rounded apex, constant descent
314
+ function progress(u, a) {
315
+ const vm = 1 / (1 - a);
316
+ if (u <= 0) return 0;
317
+ if (u >= 1) return 1;
318
+ if (u < a) return (vm * u * u) / (2 * a);
319
+ if (u > 1 - a) { const r = 1 - u; return 1 - (vm * r * r) / (2 * a); }
320
+ return vm * (u - a / 2);
321
+ }
322
+ function hump(u) {
323
+ // vertical speed +c on [0, u1], easing linearly to -c2 over [u1, u2], -c2 on [u2, 1]; c2 makes the
324
+ // path end at 0; normalised to a peak of 1
325
+ if (u <= 0 || u >= 1) return 0;
326
+ const u1 = 0.3, u2 = 0.6, T = u2 - u1;
327
+ const c2 = (u1 + 0.5 * T) / (0.5 * T + (1 - u2));
328
+ const peak = u1 + (0.5 * T) / (1 + c2);
329
+ let y;
330
+ if (u < u1) y = u;
331
+ else if (u < u2) { const t = u - u1; y = u1 + t - ((1 + c2) * t * t) / (2 * T); }
332
+ else y = c2 * (1 - u);
333
+ return y / peak;
334
+ }
335
+
336
+ // ------------------------------------------------------------------------------------------------
337
+ // actions
338
+ const POSTURES = new Set(['sit', 'lie', 'sleep', 'death']);
339
+ const ONESHOTS = new Set(['jump', 'attack', 'hit', 'eat', 'drink']);
340
+ export const SPIDER_ACTIONS = ['idle', 'jump', 'sit', 'lie', 'sleep', 'eat', 'drink', 'attack', 'hit', 'death', 'stand'];
341
+
342
+ function createParams() {
343
+ return {
344
+ gaitW: 1, speedScale: 1, crouch: 0, drop: 0, pitch: 0, roll: 0, fwd: 0, side: 0, yaw: 0,
345
+ legIn: 0, drawIn: 0, tuck: 0, curl2: 0, raise: new Float32Array(8), hold: new Float32Array(8), curl: 0, air: 0, lean: 0,
346
+ palpRaise: 0, palpReach: 0, palpWork: 0, palpDown: 0, palpFold: 0,
347
+ chelOpen: 0, chelWork: 0, fangOpen: 0, abdPitch: 0, abdDown: 0, breathRate: 1, breathAmp: 1,
348
+ flip: 0, reviving: 0, pullIn: 0, flipSide: 1, flipAngle: Math.PI, dead: 0, lookW: 1, look: null, twitch: 1,
349
+ };
350
+ }
351
+ function resetParams(P) {
352
+ P.gaitW = 1; P.speedScale = 1; P.crouch = P.drop = P.pitch = P.roll = P.fwd = P.side = P.yaw = 0;
353
+ P.legIn = 0; P.drawIn = 0; P.tuck = 0; P.curl2 = 0; P.raise.fill(0); P.hold.fill(0); P.curl = P.tuck = P.air = P.lean = 0;
354
+ P.palpRaise = P.palpReach = P.palpWork = P.palpDown = P.palpFold = 0;
355
+ P.chelOpen = P.chelWork = P.fangOpen = P.abdPitch = P.abdDown = 0; P.breathRate = 1; P.breathAmp = 1;
356
+ P.flip = 0; P.reviving = 0; P.pullIn = 0; P.dead = 0; P.lookW = 1; P.look = null; P.twitch = 1;
357
+ }
358
+ const mx = (P, k, v, w) => { P[k] += (v - P[k]) * w; };
359
+ const mxA = (arr, i, v, w) => { arr[i] += (v - arr[i]) * w; };
360
+
361
+ const POSTURE_DEFS = {
362
+ // body lowered, abdomen resting on the ground, legs a little drawn in
363
+ sit: {
364
+ fadeIn: 0.7, fadeOut: 0.6,
365
+ apply(P, w) { mx(P, 'gaitW', 0, w); mx(P, 'crouch', 0.5, w); mx(P, 'legIn', 0.25, w); mx(P, 'abdDown', 1, w); mx(P, 'palpDown', 0.4, w); mx(P, 'pitch', 0.14, w); },
366
+ },
367
+ // flat on the ground, legs drawn in
368
+ lie: {
369
+ fadeIn: 0.9, fadeOut: 0.8,
370
+ apply(P, w) { mx(P, 'gaitW', 0, w); mx(P, 'crouch', 0.8, w); mx(P, 'legIn', 0.5, w); mx(P, 'abdDown', 1, w); mx(P, 'palpDown', 0.8, w); mx(P, 'breathRate', 0.8, w); },
371
+ },
372
+ // lie with the legs pulled in tight, palps folded, slow breathing
373
+ sleep: {
374
+ fadeIn: 1.1, fadeOut: 1.0,
375
+ apply(P, w, inst, e) {
376
+ POSTURE_DEFS.lie.apply(P, w);
377
+ const c = smooth(0.4, 2.0, inst.t) * w;
378
+ const tuck = e.cfg.actions.sleep?.tuck ?? 0.8;
379
+ mx(P, 'crouch', 0.92, c); mx(P, 'legIn', 0.5 + 0.35 * tuck, c); mx(P, 'palpFold', 1, c);
380
+ // the claws are drawn in along the ground, the front legs folded against the chelicerae
381
+ mx(P, 'drawIn', tuck, c); mx(P, 'pullIn', tuck, c);
382
+ mx(P, 'breathRate', 0.45, c); mx(P, 'breathAmp', 1.3, c); mx(P, 'twitch', 0.1, c); mx(P, 'lookW', 0, c);
383
+ for (let i = 0; i < 8; i++) if (e.legs[i].def.i === 1) mxA(P.hold, i, 0.3 * tuck, c);
384
+ },
385
+ },
386
+ // hydraulic collapse: the legs curl under the body, it drops, then rolls onto its back or side
387
+ death: {
388
+ fadeIn: 0.2, fadeOut: 3.0,
389
+ apply(P, w, inst) {
390
+ const t = inst.t;
391
+ mx(P, 'gaitW', 0, w); mx(P, 'dead', 1, w);
392
+ // the legs lose pressure: the body sinks and the claws are drawn in, then the legs fold up
393
+ // (reviving runs the other way round: it rolls back first, then uncurls, then stands up)
394
+ mx(P, 'drawIn', smooth(0.05, 0.6, t), smooth(0, 0.35, w));
395
+ // (no dorsal tuck any more: the legs flex ventrally at once, the body hunches on them and tips over)
396
+ // on its back the legs contract further, ventrally (over the sternum)
397
+ mx(P, 'curl2', smooth(0.3, 1.3, t), smooth(0.3, 0.8, w));
398
+ mx(P, 'crouch', smooth(0, 0.35, t), smooth(0, 0.45, w));
399
+ const f = smooth(1.0, 2.0, t);
400
+ const settle = t > 2.0 ? Math.exp(-(t - 2.0) * 5) * Math.sin((t - 2.0) * 14) * 0.05 : 0;
401
+ mx(P, 'flip', clamp(f - settle, 0, 1.05), smooth(0.6, 1, w));
402
+ P.flipSide = inst.side; P.flipAngle = inst.flipAngle;
403
+ if (inst.target < 0.5) P.reviving = 1;
404
+ mx(P, 'palpFold', 1, w); mx(P, 'fangOpen', 0.3, w); mx(P, 'breathAmp', 0, w); mx(P, 'breathRate', 0, w);
405
+ mx(P, 'twitch', 0, w); mx(P, 'lookW', 0, w); mx(P, 'abdDown', 0, w);
406
+ },
407
+ },
408
+ };
409
+
410
+ const ONESHOT_DEFS = {
411
+ // pounce: crouch, push off with legs III-IV, fly with the front legs raised to grab, land
412
+ jump: {
413
+ start(inst, e) {
414
+ const cfg = e.cfg, o = cfg.actions.jump || {};
415
+ inst.tc = (o.crouch ?? 0.22) * cfg.sq;
416
+ const h = (o.height ?? 0.014) * cfg.s;
417
+ // apex h above the take-off height, counting the push-off and coast rise (vy (PUSH / 2 + COAST))
418
+ const k = PUSH / 2 + COAST;
419
+ inst.vy = G_ACC * (-k + Math.sqrt(k * k + (2 * h) / G_ACC));
420
+ const T = (2 * inst.vy) / G_ACC + PUSH * 0.5 + 2 * COAST;
421
+ let dist = (o.distance ?? 0.05) * cfg.s;
422
+ if (inst.opts.target) { const t = inst.opts.target; dist = clamp(Math.hypot(t.x - e.pos.x, t.z - e.pos.z), 0.3 * dist, dist * 2); }
423
+ inst.vh = Math.max(e.speed, dist / T);
424
+ inst.phase = 'crouch'; inst.dur = Infinity;
425
+ },
426
+ update(inst, dt, e) {
427
+ if (inst.phase === 'crouch' && inst.t >= inst.tc) { inst.phase = 'air'; inst.tAir = inst.t; e.takeOff(inst.vy, inst.vh); }
428
+ if (inst.phase === 'air' && inst.t - inst.tAir > 3) e.touchDown();
429
+ // pose weights are states with finite rates (the flight of a small spider lasts ~0.1 s)
430
+ inst.aw = clamp((inst.aw || 0) + (inst.phase === 'air' ? dt / 0.1 : inst.phase === 'land' ? -dt / 0.25 : 0), 0, 1);
431
+ return inst.phase === 'land' && inst.t - inst.tLand > 0.45;
432
+ },
433
+ onLand(inst) { inst.phase = 'land'; inst.tLand = inst.t; },
434
+ apply(P, w, inst, e) {
435
+ const aw = (inst.aw || 0) * w;
436
+ const pre = inst.phase === 'crouch' ? smooth(0, inst.tc, inst.t) : inst.phase === 'air' ? 1 : 1 - smooth(0, 0.3, inst.t - inst.tLand);
437
+ const land = inst.phase === 'land' ? envelope(inst.t - inst.tLand, 0.45, 0.06, 0.35) : 0;
438
+ const cr = inst.phase === 'crouch' ? smooth(0, inst.tc, inst.t) : inst.phase === 'air' ? 1 - smooth(0, PUSH + 0.03, inst.t - inst.tAir) : 0;
439
+ mx(P, 'crouch', 0.55, cr * w); mx(P, 'crouch', 0.4, land * w); mx(P, 'pitch', 0.1, pre * w * (1 - aw));
440
+ mx(P, 'air', 1, aw);
441
+ // landing contacts are not gait contacts (the claws settle while the body brakes)
442
+ if (inst.phase === 'land') mx(P, 'gaitW', 0, (1 - smooth(0.3, 0.45, inst.t - inst.tLand)) * w);
443
+ mx(P, 'palpRaise', 1, Math.max(smooth(0, 1, aw), 0.5 * pre) * w); mx(P, 'fangOpen', 1, Math.max(aw, 0.4 * pre) * w); mx(P, 'chelOpen', 0.8, aw);
444
+ for (let i = 0; i < 8; i++) {
445
+ const li = e.legs[i].def.i;
446
+ // front legs lift already in the crouch and reach up and forward to grab; after landing they
447
+ // come down onto the prey
448
+ if (li === 1) { mxA(P.raise, i, 1, Math.max(aw, 0.35 * pre) * w); mxA(P.hold, i, 0.6, land * (1 - aw) * w); }
449
+ else if (li === 2) mxA(P.raise, i, 0.6, aw);
450
+ }
451
+ },
452
+ },
453
+ // threat display (front legs raised, fangs out, body reared), then a lunge with a fang strike
454
+ attack: {
455
+ start(inst, e) {
456
+ const o = e.cfg.actions.attack || {};
457
+ inst.threat = (o.threat ?? 0.7) * (inst.opts.duration ? inst.opts.duration / 1.7 : 1);
458
+ inst.dur = inst.threat + 1.0;
459
+ inst.hit = false;
460
+ if (inst.opts.target && !e.input.follow) {
461
+ const t = inst.opts.target;
462
+ const want = Math.atan2(t.x - e.pos.x, t.z - e.pos.z);
463
+ if (Math.abs(angDiff(e.heading, want)) > 0.2) e._turnTo = want;
464
+ }
465
+ },
466
+ update(inst, dt, e) {
467
+ const tl = inst.t - inst.threat;
468
+ if (!inst.hit && tl >= 0.16) {
469
+ inst.hit = true;
470
+ const cfg = e.cfg;
471
+ const p = e.localToWorld(cfg.mouth.x, cfg.mouth.y - 0.002 * cfg.s, cfg.mouth.z + 0.003 * cfg.s, new THREE.Vector3());
472
+ e.emit('attackHit', { position: p, direction: e.forward(e.heading, new THREE.Vector3()), style: 'bite' });
473
+ }
474
+ return inst.t >= inst.dur;
475
+ },
476
+ apply(P, w, inst, e) {
477
+ const o = e.cfg.actions.attack || {}, cfg = e.cfg;
478
+ const t = inst.t, T = inst.threat, tl = t - T;
479
+ const rear = smooth(0, Math.min(0.35, T), t) * (1 - smooth(0.05, 0.2, tl) * 0.8) * (1 - smooth(0.45, 0.95, tl));
480
+ const lunge = smooth(0, 0.14, tl) * (1 - smooth(0.4, 0.9, tl));
481
+ const still = 1 - 0.75 * smooth(0, 0.3 * cfg.maxSpeed, e.speed); // on the run it rears less
482
+ mx(P, 'pitch', ((o.rear ?? 0.32) * rear - 0.12 * lunge) * still, w);
483
+ mx(P, 'crouch', 0.15 * rear + 0.25 * lunge, w);
484
+ // the lunge is a body thrust over planted claws (smaller on the run: the gait carries it)
485
+ mx(P, 'fwd', (o.lunge ?? 0.012) * cfg.s * lunge * (1 - smooth(0, 0.3 * cfg.maxSpeed, e.speed)), w);
486
+ mx(P, 'fangOpen', smooth(0.1, 0.4, t) * (1 - smooth(0.3, 0.6, tl) * 0.7) * (1 - smooth(0.7, 0.95, tl)), w);
487
+ mx(P, 'chelOpen', 0.8 * rear + 0.3 * lunge, w);
488
+ mx(P, 'palpRaise', 0.9 * rear, w);
489
+ mx(P, 'abdPitch', -0.15 * rear, w);
490
+ const strike = lunge * (1 - smooth(0.2, 0.5, tl));
491
+ for (let i = 0; i < 8; i++) {
492
+ const li = e.legs[i].def.i;
493
+ if (li === 1) { mxA(P.raise, i, rear, w); mxA(P.hold, i, strike, w); }
494
+ else if (li === 2) mxA(P.raise, i, (o.raise2 ?? 0.45) * rear, w);
495
+ }
496
+ },
497
+ },
498
+ // flinch, then scuttle back
499
+ hit: {
500
+ start(inst, e) {
501
+ const cfg = e.cfg;
502
+ inst.dur = 0.9;
503
+ const d = inst.opts.direction;
504
+ const dir = tv();
505
+ if (d && d.lengthSq() > 1e-8) dir.set(d.x, 0, d.z).normalize(); else e.forward(e.heading, dir).negate();
506
+ // scuttle away from the blow, mostly backwards
507
+ const back = e.forward(e.heading, tv()).negate();
508
+ dir.lerp(back, 0.5).normalize();
509
+ e.push.addScaledVector(dir, 5.5 * cfg.bodyLen / Math.max(0.5, cfg.sq));
510
+ },
511
+ update(inst) { return inst.t >= inst.dur; },
512
+ apply(P, w, inst, e) {
513
+ const t = inst.t;
514
+ const fl = envelope(t, 0.9, 0.05, 0.55);
515
+ const jerk = smooth(0, 0.12, t) * (1 - smooth(0.15, 0.45, t));
516
+ mx(P, 'crouch', 0.3 * fl, w); mx(P, 'pitch', 0.18 * jerk, w); mx(P, 'fwd', -0.06 * e.cfg.bodyLen * jerk, w);
517
+ mx(P, 'palpRaise', 0.6 * fl, w); mx(P, 'abdPitch', 0.12 * jerk, w);
518
+ for (let i = 0; i < 8; i++) if (e.legs[i].def.i === 1) mxA(P.raise, i, 0.45 * jerk, w);
519
+ },
520
+ },
521
+ // prey held under the chelicerae by legs I and the palps; chelicerae working
522
+ eat: {
523
+ start(inst) { inst.dur = inst.opts.duration ?? (inst.opts.loop ? Infinity : 4); },
524
+ update(inst) { return inst.t >= inst.dur; },
525
+ apply(P, w, inst, e) {
526
+ const a = smooth(0, 0.5, inst.t) * w;
527
+ mx(P, 'crouch', 0.3, a); mx(P, 'pitch', -0.06, a);
528
+ mx(P, 'palpWork', 1, a); mx(P, 'chelWork', 1, a); mx(P, 'fangOpen', 0.35, a); mx(P, 'palpReach', 0.3, a);
529
+ for (let i = 0; i < 8; i++) if (e.legs[i].def.i === 1) mxA(P.hold, i, 0.7, a);
530
+ },
531
+ },
532
+ // mouthparts down to the ground / water
533
+ drink: {
534
+ start(inst) { inst.dur = inst.opts.duration ?? (inst.opts.loop ? Infinity : 3); },
535
+ update(inst) { return inst.t >= inst.dur; },
536
+ apply(P, w, inst) {
537
+ const a = smooth(0, 0.6, inst.t) * w;
538
+ mx(P, 'crouch', 0.35, a); mx(P, 'pitch', -0.2, a); mx(P, 'palpRaise', -0.2, a); mx(P, 'palpReach', 0.5, a);
539
+ mx(P, 'chelWork', 0.25, a); mx(P, 'chelOpen', 0.2, a); mx(P, 'abdPitch', 0.05, a);
540
+ },
541
+ },
542
+ };
543
+
544
+ class SpiderActions {
545
+ constructor(engine) {
546
+ this.e = engine;
547
+ this.names = SPIDER_ACTIONS.slice();
548
+ this.postures = [];
549
+ this.oneshots = [];
550
+ this.posture = 'stand';
551
+ this.standReq = null;
552
+ this.idle = { w: 0, lookT: 1, look: new THREE.Vector3(), hasLook: false, twitchT: 2, palpT: 1 };
553
+ }
554
+ holdsStill() { for (const p of this.postures) if (p.target > 0 || p.w > 0.02) return true; return false; }
555
+ // automatic stand-up (asked to move): never revives a dead spider
556
+ requestStand() { if (!this.standReq && !this.dead()) this.play('stand'); }
557
+ // arbitration: a new one-shot interrupts the running
558
+ // ones (fade out, 'interrupted'); a jump in the air is committed (only a hit layers over it); a
559
+ // posture ends the one-shots (except a hit); stop() ends everything but death and never cuts a
560
+ // jump in the air; dead: only hit is taken. Promises resolve 'done' | 'interrupted' | 'stopped' |
561
+ // 'refused'.
562
+ committed(a) { return !a.stopping && a.name === 'jump' && this.e.air.active; }
563
+ end(a, reason) { if (a.stopping) return; a.stopping = true; a.reason = reason; }
564
+ current() {
565
+ for (const a of this.oneshots) if (!a.stopping) return a.name;
566
+ for (const p of this.postures) if (p.target > 0) return p.name;
567
+ return this.standReq ? 'stand' : null;
568
+ }
569
+ clear() { for (const a of [...this.postures, ...this.oneshots]) { a.resolve?.('stopped'); a.resolveIn?.('stopped'); } this.postures.length = 0; this.oneshots.length = 0; }
570
+ dead() { for (const p of this.postures) if (p.name === 'death' && p.target > 0) return true; return false; }
571
+
572
+ play(name, opts = {}) {
573
+ const e = this.e;
574
+ if (name === 'idle') name = 'stand';
575
+ if (name === 'stand') {
576
+ if (!this.postures.length) return Promise.resolve('done');
577
+ for (const p of this.postures) if (p.target > 0) { p.target = 0; p.resolveIn?.('interrupted'); }
578
+ e.emit('actionStart', { name: 'stand' });
579
+ if (this.standReq) return this.standReq.promise;
580
+ let resolve;
581
+ const promise = new Promise((r) => { resolve = r; });
582
+ this.standReq = { promise, resolve };
583
+ return promise;
584
+ }
585
+ if (POSTURES.has(name)) {
586
+ const cur = this.postures.find((p) => p.target > 0);
587
+ if (cur && cur.name === 'death' && name !== 'death') return Promise.resolve('refused');
588
+ if (cur && cur.name === name) return cur.promiseIn;
589
+ if (name !== 'death' && this.oneshots.some((a) => this.committed(a))) return Promise.resolve('refused');
590
+ for (const p of this.postures) if (p.target > 0) { p.target = 0; p.resolveIn?.('interrupted'); }
591
+ if (this.standReq) { this.standReq.resolve('interrupted'); this.standReq = null; }
592
+ for (const a of this.oneshots) if (a.name !== 'hit' && !this.committed(a)) this.end(a, 'interrupted');
593
+ const def = POSTURE_DEFS[name];
594
+ let side = opts.side ?? (e.rand() < 0.5 ? -1 : 1);
595
+ if (opts.direction && name === 'death') { const d = opts.direction; side = (d.x * Math.cos(e.heading) - d.z * Math.sin(e.heading)) > 0 ? 1 : -1; }
596
+ const inst = { name, t: 0, w: 0, target: 1, fadeIn: def.fadeIn / (opts.speed || 1), fadeOut: def.fadeOut, side, opts, reached: false };
597
+ // death: on its back most of the time, sometimes on its side
598
+ inst.flipAngle = opts.onSide ?? (e.rand() < 0.35) ? Math.PI * 0.52 : Math.PI * (0.9 + 0.08 * e.rand());
599
+ const prev = this.postures.find((p) => p.name === 'lie');
600
+ if (prev && name === 'sleep') { inst.w = prev.w; prev.w = 0; this.postures = this.postures.filter((p) => p !== prev); }
601
+ inst.promiseIn = new Promise((r) => { inst.resolveIn = r; });
602
+ this.postures.push(inst);
603
+ e.emit('actionStart', { name });
604
+ if (name === 'death') e.emit('death', { position: e.pos.clone() });
605
+ return inst.promiseIn;
606
+ }
607
+ if (!ONESHOTS.has(name)) return Promise.reject(new Error(`procedural-animals: unknown action "${name}"`));
608
+ if (this.dead() && name !== 'hit') return Promise.resolve('refused');
609
+ if (this.holdsStill() && (name === 'jump' || name === 'attack')) return this.play('stand').then((r) => (r === 'refused' ? r : this.play(name, opts)));
610
+ // arbitration: a new one-shot interrupts the running ones; in the air only a hit is taken
611
+ if (e.air.active || this.oneshots.some((a) => this.committed(a))) {
612
+ if (name !== 'hit') return Promise.resolve('refused');
613
+ for (const a of this.oneshots) if (a.name === 'hit') this.end(a, 'interrupted');
614
+ } else for (const a of this.oneshots) this.end(a, 'interrupted');
615
+ const inst = { name, t: 0, w: 0, dur: 1, stopping: false, opts, fade: name === 'hit' ? 0.06 : 0.15, speed: opts.speed || 1 };
616
+ ONESHOT_DEFS[name].start(inst, e);
617
+ inst.promise = new Promise((r) => { inst.resolve = r; });
618
+ this.oneshots.push(inst);
619
+ e.emit('actionStart', { name });
620
+ return inst.promise;
621
+ }
622
+
623
+ stop(name) {
624
+ for (const a of this.oneshots) if ((!name || a.name === name) && !this.committed(a)) this.end(a, 'stopped');
625
+ if (!name || POSTURES.has(name)) if (this.postures.some((p) => p.target > 0 && (name ? p.name === name : p.name !== 'death'))) this.play('stand');
626
+ }
627
+ onLand() { for (const a of this.oneshots) if (ONESHOT_DEFS[a.name].onLand) ONESHOT_DEFS[a.name].onLand(a, this.e); }
628
+
629
+ update(dt) {
630
+ const e = this.e, P = e.P;
631
+ resetParams(P);
632
+ for (const p of this.postures) {
633
+ p.t += dt;
634
+ const rate = p.target > p.w ? 1 / p.fadeIn : 1 / p.fadeOut;
635
+ p.w = clamp(p.w + clamp(p.target - p.w, -rate * dt, rate * dt), 0, 1);
636
+ if (!p.reached && p.target > 0 && p.w >= 1 && (p.name !== 'death' || p.t > 2.0) && (p.name !== 'sleep' || p.t > 2.0)) { p.reached = true; p.resolveIn('done'); e.emit('actionEnd', { name: p.name }); }
637
+ }
638
+ for (let i = this.postures.length - 1; i >= 0; i--) { const p = this.postures[i]; if (!(p.target > 0 || p.w > 0)) this.postures.splice(i, 1); }
639
+ let active = null;
640
+ for (const p of this.postures) if (p.target > 0) active = p;
641
+ this.posture = active ? (active.name === 'death' ? 'dead' : active.name) : this.postures.length ? 'standing-up' : 'stand';
642
+ if (this.standReq && !this.postures.length) { this.standReq.resolve('done'); this.standReq = null; e.emit('actionEnd', { name: 'stand' }); }
643
+ // idle
644
+ const still = e.speed < 0.08 * e.cfg.maxSpeed && !e.input.follow && e.push.lengthSq() < 1e-6 && e.pushV.lengthSq() < 1e-6;
645
+ let busy = false;
646
+ for (const a of this.oneshots) if (!a.stopping && a.name !== 'hit') busy = true;
647
+ const idleT = still && !this.postures.length && !busy ? 1 : 0;
648
+ const id = this.idle;
649
+ id.w += clamp(idleT - id.w, -dt * 2, dt * 0.8);
650
+ this.applyIdle(P, id.w, dt);
651
+ for (const p of this.postures) { const w = p.w * p.w * (3 - 2 * p.w); POSTURE_DEFS[p.name].apply(P, w, p, e); }
652
+ const wantsToMove = e.wantSpeed > 0.02 * e.cfg.maxSpeed || !!e.input.follow;
653
+ for (const a of this.oneshots) {
654
+ if (wantsToMove && (a.name === 'eat' || a.name === 'drink')) this.end(a, 'interrupted');
655
+ a.t += dt * a.speed;
656
+ if (!a.stopping && ONESHOT_DEFS[a.name].update(a, dt * a.speed, e)) this.end(a, 'done');
657
+ const fadeOut = a.name === 'jump' && a.reason === 'done' ? 0.05 : a.name === 'attack' ? 0.25 : a.fade;
658
+ if (a.stopping) a.w = Math.max(0, a.w - dt / fadeOut); else a.w = Math.min(1, a.w + dt / a.fade);
659
+ const w = a.w * a.w * (3 - 2 * a.w);
660
+ if (w > 0) ONESHOT_DEFS[a.name].apply(P, w, a, e);
661
+ }
662
+ for (let i = this.oneshots.length - 1; i >= 0; i--) {
663
+ const a = this.oneshots[i];
664
+ if (a.stopping && a.w <= 0) { this.oneshots.splice(i, 1); a.resolve(a.reason || 'done'); e.emit('actionEnd', { name: a.name, reason: a.reason || 'done' }); }
665
+ }
666
+ if (e.air.active) { let air = false; for (const a of this.oneshots) if (a.name === 'jump') air = true; if (!air) e.touchDown(); }
667
+ }
668
+
669
+ applyIdle(P, w, dt) {
670
+ const e = this.e, id = this.idle, cfg = e.cfg;
671
+ if (w <= 0.001) { id.hasLook = false; return; }
672
+ // sit-and-wait: front legs a little raised now and then
673
+ const rf = cfg.idle.raiseFront * (0.5 + 0.5 * Math.sin(e.time * 0.21 + 1.3));
674
+ for (let i = 0; i < 8; i++) if (e.legs[i].def.i === 1) mxA(P.raise, i, rf, w);
675
+ // leg twitch: one leg lifts a touch and sets down again
676
+ id.twitchT -= dt;
677
+ if (id.twitchT <= 0 && w > 0.9) {
678
+ id.twitchT = (2.5 + e.rand() * 5) / Math.max(0.05, cfg.idle.twitch);
679
+ e.twitchLeg(Math.floor(e.rand() * 8));
680
+ }
681
+ // look around
682
+ if (!e.input.look) {
683
+ id.lookT -= dt;
684
+ if (id.lookT <= 0) {
685
+ id.lookT = 1.5 + e.rand() * 3;
686
+ if (e.rand() < 0.35) id.hasLook = false;
687
+ else {
688
+ const a = e.heading + (e.rand() - 0.5) * 2 * cfg.idle.look;
689
+ const d = 10 * cfg.bodyLen;
690
+ const px = e.pos.x + Math.sin(a) * d, pz = e.pos.z + Math.cos(a) * d;
691
+ id.look.set(px, e.terrainH(px, pz) + (e.rand() * 3) * cfg.bodyLen, pz);
692
+ id.hasLook = true;
693
+ }
694
+ }
695
+ if (id.hasLook) { P.look = id.look; P.lookW = w; }
696
+ }
697
+ }
698
+ }
699
+
700
+ // ------------------------------------------------------------------------------------------------
701
+ export function createSpiderMotion(ctx) { return new SpiderMotion(ctx); }
702
+
703
+ export class SpiderMotion {
704
+ constructor({ species, data, skeleton, ground, water, position, heading = 0, emit }) {
705
+ this.species = species;
706
+ this.sk = skeleton;
707
+ this.cfg = resolveConfig(species, data, skeleton);
708
+ const cfg = this.cfg;
709
+ this.ground = ground || (() => 0);
710
+ this.water = water || null;
711
+ this.emit = emit || (() => {});
712
+ this.rand = prng((data.seed || 1) * 7919 + 29);
713
+ this.input = { speed: 0, heading, climb: 0, look: null, follow: null, target: null, targetSpeed: cfg.gears.run || 0.1, crouch: 0, gait: null };
714
+ this.pos = new THREE.Vector3();
715
+ this.velocity = new THREE.Vector3();
716
+ this.heading = heading; this.speed = 0; this.yawRate = 0; this.phase = 0; this.time = 0; this.lod = 0; this.frame = 0;
717
+ this.wantSpeed = 0; this.wantHeading = heading;
718
+ this.push = new THREE.Vector3(); this.pushV = new THREE.Vector3();
719
+ this.moveDir = new THREE.Vector3(0, 0, 1);
720
+ this.air = { active: false, t: 0, vy: 0, vh: 0, y0: 0, T: 0, dir: new THREE.Vector3() };
721
+ this.state = {
722
+ position: this.pos, heading, speed: 0, velocity: this.velocity, gait: 'stand', grounded: true,
723
+ action: null, posture: 'stand', eyelid: 0, lookTarget: null,
724
+ stats: { gait: 'stand', freq: 0, duty: 1 },
725
+ legs: LEG_KEYS.map((key) => ({ key, stance: true, contact: new THREE.Vector3() })),
726
+ };
727
+ this.headPose = { position: new THREE.Vector3(), quaternion: new THREE.Quaternion() };
728
+ this.debug = { targets: Array.from({ length: 8 }, () => new THREE.Vector3()) };
729
+ this.gaits = [...new Set(cfg.gaits.map((g) => g.name))];
730
+ this.gears = cfg.gears;
731
+ this.gait = { name: 'walk', f: 1, D: 0.7, lift: 0.003, bob: 0, sway: 0, off: new Float64Array(8) };
732
+ // springs
733
+ this.hS = new Spring(0); this.pitchS = new Spring(0); this.rollS = new Spring(0);
734
+ this.lookYawS = new Spring(0); this.lookPitchS = new Spring(0);
735
+ this.abdPitchS = new Spring(0); this.abdYawS = new Spring(0);
736
+ this.supportS = new Spring(0); this.speedS = new Spring(0);
737
+ this.breath = 0; this._lift = 0; this.brakeA = 0;
738
+ this.lastVel = new THREE.Vector3(); this.accelV = new THREE.Vector3();
739
+ // legs
740
+ this.legs = cfg.legs.map((L) => ({
741
+ def: L, state: 'stance', mode: 'gait',
742
+ plant: new THREE.Vector3(), lift: new THREE.Vector3(), land: new THREE.Vector3(), landBase: new THREE.Vector3(), tip: new THREE.Vector3(),
743
+ swingT: 0, swingDur: 0.1, liftH: 0.002, hMax: 0, stanceTime: 0, prevPhase: 0, windowUsed: false, heel: 0,
744
+ az: new Float64Array(7), el: new Float64Array(7), jp: Array.from({ length: 8 }, () => new THREE.Vector3()),
745
+ airFrom: new THREE.Vector3(), airRoot: new THREE.Vector3(), airT: 1, carry0: 0, swingInit: false, liftAz: new Float64Array(7), liftEl: new Float64Array(7), rawAz: new Float64Array(7), rawEl: new Float64Array(7), ikEl: new Float64Array(7), ikAz: new Float64Array(7),
746
+ }));
747
+ this.palpState = cfg.palpDefs.map(() => ({ twitch: new Float64Array(6), tgt: new Float64Array(6), twT: 0.5, az: new Float64Array(6), el: new Float64Array(6), jp: Array.from({ length: 7 }, () => new THREE.Vector3()) }));
748
+ // frames (preallocated)
749
+ this.qB = new THREE.Quaternion(); this.qBinv = new THREE.Quaternion(); this.upB = new THREE.Vector3(0, 1, 0); this.root = new THREE.Vector3();
750
+ this.qAbd = new THREE.Quaternion(); this.qPed = new THREE.Quaternion();
751
+ this._e = new THREE.Euler(); this._q = new THREE.Quaternion(); this._q2 = new THREE.Quaternion(); this._q3 = new THREE.Quaternion();
752
+ this._qc1 = new THREE.Quaternion(); this._qc2 = new THREE.Quaternion(); this._qc3 = new THREE.Quaternion();
753
+ this._scale = new THREE.Vector3(1, 1, 1); this._v = new THREE.Vector3(); this._w = new THREE.Vector3();
754
+ this.lookTarget = new THREE.Vector3();
755
+ this.P = createParams();
756
+ this.actionsLayer = new SpiderActions(this);
757
+ this.actions = this.actionsLayer.names;
758
+ this.attachmentPoints = this._attachments(data);
759
+ this.reset(position || new THREE.Vector3(), heading);
760
+ }
761
+
762
+ _attachments(data) {
763
+ const cfg = this.cfg, J = cfg.J;
764
+ const at = (bone, p) => {
765
+ const i = data.bones.findIndex((b) => b.name === bone);
766
+ if (i < 0) return null;
767
+ const inv = this.sk.inverses ? this.sk.inverses[i] : this.sk.bind[i].clone().invert();
768
+ return { bone, local: new THREE.Matrix4().makeTranslation(p.x, p.y, p.z).premultiply(inv) };
769
+ };
770
+ const mouth = cfg.mouth.clone().add(cfg.root);
771
+ let headP = J.ceph.clone().lerp(J.front, 0.6);
772
+ if (data.eyes?.length) { headP = new THREE.Vector3(); for (const e of data.eyes) headP.add(new THREE.Vector3(...e.c)); headP.multiplyScalar(1 / data.eyes.length); }
773
+ this._eyeLocal = headP.clone().sub(cfg.root);
774
+ const back = cfg.abdC.clone().add(cfg.root); back.y += cfg.abdR * 0.9;
775
+ const out = { mouth: at('head', mouth), head: at('head', headP), back: at('abdomen', back) };
776
+ for (const k of Object.keys(out)) if (!out[k]) delete out[k];
777
+ return out;
778
+ }
779
+
780
+ // ----------------------------------------------------------------- frames
781
+ forward(h, out) { return out.set(Math.sin(h), 0, Math.cos(h)); }
782
+ left(h, out) { return out.set(Math.cos(h), 0, -Math.sin(h)); }
783
+ terrainH(x, z) { const h = this.ground(x, z); return Number.isFinite(h) ? h : 0; }
784
+ localToWorld(x, y, z, out) { return out.set(x, y, z).applyQuaternion(this.qB).add(this.root); }
785
+ // heading frame (no pitch / roll) about the ground point p
786
+ headingToWorld(lx, lz, h, px, pz, out) {
787
+ const sh = Math.sin(h), ch = Math.cos(h);
788
+ return out.set(px + ch * lx + sh * lz, 0, pz - sh * lx + ch * lz);
789
+ }
790
+
791
+ reset(p, heading = this.heading) {
792
+ this.pos.set(p.x, this.terrainH(p.x, p.z), p.z);
793
+ this.heading = heading; this.input.heading = heading; this.wantHeading = heading;
794
+ this.speed = 0; this.yawRate = 0; this.velocity.set(0, 0, 0); this.lastVel.set(0, 0, 0);
795
+ this.gaitAt(0);
796
+ for (const leg of this.legs) {
797
+ this.neutral(leg, 0, 0, 0, leg.plant);
798
+ leg.state = 'stance'; leg.stanceTime = 1;
799
+ }
800
+ this.hS.reset(this.cfg.rootH); this.pitchS.reset(0); this.rollS.reset(0);
801
+ this._fitTerrain();
802
+ this.hS.reset(this._fit.h + this.cfg.rootH * this.cfg.stance.height);
803
+ this.pitchS.reset(this._fit.pitch); this.rollS.reset(this._fit.roll);
804
+ }
805
+
806
+ // ----------------------------------------------------------------- gait table
807
+ gaitAt(vn) {
808
+ const T = this.cfg.gaits, g = this.gait;
809
+ let i = 0;
810
+ while (i < T.length - 2 && vn > T[i + 1].v) i++;
811
+ const a = T[i], b = T[Math.min(i + 1, T.length - 1)];
812
+ const t = a === b ? 0 : smooth(0, 1, (vn - a.v) / (b.v - a.v));
813
+ g.name = t < 0.5 ? a.name : b.name;
814
+ g.f = lerp(a.f, b.f, t); g.D = lerp(a.D, b.D, t);
815
+ g.lift = lerp(a.lift, b.lift, t); g.bob = lerp(a.bob, b.bob, t); g.sway = lerp(a.sway, b.sway, t);
816
+ for (let j = 0; j < 8; j++) g.off[j] = wrap01(a.off[j] + d01(a.off[j], b.off[j]) * t);
817
+ if (vn > b.v) g.f = b.f * Math.min(1.3, vn / Math.max(b.v, 1e-6));
818
+ const cfg = this.cfg;
819
+ g.f /= cfg.sq;
820
+ g.lift *= cfg.s; g.bob *= cfg.s;
821
+ return g;
822
+ }
823
+
824
+ // neutral claw position (world, on the terrain + tip height) tAhead seconds from now, plus a lead
825
+ // along the velocity (half a stance sweep) and the yaw of the turn
826
+ neutral(leg, tAhead, lead, yawLead, out) {
827
+ const L = leg.def, P = this.P;
828
+ const hp = this.heading + clamp(this.yawRate * tAhead + yawLead, -0.45, 0.45);
829
+ const bx = this.pos.x + this.velocity.x * tAhead, bz = this.pos.z + this.velocity.z * tAhead;
830
+ const k = 1 - 0.22 * P.legIn * (L.i === 1 || L.i === 4 ? 1.2 : 1);
831
+ this.headingToWorld(L.neutral.x * k, L.neutral.z * k, hp, bx, bz, out);
832
+ if (lead) { const vl = Math.hypot(this.velocity.x, this.velocity.z); if (vl > 1e-6) { out.x += (this.velocity.x / vl) * lead; out.z += (this.velocity.z / vl) * lead; } }
833
+ out.y = this.terrainH(out.x, out.z) + L.tipY;
834
+ return out;
835
+ }
836
+
837
+ // ----------------------------------------------------------------- public API
838
+ play(name, opts) { return this.actionsLayer.play(name, opts || {}); }
839
+ stop(name) { this.actionsLayer.stop(name); }
840
+ setLod(level) { this.lod = clamp(level | 0, 0, 2); return this; }
841
+ dispose() { this.actionsLayer.clear(); }
842
+
843
+ update(dt) {
844
+ if (!(dt > 1e-4) || !Number.isFinite(dt)) dt = 1e-4;
845
+ this.frame++;
846
+ // LOD 2 (crowds): the whole rig runs at half rate (a spider in a crowd is a few pixels tall)
847
+ if (this.lod === 2 && !this.input.follow) {
848
+ this._lodDt = (this._lodDt || 0) + dt;
849
+ if ((this.frame & 1) === 1 && this._lodDt < 1 / 24) return;
850
+ dt = this._lodDt; this._lodDt = 0;
851
+ }
852
+ dt = Math.min(dt, 1 / 20);
853
+ this.drive(dt);
854
+ this.actionsLayer.update(dt);
855
+ const sub = dt > 1 / 70 && this.lod === 0 ? 2 : 1;
856
+ for (let i = 0; i < sub; i++) this.step(dt / sub);
857
+ this.pose(dt);
858
+ this.publish();
859
+ }
860
+
861
+ // steering input -> wanted speed / heading
862
+ drive(dt) {
863
+ const inp = this.input, cfg = this.cfg;
864
+ this.wantSpeed = clamp(inp.speed || 0, 0, cfg.maxSpeed);
865
+ this.wantHeading = inp.heading ?? this.heading;
866
+ if (inp.target && !inp.follow) {
867
+ const dx = inp.target.x - this.pos.x, dz = inp.target.z - this.pos.z;
868
+ const d = Math.hypot(dx, dz);
869
+ const stopR = 0.4 * cfg.bodyLen + 0.05 * this.speed;
870
+ if (d < stopR) { inp.target = null; inp.speed = 0; this.wantSpeed = 0; this.emit('arrive', {}); }
871
+ else {
872
+ const want = Math.atan2(dx, dz);
873
+ inp.heading = want; this.wantHeading = want;
874
+ const ang = Math.abs(angDiff(this.heading, want));
875
+ const brake = Math.sqrt(Math.max(0, 2 * cfg.decel * 0.7 * (d - stopR)));
876
+ this.wantSpeed = Math.min(inp.targetSpeed || cfg.gears.run, cfg.maxSpeed, brake + 0.01 * cfg.sq, ang > 1.0 ? cfg.gears.walk || 0.05 : 1e9);
877
+ }
878
+ }
879
+ if (this.actionsLayer.holdsStill()) {
880
+ if (this.wantSpeed > 0.02 * cfg.maxSpeed && !inp.follow) this.actionsLayer.requestStand();
881
+ this.wantSpeed = 0; this.wantHeading = this.heading; this._turnTo = undefined;
882
+ } else if (!inp.follow && this.wantSpeed < 0.02 * cfg.maxSpeed) {
883
+ // turn on the spot toward a look target outside the body's twist range (or an attack target)
884
+ const lt = inp.look;
885
+ if (lt) {
886
+ const want = Math.atan2(lt.x - this.pos.x, lt.z - this.pos.z);
887
+ const off = angDiff(this.heading, want);
888
+ if (Math.abs(off) > cfg.look.yaw * 1.1) this._turnTo = want;
889
+ else if (this._turnTo !== undefined && Math.abs(angDiff(this.heading, this._turnTo)) < 0.15) this._turnTo = undefined;
890
+ } else if (this._turnTo !== undefined && Math.abs(angDiff(this.heading, this._turnTo)) < 0.08) this._turnTo = undefined;
891
+ if (this._turnTo !== undefined) { this.wantHeading = this._turnTo; inp.heading = this._turnTo; }
892
+ }
893
+ }
894
+
895
+ // ----------------------------------------------------------------- simulation step
896
+ step(dt) {
897
+ this.time += dt;
898
+ this.landT = (this.landT ?? 9) + dt;
899
+ const cfg = this.cfg, inp = this.input, P = this.P, air = this.air;
900
+ const F = this.forward(this.heading, tv());
901
+ if (inp.follow && !air.active) {
902
+ const fv = inp.follow.velocity;
903
+ const vx = fv ? fv.x : 0, vz = fv ? fv.z : 0;
904
+ const sp = Math.hypot(vx, vz);
905
+ this.speed = Math.max(0, this.speedS.step(sp, 12, dt));
906
+ let hT = inp.follow.heading;
907
+ if (hT === null || hT === undefined) hT = sp > 0.1 * cfg.maxSpeed ? Math.atan2(vx, vz) : this.heading;
908
+ const wMax = cfg.turnRate * 1.5;
909
+ const wT = clamp(angDiff(this.heading, hT) * 6, -wMax, wMax);
910
+ this.yawRate += clamp(wT - this.yawRate, -40 * dt, 40 * dt);
911
+ this.heading += this.yawRate * dt;
912
+ this.pos.x = inp.follow.position.x; this.pos.z = inp.follow.position.z;
913
+ this.velocity.set(vx, 0, vz);
914
+ } else {
915
+ let vT = this.wantSpeed * P.speedScale * P.gaitW;
916
+ if (air.active) vT = this.speed;
917
+ const acc = vT > this.speed ? cfg.accel : this.landT < COAST ? 0 : this.landT < 0.2 ? Math.max(cfg.decel, this.landV / 0.1) : cfg.decel;
918
+ this.speed += clamp(vT - this.speed, -acc * dt, acc * dt);
919
+ if (air.active) this.speed = lerp(air.v0, air.vh, Math.min(1, air.t / PUSH));
920
+ const vn = this.speed / cfg.maxSpeed;
921
+ const wMax = air.active ? 0 : Math.min(cfg.turnRate * lerp(0.7, 1, smooth(0, 0.5, vn)), cfg.latAccelMax / Math.max(this.speed, 1e-3)) * (P.gaitW > 0.5 ? 1 : 0);
922
+ const wT = clamp(angDiff(this.heading, this.wantHeading) * 5, -wMax, wMax);
923
+ this.yawRate += clamp(wT - this.yawRate, -30 * dt, 30 * dt);
924
+ this.heading += this.yawRate * dt;
925
+ this.forward(this.heading, F);
926
+ this.velocity.copy(air.active ? air.dir : F).multiplyScalar(this.speed);
927
+ if (this.push.lengthSq() > 1e-12 || this.pushV.lengthSq() > 1e-12) {
928
+ // a shove: the velocity it adds rises over a few frames and decays (no velocity step)
929
+ this.pushV.lerp(this.push, 1 - Math.exp(-dt / 0.05));
930
+ this.velocity.add(this.pushV);
931
+ this.push.multiplyScalar(Math.exp(-dt * 7));
932
+ if (this.push.lengthSq() < 1e-10) this.push.set(0, 0, 0);
933
+ if (this.pushV.lengthSq() < 1e-10 && this.push.lengthSq() === 0) this.pushV.set(0, 0, 0);
934
+ }
935
+ this.pos.x += this.velocity.x * dt; this.pos.z += this.velocity.z * dt;
936
+ }
937
+ if (this.heading > Math.PI * 64 || this.heading < -Math.PI * 64) this.heading = angDiff(0, this.heading);
938
+ this.pos.y = this.terrainH(this.pos.x, this.pos.z);
939
+ this.accelV.copy(this.velocity).sub(this.lastVel).divideScalar(dt);
940
+ this.lastVel.copy(this.velocity);
941
+
942
+ // jump flight
943
+ if (air.active) {
944
+ air.t += dt;
945
+ // push-off: the legs extend over PUSH seconds (body accelerating, claws still planted), then
946
+ // the flight is ballistic
947
+ // (the vertical acceleration never flips sign from one frame to the next: push, a coast
948
+ // frame, free fall, and at touchdown another coast frame before the legs brake the fall)
949
+ const t = air.t, Tp = PUSH, Tc = PUSH + COAST;
950
+ if (t < Tp) this.airY = air.y0 + (air.vy * t * t) / (2 * Tp);
951
+ else if (t < Tc) this.airY = air.y0 + air.vy * Tp * 0.5 + air.vy * (t - Tp);
952
+ else { const u = t - Tc; this.airY = air.y0 + air.vy * (Tp * 0.5 + COAST) + air.vy * u - 0.5 * G_ACC * u * u; }
953
+ if (!air.legsOff && t >= Tp * 0.0) {
954
+ air.legsOff = true;
955
+ for (const leg of this.legs) if (leg.state === 'stance') { leg.carry0 = 0; leg.state = 'air'; this.fkTip(leg, leg.rawAz, leg.rawEl, leg.airFrom); leg.airRoot.copy(this.root); leg.airT = 0; leg.liftAz.set(leg.rawAz); leg.liftEl.set(leg.rawEl); }
956
+ }
957
+ // the legs reach the ground well before the body comes down: it is braked on them
958
+ const yLand = this.pos.y + air.h0 + 4.2 * cfg.rootH;
959
+ if (air.t > PUSH + COAST + 0.02 && air.vy - G_ACC * (air.t - PUSH - COAST) < 0 && this.airY <= yLand) this.touchDown();
960
+ }
961
+
962
+ // gait
963
+ const vPlanar = Math.hypot(this.velocity.x, this.velocity.z);
964
+ const vEff = Math.max(vPlanar, Math.abs(this.yawRate) * cfg.turnR * 0.8);
965
+ const g = this.gaitAt(vEff / cfg.sq);
966
+ const moving = vEff > 0.012 * cfg.maxSpeed && P.gaitW > 0.5 && !air.active;
967
+ if (moving) this.phase = wrap01(this.phase + g.f * dt);
968
+ const st = this.state.stats;
969
+ st.gait = moving ? g.name : 'stand'; st.freq = moving ? g.f : 0; st.duty = g.D;
970
+ this._moving = moving;
971
+ let swinging = 0, swingSet = -1;
972
+ for (const leg of this.legs) if (leg.state === 'swing') { swinging++; swingSet = leg.def.set; }
973
+ const tStance = g.D / g.f;
974
+ for (let li = 0; li < 8; li++) {
975
+ const leg = this.legs[li], L = leg.def;
976
+ const p = wrap01(this.phase - g.off[li]);
977
+ if (p < leg.prevPhase - 0.5) leg.windowUsed = false;
978
+ if (air.active) {
979
+ leg.prevPhase = p;
980
+ continue;
981
+ }
982
+ if (leg.state === 'stance') {
983
+ leg.stanceTime += dt;
984
+ if (moving) {
985
+ // lift-off: at the end of the stance window, or early if the claw trails too far
986
+ const nb = this.neutral(leg, 0, 0, 0, tv());
987
+ const fx = leg.plant.x - nb.x, fz = leg.plant.z - nb.z;
988
+ const trail = -(fx * this.velocity.x + fz * this.velocity.z) / Math.max(vPlanar, 1e-6);
989
+ const off = Math.hypot(fx, fz);
990
+ const rr = this.coxaDist(leg, leg.plant);
991
+ const caz = Math.abs(this.coxaAz(leg, leg.plant));
992
+ // early lift-off when the claw drifts out of comfortable range (after a minimum stance), at
993
+ // once when it is about to leave the reachable workspace
994
+ const soft = (vPlanar > 1e-4 && trail > 0.32 * L.total) || off > 0.42 * L.total || rr < L.rMin * 0.92 || rr > L.rMax || caz > L.azMax * 1.15;
995
+ const hard = rr < L.rMin * 0.8 || rr > L.rMax * 1.06 || caz > L.azMax * 1.4;
996
+ const drag = (leg.reachErr || 0) > 0.004 * L.total; // the claw can no longer be held: step at once
997
+ // (while shoved, early lifts are capped at four legs so it scrambles on a tetrapod)
998
+ // (just after a landing the claws are wherever they came down: they re-step one tetrapod at a
999
+ // time instead of all together)
1000
+ const settle = this.landT < 0.8 && !((swinging === 0 || swingSet === L.set) && swinging < 4);
1001
+ const over = drag || hard || (!settle && soft && leg.stanceTime > 0.4 * tStance && (swinging < 4 || this.pushV.lengthSq() < 1e-8));
1002
+ if (((p >= g.D && leg.stanceTime > 0.3 * tStance) || over) && !leg.windowUsed) {
1003
+ leg.windowUsed = true;
1004
+ const remain = Math.max((1 - p) / g.f, 0.45 * (1 - g.D) / g.f);
1005
+ this.startSwing(leg, remain, g.lift, 'gait');
1006
+ swinging++; swingSet = L.set;
1007
+ }
1008
+ } else if (P.gaitW > 0.5 || P.dead < 0.5) {
1009
+ // standing: re-step feet that are far from their neutral foothold, one tetrapod at a time
1010
+ const nb = this.neutral(leg, 0, 0, 0, tv());
1011
+ const err = Math.hypot(nb.x - leg.plant.x, nb.z - leg.plant.z);
1012
+ const thr = (P.gaitW > 0.5 ? 0.075 : 0.05) * L.total;
1013
+ if (err > thr && leg.stanceTime > 0.12 && this.wantSpeed < 0.02 * cfg.maxSpeed && P.curl < 0.05 && P.dead < 0.02 && P.drawIn < 0.02 && P.raise[li] + P.hold[li] < 0.05 && (swinging === 0 || swingSet === L.set) && swinging < 4) {
1014
+ this.startSwing(leg, (0.12 + 0.5 * err / L.total) * cfg.sq, 0.06 * L.total * (P.gaitW > 0.5 ? 1 : 0.6), 'step');
1015
+ swinging++; swingSet = L.set;
1016
+ }
1017
+ }
1018
+ } else if (leg.state === 'swing') {
1019
+ leg.swingT += dt / leg.swingDur;
1020
+ const remain = Math.max(0, (1 - leg.swingT) * leg.swingDur);
1021
+ if (leg.mode === 'gait' || leg.mode === 'step') {
1022
+ const half = moving ? Math.min(vPlanar * tStance * 0.5, 0.3 * L.total) : 0;
1023
+ const yawLead = moving ? this.yawRate * tStance * 0.5 : 0;
1024
+ // the landing spot is predicted once, at lift-off (a swing lasts a few frames; a target that
1025
+ // moved under a fast claw would make it jerk); the next step corrects any change of course
1026
+ if (!leg.targetSet) {
1027
+ leg.targetSet = true;
1028
+ this.neutral(leg, remain, half, yawLead, leg.landBase);
1029
+ }
1030
+ }
1031
+ // the landing spot stays inside the workspace of where the body will actually be (the clamp
1032
+ // moves continuously with the body, so the claw is never yanked)
1033
+ if (leg.mode !== 'land') {
1034
+ leg.land.copy(leg.landBase);
1035
+ this.clampWorkspace(leg, leg.land, remain);
1036
+ this._swingClear(leg);
1037
+ }
1038
+ if (leg.swingT >= 1) {
1039
+ leg.state = 'stance'; leg.windowUsed = leg.mode === 'gait' ? leg.windowUsed : false;
1040
+ leg.plant.copy(leg.land); leg.stanceTime = 0;
1041
+ if (leg.mode === 'gait') leg.windowUsed = false;
1042
+ if (leg.mode === 'gait' && P.gaitW > 0.5) this.emit('footstep', { foot: L.key, position: leg.plant.clone(), speed: this.speed, strength: clamp(0.15 + this.speed / cfg.maxSpeed * 0.5, 0, 1) });
1043
+ }
1044
+ }
1045
+ leg.prevPhase = p;
1046
+ }
1047
+ }
1048
+
1049
+ // horizontal distance of a world point from the leg's coxa base (current body, heading frame)
1050
+ coxaDist(leg, w) {
1051
+ const L = leg.def, h = this.heading, sh = Math.sin(h), ch = Math.cos(h);
1052
+ const dx = w.x - this.pos.x, dz = w.z - this.pos.z;
1053
+ const lx = dx * ch - dz * sh, lz = dx * sh + dz * ch;
1054
+ return Math.hypot(lx - L.P0[0].x, lz - L.P0[0].z);
1055
+ }
1056
+ // azimuth of a world point about the coxa base relative to the leg's neutral direction
1057
+ coxaAz(leg, w) {
1058
+ const L = leg.def, h = this.heading, sh = Math.sin(h), ch = Math.cos(h);
1059
+ const dx = w.x - this.pos.x, dz = w.z - this.pos.z;
1060
+ const lx = dx * ch - dz * sh - L.P0[0].x, lz = dx * sh + dz * ch - L.P0[0].z;
1061
+ return angDiff(L.azN, Math.atan2(lx * L.side, lz));
1062
+ }
1063
+ // keep a landing target inside the leg's workspace (radially about the coxa base)
1064
+ clampWorkspace(leg, w, tAhead = 0) {
1065
+ // relative to where the body will be at touchdown
1066
+ const L = leg.def, h = this.heading + clamp(this.yawRate * tAhead, -0.45, 0.45), sh = Math.sin(h), ch = Math.cos(h);
1067
+ const px = this.pos.x + this.velocity.x * tAhead, pz = this.pos.z + this.velocity.z * tAhead;
1068
+ const dx = w.x - px, dz = w.z - pz;
1069
+ let lx = dx * ch - dz * sh - L.P0[0].x, lz = dx * sh + dz * ch - L.P0[0].z;
1070
+ const r = Math.hypot(lx, lz) || 1e-9;
1071
+ const rc = clamp(r, L.rMin * 1.04, L.rMax * 0.96);
1072
+ // and within +-L.azMax of the neutral direction
1073
+ const a = Math.atan2(lx * L.side, lz), da = angDiff(L.azN, a);
1074
+ const ac = L.azN + clamp(da, -L.azMax, L.azMax);
1075
+ if (rc === r && ac === L.azN + da) return w;
1076
+ lx = Math.sin(ac) * rc * L.side + L.P0[0].x; lz = Math.cos(ac) * rc + L.P0[0].z;
1077
+ w.x = px + ch * lx + sh * lz; w.z = pz - sh * lx + ch * lz;
1078
+ w.y = this.terrainH(w.x, w.z) + L.tipY;
1079
+ return w;
1080
+ }
1081
+
1082
+ // swing clearance: sample the terrain along the path so the claw steps over bumps
1083
+ _swingClear(leg) {
1084
+ let m = -1e9;
1085
+ for (let k = 1; k <= 3; k++) {
1086
+ const u = k / 4;
1087
+ const x = lerp(leg.lift.x, leg.land.x, u), z = lerp(leg.lift.z, leg.land.z, u);
1088
+ const base = lerp(leg.lift.y, leg.land.y, u);
1089
+ m = Math.max(m, this.terrainH(x, z) + leg.def.tipY - base);
1090
+ }
1091
+ leg.hMax = Math.max(0, m);
1092
+ }
1093
+
1094
+ startSwing(leg, dur, lift, mode) {
1095
+ leg.lift.copy(leg.plant);
1096
+ leg.land.copy(leg.plant); leg.landBase.copy(leg.plant);
1097
+ leg.state = 'swing'; leg.mode = mode;
1098
+ leg.swingT = 0; leg.swingDur = Math.max(0.05, dur);
1099
+ leg.liftH = lift;
1100
+ leg.swingInit = true;
1101
+ leg.targetSet = mode === 'twitch';
1102
+ this._swingClear(leg);
1103
+ }
1104
+
1105
+ // an idle twitch: lift the claw a little and set it down again (almost) in place
1106
+ twitchLeg(i) {
1107
+ const leg = this.legs[i];
1108
+ if (leg.state !== 'stance' || this._moving || this.P.gaitW < 0.5) return;
1109
+ this.startSwing(leg, 0.22 * this.cfg.sq, 0.05 * leg.def.total, 'twitch');
1110
+ const nb = this.neutral(leg, 0, 0, 0, tv());
1111
+ leg.land.lerp(nb, 0.5); leg.land.y = this.terrainH(leg.land.x, leg.land.z) + leg.def.tipY;
1112
+ leg.landBase.copy(leg.land);
1113
+ this._swingClear(leg);
1114
+ }
1115
+
1116
+ takeOff(vy, vh) {
1117
+ const a = this.air, cfg = this.cfg;
1118
+ a.active = true; a.t = 0; a.vy = vy; a.vh = vh; a.v0 = this.speed;
1119
+ this.forward(this.heading, a.dir);
1120
+ a.y0 = this.root.y;
1121
+ a.h0 = this.root.y - this.pos.y;
1122
+ a.T = (2 * vy) / G_ACC;
1123
+ this.airY = a.y0;
1124
+ a.legsOff = false;
1125
+ // legs already in the air (mid-swing) follow the body at once; planted ones push off first
1126
+ for (const leg of this.legs) if (leg.state !== 'stance' || leg.def.i <= 2) { leg.carry0 = leg.state === 'stance' ? 0 : 1; leg.state = 'air'; this.fkTip(leg, leg.rawAz, leg.rawEl, leg.airFrom); leg.airRoot.copy(this.root); leg.airT = 0; leg.liftAz.set(leg.rawAz); leg.liftEl.set(leg.rawEl); }
1127
+ this.state.grounded = false;
1128
+ this.emit('takeoff', {});
1129
+ void cfg;
1130
+ }
1131
+ touchDown() {
1132
+ const a = this.air;
1133
+ if (!a.active) return;
1134
+ a.active = false;
1135
+ this.state.grounded = true;
1136
+ // every claw comes down near its neutral foothold, the legs reaching out from the tuck
1137
+ // (clamped into the workspace around where the body stops: a coast frame, then the brake)
1138
+ const tStop = COAST + Math.min(this.speed / (2 * this.cfg.decel), 0.05);
1139
+ for (const leg of this.legs) {
1140
+ // (the legs reach out to spread footholds as they land: a landing on legs tucked under the
1141
+ // body would leave it standing on stilts)
1142
+ const nb = this.neutral(leg, tStop, 0, 0, tv());
1143
+ leg.plant.set(lerp(leg.tip.x, nb.x, 0.85), 0, lerp(leg.tip.z, nb.z, 0.85));
1144
+ leg.plant.y = this.terrainH(leg.plant.x, leg.plant.z) + leg.def.tipY;
1145
+ this.clampWorkspace(leg, leg.plant, tStop);
1146
+ leg.lift.copy(leg.tip);
1147
+ leg.land.copy(leg.plant);
1148
+ leg.state = 'swing'; leg.mode = 'land'; leg.swingInit = true; leg.swingT = 0; leg.swingDur = 0.13 * this.cfg.sq; leg.liftH = 0; leg.hMax = 0;
1149
+ }
1150
+ this.landV = this.speed;
1151
+ // the body keeps its falling speed into the landing spring (legs absorb the impact)
1152
+ this.hS.x = this.airY; this.hS.v = Math.min(0, a.vy - G_ACC * Math.max(0, a.t - PUSH - COAST));
1153
+ {
1154
+ const v = -this.hS.v, rest = this.pos.y + this.cfg.rootH * 0.78, d = Math.max(1e-4, this.airY - v * COAST - rest);
1155
+ this.brakeA = (v * v) / (2 * d);
1156
+ this.landFrames = 0;
1157
+ }
1158
+ this.actionsLayer.onLand();
1159
+ this.landT = 0;
1160
+ this.emit('land', {});
1161
+ }
1162
+
1163
+ _fitTerrain() {
1164
+ const cfg = this.cfg, W = cfg.fitW, h = this.heading;
1165
+ let a = 0, b = 0, c = 0;
1166
+ const tmp = tv();
1167
+ for (let i = 0; i < 8; i++) {
1168
+ const L = cfg.legs[i];
1169
+ this.headingToWorld(L.neutral.x, L.neutral.z, h, this.pos.x, this.pos.z, tmp);
1170
+ const y = this.terrainH(tmp.x, tmp.z);
1171
+ a += W[0][i] * y; b += W[1][i] * y; c += W[2][i] * y;
1172
+ }
1173
+ this._fit = this._fit || { h: 0, pitch: 0, roll: 0 };
1174
+ this._fit.h = a; this._fit.pitch = Math.atan(c) * 0.85; this._fit.roll = Math.atan(b) * 0.85;
1175
+ return this._fit;
1176
+ }
1177
+
1178
+ // ----------------------------------------------------------------- pose
1179
+ pose(dt) {
1180
+ const cfg = this.cfg, P = this.P, g = this.gait, t = this.time, air = this.air;
1181
+ const fit = this._fitTerrain();
1182
+ const vn = this.speed / cfg.maxSpeed;
1183
+ // look: body twist and rearing toward the look target
1184
+ const look = this.input.look || P.look;
1185
+ let lyT = 0, lpT = 0;
1186
+ if (look && P.lookW > 0.01 && P.dead < 0.5) {
1187
+ const dx = look.x - this.pos.x, dz = look.z - this.pos.z;
1188
+ const rel = angDiff(this.heading, Math.atan2(dx, dz));
1189
+ lyT = clamp(rel, -cfg.look.yaw, cfg.look.yaw) * P.lookW * (1 - smooth(0.2, 0.6, vn) * 0.8);
1190
+ const dy = look.y - (this.pos.y + cfg.rootH * 1.5);
1191
+ lpT = clamp(Math.atan2(dy, Math.hypot(dx, dz) + 1e-6), 0, cfg.look.pitch) * P.lookW * (1 - smooth(0.1, 0.4, vn));
1192
+ this.lookTarget.copy(look); this.state.lookTarget = this.lookTarget;
1193
+ } else this.state.lookTarget = null;
1194
+ const lookYaw = this.lookYawS.step(lyT, 7, dt), lookPitch = this.lookPitchS.step(lpT, 6, dt);
1195
+ // raise the front legs a little when rearing to look up
1196
+ if (lookPitch > 0.01) for (let i = 0; i < 8; i++) if (this.legs[i].def.i === 1) P.raise[i] = Math.max(P.raise[i], 0.8 * lookPitch / cfg.look.pitch);
1197
+ // body height, pitch and roll from the terrain plane, the posture and the gait
1198
+ const moving = this._moving ? smooth(0, 0.15, vn) : 0;
1199
+ const bob = g.bob * moving * Math.cos(TAU * 2 * this.phase);
1200
+ const sway = g.sway * moving * Math.sin(TAU * this.phase);
1201
+ const dropK = 1 - 0.55 * P.crouch;
1202
+ const hT = fit.h + cfg.rootH * cfg.stance.height * dropK - P.drop + bob;
1203
+ // a stiffer spring for a moment after landing (the legs absorb the fall)
1204
+ let h;
1205
+ if (this.brakeA > 0 && this.hS.v < 0) {
1206
+ // landing: a coast frame, then constant braking on the legs down to the stance height
1207
+ // frame 0: the landing height itself; frame 1: coasting; then braking
1208
+ const lf = this.landFrames++;
1209
+ if (lf >= 2) this.hS.v = Math.min(0, this.hS.v + this.brakeA * dt);
1210
+ if (lf >= 1) this.hS.x += this.hS.v * dt;
1211
+ h = this.hS.x;
1212
+ } else { this.brakeA = 0; h = this.hS.step(hT, 18, dt); }
1213
+ if (air.active) { h = this.airY; this.hS.x = h; this.hS.v = 0; }
1214
+ const pitch = this.pitchS.step(fit.pitch * (1 - P.dead) + P.pitch + lookPitch, 14, dt);
1215
+ const roll = this.rollS.step(fit.roll * (1 - P.dead), 14, dt);
1216
+ // body rotation: heading (+ look twist + sway), pitch (nose up), roll, then the death flip about
1217
+ // the long axis
1218
+ this._e.set(-pitch, this.heading + lookYaw + sway + P.yaw, roll + P.roll, 'YXZ');
1219
+ this.qB.setFromEuler(this._e);
1220
+ if (P.flip > 1e-4) this.qB.multiply(this._q.setFromAxisAngle(Z_AXIS, P.flip * P.flipAngle * P.flipSide));
1221
+ // lunge / side offsets in the heading frame
1222
+ const F = this.forward(this.heading, tv()), Lf = this.left(this.heading, tv());
1223
+ this.root.set(this.pos.x, h, this.pos.z).addScaledVector(F, P.fwd).addScaledVector(Lf, P.side);
1224
+ this.qBinv.copy(this.qB).invert();
1225
+ this.upB.set(0, 1, 0).applyQuaternion(this.qBinv); // world up in the body frame
1226
+
1227
+ // abdomen: hangs from the pedicel on a spring; lags in accelerations and turns; rests on the
1228
+ // ground in low postures; breathes
1229
+ const aLocal = this._v.copy(this.accelV).applyQuaternion(this.qBinv);
1230
+ const A = cfg.abd;
1231
+ const pT = -A.droop * (1 - moving) + P.abdPitch - clamp(aLocal.z * A.lag * 0.02, -0.2, 0.2) + clamp(aLocal.y * 0.002, -0.1, 0.1);
1232
+ const yT = clamp(-aLocal.x * A.lag * 0.02, -0.25, 0.25) - this.yawRate * 0.04 * A.lag * 10;
1233
+ let ap = this.abdPitchS.step(pT, A.stiffness, dt);
1234
+ const ay = this.abdYawS.step(clamp(yT, -0.3, 0.3), A.stiffness * 0.8, dt);
1235
+ const tmp = tv();
1236
+ const pb = cfg.ax.pedB, lo = cfg.abdLow;
1237
+ const abdLowAt = (a) => {
1238
+ // world position of the abdomen's lowest bind point for abdomen pitch a (no yaw)
1239
+ const c = Math.cos(a), sn = Math.sin(a);
1240
+ return this.localToWorld(pb.x + lo.x, pb.y + lo.y * c - lo.z * sn, pb.z + lo.y * sn + lo.z * c, tmp);
1241
+ };
1242
+ // (faded out continuously in death, when the body itself lies on the ground)
1243
+ const wAl = 1 - smooth(0.25, 0.6, P.dead);
1244
+ if (wAl > 0) {
1245
+ // keep the abdomen off the ground (or rest it on the ground in low postures)
1246
+ const ap0 = ap;
1247
+ abdLowAt(ap);
1248
+ const gap = tmp.y - this.terrainH(tmp.x, tmp.z) - 0.00025 * cfg.s;
1249
+ const L = Math.hypot(lo.y, lo.z);
1250
+ if (P.abdDown > 0.01 && gap > 0) ap -= P.abdDown * clamp(gap / L, 0, 0.35);
1251
+ if (gap < 0) ap += Math.min(0.6, (-gap / L) * 1.1);
1252
+ // every other extreme point too (rearing, the rear end is the lowest)
1253
+ const c = Math.cos(ap), sn = Math.sin(ap);
1254
+ let fix = 0;
1255
+ for (const p of cfg.supAbd) {
1256
+ if (p === lo || p.z > 0) continue;
1257
+ this.localToWorld(pb.x + p.x, pb.y + p.y * c - p.z * sn, pb.z + p.y * sn + p.z * c, tmp);
1258
+ const g = tmp.y - this.terrainH(tmp.x, tmp.z) - 0.00025 * cfg.s;
1259
+ if (g < 0) fix = Math.max(fix, Math.min(0.6, (-g / Math.max(1e-6, Math.hypot(p.y, p.z))) * 1.1));
1260
+ }
1261
+ ap += fix;
1262
+ ap = ap0 + (ap - ap0) * wAl;
1263
+ }
1264
+ this._e.set(ap, ay, 0, 'YXZ');
1265
+ this.qAbd.setFromEuler(this._e);
1266
+ this.qPed.identity().slerp(this.qAbd, 0.5);
1267
+ // body support: nothing of the body goes into the ground (bumps under the sternum or abdomen,
1268
+ // a dead spider lying on its back or side); a spring keeps it continuous
1269
+ let lift = 0;
1270
+ for (const p of cfg.supPro) {
1271
+ this.localToWorld(p.x, p.y, p.z, tmp);
1272
+ const pen = this.terrainH(tmp.x, tmp.z) + 0.0001 * cfg.s - tmp.y;
1273
+ if (pen > lift) lift = pen;
1274
+ }
1275
+ {
1276
+ const qa = this._q2.copy(this.qB).multiply(this.qAbd);
1277
+ const ped = this._w.subVectors(pb, cfg.ax.pedA).applyQuaternion(this._q.copy(this.qB).multiply(this.qPed));
1278
+ const pA = this.localToWorld(cfg.ax.pedA.x, cfg.ax.pedA.y, cfg.ax.pedA.z, tv()).add(ped);
1279
+ for (const p of cfg.supAbd) {
1280
+ tmp.copy(p).applyQuaternion(qa).add(pA);
1281
+ const pen = this.terrainH(tmp.x, tmp.z) + 0.0001 * cfg.s - tmp.y;
1282
+ if (pen > lift) lift = pen;
1283
+ }
1284
+ }
1285
+ // the leg roots (trochanters, femur bases) are as rigid with the body as the carapace: a deep crouch
1286
+ // or a steep rear must not push them into the ground either
1287
+ for (const leg of this.legs) for (let k = 1; k <= 2; k++) {
1288
+ const q = leg.jp[k];
1289
+ const pen = this.terrainH(q.x, q.z) + 2.2 * leg.def.tipY - (q.y - this._lift);
1290
+ if (pen > lift) lift = pen;
1291
+ }
1292
+ const wLeg = P.dead * Math.max(smooth(0, 0.08, P.flip), P.curl2); // dying, it rests on its curling legs
1293
+ if (wLeg > 1e-3) {
1294
+ // rolling over, a dead spider rests on its curled legs as well (folded tight before it rolls);
1295
+ // the joints were placed last frame including last frame's lift, so compare without it
1296
+ lift = Math.max(lift, this._legSupport(wLeg, this._lift));
1297
+ }
1298
+ const sup = this.supportS.step(lift, 30, dt);
1299
+ // smooth maximum of the spring and the raw depth (never far below the ground, no kinks)
1300
+ this._lift = -smin(-sup, -lift, 0.0003 * cfg.s);
1301
+ this.root.y += this._lift;
1302
+ this.breath += dt * TAU * cfg.breath.rate * P.breathRate;
1303
+ const br = Math.sin(this.breath) * cfg.breath.amp * P.breathAmp;
1304
+
1305
+ this.writeAxial(br);
1306
+ this.solveLegs(dt);
1307
+ if (wLeg > 1e-3) {
1308
+ // the legs of a dying spider move fast (curling, rolling over): the support above used last
1309
+ // frame's joints, so check this frame's too and lift the whole body clear if any went under
1310
+ const extra = this._legSupport(wLeg, 0);
1311
+ if (extra > 1e-6) {
1312
+ this.root.y += extra; this._lift += extra;
1313
+ this.writeAxial(br);
1314
+ this.solveLegs(0);
1315
+ }
1316
+ }
1317
+ this.solvePalps(dt);
1318
+ this.solveChelicerae(dt);
1319
+ // head pose
1320
+ const hb = this.sk.bones[cfg.bone.head];
1321
+ if (hb) {
1322
+ this.localToWorld(this._eyeLocal.x, this._eyeLocal.y, this._eyeLocal.z, this.headPose.position);
1323
+ this.headPose.quaternion.copy(this.qB);
1324
+ }
1325
+ }
1326
+
1327
+ // how far the body must rise so no leg joint is in the ground (joints nearer the knee carry a wider
1328
+ // margin: the knuckles are thick): a soft maximum over the joints, so the support does not jerk as
1329
+ // the lowest joint changes from one leg to another while it rolls
1330
+ _legSupport(w, lifted) {
1331
+ const k = 0.00006 * this.cfg.s;
1332
+ let mx = -Infinity;
1333
+ const pens = this._pens || (this._pens = new Float64Array(48));
1334
+ let n = 0;
1335
+ for (const leg of this.legs) for (let j = 2; j < 8; j++) {
1336
+ const q = leg.jp[j];
1337
+ const p = this.terrainH(q.x, q.z) + (j === 7 ? 1.6 : j === 2 ? 2.2 : 5) * leg.def.tipY - (q.y - lifted);
1338
+ pens[n++] = p; if (p > mx) mx = p;
1339
+ }
1340
+ if (mx < -8 * k) return 0;
1341
+ let sum = 0;
1342
+ for (let i = 0; i < n; i++) sum += Math.exp((pens[i] - mx) / k);
1343
+ // softplus of the soft maximum: 0 when every joint is clear, the depth when one is under
1344
+ const sm = mx + k * Math.log(sum);
1345
+ return w * (sm > 8 * k ? sm : k * Math.log1p(Math.exp(sm / k)));
1346
+ }
1347
+
1348
+ setBone(i, pos, q, scale) {
1349
+ if (i < 0) return;
1350
+ const b = this.sk.bones[i];
1351
+ b.matrixWorld.compose(pos, this._q3.copy(q).multiply(this.cfg.bindQ[i]), scale || ONE);
1352
+ }
1353
+
1354
+ writeAxial(br) {
1355
+ const cfg = this.cfg, ax = cfg.ax, B = cfg.bone;
1356
+ const p = tv();
1357
+ this.localToWorld(ax.pedA.x, ax.pedA.y, ax.pedA.z, p);
1358
+ this.setBone(B.prosoma, p, this.qB);
1359
+ this.localToWorld(ax.ceph.x, ax.ceph.y, ax.ceph.z, p);
1360
+ this.setBone(B.head, p, this.qB);
1361
+ // pedicel and abdomen hang from pedA
1362
+ const qP = this._q2.copy(this.qB).multiply(this.qPed);
1363
+ this.localToWorld(ax.pedA.x, ax.pedA.y, ax.pedA.z, p);
1364
+ this.setBone(B.pedicel, p, qP);
1365
+ const pedVec = tv().subVectors(ax.pedB, ax.pedA).applyQuaternion(qP);
1366
+ const abdHead = tv().copy(p).add(pedVec);
1367
+ const qA = tv(); void qA;
1368
+ const qAw = this._q2.copy(this.qB).multiply(this.qAbd);
1369
+ this._scale.set(1 + br, 1 + br * 0.4, 1 + br);
1370
+ this.setBone(B.abdomen, abdHead, qAw, this._scale);
1371
+ if (B.spinnerets >= 0) {
1372
+ const sv = tv().subVectors(ax.abdEnd, ax.pedB).multiplyScalar(1 + br * 0.4).applyQuaternion(qAw);
1373
+ const sp = tv().copy(abdHead).add(sv);
1374
+ // spinnerets twitch a little
1375
+ this._e.set(0.05 * Math.sin(this.time * 1.7) * this.P.twitch, 0.06 * Math.sin(this.time * 0.9 + 1) * this.P.twitch, 0, 'YXZ');
1376
+ this.setBone(B.spinnerets, sp, this._q.copy(qAw).multiply(this._q3.setFromEuler(this._e)));
1377
+ }
1378
+ }
1379
+
1380
+ // ---------------------------------------------------------------- legs
1381
+ solveLegs(dt) {
1382
+ const P = this.P, cfg = this.cfg, air = this.air;
1383
+ const tipL = tv();
1384
+ for (let li = 0; li < 8; li++) {
1385
+ const leg = this.legs[li], L = leg.def;
1386
+ // world claw target
1387
+ const T = tv();
1388
+ let heel = 0;
1389
+ if (leg.state === 'stance') {
1390
+ this.postureTarget(leg, T.copy(leg.plant));
1391
+ if (this._moving) heel = smooth(0.5, 1.0, leg.stanceTime * this.gait.f / Math.max(this.gait.D, 0.05));
1392
+ } else if (leg.state === 'swing') {
1393
+ // swing in joint space: from the lift-off angles to the angles that reach the (moving) landing
1394
+ // target, with short speed ramps, plus a femur lift that clears the terrain on the way. Each
1395
+ // joint then sweeps a smooth arc instead of folding and unfolding to follow a straight path.
1396
+ const u = clamp(leg.swingT, 0, 1);
1397
+ const s = progress(u, TUNE.ramp);
1398
+ if (leg.swingInit) { leg.liftAz.set(leg.rawAz); leg.liftEl.set(leg.rawEl); leg.liftHeel = leg.heel; leg.swingInit = false; }
1399
+ // the landing pose is solved in the body frame predicted for the moment of touchdown (in the
1400
+ // current frame a claw landing ahead would look further away than it will be)
1401
+ const rem = Math.max(0, (1 - u) * leg.swingDur);
1402
+ const qp = this._q2.setFromAxisAngle(Y_AXIS, clamp(this.yawRate * rem, -0.45, 0.45)).multiply(this.qB).invert();
1403
+ this.postureTarget(leg, tipL.copy(leg.land));
1404
+ tipL.x -= this.root.x + this.velocity.x * rem; tipL.y -= this.root.y; tipL.z -= this.root.z + this.velocity.z * rem;
1405
+ tipL.applyQuaternion(qp);
1406
+ const az = leg.az, el = leg.el;
1407
+ let reach = Math.max(0.3 * L.total, Math.hypot(tipL.x - L.P0[2].x, tipL.z - L.P0[2].z));
1408
+ if (TUNE.path === 'polar') {
1409
+ // claw path in polar coordinates about the coxa base (radius and azimuth interpolate, so a
1410
+ // radial leg neither overshoots nor cuts across), height with the clearance hump
1411
+ const B = L.P0[0], sd = L.side;
1412
+ const lL = tv().copy(leg.lift).sub(this.root).applyQuaternion(this.qBinv);
1413
+ const r0 = Math.hypot(lL.x - B.x, lL.z - B.z), r1 = Math.hypot(tipL.x - B.x, tipL.z - B.z);
1414
+ const a0 = Math.atan2((lL.x - B.x) * sd, lL.z - B.z), a1 = Math.atan2((tipL.x - B.x) * sd, tipL.z - B.z);
1415
+ const r = lerp(r0, r1, s), a = a0 + angDiff(a0, a1) * s;
1416
+ const y = lerp(lL.y, tipL.y, s) + (leg.liftH + leg.hMax) * hump(u);
1417
+ tipL.set(B.x + Math.sin(a) * r * sd, y, B.z + Math.cos(a) * r);
1418
+ this.solveLeg(leg, tipL, leg.liftHeel * (1 - s) * cfg.heelLift);
1419
+ } else {
1420
+ this.solveLeg(leg, tipL, 0);
1421
+ if (TUNE.debug) leg._landEl = Array.from(el);
1422
+ for (let k = 0; k < 7; k++) { az[k] = leg.liftAz[k] + angDiff(leg.liftAz[k], az[k]) * s; el[k] = leg.liftEl[k] + angDiff(leg.liftEl[k], el[k]) * s; }
1423
+ const b = ((leg.liftH + leg.hMax) * hump(u)) / reach;
1424
+ el[2] += b * TUNE.bf; el[3] -= b * TUNE.bp; el[5] -= b * TUNE.bm;
1425
+ }
1426
+ this._unwrapIK(leg, li);
1427
+ leg.rawAz.set(az); leg.rawEl.set(el);
1428
+ T.lerpVectors(leg.lift, leg.land, s);
1429
+ const wPose = this.blendLegPose(leg, li);
1430
+ this.writeLeg(leg);
1431
+ if (wPose > 1e-3 && (P.dead < 0.5 || P.flip < 0.3)) this.groundGuard(leg, (P.dead < 0.5 ? 1 : 1 - smooth(0.02, 0.3, P.flip)) * smooth(0, 0.05, wPose));
1432
+ // a joint-space swing can dip toward the ground between lift-off and touchdown: lift the femur
1433
+ // by just enough to keep the claw and the tarsus base clear (continuous in the depth)
1434
+ {
1435
+ // (faded in and out at the ends of the swing, where the claw is on the ground anyway)
1436
+ const r = smooth(0, 0.05, u) * (1 - smooth(0.95, 1, u));
1437
+ // (the tibia end first: the femur lifts it; then the claw and tarsus: the metatarsus)
1438
+ const q5 = leg.jp[5], p5 = this.terrainH(q5.x, q5.z) + L.tipY * 2 - q5.y;
1439
+ if (p5 > 0 && r > 0) { const de = r * Math.min(0.6, (p5 * 1.2) / (L.len[2] + L.Lpt)); el[2] += de; leg.rawEl[2] += de; this.writeLeg(leg); }
1440
+ let pen = 0;
1441
+ for (let k = 6; k <= 7; k++) { const q = leg.jp[k]; pen = Math.max(pen, this.terrainH(q.x, q.z) + L.tipY * (k === 7 ? 1 : 1.5) - q.y); }
1442
+ if (pen > 0 && r > 0) { const de = r * Math.min(0.8, (pen * 1.3) / (L.len[5] * Math.max(0.3, Math.cos(el[5])))); el[5] += de; leg.rawEl[5] += de; this.writeLeg(leg); }
1443
+ }
1444
+ leg.ikEl.set(leg.el); leg.ikAz.set(leg.az);
1445
+ if (this.debug.targets[li]) this.debug.targets[li].copy(T);
1446
+ continue;
1447
+ } else {
1448
+ // in the air: the claw leaves the ground picking up the body's motion gradually, then settles
1449
+ // on its place relative to the body (the action pose does the rest)
1450
+ const nb = tv().copy(L.P0[7]).applyQuaternion(this.qB).add(this.root);
1451
+ leg.airT = Math.min(1, leg.airT + dt / 0.25);
1452
+ const carry = Math.min(1, leg.carry0 + leg.airT * 2.5);
1453
+ T.copy(leg.airFrom).addScaledVector(tv().subVectors(this.root, leg.airRoot), carry);
1454
+ T.lerp(nb, smooth(0.2, 1, leg.airT));
1455
+ }
1456
+ // the heel eases (a leg that has stood for a while must not snap to a lifted heel)
1457
+ leg.heel += (heel - leg.heel) * (1 - Math.exp(-dt / 0.045));
1458
+ heel = leg.heel;
1459
+ // body frame
1460
+ tipL.copy(T).sub(this.root).applyQuaternion(this.qBinv);
1461
+ this.solveLeg(leg, tipL, heel * cfg.heelLift);
1462
+ if (leg.state === 'air' && leg.airT < 0.6) {
1463
+ // leaving the ground (possibly mid-swing, whose joint-space pose is not the IK one): the
1464
+ // joints ease from their take-off angles into the solved ones
1465
+ const w = smooth(0, 0.6, leg.airT), az = leg.az, el = leg.el;
1466
+ for (let k = 0; k < 7; k++) { az[k] = leg.liftAz[k] + angDiff(leg.liftAz[k], az[k]) * w; el[k] = leg.liftEl[k] + (el[k] - leg.liftEl[k]) * w; }
1467
+ }
1468
+ this._unwrapIK(leg, li);
1469
+ leg.rawAz.set(leg.az); leg.rawEl.set(leg.el);
1470
+ // action poses blended in joint-angle space
1471
+ const wPose = this.blendLegPose(leg, li);
1472
+ this.writeLeg(leg);
1473
+ // a planted claw the leg can no longer reach (body shoved or lunging past it) must step
1474
+ leg.reachErr = leg.state === 'stance' && wPose < 1e-3 ? Math.hypot(leg.tip.x - T.x, leg.tip.z - T.z) : 0;
1475
+ if (wPose > 1e-3 && (P.dead < 0.5 || P.flip < 0.3)) this.groundGuard(leg, (P.dead < 0.5 ? 1 : 1 - smooth(0.02, 0.3, P.flip)) * smooth(0, 0.05, wPose));
1476
+ leg.ikEl.set(leg.el); leg.ikAz.set(leg.az); // the displayed angles: next frame's IK is unwrapped against them
1477
+ if (this.debug.targets[li]) this.debug.targets[li].copy(T);
1478
+ }
1479
+ void air; void P;
1480
+ }
1481
+
1482
+ solveLeg(leg, T, heel) {
1483
+ const L = leg.def, az = leg.az, el = leg.el, s = L.side;
1484
+ const B = L.P0[0];
1485
+ // tarsus along the leg azimuth at its bind slope (steeper while the heel lifts)
1486
+ // azimuths of points very close to the coxa are ill-defined: fall back to the neutral one
1487
+ const rS = L.rMin;
1488
+ const azG = (dx, dz, r0) => {
1489
+ const a = Math.atan2(dx * s, dz), d = Math.hypot(dx, dz);
1490
+ return L.phi0 + angDiff(L.phi0, a) * smooth(0.3 * r0, 0.75 * r0, d);
1491
+ };
1492
+ const phT = azG(T.x - B.x, T.z - B.z, rS);
1493
+ // the tarsus keeps its slope relative to the ground, not the body (a planted tarsus does not roll
1494
+ // on its pad when the body pitches or rolls)
1495
+ const u = this.upB, tilt = Math.asin(clamp(u.x * Math.sin(phT) * s + u.z * Math.cos(phT), -1, 1));
1496
+ const eta = L.el0[6] - heel - clamp(tilt, -0.7, 0.7) * (1 - this.P.dead);
1497
+ const cT = Math.cos(eta);
1498
+ const Ax = T.x - L.len[6] * Math.sin(phT) * cT * s, Ay = T.y - L.len[6] * Math.sin(eta), Az = T.z - L.len[6] * Math.cos(phT) * cT;
1499
+ // coxa yaws part of the way, the trochanter the rest
1500
+ const phA = azG(Ax - B.x, Az - B.z, rS);
1501
+ const dph = angDiff(L.phi0, phA);
1502
+ const phC = L.az0[0] + clamp(0.55 * dph, -0.6, 0.6);
1503
+ const eC = L.el0[0];
1504
+ let x = B.x + L.len[0] * Math.sin(phC) * Math.cos(eC) * s, y = B.y + L.len[0] * Math.sin(eC), z = B.z + L.len[0] * Math.cos(phC) * Math.cos(eC);
1505
+ const phTr = azG(Ax - x, Az - z, rS * 0.7);
1506
+ const eTr = L.el0[1];
1507
+ x += L.len[1] * Math.sin(phTr) * Math.cos(eTr) * s; y += L.len[1] * Math.sin(eTr); z += L.len[1] * Math.cos(phTr) * Math.cos(eTr);
1508
+ // vertical leg plane from the femur base to the metatarsus-tarsus joint
1509
+ const phP = azG(Ax - x, Az - z, rS * 0.6);
1510
+ const ux = Math.sin(phP) * s, uz = Math.cos(phP);
1511
+ const h = (Ax - x) * ux + (Az - z) * uz, v = Ay - y;
1512
+ const Lf = L.len[2], Lpt = L.Lpt, Lm = L.len[5];
1513
+ const d1 = (Lf + Lpt) * 0.97;
1514
+ // metatarsus: preferred slope, flattening as the leg stretches
1515
+ const D = Math.hypot(h, v);
1516
+ let em = L.el0[5] + clamp(1.1 * (D - L.D0) / L.D0, -0.6, 0.9);
1517
+ em = Math.min(em, -0.05);
1518
+ let mh = h - Lm * Math.cos(em), mv = v - Lm * Math.sin(em);
1519
+ let dm = Math.hypot(mh, mv);
1520
+ {
1521
+ // too far for femur + patella/tibia with the preferred slope: turn the metatarsus toward the
1522
+ // femur base (its straightest, longest reach), smoothly with the reach excess
1523
+ const w = smooth(0.84 * d1, 1.0 * d1, dm);
1524
+ if (w > 0) {
1525
+ em = em + angDiff(em, Math.atan2(v, h)) * w;
1526
+ mh = h - Lm * Math.cos(em); mv = v - Lm * Math.sin(em);
1527
+ dm = Math.hypot(mh, mv);
1528
+ }
1529
+ }
1530
+ // two-link femur + (patella, tibia), knee up, soft reach limit
1531
+ const dr = clamp(softMin(dm, d1, 0.04 * d1), Math.abs(Lf - Lpt) * 1.02 + 1e-7, d1);
1532
+ const al = Math.atan2(mv, mh);
1533
+ const cb = clamp((Lf * Lf + dr * dr - Lpt * Lpt) / (2 * Lf * dr), -1, 1);
1534
+ const ef = al + Math.acos(cb);
1535
+ const kh = Lf * Math.cos(ef), kv = Lf * Math.sin(ef);
1536
+ const mhr = mh * dr / Math.max(dm, 1e-9), mvr = mv * dr / Math.max(dm, 1e-9);
1537
+ const ept = Math.atan2(mvr - kv, mhr - kh);
1538
+ az[0] = phC; el[0] = eC;
1539
+ az[1] = phTr; el[1] = eTr;
1540
+ az[2] = az[3] = az[4] = az[5] = phP;
1541
+ el[2] = ef; el[3] = ept + L.dp; el[4] = ept + L.dt; el[5] = em;
1542
+ az[6] = phT; el[6] = eta;
1543
+ }
1544
+
1545
+ // Action poses in joint-angle space. Relative bends (deg) from the femur outward:
1546
+ // raise (threat / grab): femur up, leg nearly straight, pointing up and forward
1547
+ // hold (prey, sleep tuck): femur up, knee sharply flexed, tip toward the mouth
1548
+ // curl (death): femur down, every joint flexed: the leg folds under the body
1549
+ // air (jump): front legs by raise, the hind legs trailing, all a little flexed
1550
+ blendLegPose(leg, li) {
1551
+ const P = this.P, L = leg.def, az = leg.az, el = leg.el;
1552
+ // the in-air pose takes a leg only once it has left the ground (pushing legs stay planted)
1553
+ const r = P.raise[li], hd = P.hold[li], c = P.curl, a = P.air > 0 ? P.air * (leg.state === 'stance' && this.air.active ? 0 : smooth(0, 0.35, leg.airT)) : 0;
1554
+ if (r + hd + c + a + P.curl2 < 1e-4) return 0;
1555
+ const front = L.i === 1 ? 1 : L.i === 2 ? 0.7 : 0;
1556
+ if (a > 0) this.poseBlend(leg, a * (1 - front * 0.7), L.i >= 3 ? 10 : 30, L.i >= 3 ? P_AIR_H : P_AIR_F, L.i >= 3 ? 8 : -6, false);
1557
+ if (r > 0) this.poseBlend(leg, r, 62 + 8 * front, P_RAISE, -10 * front, false);
1558
+ if (hd > 0) this.poseBlend(leg, hd, 42, P_HOLD, -22 * front - 6, false);
1559
+ // death: every joint flexes ventrally, distal joints first (the hydraulic collapse), and the legs
1560
+ // gather under the sternum (over it once it lies on its back)
1561
+ if (c > 0) this.poseBlend(leg, c, 75, P_TUCK, -10, false);
1562
+ if (P.curl2 > 1e-4) {
1563
+ // upright, the legs half-flex with the knees up (the dying spider sinks, legs drawn in); as the
1564
+ // body rolls over the femurs swing ventrally and every joint flexes fully, so the corpse rests
1565
+ // on its back or side inside the cage of its legs. Once curl2 is 1 no angle depends on the IK.
1566
+ const cu = P.flipAngle < 2 && L.side === -P.flipSide ? L.curlSide : L.curlBack;
1567
+ const down = L.side === -P.flipSide, roll = P.flip * P.flipAngle;
1568
+ // (the legs it rolls onto fold ventrally first, the upper ones as they come off the ground)
1569
+ const w = P.curl2, fl = down ? smooth(0, 0.3 * Math.PI, roll) : smooth(0.3 * Math.PI, 0.6 * Math.PI, roll), cp = L.curlUp;
1570
+ if (P.flip > 1e-4 || (P.reviving && this._lift > 0.4 * this.cfg.rootH)) {
1571
+ // rolled over, the claws' stance targets mean nothing: the IK is replaced by the upright
1572
+ // curl as the base the pose blends from (the IK comes back only once it is upright)
1573
+ const g = Math.max(smooth(0, 0.1, P.flip), P.reviving * smooth(0.4 * this.cfg.rootH, 1.2 * this.cfg.rootH, this._lift));
1574
+ el[0] += (cp.C - el[0]) * g; el[1] += (cp.C - el[1]) * g;
1575
+ let e0 = cp.F;
1576
+ el[2] += (e0 - el[2]) * g;
1577
+ for (let k = 3; k < 7; k++) { e0 += cp.B[k - 3]; el[k] += (e0 - el[k]) * g; }
1578
+ az[0] += (L.az0[0] - az[0]) * g;
1579
+ for (let k = 1; k < 7; k++) az[k] += (L.phi0 - az[k]) * g;
1580
+ }
1581
+ const cT = lerp(cp.C, cu.C, fl);
1582
+ el[0] += (cT - el[0]) * w; el[1] += (cT - el[1]) * w;
1583
+ el[2] += (lerp(cp.F, cu.F, fl) - el[2]) * w;
1584
+ let e = el[2];
1585
+ for (let k = 3; k < 7; k++) { e += lerp(cp.B[k - 3], cu.B[k - 3], fl); el[k] += (e - el[k]) * w; }
1586
+ az[0] += (L.az0[0] - az[0]) * w;
1587
+ for (let k = 1; k < 7; k++) az[k] += (L.phi0 - az[k]) * w;
1588
+ }
1589
+ return Math.min(1, r + hd + c + a + P.curl2);
1590
+ }
1591
+
1592
+ // posture changes of a claw target (world): drawn in toward the coxa along the ground (a collapsing
1593
+ // spider), lifted while the legs curl or uncurl (so the blend never sweeps through the ground),
1594
+ // tucked under the sternum; the same for planted claws and for landing targets
1595
+ postureTarget(leg, T) {
1596
+ const P = this.P, L = leg.def;
1597
+ if (P.drawIn > 1e-3) {
1598
+ // (sleep pulls them in further than death: the living legs keep their pose)
1599
+ const cb = tv().copy(L.P0[0]).applyQuaternion(this.qB).add(this.root);
1600
+ const k = 0.3 * P.drawIn + 0.2 * P.pullIn;
1601
+ T.x += (cb.x - T.x) * k; T.z += (cb.z - T.z) * k;
1602
+ T.y = this.terrainH(T.x, T.z) + L.tipY;
1603
+ }
1604
+ if (P.dead > 1e-3 && P.curl > 1e-3) T.y += 0.28 * L.total * smooth(0, 0.6, P.curl);
1605
+ if (P.tuck > 1e-3) {
1606
+ const B = L.P0[0];
1607
+ T.lerp(this.localToWorld(B.x * 0.35, B.y - 0.25 * L.len[2], B.z * 0.6, tv()), P.tuck);
1608
+ }
1609
+ return T;
1610
+ }
1611
+
1612
+ // The IK's elevations are defined modulo a turn. Pose blends interpolate them linearly toward fixed
1613
+ // targets, so while any pose is blended in they are taken on the turn nearest to what was shown
1614
+ // last frame (a leg released from a pose takes the short way back, never a full spin); with no pose
1615
+ // active they are brought back to (-pi, pi], which changes nothing visible.
1616
+ _unwrapIK(leg, li) {
1617
+ const P = this.P, el = leg.el, az = leg.az, pv = leg.ikEl, pa = leg.ikAz;
1618
+ const posed = P.raise[li] + P.hold[li] + P.curl + P.air + P.curl2 > 1e-5;
1619
+ for (let k = 0; k < 7; k++) {
1620
+ el[k] = posed ? pv[k] + angDiff(pv[k], el[k]) : angDiff(0, el[k]);
1621
+ az[k] = posed ? pa[k] + angDiff(pa[k], az[k]) : angDiff(0, az[k]);
1622
+ }
1623
+ }
1624
+
1625
+ // blend one pose into the solved joint angles: femur elevation (deg) and the bends of the following
1626
+ // joints relative to it; staggered = distal joints move first
1627
+ poseBlend(leg, w, femur, bends, dAz, staggered) {
1628
+ if (w < 1e-4) return;
1629
+ const L = leg.def, az = leg.az, el = leg.el;
1630
+ let e = femur * DEG;
1631
+ el[2] += (e - el[2]) * (staggered ? smooth(0.35, 1, w) : w);
1632
+ for (let k = 3; k < 7; k++) {
1633
+ e += bends[k - 3] * DEG;
1634
+ const wk = staggered ? smooth(STAG[k][0], STAG[k][1], w) : w;
1635
+ el[k] += (e - el[k]) * wk;
1636
+ }
1637
+ const tAz = L.phi0 + dAz * DEG;
1638
+ az[0] += (L.az0[0] - az[0]) * w;
1639
+ for (let k = 1; k < 7; k++) az[k] += (tAz - az[k]) * w;
1640
+ }
1641
+
1642
+ // A posed leg (raised, holding, curled, tucked) must not pass through the ground: rotate the chain
1643
+ // from the femur outward about its base by just enough to lift the deepest joint clear (the
1644
+ // direction that raises that joint, found numerically, so it also works upside down).
1645
+ groundGuard(leg, strength = 1) {
1646
+ const L = leg.def, el = leg.el, jp = leg.jp, m = L.tipY * 1.1, k2 = (0.1 * L.total) ** 2;
1647
+ const pen = this._gpen || (this._gpen = new Float64Array(8)), y0 = this._gy0 || (this._gy0 = new Float64Array(8));
1648
+ for (let it = 0; it < 3; it++) {
1649
+ let any = false;
1650
+ for (let k = 3; k < 8; k++) { const q = jp[k]; pen[k] = Math.max(0, this.terrainH(q.x, q.z) + (k === 7 ? m : 1.8 * m) - q.y); y0[k] = q.y; if (pen[k] > 0) any = true; }
1651
+ if (!any) return;
1652
+ const e = 0.05;
1653
+ for (let k = 2; k < 7; k++) el[k] += e;
1654
+ this.writeLeg(leg);
1655
+ // least-squares step over every joint in the ground (each enters with zero depth: continuous)
1656
+ let num = 0, den = k2;
1657
+ for (let k = 3; k < 8; k++) { const sl = (jp[k].y - y0[k]) / e; num += pen[k] * sl; den += pen[k] > 0 ? sl * sl : 0; }
1658
+ const dd = clamp((1.05 * num) / den, -0.6, 0.6) * strength - e;
1659
+ for (let k = 2; k < 7; k++) el[k] += dd;
1660
+ this.writeLeg(leg);
1661
+ }
1662
+ }
1663
+
1664
+
1665
+
1666
+
1667
+ // claw tip (world) of a leg for joint angles az / el (no bones written)
1668
+ fkTip(leg, az, el, out) {
1669
+ const L = leg.def, s = L.side;
1670
+ let x = L.P0[0].x, y = L.P0[0].y, z = L.P0[0].z;
1671
+ for (let k = 0; k < 7; k++) {
1672
+ const ce = Math.cos(el[k]);
1673
+ x += L.len[k] * Math.sin(az[k]) * ce * s; y += L.len[k] * Math.sin(el[k]); z += L.len[k] * Math.cos(az[k]) * ce;
1674
+ }
1675
+ return this.localToWorld(x, y, z, out);
1676
+ }
1677
+
1678
+ writeLeg(leg) {
1679
+ const L = leg.def, az = leg.az, el = leg.el, s = L.side, jp = leg.jp;
1680
+ const q = this._q, qy = this._q2;
1681
+ const p = tv().copy(L.P0[0]);
1682
+ this.localToWorld(p.x, p.y, p.z, jp[0]);
1683
+ for (let k = 0; k < 7; k++) {
1684
+ // rotation from bind: body x yaw about body up x pitch about the bind leg-plane normal
1685
+ qy.setFromAxisAngle(Y_AXIS, s * angDiff(L.az0[k], az[k]));
1686
+ q.setFromAxisAngle(L.n0, el[k] - L.el0[k]);
1687
+ qy.multiply(q);
1688
+ q.copy(this.qB).multiply(qy);
1689
+ this.setBone(L.bones[k], jp[k], q);
1690
+ const ce = Math.cos(el[k]);
1691
+ p.x += L.len[k] * Math.sin(az[k]) * ce * s; p.y += L.len[k] * Math.sin(el[k]); p.z += L.len[k] * Math.cos(az[k]) * ce;
1692
+ this.localToWorld(p.x, p.y, p.z, jp[k + 1]);
1693
+ }
1694
+ leg.tip.copy(jp[7]);
1695
+ }
1696
+
1697
+ // ---------------------------------------------------------------- palps
1698
+ solvePalps(dt) {
1699
+ const cfg = this.cfg, P = this.P, t = this.time;
1700
+ // (eased: the gait can switch on and off for a frame, e.g. at a landing)
1701
+ this._palpMove = expTo(this._palpMove || 0, this._moving ? 1 : 0, dt, 0.12);
1702
+ const moving = this._palpMove;
1703
+ for (let k = 0; k < 2; k++) {
1704
+ const D = cfg.palpDefs[k], S = this.palpState[k], az = S.az, el = S.el, s = D.side;
1705
+ // idle twitches: random small targets, eased
1706
+ S.twT -= dt;
1707
+ if (S.twT <= 0) {
1708
+ S.twT = 0.4 + this.rand() * 2.2;
1709
+ const amp = 0.25 * cfg.palps.twitch * P.twitch * (1 - moving * 0.6);
1710
+ for (let j = 0; j < 6; j++) S.tgt[j] = (this.rand() - 0.5) * amp;
1711
+ }
1712
+ const kT = 1 - Math.exp(-dt / 0.07), kD = 1 - Math.exp(-dt / 0.6);
1713
+ const work = P.palpWork * Math.sin(t * TAU * 2.2 + k * Math.PI);
1714
+ const gaitSwing = moving * cfg.palps.tap * 0.25 * Math.sin(TAU * (this.phase + k * 0.5));
1715
+ for (let j = 0; j < 6; j++) {
1716
+ S.twitch[j] += (S.tgt[j] - S.twitch[j]) * kT;
1717
+ S.tgt[j] -= S.tgt[j] * kD;
1718
+ az[j] = D.az0[j] - s * 0; el[j] = D.el0[j];
1719
+ }
1720
+ const raise = P.palpRaise, reach = P.palpReach, down = P.palpDown, fold = P.palpFold;
1721
+ const ftw = S.twitch[2] + S.twitch[3];
1722
+ el[2] += 0.85 * raise + 0.2 * reach - 0.35 * down + 0.3 * work + gaitSwing + ftw * 0.6 + 0.5 * fold;
1723
+ el[3] += -0.3 * raise - 0.4 * down - 0.2 * work - 0.9 * fold + S.twitch[4] * 0.5;
1724
+ el[4] += -0.2 * raise + 0.35 * reach - 0.25 * work - 0.7 * fold;
1725
+ el[5] += -0.4 * raise + 0.45 * reach + 0.35 * work - 0.4 * fold + S.twitch[5] * 0.5;
1726
+ for (let j = 1; j < 6; j++) az[j] += (-0.12 * raise - 0.05 * work + 0.1 * S.twitch[1]) ;
1727
+ this.writePalp(D, S);
1728
+ // keep the palp tip above the ground: lift the femur (continuous in the penetration depth)
1729
+ const tip = S.jp[6];
1730
+ let pen = this._palpPen(D, S);
1731
+ for (let it = 0; it < 3 && pen > 0; it++) {
1732
+ // lift the palp femur (and flatten the tarsus) until the tip clears
1733
+ const reachL = D.len[2] + D.len[3] + D.len[4] + D.len[5];
1734
+ el[2] += Math.min(0.9, Math.asin(Math.min(1, pen / reachL)) * 1.4);
1735
+ el[5] += Math.min(0.6, pen / D.len[5]);
1736
+ this.writePalp(D, S);
1737
+ pen = this._palpPen(D, S);
1738
+ }
1739
+ }
1740
+ }
1741
+ // deepest ground penetration of the palp tip and of the patella, tibia and tarsus (ends and middles)
1742
+ _palpPen(D, S) {
1743
+ const tip = S.jp[6];
1744
+ let m = this.terrainH(tip.x, tip.z) + D.tipR - tip.y;
1745
+ for (let j = 3; j <= 5; j++) {
1746
+ const q = S.jp[j], q2 = S.jp[j + 1];
1747
+ m = Math.max(m, this.terrainH(q.x, q.z) + D.tipR * 2.4 - q.y);
1748
+ const mx_ = (q.x + q2.x) * 0.5, my = (q.y + q2.y) * 0.5, mz = (q.z + q2.z) * 0.5;
1749
+ m = Math.max(m, this.terrainH(mx_, mz) + D.tipR * 2.2 - my);
1750
+ }
1751
+ return m;
1752
+ }
1753
+ writePalp(D, S) {
1754
+ const az = S.az, el = S.el, s = D.side, jp = S.jp;
1755
+ const q = this._q, qy = this._q2;
1756
+ const p = tv().copy(D.P0[0]);
1757
+ this.localToWorld(p.x, p.y, p.z, jp[0]);
1758
+ for (let k = 0; k < 6; k++) {
1759
+ qy.setFromAxisAngle(Y_AXIS, s * angDiff(D.az0[k], az[k]));
1760
+ q.setFromAxisAngle(D.n0, el[k] - D.el0[k]);
1761
+ qy.multiply(q);
1762
+ q.copy(this.qB).multiply(qy);
1763
+ this.setBone(D.bones[k], jp[k], q);
1764
+ const ce = Math.cos(el[k]);
1765
+ p.x += D.len[k] * Math.sin(az[k]) * ce * s; p.y += D.len[k] * Math.sin(el[k]); p.z += D.len[k] * Math.cos(az[k]) * ce;
1766
+ this.localToWorld(p.x, p.y, p.z, jp[k + 1]);
1767
+ }
1768
+ }
1769
+
1770
+ // ---------------------------------------------------------------- chelicerae and fangs
1771
+ solveChelicerae() {
1772
+ const cfg = this.cfg, P = this.P, C = cfg.chel, t = this.time;
1773
+ const q = this._qc1, qw = this._qc2, qf = this._qc3;
1774
+ for (let k = 0; k < 2; k++) {
1775
+ const ch = cfg.chels[k], s = ch.side;
1776
+ const work = P.chelWork * Math.sin(t * TAU * 2.6 + k * Math.PI);
1777
+ const pitch = C.pitch * (0.35 * P.chelOpen + 0.25 * work) - 0.1 * P.dead;
1778
+ const spread = C.spread * (P.chelOpen + 0.3 * Math.max(0, work)) * s;
1779
+ // chelicera: pitch about the lateral axis (tips forward), spread about the forward axis
1780
+ q.setFromAxisAngle(X_AXIS, -pitch).premultiply(qf.setFromAxisAngle(Z_AXIS, spread));
1781
+ qw.copy(this.qB).multiply(q);
1782
+ const base = this.localToWorld(ch.base.x, ch.base.y, ch.base.z, tv());
1783
+ this.setBone(ch.bc, base, qw);
1784
+ // fang: unfolds about its hinge
1785
+ const tipLocal = tv().subVectors(ch.tip, ch.base).applyQuaternion(q).add(ch.base);
1786
+ const tipW = this.localToWorld(tipLocal.x, tipLocal.y, tipLocal.z, tv());
1787
+ const open = clamp(P.fangOpen + 0.15 * Math.max(0, work) * P.chelWork, 0, 1.1) * C.fangOpen;
1788
+ qw.multiply(qf.setFromAxisAngle(ch.fangAxis, open));
1789
+ this.setBone(ch.bf, tipW, qw);
1790
+ }
1791
+ }
1792
+
1793
+ // ----------------------------------------------------------------- state
1794
+ publish() {
1795
+ const st = this.state, P = this.P;
1796
+ st.heading = this.heading;
1797
+ st.speed = Math.hypot(this.velocity.x, this.velocity.z);
1798
+ st.gait = this.air.active ? 'jump' : st.stats.gait;
1799
+ st.grounded = !this.air.active;
1800
+ st.eyelid = 0;
1801
+ st.action = this.actionsLayer.current();
1802
+ st.posture = this.actionsLayer.posture;
1803
+ for (let i = 0; i < 8; i++) {
1804
+ const leg = this.legs[i], o = st.legs[i];
1805
+ o.stance = leg.state === 'stance' && !this.air.active && P.gaitW > 0.5 && P.raise[i] + P.hold[i] + P.curl + P.curl2 + P.air + P.drawIn + P.tuck + P.dead < 0.004;
1806
+ if (leg.state === 'stance') o.contact.copy(leg.plant); else o.contact.copy(leg.tip);
1807
+ o.contact.y -= leg.def.tipY;
1808
+ }
1809
+ }
1810
+ }