@bitefoundry/retrieviewer 0.0.3 → 0.0.5

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 (263) hide show
  1. package/ASSET-PROVENANCE.md +11 -0
  2. package/ASSETS-LICENSE.md +2 -1
  3. package/ATTRIBUTION.md +6 -1
  4. package/COMFORT-ART.md +95 -0
  5. package/DESKTOP-PLATFORMS.md +6 -1
  6. package/INSTALLING.md +4 -0
  7. package/PLUGIN.md +13 -10
  8. package/PREREQUISITES.md +95 -0
  9. package/PUPPY-MEMORY.md +102 -1
  10. package/README.ko.md +116 -0
  11. package/README.md +121 -59
  12. package/RELEASING.md +33 -13
  13. package/THIRD-PARTY-NOTICES.md +5 -3
  14. package/braille.mjs +35 -9
  15. package/examples/live.mjs +2 -2
  16. package/integrations/linux-overlay/overlay.py +8 -2
  17. package/integrations/macos-overlay/overlay.jxa +9 -7
  18. package/integrations/opencode/runtime.mjs +4 -4
  19. package/integrations/opencode/tui.tsx +1 -1
  20. package/integrations/windows-overlay/Overlay.cs +7 -6
  21. package/integrations/windows-overlay/desktop.mjs +2 -1
  22. package/integrations/windows-overlay/protocol.mjs +13 -7
  23. package/integrations/windows-overlay/worker.mjs +1 -1
  24. package/package.json +34 -13
  25. package/puppy-comfort-sprites.mjs +33 -0
  26. package/puppy-companion-comfort-q5.bin.gz +0 -0
  27. package/puppy-companion-comfort.json +140 -0
  28. package/puppy-companion-life.mjs +58 -18
  29. package/puppy-companion-objects.mjs +12 -0
  30. package/puppy-companion-sprites.mjs +4 -2
  31. package/puppy-frame-predictive.mjs +15 -9
  32. package/puppy-frame-store.mjs +2 -2
  33. package/puppy-paper-actions.mjs +4 -2
  34. package/puppy-paper-behaviors.mjs +1 -1
  35. package/puppy-paper-contact.mjs +4 -2
  36. package/puppy-paper-seat.mjs +4 -2
  37. package/puppy-paper-world.mjs +14 -0
  38. package/puppy-paper.mjs +79 -21
  39. package/retrieviewer-cli.mjs +8 -6
  40. package/retrieviewer-plugin.d.mts +1 -1
  41. package/retrieviewer-plugin.mjs +1 -1
  42. package/retrieviewer.d.mts +12 -4
  43. package/retrieviewer.mjs +20 -14
  44. package/locomotion-core.mjs +0 -21
  45. package/locomotion-puppy.animal +0 -0
  46. package/vendor/procedural-animals/package.json +0 -40
  47. package/vendor/procedural-animals/src/animal.js +0 -146
  48. package/vendor/procedural-animals/src/bake.js +0 -51
  49. package/vendor/procedural-animals/src/build.js +0 -81
  50. package/vendor/procedural-animals/src/core/build/coatKit.js +0 -121
  51. package/vendor/procedural-animals/src/core/build/featherCards.js +0 -189
  52. package/vendor/procedural-animals/src/core/build/finEdges.js +0 -85
  53. package/vendor/procedural-animals/src/core/build/furSlope.js +0 -115
  54. package/vendor/procedural-animals/src/core/build/mesh.js +0 -67
  55. package/vendor/procedural-animals/src/core/build/pipeline.js +0 -272
  56. package/vendor/procedural-animals/src/core/build/seams.js +0 -183
  57. package/vendor/procedural-animals/src/core/build/warp.js +0 -112
  58. package/vendor/procedural-animals/src/core/build/weights.js +0 -393
  59. package/vendor/procedural-animals/src/core/math/vec.js +0 -81
  60. package/vendor/procedural-animals/src/core/motion/actions.js +0 -800
  61. package/vendor/procedural-animals/src/core/motion/bird.js +0 -3063
  62. package/vendor/procedural-animals/src/core/motion/birdActions.js +0 -1059
  63. package/vendor/procedural-animals/src/core/motion/common.js +0 -158
  64. package/vendor/procedural-animals/src/core/motion/index.js +0 -22
  65. package/vendor/procedural-animals/src/core/motion/quadruped.js +0 -2875
  66. package/vendor/procedural-animals/src/core/motion/snake.js +0 -1594
  67. package/vendor/procedural-animals/src/core/motion/spider.js +0 -1810
  68. package/vendor/procedural-animals/src/core/motion/swimmer.js +0 -1047
  69. package/vendor/procedural-animals/src/core/motion/swimmerActions.js +0 -613
  70. package/vendor/procedural-animals/src/core/render/animalObject.js +0 -262
  71. package/vendor/procedural-animals/src/core/render/coatMaterial.js +0 -1301
  72. package/vendor/procedural-animals/src/core/render/dqs.js +0 -159
  73. package/vendor/procedural-animals/src/core/render/eyes.js +0 -676
  74. package/vendor/procedural-animals/src/core/render/membranes.js +0 -77
  75. package/vendor/procedural-animals/src/core/render/skeleton.js +0 -41
  76. package/vendor/procedural-animals/src/core/rig/bird.js +0 -327
  77. package/vendor/procedural-animals/src/core/rig/quadruped.js +0 -76
  78. package/vendor/procedural-animals/src/core/rig/rig.js +0 -79
  79. package/vendor/procedural-animals/src/core/rig/snake.js +0 -76
  80. package/vendor/procedural-animals/src/core/rig/spider.js +0 -110
  81. package/vendor/procedural-animals/src/core/rig/swimmer.js +0 -181
  82. package/vendor/procedural-animals/src/core/sdf/eyeSocket.js +0 -53
  83. package/vendor/procedural-animals/src/core/sdf/mesher.js +0 -238
  84. package/vendor/procedural-animals/src/core/sdf/sdf.js +0 -263
  85. package/vendor/procedural-animals/src/index.d.ts +0 -98
  86. package/vendor/procedural-animals/src/index.js +0 -29
  87. package/vendor/procedural-animals/src/registry.js +0 -48
  88. package/vendor/procedural-animals/src/species/bear/behaviour.js +0 -463
  89. package/vendor/procedural-animals/src/species/bear/claws.js +0 -147
  90. package/vendor/procedural-animals/src/species/bear/coat.js +0 -726
  91. package/vendor/procedural-animals/src/species/bear/index.js +0 -139
  92. package/vendor/procedural-animals/src/species/bear/motion.js +0 -112
  93. package/vendor/procedural-animals/src/species/bear/regions.js +0 -78
  94. package/vendor/procedural-animals/src/species/bear/rig.js +0 -112
  95. package/vendor/procedural-animals/src/species/bear/sculpt.js +0 -272
  96. package/vendor/procedural-animals/src/species/boar/behaviour.js +0 -335
  97. package/vendor/procedural-animals/src/species/boar/coat.js +0 -372
  98. package/vendor/procedural-animals/src/species/boar/index.js +0 -110
  99. package/vendor/procedural-animals/src/species/boar/motion.js +0 -85
  100. package/vendor/procedural-animals/src/species/boar/regions.js +0 -88
  101. package/vendor/procedural-animals/src/species/boar/rig.js +0 -111
  102. package/vendor/procedural-animals/src/species/boar/sculpt.js +0 -285
  103. package/vendor/procedural-animals/src/species/cat/behaviour.js +0 -559
  104. package/vendor/procedural-animals/src/species/cat/coat.js +0 -841
  105. package/vendor/procedural-animals/src/species/cat/index.js +0 -211
  106. package/vendor/procedural-animals/src/species/cat/motion.js +0 -101
  107. package/vendor/procedural-animals/src/species/cat/regions.js +0 -82
  108. package/vendor/procedural-animals/src/species/cat/rig.js +0 -100
  109. package/vendor/procedural-animals/src/species/cat/sculpt.js +0 -266
  110. package/vendor/procedural-animals/src/species/cat/whiskers.js +0 -110
  111. package/vendor/procedural-animals/src/species/cheetah/coat.js +0 -427
  112. package/vendor/procedural-animals/src/species/cheetah/index.js +0 -51
  113. package/vendor/procedural-animals/src/species/cheetah/joints.js +0 -3
  114. package/vendor/procedural-animals/src/species/cheetah/motion.js +0 -77
  115. package/vendor/procedural-animals/src/species/cheetah/regions.js +0 -95
  116. package/vendor/procedural-animals/src/species/cheetah/rig.js +0 -66
  117. package/vendor/procedural-animals/src/species/cheetah/sculpt.js +0 -158
  118. package/vendor/procedural-animals/src/species/chicken/coat.js +0 -313
  119. package/vendor/procedural-animals/src/species/chicken/coverts.js +0 -320
  120. package/vendor/procedural-animals/src/species/chicken/index.js +0 -110
  121. package/vendor/procedural-animals/src/species/chicken/motion.js +0 -177
  122. package/vendor/procedural-animals/src/species/chicken/regions.js +0 -66
  123. package/vendor/procedural-animals/src/species/chicken/rig.js +0 -144
  124. package/vendor/procedural-animals/src/species/chicken/sculpt.js +0 -389
  125. package/vendor/procedural-animals/src/species/chicken/wingLook.js +0 -66
  126. package/vendor/procedural-animals/src/species/chicken/wingStack.js +0 -124
  127. package/vendor/procedural-animals/src/species/cow/behaviour.js +0 -521
  128. package/vendor/procedural-animals/src/species/cow/coat.js +0 -465
  129. package/vendor/procedural-animals/src/species/cow/index.js +0 -118
  130. package/vendor/procedural-animals/src/species/cow/motion.js +0 -81
  131. package/vendor/procedural-animals/src/species/cow/regions.js +0 -98
  132. package/vendor/procedural-animals/src/species/cow/rig.js +0 -116
  133. package/vendor/procedural-animals/src/species/cow/sculpt.js +0 -339
  134. package/vendor/procedural-animals/src/species/crow/coat.js +0 -145
  135. package/vendor/procedural-animals/src/species/crow/index.js +0 -70
  136. package/vendor/procedural-animals/src/species/crow/joints.js +0 -3
  137. package/vendor/procedural-animals/src/species/crow/motion.js +0 -101
  138. package/vendor/procedural-animals/src/species/crow/regions.js +0 -50
  139. package/vendor/procedural-animals/src/species/crow/rig.js +0 -83
  140. package/vendor/procedural-animals/src/species/crow/sculpt.js +0 -118
  141. package/vendor/procedural-animals/src/species/deer/behaviour.js +0 -513
  142. package/vendor/procedural-animals/src/species/deer/coat.js +0 -391
  143. package/vendor/procedural-animals/src/species/deer/index.js +0 -106
  144. package/vendor/procedural-animals/src/species/deer/motion.js +0 -97
  145. package/vendor/procedural-animals/src/species/deer/regions.js +0 -60
  146. package/vendor/procedural-animals/src/species/deer/rig.js +0 -124
  147. package/vendor/procedural-animals/src/species/deer/sculpt.js +0 -297
  148. package/vendor/procedural-animals/src/species/dog/behaviour.js +0 -521
  149. package/vendor/procedural-animals/src/species/dog/coat.js +0 -495
  150. package/vendor/procedural-animals/src/species/dog/index.js +0 -144
  151. package/vendor/procedural-animals/src/species/dog/motion.js +0 -118
  152. package/vendor/procedural-animals/src/species/dog/regions.js +0 -61
  153. package/vendor/procedural-animals/src/species/dog/rig.js +0 -129
  154. package/vendor/procedural-animals/src/species/dog/sculpt.js +0 -246
  155. package/vendor/procedural-animals/src/species/eagle/coat.js +0 -250
  156. package/vendor/procedural-animals/src/species/eagle/index.js +0 -76
  157. package/vendor/procedural-animals/src/species/eagle/joints.js +0 -3
  158. package/vendor/procedural-animals/src/species/eagle/motion.js +0 -137
  159. package/vendor/procedural-animals/src/species/eagle/regions.js +0 -57
  160. package/vendor/procedural-animals/src/species/eagle/rig.js +0 -107
  161. package/vendor/procedural-animals/src/species/eagle/sculpt.js +0 -137
  162. package/vendor/procedural-animals/src/species/fish/coat.js +0 -271
  163. package/vendor/procedural-animals/src/species/fish/index.js +0 -88
  164. package/vendor/procedural-animals/src/species/fish/motion.js +0 -92
  165. package/vendor/procedural-animals/src/species/fish/regions.js +0 -27
  166. package/vendor/procedural-animals/src/species/fish/rig.js +0 -143
  167. package/vendor/procedural-animals/src/species/fish/sculpt.js +0 -214
  168. package/vendor/procedural-animals/src/species/fish/variants.js +0 -126
  169. package/vendor/procedural-animals/src/species/fox/behaviour.js +0 -314
  170. package/vendor/procedural-animals/src/species/fox/coat.js +0 -444
  171. package/vendor/procedural-animals/src/species/fox/index.js +0 -120
  172. package/vendor/procedural-animals/src/species/fox/motion.js +0 -81
  173. package/vendor/procedural-animals/src/species/fox/regions.js +0 -59
  174. package/vendor/procedural-animals/src/species/fox/rig.js +0 -91
  175. package/vendor/procedural-animals/src/species/fox/sculpt.js +0 -202
  176. package/vendor/procedural-animals/src/species/frog/behaviour.js +0 -590
  177. package/vendor/procedural-animals/src/species/frog/coat.js +0 -305
  178. package/vendor/procedural-animals/src/species/frog/index.js +0 -97
  179. package/vendor/procedural-animals/src/species/frog/motion.js +0 -69
  180. package/vendor/procedural-animals/src/species/frog/regions.js +0 -46
  181. package/vendor/procedural-animals/src/species/frog/rig.js +0 -156
  182. package/vendor/procedural-animals/src/species/frog/sculpt.js +0 -240
  183. package/vendor/procedural-animals/src/species/goat/behaviour.js +0 -509
  184. package/vendor/procedural-animals/src/species/goat/coat.js +0 -590
  185. package/vendor/procedural-animals/src/species/goat/index.js +0 -149
  186. package/vendor/procedural-animals/src/species/goat/motion.js +0 -95
  187. package/vendor/procedural-animals/src/species/goat/regions.js +0 -98
  188. package/vendor/procedural-animals/src/species/goat/rig.js +0 -181
  189. package/vendor/procedural-animals/src/species/goat/sculpt.js +0 -547
  190. package/vendor/procedural-animals/src/species/horse/coat.js +0 -361
  191. package/vendor/procedural-animals/src/species/horse/coatSeam.js +0 -122
  192. package/vendor/procedural-animals/src/species/horse/index.js +0 -80
  193. package/vendor/procedural-animals/src/species/horse/motion.js +0 -245
  194. package/vendor/procedural-animals/src/species/horse/regions.js +0 -60
  195. package/vendor/procedural-animals/src/species/horse/rig.js +0 -117
  196. package/vendor/procedural-animals/src/species/horse/sculpt.js +0 -316
  197. package/vendor/procedural-animals/src/species/horse/seamWeights.js +0 -272
  198. package/vendor/procedural-animals/src/species/lion/behaviour.js +0 -509
  199. package/vendor/procedural-animals/src/species/lion/coat.js +0 -1016
  200. package/vendor/procedural-animals/src/species/lion/index.js +0 -111
  201. package/vendor/procedural-animals/src/species/lion/motion.js +0 -87
  202. package/vendor/procedural-animals/src/species/lion/regions.js +0 -106
  203. package/vendor/procedural-animals/src/species/lion/rig.js +0 -110
  204. package/vendor/procedural-animals/src/species/lion/sculpt.js +0 -384
  205. package/vendor/procedural-animals/src/species/lion/whiskers.js +0 -132
  206. package/vendor/procedural-animals/src/species/pig/behaviour.js +0 -575
  207. package/vendor/procedural-animals/src/species/pig/coat.js +0 -730
  208. package/vendor/procedural-animals/src/species/pig/ears.js +0 -242
  209. package/vendor/procedural-animals/src/species/pig/index.js +0 -161
  210. package/vendor/procedural-animals/src/species/pig/motion.js +0 -105
  211. package/vendor/procedural-animals/src/species/pig/regions.js +0 -100
  212. package/vendor/procedural-animals/src/species/pig/rig.js +0 -152
  213. package/vendor/procedural-animals/src/species/pig/sculpt.js +0 -382
  214. package/vendor/procedural-animals/src/species/rabbit/behaviour.js +0 -605
  215. package/vendor/procedural-animals/src/species/rabbit/coat.js +0 -285
  216. package/vendor/procedural-animals/src/species/rabbit/index.js +0 -85
  217. package/vendor/procedural-animals/src/species/rabbit/motion.js +0 -85
  218. package/vendor/procedural-animals/src/species/rabbit/regions.js +0 -48
  219. package/vendor/procedural-animals/src/species/rabbit/rig.js +0 -99
  220. package/vendor/procedural-animals/src/species/rabbit/sculpt.js +0 -148
  221. package/vendor/procedural-animals/src/species/rat/behaviour.js +0 -539
  222. package/vendor/procedural-animals/src/species/rat/coat.js +0 -341
  223. package/vendor/procedural-animals/src/species/rat/index.js +0 -111
  224. package/vendor/procedural-animals/src/species/rat/motion.js +0 -90
  225. package/vendor/procedural-animals/src/species/rat/regions.js +0 -92
  226. package/vendor/procedural-animals/src/species/rat/rig.js +0 -112
  227. package/vendor/procedural-animals/src/species/rat/sculpt.js +0 -163
  228. package/vendor/procedural-animals/src/species/rat/whiskers.js +0 -119
  229. package/vendor/procedural-animals/src/species/shark/coat.js +0 -383
  230. package/vendor/procedural-animals/src/species/shark/index.js +0 -104
  231. package/vendor/procedural-animals/src/species/shark/motion.js +0 -69
  232. package/vendor/procedural-animals/src/species/shark/regions.js +0 -34
  233. package/vendor/procedural-animals/src/species/shark/rig.js +0 -194
  234. package/vendor/procedural-animals/src/species/shark/sculpt.js +0 -343
  235. package/vendor/procedural-animals/src/species/shark/variants.js +0 -105
  236. package/vendor/procedural-animals/src/species/sheep/behaviour.js +0 -522
  237. package/vendor/procedural-animals/src/species/sheep/coat.js +0 -529
  238. package/vendor/procedural-animals/src/species/sheep/index.js +0 -152
  239. package/vendor/procedural-animals/src/species/sheep/motion.js +0 -86
  240. package/vendor/procedural-animals/src/species/sheep/neck.js +0 -41
  241. package/vendor/procedural-animals/src/species/sheep/regions.js +0 -62
  242. package/vendor/procedural-animals/src/species/sheep/rig.js +0 -163
  243. package/vendor/procedural-animals/src/species/sheep/sculpt.js +0 -542
  244. package/vendor/procedural-animals/src/species/snake/coat.js +0 -315
  245. package/vendor/procedural-animals/src/species/snake/index.js +0 -73
  246. package/vendor/procedural-animals/src/species/snake/motion.js +0 -51
  247. package/vendor/procedural-animals/src/species/snake/regions.js +0 -51
  248. package/vendor/procedural-animals/src/species/snake/rig.js +0 -108
  249. package/vendor/procedural-animals/src/species/snake/sculpt.js +0 -122
  250. package/vendor/procedural-animals/src/species/spider/coat.js +0 -279
  251. package/vendor/procedural-animals/src/species/spider/index.js +0 -78
  252. package/vendor/procedural-animals/src/species/spider/motion.js +0 -72
  253. package/vendor/procedural-animals/src/species/spider/regions.js +0 -87
  254. package/vendor/procedural-animals/src/species/spider/rig.js +0 -118
  255. package/vendor/procedural-animals/src/species/spider/sculpt.js +0 -182
  256. package/vendor/procedural-animals/src/species/wolf/behaviour.js +0 -160
  257. package/vendor/procedural-animals/src/species/wolf/coat.js +0 -749
  258. package/vendor/procedural-animals/src/species/wolf/index.js +0 -107
  259. package/vendor/procedural-animals/src/species/wolf/motion.js +0 -80
  260. package/vendor/procedural-animals/src/species/wolf/regions.js +0 -57
  261. package/vendor/procedural-animals/src/species/wolf/rig.js +0 -87
  262. package/vendor/procedural-animals/src/species/wolf/sculpt.js +0 -339
  263. package/vendor/procedural-animals/src/worker.js +0 -29
@@ -1,1594 +0,0 @@
1
- // Snake motion engine: path-following lateral undulation over uneven terrain, a curvature-space pose
2
- // layer for everything that leaves the path (raised forebody, strike S-coil, flinch, writhing), a
3
- // scripted-path coil, head / jaw / tongue / fang / rattle control, and the snake action set.
4
- //
5
- // Locomotion. A virtual leader (the occiput, joint v0) moves along a serpentine path: a centre point
6
- // C travels at the commanded speed and heading, and the leader swings about it,
7
- // leader = C + left(heading) * A * sin(phi), phi += 2 pi dx / lambda
8
- // with amplitude A and wavelength lambda from the gait table (springs in the travelled distance, so
9
- // the path is C1 whatever the speed does; A starts from 0 when a snake sets off, so the new path
10
- // leaves exactly along its neck). The leader lays a trail on the terrain (a ring buffer of ground
11
- // points parameterised by arc length); every spine joint sits on the trail at its arc distance behind
12
- // the leader (non-uniform Catmull-Rom interpolation), lifted along the ground normal by its axis
13
- // height. So the body follows the head's path exactly (no lateral slip), hugs the ground and cannot
14
- // float or sink. Turns are rate limited (min radius, and so the leader never moves backwards).
15
- // state.position is C (the smooth centre of the wave, the point games follow / steer).
16
- //
17
- // Pose layer. Actions write, per joint, a lift above the ground, a yaw offset per segment and a roll.
18
- // The body is then re-integrated segment by segment from the head in the horizontal plane (exact
19
- // segment lengths: a lifted segment gets shorter horizontally) and translated so that the joints the
20
- // actions leave alone stay exactly on the trail (weighted fit). Heights always come from the terrain:
21
- // y = ground + axis height (+ lift), so lifted parts never go below it. A coil is not a pose but a
22
- // scripted path: the leader crawls an inward Archimedean spiral (curvature integrated along arc
23
- // length) and the body follows it into a flat coil. When a posture that changed the body shape ends
24
- // ('stand'), the current shape is baked into the trail, so the snake crawls off along its own body.
25
- // Self-contact: a part of the body that crosses over another rides on top of it (anterior over
26
- // posterior), with a smooth ramp along the chain.
27
- //
28
- // Lessons kept from the cheetah: continuous solves (nothing jumps between frames; weights and poses
29
- // are smooth functions of time), motion from phase (the wave is a function of travelled distance),
30
- // springs for secondary motion (head, jaw), zero-dt safety, no allocations in hot paths.
31
- //
32
- // Bones (core/rig/snake.js): head (v0 -> nose), jaw, tongue, tongueTip, fang, spine0..spineN-1
33
- // (v0 -> vN). Joint vi's bind height is its axis height above the belly.
34
- //
35
- // ------------------------------------------------------------------------------------------------
36
- // species.motion schema (reference size; lengths x total length TL where marked)
37
- // ------------------------------------------------------------------------------------------------
38
- // maxSpeed m/s, accel m/s^2, decel m/s^2, turnRate rad/s, minTurnRadius (x TL)
39
- // gears { name: speed } named speeds (crawl, slither, dash ...)
40
- // gaits [{ name, v, wave (x TL), amp (x TL), headLift (m), stab (0..1 head yaw stabilisation) }]
41
- // sorted by v; rows are blended by speed
42
- // body { neck: x TL (part raised to look around), forebody: x TL (part used by strikes) }
43
- // tongue { rate: flicks per s at idle, length: extension (x head length), flicks: [min, max] tip
44
- // oscillations per flick, freq: tip oscillation Hz }
45
- // breath { rate Hz, roll rad }
46
- // actions { strike: { reach (x TL), speed (m/s), gape (deg), coilHeight (x TL), rise (x TL) },
47
- // coil: { pitch (ring spacing / body width), speed (m/s) }, sit: { raise (x TL) },
48
- // eat: { swallow (s) }, drink: { pumps } }
49
- // variants { <params.variant>: { speedK, ampK, strike: {...}, rattle: bool } } per-variant overrides
50
- // ------------------------------------------------------------------------------------------------
51
- // Contract extras: state.contacts = [{ key: 'v<i>', contact, position }] (ground contact of every
52
- // spine joint; contact = lying on the ground), motion.chain (spine bone indices, head -> tail),
53
- // state.stats.freq (undulation frequency), state.posture, events attackHit / actionStart /
54
- // actionEnd / death / arrive. Actions: idle, stand, sit (alert coil, head raised), lie (stretch out),
55
- // sleep (resting coil), coil, strike = attack, jump (a forward lunge), eat (strike + swallow), drink,
56
- // hit (flinch and whip away), death (writhe, go limp, roll half belly-up).
57
- import * as THREE from 'three';
58
- import { TAU, clamp, lerp, smooth, angDiff, Spring, prng, writeFrame } from './common.js';
59
-
60
- const DEFAULTS = {
61
- maxSpeed: 0.8, accel: 0.9, decel: 1.6, turnRate: 2.2, minTurnRadius: 0.1,
62
- body: { neck: 0.12, forebody: 0.3 },
63
- tongue: { rate: 0.6, length: 0.6, flicks: [3, 6], freq: 7.5 },
64
- breath: { rate: 0.28, roll: 0.03 },
65
- };
66
- const STRIKE = { reach: 0.33, speed: 2.8, gape: 75, coilHeight: 0.07, rise: 0.12 };
67
- const POSTURES = ['sit', 'lie', 'sleep', 'death', 'coil'];
68
- const COILED = { sit: 1, sleep: 1, coil: 1 };
69
-
70
- const _m1 = new THREE.Matrix4(), _m2 = new THREE.Matrix4(), _m3 = new THREE.Matrix4();
71
- const _q = new THREE.Quaternion(), _q2 = new THREE.Quaternion();
72
- const _v = new THREE.Vector3(), _v2 = new THREE.Vector3(), _v3 = new THREE.Vector3(), _v4 = new THREE.Vector3();
73
- const h2 = (a, b) => Math.sqrt(a * a + b * b);
74
- const h3 = (a, b, c) => Math.sqrt(a * a + b * b + c * c);
75
- const _X = new THREE.Vector3(1, 0, 0), _Y = new THREE.Vector3(0, 1, 0), _Z = new THREE.Vector3(0, 0, 1);
76
-
77
- // ------------------------------------------------------------------------------------------------
78
- // Trail: ground points by arc length (ring buffer), newest point = the leader
79
- // ------------------------------------------------------------------------------------------------
80
- class Trail {
81
- constructor(cap) {
82
- this.cap = cap;
83
- this.x = new Float64Array(cap); this.y = new Float64Array(cap); this.z = new Float64Array(cap); this.s = new Float64Array(cap);
84
- this.o = 0; this.n = 0; this.ver = 0; this.hit = 0;
85
- }
86
- clear() { this.o = 0; this.n = 0; this.ver++; }
87
- // linear storage: logical k lives at o + k; when the end is reached the newest half moves to the front
88
- idx(k) { return this.o + k; }
89
- push(x, y, z, s) {
90
- this.ver++;
91
- if (this.o + this.n === this.cap) {
92
- const keep = this.n >> 1, from = this.o + this.n - keep;
93
- this.x.copyWithin(0, from, from + keep); this.y.copyWithin(0, from, from + keep);
94
- this.z.copyWithin(0, from, from + keep); this.s.copyWithin(0, from, from + keep);
95
- this.o = 0; this.n = keep;
96
- }
97
- const i = this.o + this.n;
98
- this.x[i] = x; this.y[i] = y; this.z[i] = z; this.s[i] = s;
99
- this.n++;
100
- }
101
- get lastS() { return this.s[this.o + this.n - 1]; }
102
- // leader moved: update the provisional newest point, commit it once it is `spacing` from the one before
103
- lead(x, y, z, spacing) {
104
- const a = this.o + this.n - 1, b = a - 1;
105
- const ax = x - this.x[a], ay = y - this.y[a], az = z - this.z[a];
106
- if (ax * ax + ay * ay + az * az < 1e-18) return;
107
- const bx = x - this.x[b], by = y - this.y[b], bz = z - this.z[b];
108
- const db = Math.sqrt(bx * bx + by * by + bz * bz);
109
- if (db >= spacing) this.push(x, y, z, this.s[a] + Math.sqrt(ax * ax + ay * ay + az * az));
110
- else { this.x[a] = x; this.y[a] = y; this.z[a] = z; this.s[a] = this.s[b] + db; this.ver++; }
111
- }
112
- // position at arc s (Hermite with finite-difference tangents: C1 for any spacing); beyond the
113
- // oldest point the trail is extended straight. `hint`: a nearby logical segment (walk from it).
114
- sample(s, out, hint = -1) {
115
- const n = this.n, o = this.o, X = this.x, Y = this.y, Z = this.z, S = this.s;
116
- this.hit = 0;
117
- if (s <= S[o]) {
118
- const i0 = o, i1 = o + 1, k = (s - S[i0]) / (S[i1] - S[i0] || 1e-9);
119
- out[0] = X[i0] + (X[i1] - X[i0]) * k; out[1] = Y[i0] + (Y[i1] - Y[i0]) * k; out[2] = Z[i0] + (Z[i1] - Z[i0]) * k;
120
- return out;
121
- }
122
- const iN = o + n - 1;
123
- if (s >= S[iN]) { out[0] = X[iN]; out[1] = Y[iN]; out[2] = Z[iN]; this.hit = n - 2; return out; }
124
- let lo;
125
- if (hint >= 0 && hint < n - 1) {
126
- lo = hint;
127
- while (lo > 0 && S[o + lo] > s) lo--;
128
- while (lo < n - 2 && S[o + lo + 1] <= s) lo++;
129
- } else {
130
- lo = 0; let hi = n - 1;
131
- while (hi - lo > 1) { const md = (lo + hi) >> 1; if (S[o + md] <= s) lo = md; else hi = md; }
132
- }
133
- this.hit = lo;
134
- const a = o + lo, b = a + 1;
135
- const h = S[b] - S[a] || 1e-9, u = (s - S[a]) / h;
136
- // tangents per unit arc at a and b from their neighbours
137
- const a0 = lo > 0 ? a - 1 : a, b1 = lo + 2 < n ? b + 1 : b;
138
- const da = S[b] - S[a0] || 1e-9, db = S[b1] - S[a] || 1e-9;
139
- const u2 = u * u, u3 = u2 * u;
140
- const h00 = 2 * u3 - 3 * u2 + 1, h10 = ((u3 - 2 * u2 + u) * h) / da, h01 = -2 * u3 + 3 * u2, h11 = ((u3 - u2) * h) / db;
141
- out[0] = h00 * X[a] + h10 * (X[b] - X[a0]) + h01 * X[b] + h11 * (X[b1] - X[a]);
142
- out[1] = h00 * Y[a] + h10 * (Y[b] - Y[a0]) + h01 * Y[b] + h11 * (Y[b1] - Y[a]);
143
- out[2] = h00 * Z[a] + h10 * (Z[b] - Z[a0]) + h01 * Z[b] + h11 * (Z[b1] - Z[a]);
144
- return out;
145
- }
146
- }
147
- const S3 = new Float64Array(3);
148
-
149
- // ------------------------------------------------------------------------------------------------
150
- export function createSnakeMotion(ctx) { return new SnakeMotion(ctx); }
151
-
152
- export class SnakeMotion {
153
- constructor({ species, data, skeleton, ground, water, position, heading = 0, emit }) {
154
- this.species = species;
155
- this.sk = skeleton;
156
- this.ground = ground || (() => 0);
157
- this.water = water || null;
158
- this.emit = emit || (() => {});
159
- this.rand = prng((data.seed || 1) * 7919 + 29);
160
- this.seed = data.seed || 1;
161
- const params = data.params || {};
162
- const spec = species.motion || {};
163
- const vk = (spec.variants && spec.variants[params.variant]) || {};
164
- this.viper = params.variant === 'rattlesnake';
165
- // ---- skeleton
166
- const J = {};
167
- for (const [k, v] of Object.entries(data.joints)) J[k] = new THREE.Vector3(v[0], v[1], v[2]);
168
- this.J = J;
169
- let N = 0;
170
- while (J['v' + (N + 1)]) N++;
171
- this.N = N;
172
- const bi = {};
173
- data.bones.forEach((b, i) => { bi[b.name] = i; });
174
- this.bi = bi;
175
- this.spine = Array.from({ length: N }, (_, i) => skeleton.bones[bi['spine' + i]]);
176
- this.chain = Array.from({ length: N }, (_, i) => bi['spine' + i]);
177
- this.bHead = skeleton.bones[bi.head]; this.bJaw = skeleton.bones[bi.jaw];
178
- this.bTongue = skeleton.bones[bi.tongue]; this.bTip = skeleton.bones[bi.tongueTip]; this.bFang = skeleton.bones[bi.fang];
179
- const bind = (n) => skeleton.bind[bi[n]];
180
- this.bindHead = bind('head'); this.bindJaw = bind('jaw'); this.bindTongue = bind('tongue'); this.bindTip = bind('tongueTip'); this.bindFang = bind('fang');
181
- // ---- dimensions
182
- const s = params.size || 1;
183
- this.s = s;
184
- this.sq = Math.sqrt(s);
185
- this.len = new Float64Array(N); this.sig = new Float64Array(N + 1); this.r = new Float64Array(N + 1); this.hw = new Float64Array(N + 1);
186
- const flat = spec.flat || (params.variant === 'rattlesnake' ? 0.64 : 0.74);
187
- for (let i = 0; i < N; i++) { this.len[i] = J['v' + i].distanceTo(J['v' + (i + 1)]); this.sig[i + 1] = this.sig[i] + this.len[i]; }
188
- for (let i = 0; i <= N; i++) { this.r[i] = J['v' + i].y; this.hw[i] = this.r[i] / flat; }
189
- this.bodyLen = this.sig[N];
190
- // support of each body cross-section toward the ground as a function of roll, measured on the
191
- // bind mesh (vertices skinned mostly to the bone that starts at the joint): the body can lie on its
192
- // side or back on any terrain without sinking in or floating
193
- {
194
- const NA = (this.supN = 48), tab = (this.supTab = new Float32Array((N + 1) * NA)), idx = new Int16Array(data.bones.length).fill(-1);
195
- for (let i = 0; i < N; i++) idx[bi['spine' + i]] = i;
196
- for (let v = 0; v < data.nV; v++) {
197
- if (data.skinWeight[v * 4] < 0.4) continue;
198
- const i = idx[data.skinIndex[v * 4]];
199
- if (i < 0) continue;
200
- const z = data.pos[v * 3 + 2];
201
- // only the body's own section (not a rattle or other rigid ornament beyond the joints)
202
- if (z > J['v' + i].z + 0.3 * this.len[i] || z < J['v' + (i + 1)].z - 0.3 * this.len[i]) continue;
203
- const x = data.pos[v * 3], y = data.pos[v * 3 + 1] - J['v' + i].y;
204
- for (let k = 0; k < NA; k++) {
205
- const ro = -Math.PI + (TAU * k) / NA;
206
- const d = -(x * Math.sin(ro) + y * Math.cos(ro));
207
- if (d > tab[i * NA + k]) tab[i * NA + k] = d;
208
- }
209
- }
210
- for (let k = 0; k < NA; k++) tab[N * NA + k] = tab[(N - 1) * NA + k];
211
- // sections with no data (none expected): the analytic loaf
212
- for (let i = 0; i <= N; i++) if (tab[i * NA + (NA >> 1)] <= 0) for (let k = 0; k < NA; k++) tab[i * NA + k] = J['v' + i].y;
213
- }
214
- this.headLen = J.nose.distanceTo(J.v0);
215
- this.TL = this.bodyLen + this.headLen;
216
- let hwMax = 0; for (let i = 0; i <= N; i++) hwMax = Math.max(hwMax, this.hw[i]);
217
- this.hwMax = hwMax;
218
- this.restCurl = spec.restCurl ?? 1;
219
- // ---- config
220
- const m = (a, b) => ({ ...a, ...(b || {}) });
221
- const speedK = (vk.speedK ?? 1) * this.sq;
222
- this.cfg = {
223
- maxSpeed: vk.maxSpeed !== undefined ? vk.maxSpeed * this.sq : (spec.maxSpeed ?? DEFAULTS.maxSpeed) * speedK,
224
- accel: (spec.accel ?? DEFAULTS.accel) * this.sq, decel: (spec.decel ?? DEFAULTS.decel) * this.sq,
225
- turnRate: (spec.turnRate ?? DEFAULTS.turnRate) / this.sq,
226
- minR: (spec.minTurnRadius ?? DEFAULTS.minTurnRadius) * this.TL,
227
- body: m(DEFAULTS.body, spec.body), tongue: m(m(DEFAULTS.tongue, spec.tongue), vk.tongue), breath: m(DEFAULTS.breath, spec.breath),
228
- actions: spec.actions || {},
229
- strike: m(m(STRIKE, spec.actions?.strike), vk.strike),
230
- rattle: !!vk.rattle,
231
- ampK: vk.ampK ?? 1,
232
- };
233
- // a variant may bring its own gait table / gears (reference speeds, not scaled by speedK)
234
- const G = vk.gaits || (spec.gaits && spec.gaits.length ? spec.gaits : [{ name: 'slither', v: 0, wave: 0.42, amp: 0.07, headLift: 0.008, stab: 0.6 }]);
235
- const gK = vk.gaits ? this.sq : speedK, aK = vk.gaits ? 1 : this.cfg.ampK;
236
- this.gaitRows = G.map((g) => [g.v * gK, g.wave, g.amp * aK, g.headLift * s, g.stab ?? 0.6]);
237
- this.gaitNames = G.map((g) => g.name);
238
- this.gaits = [...new Set(this.gaitNames)];
239
- this.gears = vk.gears ? Object.fromEntries(Object.entries(vk.gears).map(([k, v]) => [k, v * this.sq])) : spec.gears ? Object.fromEntries(Object.entries(spec.gears).map(([k, v]) => [k, v * speedK])) : { slither: 0.25 * speedK };
240
- this.cfg.tailCarry = (vk.tailCarry ?? 0) * s;
241
- this._row = [0, 0, 0, 0];
242
- // ---- bind-space head landmarks (for ground clearance and attachments)
243
- this.v0b = J.v0.clone();
244
- this.noseB = J.nose.clone();
245
- this.chinB = J.jawTip.clone(); this.chinB.y = 0.0005 * s;
246
- this.hingeB = J.jawHinge.clone();
247
- // outer corner of the lower jaw (bind), for ground clearance when the head rolls
248
- this.jawCorner = new THREE.Vector3(0, 0.0005 * s, J.jawHinge.z);
249
- this.jawMid = new THREE.Vector3(0, 0.0005 * s, lerp(J.jawHinge.z, J.jawTip.z, 0.55));
250
- { const jb = bi.jaw; let mx = 0; for (let v = 0; v < data.nV; v++) if (data.skinIndex[v * 4] === jb && data.skinWeight[v * 4] > 0.5 && Math.abs(data.pos[v * 3]) > mx) { mx = Math.abs(data.pos[v * 3]); this.jawCorner.z = data.pos[v * 3 + 2]; } this.jawCorner.x = mx; this.jawMid.x = 0.5 * mx; }
251
- this.tongueBaseB = J.tongueBase.clone(); this.tongueForkB = J.tongueFork.clone();
252
- this.tongueDirB = J.tongueFork.clone().sub(J.tongueBase).normalize();
253
- this.fangBaseB = J.fangBase.clone();
254
- this.tipLen = J.tongueTip.distanceTo(J.tongueFork);
255
- // ---- public contract
256
- this.input = { speed: 0, heading, climb: 0, look: null, follow: null, target: null, targetSpeed: 0.3, crouch: 0, gait: null };
257
- this.pos = new THREE.Vector3();
258
- this.velocity = new THREE.Vector3();
259
- this.heading = heading;
260
- this.speed = 0;
261
- this.yawRate = 0;
262
- this.time = 0;
263
- this.lod = 0;
264
- this.frame = 0; this._acc = 0; this._posed = false;
265
- this.lookTarget = new THREE.Vector3();
266
- this.state = {
267
- position: this.pos, heading, speed: 0, velocity: this.velocity, gait: 'stand', grounded: true,
268
- action: null, posture: 'stand', eyelid: 0, lookTarget: null,
269
- stats: { gait: 'stand', freq: 0, duty: 1 },
270
- legs: [],
271
- contacts: Array.from({ length: N + 1 }, (_, i) => ({ key: 'v' + i, contact: true, position: new THREE.Vector3() })),
272
- };
273
- this.headPose = { position: new THREE.Vector3(), quaternion: new THREE.Quaternion() };
274
- this.debug = { targets: Array.from({ length: 6 }, () => new THREE.Vector3()) };
275
- this.actions = ['idle', 'stand', 'sit', 'lie', 'sleep', 'coil', 'eat', 'drink', 'attack', 'strike', 'jump', 'hit', 'death'];
276
- // ---- locomotion state
277
- this.trail = new Trail(2048);
278
- this.C = new THREE.Vector3();
279
- this.leader = new THREE.Vector3();
280
- this.x = 0; this.phi = 0;
281
- this.A = new Spring(0); this.lam = new Spring(0.42 * this.TL);
282
- this.reseed = true;
283
- this.script = null; // scripted leader path (coil)
284
- this.follow = { init: false, p: new THREE.Vector3() };
285
- this.pursuit = { on: false, psi: 0, dx: 0, dz: 0, follow: false };
286
- // ---- per-joint arrays
287
- const F = (n) => new Float64Array(n);
288
- this.gx = F(N + 1); this.gy = F(N + 1); this.gz = F(N + 1); // base ground points (trail)
289
- this.nx = F(N + 1); this.ny = F(N + 1); this.nz = F(N + 1); // ground normals
290
- this.px = F(N + 1); this.py = F(N + 1); this.pz = F(N + 1); // final axis points
291
- this.hx = F(N + 1); this.hz = F(N + 1);
292
- this.byaw = F(N); this.byawU = F(N); this.blh = F(N); this.bch = F(N); this.bsin = F(N); // base segment yaw, horizontal length, chord, elevation sine
293
- this.lift = F(N + 1); this.dyaw = F(N); this.roll = F(N + 1); this.affect = F(N + 1); this.stack = F(N + 1); this.stackTmp = F(N + 1);
294
- this.grid = { head: new Int16Array(256), next: new Int16Array(N + 1) };
295
- this.lat = Array.from({ length: N }, () => new THREE.Vector3());
296
- this.contact = new Uint8Array(N + 1);
297
- // ---- head & secondary springs
298
- this.hYaw = new Spring(0); this.hPitch = new Spring(0); this.hRoll = new Spring(0);
299
- this.gape = new Spring(0); this.fang = new Spring(0);
300
- this.tongue = { t: 0, dur: 0, next: 1 + this.rand() * 2, osc: 4, ext: new Spring(0), tipS: new Spring(0), pitchS: new Spring(0), pitch: 0, tip: 0, extD: 0 };
301
- this.qHead = new THREE.Quaternion();
302
- this.Tm = new THREE.Matrix4(); // head-rigid transform: bind space -> world
303
- this._headInit = false;
304
- // ---- actions
305
- this.P = makePose(N);
306
- this.layer = new SnakeActions(this);
307
- this.attachmentPoints = this._attachments(data);
308
- this.reset(position || new THREE.Vector3(), heading);
309
- }
310
-
311
- _attachments(data) {
312
- const J = this.J;
313
- const at = (bone, p) => {
314
- const i = data.bones.findIndex((b) => b.name === bone);
315
- if (i < 0) return null;
316
- const inv = this.sk.inverses ? this.sk.inverses[i] : this.sk.bind[i].clone().invert();
317
- return { bone, local: new THREE.Matrix4().makeTranslation(p.x, p.y, p.z).premultiply(inv) };
318
- };
319
- let headP = J.v0.clone().lerp(J.nose, 0.55);
320
- 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); }
321
- const mid = this.N >> 1;
322
- const back = J['v' + mid].clone(); back.y += this.hw[mid];
323
- const out = { mouth: at('head', J.nose.clone().lerp(J.jawTip, 0.5)), head: at('head', headP), back: at('spine' + mid, back) };
324
- for (const k of Object.keys(out)) if (!out[k]) delete out[k];
325
- return out;
326
- }
327
-
328
- // ----------------------------------------------------------------- terrain
329
- terrainH(x, z) { const h = this.ground(x, z); return Number.isFinite(h) ? h : 0; }
330
- terrainN(x, z, out) {
331
- const e = 0.05 * this.s, h0 = this.terrainH(x, z);
332
- out[0] = h0 - this.terrainH(x + e, z); out[1] = e; out[2] = h0 - this.terrainH(x, z + e);
333
- const l = h3(out[0], out[1], out[2]);
334
- out[0] /= l; out[1] /= l; out[2] /= l;
335
- return out;
336
- }
337
-
338
- /** Lay the body straight behind p along `heading` (bind layout). */
339
- reset(p, heading = this.heading) {
340
- const N = this.N, tr = this.trail;
341
- tr.clear();
342
- // rest in a loose S behind the head (curvature integrated backwards along the body)
343
- const extra = 0.25 * this.TL;
344
- const total = this.bodyLen + extra;
345
- const step = 0.01 * this.s;
346
- const nPts = Math.ceil(total / step) + 1;
347
- const X = new Float64Array(nPts), Z = new Float64Array(nPts);
348
- let x = p.x, z = p.z, psi = heading;
349
- const rr = prng(Math.imul(this.seed || 1, 2654435761) ^ 0x5bd1e995), ph = rr() * TAU;
350
- const lam = (0.42 + 0.16 * rr()) * this.TL, amp = (0.3 + 0.45 * rr()) * this.restCurl;
351
- for (let k = 0; k < nPts; k++) {
352
- X[k] = x; Z[k] = z;
353
- const d = k * step;
354
- const a = heading + amp * (Math.sin((TAU * d) / lam + ph) - Math.sin(ph)) * smooth(0.02 * this.TL, 0.15 * this.TL, d) * (1 - smooth(this.bodyLen, total, d));
355
- psi = a;
356
- x -= Math.sin(psi) * step; z -= Math.cos(psi) * step;
357
- }
358
- let s = 0, lx = 0, ly = 0, lz = 0;
359
- for (let k = nPts - 1; k >= 0; k--) {
360
- const y = this.terrainH(X[k], Z[k]);
361
- if (k < nPts - 1) s += h3(X[k] - lx, y - ly, Z[k] - lz);
362
- tr.push(X[k], y, Z[k], s);
363
- lx = X[k]; ly = y; lz = Z[k];
364
- }
365
- this.heading = heading; this.input.heading = heading;
366
- this.speed = 0; this.yawRate = 0; this.velocity.set(0, 0, 0);
367
- this.C.set(p.x, this.terrainH(p.x, p.z), p.z);
368
- this.leader.copy(this.C);
369
- this.pos.copy(this.C);
370
- this.x = 0; this.phi = 0; this.A.reset(0); this.lam.reset(0.42 * this.TL);
371
- this.reseed = true;
372
- this.script = null;
373
- this._headInit = false;
374
- this.follow.init = false;
375
- this.pursuit.on = false;
376
- }
377
-
378
- // ----------------------------------------------------------------- public API
379
- play(name, opts) { return this.layer.play(name, opts || {}); }
380
- stop(name) { this.layer.stop(name); }
381
- setLod(level) { this.lod = clamp(level | 0, 0, 2); return this; }
382
- dispose() { this.layer.clear(); }
383
-
384
- update(dt) {
385
- if (!(dt > 1e-4) || !Number.isFinite(dt)) dt = 1e-4;
386
- dt = Math.min(dt, 1 / 20);
387
- this.time += dt;
388
- this.drive(dt);
389
- this.locomote(dt);
390
- // crowd LOD: the pose is solved at half rate (the simulation still runs every frame)
391
- this.frame = (this.frame + 1) | 0;
392
- if (this.lod < 2 || (this.frame & 1) === 0 || !this._posed) {
393
- this.basePose();
394
- this.layer.update(dt + this._acc);
395
- this.solveBody();
396
- this.solveHead(this._acc + dt);
397
- this._acc = 0;
398
- this._posed = true;
399
- } else { this._acc += dt; }
400
- this.publish();
401
- }
402
-
403
- // ----------------------------------------------------------------- steering
404
- drive(dt) {
405
- const inp = this.input, cfg = this.cfg;
406
- this.wantSpeed = clamp(inp.speed || 0, 0, cfg.maxSpeed);
407
- this.wantHeading = inp.heading ?? this.heading;
408
- if (inp.target && !inp.follow) {
409
- const dx = inp.target.x - this.pos.x, dz = inp.target.z - this.pos.z;
410
- const d = h2(dx, dz);
411
- const stopR = 0.08 * this.TL + 0.1 * this.speed;
412
- if (d < stopR) { inp.target = null; inp.speed = 0; this.wantSpeed = 0; this.emit('arrive', {}); }
413
- else {
414
- const want = Math.atan2(dx, dz);
415
- inp.heading = want; this.wantHeading = want;
416
- const brake = Math.sqrt(Math.max(0, 2 * cfg.decel * 0.7 * (d - stopR)));
417
- this.wantSpeed = Math.min(inp.targetSpeed || 0.3, cfg.maxSpeed, brake + 0.03);
418
- }
419
- }
420
- if (this.layer.holdsStill()) {
421
- if ((this.wantSpeed > 0.01 || inp.follow) && !this.layer.dead()) this.layer.requestStand();
422
- this.wantSpeed = 0;
423
- }
424
- }
425
-
426
- gaitAt(v) {
427
- const R = this.gaitRows;
428
- let i = 0;
429
- while (i < R.length - 2 && v > R[i + 1][0]) i++;
430
- const a = R[i], b = R[Math.min(i + 1, R.length - 1)];
431
- const t = a === b ? 0 : smooth(0, 1, (v - a[0]) / (b[0] - a[0]));
432
- const o = this._row;
433
- for (let k = 0; k < 4; k++) o[k] = lerp(a[k + 1], b[k + 1], t);
434
- this.gaitName = t < 0.5 ? this.gaitNames[i] : this.gaitNames[Math.min(i + 1, R.length - 1)];
435
- return o;
436
- }
437
-
438
- // ----------------------------------------------------------------- locomotion
439
- locomote(dt) {
440
- const inp = this.input, cfg = this.cfg, P = this.P;
441
- const tr = this.trail;
442
- const spacing = 0.005 * this.s;
443
- if (this.script) { this.runScript(dt, spacing); return; }
444
- const following = !!inp.follow && !this.layer.holdsStill();
445
- let v, dx;
446
- if (following) {
447
- const f = inp.follow, fv = f.velocity;
448
- const vx = fv ? fv.x : 0, vz = fv ? fv.z : 0;
449
- const sp = h2(vx, vz);
450
- const fp = this.follow.p;
451
- if (!this.follow.init) { fp.set(f.position.x, 0, f.position.z); this.follow.init = true; this.reseed = false; this.A.reset(0); }
452
- // alpha-beta filter: the given velocity carries it, the position pulls gently (physics jitter)
453
- const ox = fp.x, oz = fp.z;
454
- fp.x += vx * dt; fp.z += vz * dt;
455
- const k = 1 - Math.exp(-dt / 0.06);
456
- fp.x += (f.position.x - fp.x) * k; fp.z += (f.position.z - fp.z) * k;
457
- v = sp;
458
- let hT = f.heading ?? (sp > 0.02 * this.sq ? Math.atan2(vx, vz) : this.heading);
459
- this.turn(hT, v, dt);
460
- const Fx = Math.sin(this.heading), Fz = Math.cos(this.heading);
461
- dx = Math.max(0, (fp.x - ox) * Fx + (fp.z - oz) * Fz);
462
- this.C.x = fp.x; this.C.z = fp.z;
463
- this.speed = v;
464
- this.velocity.set(vx, 0, vz);
465
- } else {
466
- this.follow.init = false;
467
- const vT = this.wantSpeed * P.speedScale;
468
- const acc = vT > this.speed ? cfg.accel : cfg.decel;
469
- this.speed += clamp(vT - this.speed, -acc * dt, acc * dt);
470
- if (this.speed < 1e-4 && vT <= 0) this.speed = 0;
471
- v = this.speed;
472
- if (v > 0 && this.reseed) this.startMoving();
473
- this.turn(this.wantHeading, v, dt);
474
- dx = v * dt;
475
- const Fx = Math.sin(this.heading), Fz = Math.cos(this.heading);
476
- this.C.x += Fx * dx; this.C.z += Fz * dx;
477
- this.velocity.set(Fx * v, 0, Fz * v);
478
- }
479
- this.C.y = this.terrainH(this.C.x, this.C.z);
480
- this.pos.copy(this.C);
481
- const row = this.gaitAt(v / this.sq);
482
- this.gait = row;
483
- if (dx > 0 || following) {
484
- const lamT = row[0] * this.TL, ampT = row[1] * this.TL;
485
- if (dx > 0) {
486
- const wd = TAU / Math.max(0.05, this.lam.x);
487
- this.A.step(ampT, 1.6 * wd, dx);
488
- this.lam.step(lamT, 1.6 * wd, dx);
489
- this.phi += (TAU * dx) / this.lam.x;
490
- this.x += dx;
491
- }
492
- const y = this.A.x * Math.sin(this.phi);
493
- const Lx = Math.cos(this.heading), Lz = -Math.sin(this.heading);
494
- const Dx = this.C.x + Lx * y, Dz = this.C.z + Lz * y;
495
- // The head tracks its ideal place D on the wave with a curvature-bounded steering law (heading
496
- // relaxes toward the ideal path's tangent over a short distance, plus lateral and along-track
497
- // corrections), so the laid path is smooth and never tighter than the body can bend, whatever
498
- // the input does (sharp turns, a misaligned follow start, physics jitter).
499
- const pu = this.pursuit;
500
- if (!pu.on) { pu.on = true; pu.psi = this.neckYaw(); pu.k = 0; pu.dx = Dx; pu.dz = Dz; }
501
- const tdx = Dx - pu.dx, tdz = Dz - pu.dz;
502
- pu.dx = Dx; pu.dz = Dz;
503
- const dD = h2(tdx, tdz);
504
- const psiD = dD > 1e-7 ? Math.atan2(tdx, tdz) : pu.psi;
505
- const ex = Dx - this.leader.x, ez = Dz - this.leader.z;
506
- const Fx = Math.sin(pu.psi), Fz = Math.cos(pu.psi);
507
- const lat = ex * Fz - ez * Fx, along = ex * Fx + ez * Fz;
508
- const TL = this.TL;
509
- const psiT = psiD + clamp(lat / (0.05 * TL), -0.7, 0.7);
510
- const ds = Math.max(0, dD * clamp(Math.cos(angDiff(pu.psi, psiD)), 0.25, 1) + clamp(along, -0.03 * TL, 0.06 * TL) * 0.12);
511
- const kMax = 1 / (0.036 * TL), kRate = kMax / (0.035 * TL);
512
- const nSub = Math.max(1, Math.ceil(ds / (0.004 * this.s)));
513
- let lx = this.leader.x, lz = this.leader.z;
514
- for (let k = 0; k < nSub; k++) {
515
- const h = ds / nSub;
516
- // curvature command with a smooth saturation, and a bounded rate of change along the path
517
- const kc = kMax * Math.tanh(angDiff(pu.psi, psiT) / (0.03 * TL * kMax));
518
- pu.k += clamp(kc - pu.k, -kRate * h, kRate * h);
519
- pu.psi += pu.k * h;
520
- lx += Math.sin(pu.psi) * h; lz += Math.cos(pu.psi) * h;
521
- }
522
- this.leader.set(lx, this.terrainH(lx, lz), lz);
523
- if (!following && pu.follow) { pu.follow = false; }
524
- pu.follow = following;
525
- tr.lead(this.leader.x, this.leader.y, this.leader.z, spacing);
526
- }
527
- this.freq = v > 1e-3 ? v / this.lam.x : 0;
528
- }
529
-
530
- // limited yaw rate: never tighter than the minimum radius, and slow enough that the leader keeps
531
- // moving forward along its path (the wave offset swings with the heading)
532
- turn(hT, v, dt) {
533
- const cfg = this.cfg;
534
- const wMax = Math.min(cfg.turnRate, v / cfg.minR, (0.35 * v) / Math.max(1e-3, Math.abs(this.A.x)));
535
- const wT = clamp(angDiff(this.heading, hT) * 2.2, -wMax, wMax);
536
- this.yawRate += clamp(wT - this.yawRate, -4 * dt, 4 * dt);
537
- this.yawRate = clamp(this.yawRate, -wMax, wMax);
538
- this.heading += this.yawRate * dt;
539
- }
540
-
541
- neckYaw() {
542
- const tr = this.trail, sl = tr.lastS;
543
- tr.sample(sl, S3); const ax = S3[0], az = S3[2];
544
- tr.sample(sl - 0.04 * this.TL, S3);
545
- return Math.atan2(ax - S3[0], az - S3[2]);
546
- }
547
-
548
- // a new path leaves exactly along the neck: centre on the leader, heading along the neck, no wave yet
549
- startMoving() {
550
- const tr = this.trail, sl = tr.lastS;
551
- tr.sample(sl, S3); const ax = S3[0], ay = S3[1], az = S3[2];
552
- tr.sample(sl - 0.04 * this.TL, S3);
553
- this.heading = Math.atan2(ax - S3[0], az - S3[2]);
554
- this.yawRate = 0;
555
- this.C.set(ax, ay, az);
556
- this.leader.set(ax, ay, az);
557
- this.A.reset(0);
558
- this.phi = 0;
559
- this.reseed = false;
560
- this.pursuit.on = false;
561
- }
562
-
563
- // scripted leader path (coil): curvature integrated along arc length
564
- startScript(sc) {
565
- const tr = this.trail, sl = tr.lastS;
566
- tr.sample(sl, S3); const ax = S3[0], az = S3[2];
567
- tr.sample(sl - 0.03 * this.TL, S3); const bx = S3[0], bz = S3[2];
568
- tr.sample(sl - 0.06 * this.TL, S3); const cx = S3[0], cz = S3[2];
569
- const y1 = Math.atan2(ax - bx, az - bz), y0 = Math.atan2(bx - cx, bz - cz);
570
- sc.psi = y1;
571
- sc.k0 = clamp(angDiff(y0, y1) / (0.03 * this.TL), -30, 30);
572
- sc.u = 0;
573
- sc.x = ax; sc.z = az;
574
- this.script = sc;
575
- this.speed = 0; this.yawRate = 0;
576
- }
577
-
578
- runScript(dt, spacing) {
579
- const sc = this.script;
580
- const rem = sc.total - sc.u;
581
- const v = sc.speed * (0.25 + 0.75 * smooth(0, 0.05 * this.TL, sc.u)) * Math.max(0.12, smooth(0, 0.08 * this.TL, rem));
582
- const du = Math.min(rem, v * dt);
583
- const n = Math.max(1, Math.ceil(du / (0.004 * this.s)));
584
- for (let k = 0; k < n; k++) {
585
- const h = du / n;
586
- const kap = lerp(sc.k0, sc.kappa(sc.u + h * 0.5), smooth(0, 0.07 * this.TL, sc.u));
587
- sc.psi += kap * h;
588
- sc.x += Math.sin(sc.psi) * h; sc.z += Math.cos(sc.psi) * h;
589
- sc.u += h;
590
- }
591
- this.leader.set(sc.x, this.terrainH(sc.x, sc.z), sc.z);
592
- // the head leads: the reported position follows it while the snake coils up
593
- this.C.copy(this.leader); this.pos.copy(this.C);
594
- this.trail.lead(this.leader.x, this.leader.y, this.leader.z, spacing);
595
- this.velocity.set(0, 0, 0);
596
- this.freq = 0;
597
- if (sc.u >= sc.total - 1e-6) { this.script = null; sc.done = true; this.reseed = true; this.pursuit.on = false; }
598
- }
599
-
600
- // ----------------------------------------------------------------- base pose: joints on the trail
601
- basePose() {
602
- const N = this.N, tr = this.trail, sl = tr.lastS, lod = this.lod;
603
- // nothing moved (the trail is unchanged and the terrain is static): the base pose stands
604
- if (tr.ver === this._baseVer && lod === this._baseLod) return;
605
- this._baseVer = tr.ver; this._baseLod = lod;
606
- let hint = tr.n - 2;
607
- const gx = this.gx, gy = this.gy, gz = this.gz, sig = this.sig;
608
- // crowd LOD: every other joint from the trail, the ones between at the chord midpoint
609
- const stepI = lod >= 2 ? 2 : 1;
610
- for (let i = 0; i <= N; i += stepI) {
611
- tr.sample(sl - sig[i], S3, hint);
612
- hint = tr.hit;
613
- gx[i] = S3[0]; gz[i] = S3[2];
614
- gy[i] = this.terrainH(S3[0], S3[2]);
615
- if (lod >= 2) { this.nx[i] = 0; this.ny[i] = 1; this.nz[i] = 0; }
616
- else { this.terrainN(S3[0], S3[2], S3); this.nx[i] = S3[0]; this.ny[i] = S3[1]; this.nz[i] = S3[2]; }
617
- if (stepI === 2 && i === N - 1) i--; // make sure the tail tip is sampled
618
- }
619
- if (stepI === 2) {
620
- for (let i = 1; i < N; i += 2) {
621
- if (i + 1 > N) break;
622
- const t = (sig[i] - sig[i - 1]) / (sig[i + 1] - sig[i - 1]);
623
- gx[i] = gx[i - 1] + (gx[i + 1] - gx[i - 1]) * t; gz[i] = gz[i - 1] + (gz[i + 1] - gz[i - 1]) * t;
624
- gy[i] = this.terrainH(gx[i], gz[i]);
625
- this.nx[i] = 0; this.ny[i] = 1; this.nz[i] = 0;
626
- }
627
- }
628
- for (let i = 0; i < N; i++) {
629
- const dx = this.gx[i] - this.gx[i + 1], dz = this.gz[i] - this.gz[i + 1], dy = this.gy[i] - this.gy[i + 1];
630
- const lh = h2(dx, dz);
631
- this.byaw[i] = lh > 1e-9 ? Math.atan2(dx, dz) : i > 0 ? this.byaw[i - 1] : this.heading;
632
- this.blh[i] = lh;
633
- const c = h2(lh, dy);
634
- this.bch[i] = c;
635
- this.bsin[i] = c > 1e-9 ? dy / c : 0;
636
- // yaw unwrapped along the chain (a coiled body turns through more than pi)
637
- this.byawU[i] = i === 0 ? this.byaw[0] : this.byawU[i - 1] + angDiff(this.byaw[i - 1], this.byaw[i]);
638
- }
639
- }
640
-
641
- // ----------------------------------------------------------------- pose integration and frames
642
- solveBody() {
643
- const N = this.N, P = this.P;
644
- const lift = P.lift, dyaw = P.dyaw, roll = P.roll;
645
- let any = false;
646
- for (let i = 0; i < N; i++) if (Math.abs(dyaw[i]) > 1e-7) { any = true; break; }
647
- if (!any) for (let i = 0; i <= N; i++) if (lift[i] > 1e-7) { any = true; break; }
648
- const hx = this.hx, hz = this.hz;
649
- if (any) {
650
- hx[0] = this.gx[0]; hz[0] = this.gz[0];
651
- for (let i = 0; i < N; i++) {
652
- const c = this.bch[i];
653
- const sn = clamp(this.bsin[i] + (lift[i] - lift[i + 1]) / Math.max(c, 1e-9), -0.995, 0.995);
654
- const lh = c * Math.sqrt(1 - sn * sn);
655
- const yw = this.byaw[i] + dyaw[i];
656
- hx[i + 1] = hx[i] - Math.sin(yw) * lh;
657
- hz[i + 1] = hz[i] - Math.cos(yw) * lh;
658
- }
659
- // keep the joints the actions leave alone where they were
660
- let sw = 0, tx = 0, tz = 0;
661
- for (let i = 0; i <= N; i++) {
662
- const w = Math.max(1e-3, 1 - clamp(this.affect[i], 0, 1));
663
- sw += w; tx += w * (this.gx[i] - hx[i]); tz += w * (this.gz[i] - hz[i]);
664
- }
665
- tx /= sw; tz /= sw;
666
- for (let i = 0; i <= N; i++) { hx[i] += tx; hz[i] += tz; }
667
- } else {
668
- for (let i = 0; i <= N; i++) { hx[i] = this.gx[i]; hz[i] = this.gz[i]; }
669
- }
670
- // heights from the terrain, axis offset along the ground normal (grows toward the half width
671
- // when the body is rolled onto its back)
672
- const lod = this.lod;
673
- for (let i = 0; i <= N; i++) {
674
- const x = hx[i], z = hz[i];
675
- let gy, nx, ny, nz;
676
- if (any) {
677
- gy = this.terrainH(x, z);
678
- if (lod >= 2) { nx = 0; ny = 1; nz = 0; } else { this.terrainN(x, z, S3); nx = S3[0]; ny = S3[1]; nz = S3[2]; }
679
- } else { gy = this.gy[i]; nx = this.nx[i]; ny = this.ny[i]; nz = this.nz[i]; }
680
- const ro = roll[i];
681
- // support of the cross-section toward the ground when rolled: flat belly (axis height r) when
682
- // upright, the half width on its side, the round back (~half width) when belly-up
683
- const rr = ro === 0 ? this.r[i] : this.support(i, ro);
684
- this.px[i] = x + nx * rr; this.pz[i] = z + nz * rr;
685
- this.py[i] = gy + ny * rr + lift[i];
686
- this.stack[i] = 0;
687
- }
688
- if (lod < 2 && P.stackW > 0) this.selfContact();
689
- for (let i = 0; i <= N; i++) this.py[i] += this.stack[i];
690
- // on the ground: this joint and its neighbours are not lifted (a joint next to a raised one is
691
- // the start of the rise)
692
- const tol = 0.0015 * this.s;
693
- for (let i = 0; i <= N; i++) {
694
- const a = lift[i] + this.stack[i], b = i > 0 ? lift[i - 1] + this.stack[i - 1] : 0, c = i < N ? lift[i + 1] + this.stack[i + 1] : 0;
695
- this.contact[i] = a < tol && b < tol && c < tol && !(this.affect[i] > 0.05 && lift[i] > 1e-5) ? 1 : 0;
696
- }
697
- // frames: lateral axis from the ground normal (belly down), transported where a segment stands
698
- // up steeply, then rolled
699
- const up = S3;
700
- let plx = 0, ply = 0, plz = 0, havePrev = false;
701
- for (let i = N - 1; i >= 0; i--) {
702
- const fx = this.px[i] - this.px[i + 1], fy = this.py[i] - this.py[i + 1], fz = this.pz[i] - this.pz[i + 1];
703
- const fl = h3(fx, fy, fz) || 1;
704
- const ux = fx / fl, uy = fy / fl, uz = fz / fl;
705
- const gnx = (this.nx[i] + this.nx[i + 1]) * 0.5, gny = (this.ny[i] + this.ny[i + 1]) * 0.5, gnz = (this.nz[i] + this.nz[i + 1]) * 0.5;
706
- // left = up x forward
707
- let lx = gny * uz - gnz * uy, ly = gnz * ux - gnx * uz, lz = gnx * uy - gny * ux;
708
- let ll = h3(lx, ly, lz);
709
- if (havePrev) {
710
- const d = plx * ux + ply * uy + plz * uz;
711
- let tx = plx - ux * d, ty = ply - uy * d, tz = plz - uz * d;
712
- const tl = h3(tx, ty, tz) || 1;
713
- tx /= tl; ty /= tl; tz /= tl;
714
- const w = smooth(0.25, 0.6, ll);
715
- if (ll > 1e-9) { lx /= ll; ly /= ll; lz /= ll; }
716
- lx = lerp(tx, lx, w); ly = lerp(ty, ly, w); lz = lerp(tz, lz, w);
717
- ll = h3(lx, ly, lz);
718
- }
719
- if (ll < 1e-9) { lx = Math.cos(this.heading); ly = 0; lz = -Math.sin(this.heading); ll = 1; }
720
- lx /= ll; ly /= ll; lz /= ll;
721
- plx = lx; ply = ly; plz = lz; havePrev = true;
722
- // roll about the bone (forward axis)
723
- const ro = (roll[i] + roll[i + 1]) * 0.5;
724
- const L = this.lat[i];
725
- if (Math.abs(ro) > 1e-6) {
726
- const c = Math.cos(ro), s = Math.sin(ro);
727
- // rotate lat about u by ro (Rodrigues; lat is perpendicular to u)
728
- const cxv = uy * lz - uz * ly, cyv = uz * lx - ux * lz, czv = ux * ly - uy * lx;
729
- L.set(lx * c + cxv * s, ly * c + cyv * s, lz * c + czv * s);
730
- } else L.set(lx, ly, lz);
731
- }
732
- for (let i = 0; i < N; i++) {
733
- _v.set(this.px[i], this.py[i], this.pz[i]);
734
- _v2.set(this.px[i + 1] - this.px[i], this.py[i + 1] - this.py[i], this.pz[i + 1] - this.pz[i]);
735
- writeFrame(this.spine[i].matrixWorld, _v, _v2, this.lat[i]);
736
- }
737
- void up;
738
- }
739
-
740
- // cross-section support toward the ground for roll ro (table, linear in angle)
741
- support(i, ro) {
742
- const NA = this.supN;
743
- let a = ((ro + Math.PI) / TAU) * NA;
744
- a -= Math.floor(a / NA) * NA;
745
- const k0 = Math.floor(a), k1 = (k0 + 1) % NA, t = a - k0;
746
- const T = this.supTab, o = i * NA;
747
- return T[o + k0] + (T[o + k1] - T[o + k0]) * t;
748
- }
749
-
750
- // a part of the body lying across another rides on top of it (anterior over posterior)
751
- selfContact() {
752
- const N = this.N, px = this.px, pz = this.pz, py = this.py, st = this.stack, hw = this.hw, sig = this.sig;
753
- // spatial hash of the joints already processed (posterior ones), cell >= the widest contact
754
- const G = this.grid, cs = 2 * this.hwMax, inv = 1 / cs;
755
- G.head.fill(-1);
756
- let anyHit = false;
757
- const gap = 0.02 * this.TL;
758
- for (let i = N; i >= 0; i--) {
759
- let need = 0;
760
- const cx = Math.floor(px[i] * inv), cz = Math.floor(pz[i] * inv);
761
- for (let ox = -1; ox <= 1; ox++) for (let oz = -1; oz <= 1; oz++) {
762
- for (let j = G.head[((cx + ox) * 73856093 ^ (cz + oz) * 19349663) & 255]; j >= 0; j = G.next[j]) {
763
- if (sig[j] - sig[i] < 2.5 * (hw[i] + hw[j]) + gap) continue;
764
- const D = (hw[i] + hw[j]) * 0.92;
765
- const dx = px[i] - px[j], dz = pz[i] - pz[j];
766
- const d2 = dx * dx + dz * dz;
767
- if (d2 >= D * D) continue;
768
- const q = 1 - d2 / (D * D);
769
- const h = (py[j] + st[j] + (this.r[i] + hw[j]) * q * (2 - q) - py[i]) * 1.3;
770
- if (h > need) need = h;
771
- }
772
- }
773
- st[i] = need;
774
- if (need > 0) anyHit = true;
775
- const k = (cx * 73856093 ^ cz * 19349663) & 255;
776
- G.next[i] = G.head[k]; G.head[k] = i;
777
- }
778
- if (!anyHit) return;
779
- // ramps along the chain so the climbing part forms an arch, not a step: max-plus dilation, then
780
- // a light smoothing (the requirement was padded for it)
781
- const slope = 0.2;
782
- for (let i = 1; i <= N; i++) st[i] = Math.max(st[i], st[i - 1] - slope * this.len[i - 1]);
783
- for (let i = N - 1; i >= 0; i--) st[i] = Math.max(st[i], st[i + 1] - slope * this.len[i]);
784
- const tmp = this.stackTmp;
785
- for (let it = 0; it < 3; it++) {
786
- for (let i = 0; i <= N; i++) tmp[i] = 0.25 * st[Math.max(0, i - 1)] + 0.5 * st[i] + 0.25 * st[Math.min(N, i + 1)];
787
- for (let i = 0; i <= N; i++) st[i] = tmp[i];
788
- }
789
- const k = this.P.stackW;
790
- for (let i = 0; i <= N; i++) st[i] *= k;
791
- }
792
-
793
- // ----------------------------------------------------------------- head, jaw, tongue, fangs
794
- solveHead(dt) {
795
- const P = this.P, inp = this.input;
796
- const px = this.px, py = this.py, pz = this.pz;
797
- const s = this.s;
798
- // neck direction (segment 0) and base head orientation
799
- const fx = px[0] - px[1], fy = py[0] - py[1], fz = pz[0] - pz[1];
800
- const fh = h2(fx, fz);
801
- const segYaw = fh > 1e-9 ? Math.atan2(fx, fz) : this.heading;
802
- const segEl = Math.atan2(fy, Math.max(fh, 1e-9));
803
- const moving = smooth(0.005, 0.05, this.speed / this.sq) * (this.script ? 0 : 1);
804
- const stab = (this.gait ? this.gait[3] : 0.6) * moving * P.stabW;
805
- let yawT = segYaw + clamp(angDiff(segYaw, this.heading), -0.8, 0.8) * stab;
806
- // look target: turn the head the rest of the way
807
- const look = inp.look || P.look;
808
- if (look && P.lookW > 0.01) {
809
- const want = Math.atan2(look.x - px[0], look.z - pz[0]);
810
- yawT += clamp(angDiff(yawT, want), -0.7, 0.7) * P.lookW;
811
- this.lookTarget.copy(look);
812
- this.state.lookTarget = this.lookTarget;
813
- } else this.state.lookTarget = null;
814
- yawT += P.headYaw;
815
- // elevation: level with the ground under the head when lying, level in space when raised
816
- const cyaw = Math.cos(yawT), syaw = Math.sin(yawT);
817
- const g0 = this.terrainH(px[0], pz[0]);
818
- const gN = this.terrainH(px[0] + syaw * this.headLen, pz[0] + cyaw * this.headLen);
819
- const groundEl = Math.atan2(gN - g0, this.headLen);
820
- const raised = smooth(0.003 * s, 0.02 * s, py[0] - g0 - this.r[0]);
821
- let elT = lerp(groundEl, lerp(segEl, 0, 0.75), raised) + P.headPitch;
822
- if (look && P.lookW > 0.01) {
823
- const dy = look.y - (py[0] + 0.3 * this.headLen), dd = h2(look.x - px[0], look.z - pz[0]);
824
- elT += clamp(Math.atan2(dy, dd) * 0.6, -0.4, 0.5) * P.lookW * raised;
825
- }
826
- if (!this._headInit) { this.hYaw.reset(0); this.hPitch.reset(elT); this.hRoll.reset(0); this._headInit = true; }
827
- // yaw spring works on the offset from the neck direction (no wrap-around)
828
- const yawOff = this.hYaw.step(angDiff(segYaw, yawT), P.headOmega, dt);
829
- const yaw = segYaw + yawOff;
830
- let el = this.hPitch.step(elT, P.headOmega, dt);
831
- const rollT = (P.roll[0] + P.roll[1]) * 0.5 + P.headRoll;
832
- const rl = this.hRoll.step(rollT, P.headOmega, dt);
833
- // head rotation (bind space faces +Z)
834
- const q = this.qHead;
835
- q.setFromAxisAngle(_Y, yaw);
836
- _q.setFromAxisAngle(_X, -el); q.multiply(_q);
837
- _q.setFromAxisAngle(_Z, rl); q.multiply(_q);
838
- const ga = this.gape.step(P.gape, P.gapeOmega, dt);
839
- // snout and chin (with the jaw's gape) never enter the ground: pitch up about v0 just enough
840
- for (let k = 0; k < 4; k++) {
841
- const b = k === 0 ? this.noseB : k === 1 ? this.chinB : this.jawMid;
842
- _v.copy(b);
843
- if (k === 3) _v.x = -_v.x;
844
- if (k >= 1) { rotAbout(_m1, this.hingeB, _X, ga); _v.applyMatrix4(_m1); }
845
- _v.sub(this.v0b).applyQuaternion(q);
846
- const wy = py[0] + _v.y, floor = this.terrainH(px[0] + _v.x, pz[0] + _v.z) + (k === 0 ? 0.001 * s : 0.0003 * s);
847
- if (wy < floor) {
848
- const hl = h2(_v.x, _v.z) || 1e-6;
849
- const add = Math.atan2(floor - wy, hl);
850
- el += add;
851
- q.setFromAxisAngle(_Y, yaw); _q.setFromAxisAngle(_X, -el); q.multiply(_q); _q.setFromAxisAngle(_Z, rl); q.multiply(_q);
852
- }
853
- }
854
- if (el > this.hPitch.x) { this.hPitch.x = el; this.hPitch.v = Math.max(0, this.hPitch.v); }
855
- // a rolled head (limp, dead) must not push the corners of its open jaw into the ground
856
- let rr = rl;
857
- for (let it = 0; it < 5 && Math.abs(rr) > 1e-3; it++) {
858
- let pen = false;
859
- for (let k = 0; k < 2 && !pen; k++) {
860
- _v.copy(this.jawCorner).setX(k ? -this.jawCorner.x : this.jawCorner.x);
861
- rotAbout(_m1, this.hingeB, _X, ga); _v.applyMatrix4(_m1);
862
- _v.sub(this.v0b).applyQuaternion(q);
863
- if (py[0] + _v.y < this.terrainH(px[0] + _v.x, pz[0] + _v.z)) pen = true;
864
- }
865
- if (!pen) break;
866
- rr *= 0.6;
867
- q.setFromAxisAngle(_Y, yaw); _q.setFromAxisAngle(_X, -el); q.multiply(_q); _q.setFromAxisAngle(_Z, rr); q.multiply(_q);
868
- }
869
- // head-rigid transform T = translate(v0 world) * R * translate(-v0 bind)
870
- const T = this.Tm;
871
- T.makeRotationFromQuaternion(q);
872
- _v.copy(this.v0b).applyQuaternion(q);
873
- T.setPosition(px[0] - _v.x, py[0] - _v.y, pz[0] - _v.z);
874
- this.bHead.matrixWorld.multiplyMatrices(T, this.bindHead);
875
- this.headPose.position.set(px[0], py[0], pz[0]);
876
- this.headPose.quaternion.copy(q);
877
- // jaw: gape about the quadrate hinge (bind X axis)
878
- rotAbout(_m1, this.hingeB, _X, ga);
879
- _m2.multiplyMatrices(T, _m1);
880
- this.bJaw.matrixWorld.multiplyMatrices(_m2, this.bindJaw);
881
- // fangs swing forward from the palate as the mouth opens (vipers)
882
- const fe = this.fang.step(this.viper ? smooth(0.35, 0.9, ga / Math.max(0.3, this.cfg.strike.gape * Math.PI / 180)) * 1.75 + P.fang : 0, 18, dt);
883
- rotAbout(_m1, this.fangBaseB, _X, -fe);
884
- _m3.multiplyMatrices(T, _m1);
885
- this.bFang.matrixWorld.multiplyMatrices(_m3, this.bindFang);
886
- // tongue: slides out of its sheath (with the jaw), dips, the fork tips oscillate
887
- this.solveTongue(dt, ga);
888
- const tg = this.tongue;
889
- _m3.makeTranslation(this.tongueDirB.x * tg.extD, this.tongueDirB.y * tg.extD, this.tongueDirB.z * tg.extD);
890
- rotAbout(_m1, this.tongueBaseB, _X, tg.pitch);
891
- _m3.multiply(_m1);
892
- _m2.multiply(_m3); // T * jaw * tongue
893
- this.bTongue.matrixWorld.multiplyMatrices(_m2, this.bindTongue);
894
- rotAbout(_m1, this.tongueForkB, _X, tg.tip);
895
- _m2.multiply(_m1);
896
- this.bTip.matrixWorld.multiplyMatrices(_m2, this.bindTip);
897
- // the flicking tips may brush the ground but never go through it: lift the whole tongue
898
- const e = this.bTip.matrixWorld.elements, Lt = this.tipLen;
899
- const tx = e[12] + e[4] * Lt, ty = e[13] + e[5] * Lt, tz = e[14] + e[6] * Lt;
900
- const lo = Math.min(ty, e[13]) - this.terrainH(tx, tz) - 0.0008 * this.s;
901
- // the lift is kept (it only relaxes slowly), so the tongue never snaps back into the ground
902
- const need = lo < 0 && tg.extD > 1e-5 ? -lo : 0;
903
- tg.lift = Math.max(need, (tg.lift || 0) * Math.exp(-dt * 6));
904
- if (tg.lift > 0) {
905
- this.bTongue.matrixWorld.elements[13] += tg.lift;
906
- e[13] += tg.lift;
907
- }
908
- }
909
-
910
- solveTongue(dt, gape) {
911
- const tg = this.tongue, P = this.P, cfg = this.cfg.tongue;
912
- const allow = P.tongue * (gape < 0.25 ? 1 : 0);
913
- tg.next -= dt * (0.4 + 0.6 * P.tongueRate);
914
- if (tg.dur <= 0 && tg.next <= 0 && allow > 0.5) {
915
- tg.dur = 0.35 + this.rand() * 0.45;
916
- tg.t = 0;
917
- tg.osc = cfg.flicks[0] + this.rand() * (cfg.flicks[1] - cfg.flicks[0]);
918
- tg.next = (1 / Math.max(0.05, cfg.rate)) * (0.5 + this.rand());
919
- }
920
- let ext = 0, tip = 0, pitch = 0;
921
- const amp = cfg.amp ?? 1; // a longer tongue whips its tips farther for the same angle
922
- if (tg.dur > 0) {
923
- tg.t += dt;
924
- const t = tg.t, d = tg.dur;
925
- ext = smooth(0, 0.09, t) * (1 - smooth(d - 0.09, d, t));
926
- // tips oscillate at the species' flick frequency, ramped in and out over the flick
927
- const env = Math.sin(clamp((t - 0.05) / (d - 0.1), 0, 1) * Math.PI);
928
- const osc = Math.sin(TAU * cfg.freq * Math.max(0, t - 0.05)) * env;
929
- tip = 0.45 * amp * osc * ext;
930
- pitch = (0.16 + 0.18 * amp * osc) * ext;
931
- if (t >= d) tg.dur = 0;
932
- if (allow < 0.5 && t < d - 0.09) tg.t = Math.max(t, d - 0.09); // cut short: retract now
933
- }
934
- ext = Math.max(ext, P.tongueOut);
935
- // springs keep the tongue's own motion smooth whatever starts or cuts a flick
936
- const w = 55;
937
- tg.ext.step(ext, w, dt);
938
- tg.tipS.step(tip, w, dt);
939
- tg.pitchS.step(pitch + 0.25 * P.tongueOut + 0.08 * (amp < 1 ? 1 : 0) * ext, w, dt);
940
- const e = clamp(tg.ext.x, 0, 1);
941
- tg.extD = e * cfg.length * this.headLen;
942
- tg.tip = tg.tipS.x;
943
- tg.pitch = tg.pitchS.x;
944
- }
945
-
946
- // ----------------------------------------------------------------- state
947
- publish() {
948
- const st = this.state, N = this.N;
949
- st.heading = this.heading;
950
- st.speed = this.speed;
951
- const moving = this.speed > 0.004 * this.sq || this.script;
952
- st.gait = moving ? (this.script ? 'slither' : this.gaitName) : 'stand';
953
- st.stats.gait = st.gait; st.stats.freq = this.freq || 0; st.stats.duty = 1;
954
- st.action = this.layer.current();
955
- st.posture = this.layer.posture;
956
- for (let i = 0; i <= N; i++) {
957
- const c = st.contacts[i];
958
- c.contact = !!this.contact[i];
959
- c.position.set(this.hx[i], this.terrainH(this.hx[i], this.hz[i]), this.hz[i]);
960
- }
961
- const dbg = this.debug.targets;
962
- dbg[0].copy(this.leader); dbg[1].copy(this.C);
963
- }
964
- }
965
-
966
- // rotation by angle a about an axis through point c (bind space) -> out
967
- function rotAbout(out, c, axis, a) {
968
- _q2.setFromAxisAngle(axis, a);
969
- out.makeRotationFromQuaternion(_q2);
970
- _v4.copy(c).applyQuaternion(_q2);
971
- out.setPosition(c.x - _v4.x, c.y - _v4.y, c.z - _v4.z);
972
- return out;
973
- }
974
-
975
- // ------------------------------------------------------------------------------------------------
976
- // Pose parameters (filled by the action layer every frame)
977
- // ------------------------------------------------------------------------------------------------
978
- function makePose(N) {
979
- return {
980
- lift: new Float64Array(N + 1), dyaw: new Float64Array(N), roll: new Float64Array(N + 1),
981
- speedScale: 1, headYaw: 0, headPitch: 0, headRoll: 0, headOmega: 9, stabW: 1,
982
- gape: 0, gapeOmega: 14, fang: 0, tongue: 1, tongueRate: 1, tongueOut: 0,
983
- look: null, lookW: 1, stackW: 1,
984
- };
985
- }
986
- function resetPose(P, affect) {
987
- P.lift.fill(0); P.dyaw.fill(0); P.roll.fill(0); affect.fill(0);
988
- P.speedScale = 1; P.headYaw = 0; P.headPitch = 0; P.headRoll = 0; P.headOmega = 9; P.stabW = 1;
989
- P.gape = 0; P.gapeOmega = 14; P.fang = 0; P.tongue = 1; P.tongueRate = 1; P.tongueOut = 0;
990
- P.look = null; P.lookW = 1; P.stackW = 1;
991
- }
992
-
993
- // ------------------------------------------------------------------------------------------------
994
- // Actions
995
- // ------------------------------------------------------------------------------------------------
996
- // Postures (sit, lie, sleep, coil, death) hold until 'stand' or a move request. Coil-family postures
997
- // (coil = resting coil, sleep = coil + stillness, sit = alert coil with the forebody raised in an S)
998
- // crawl into a spiral first. One-shots (strike / attack, jump, eat, drink, hit) blend over whatever
999
- // runs and end by themselves. play() returns a Promise (postures: resolved once in the pose).
1000
- class SnakeActions {
1001
- constructor(e) {
1002
- this.e = e;
1003
- this.postures = []; // { name, t, w, target, shapeW, reached, resolveIn, side, ... }
1004
- this.oneshots = [];
1005
- this.posture = 'stand';
1006
- this.standReq = null;
1007
- this.idle = { w: 0, t: 0, lookT: 2, look: new THREE.Vector3(), has: false, raise: 0, raiseT: 3, raiseS: new Spring(0), yawS: new Spring(0), bend: 0 };
1008
- this.lookS = { raise: new Spring(0), bend: new Spring(0), w: new Spring(0) };
1009
- this.coiled = false;
1010
- this._t = new THREE.Vector3();
1011
- }
1012
- holdsStill() {
1013
- if (this.postures.length) return true;
1014
- for (let i = 0; i < this.oneshots.length; i++) { const a = this.oneshots[i]; if (a.still && !a.stopping) return true; }
1015
- return false;
1016
- }
1017
- dead() { for (let i = 0; i < this.postures.length; i++) { const p = this.postures[i]; if (p.name === 'death' && p.target > 0) return true; } return false; }
1018
- // automatic stand-up (asked to move): never revives a dead snake
1019
- requestStand() { if (!this.standReq && !this.dead()) this.play('stand'); }
1020
- // arbitration: a new one-shot interrupts the running
1021
- // ones (fade out, the promise resolves 'interrupted'); a posture ends them (except a hit); stop()
1022
- // ends everything but death; dead: only hit is taken. Promises resolve 'done' | 'interrupted' |
1023
- // 'stopped' | 'refused'.
1024
- end(a, reason) { if (a.stopping) return; a.stopping = true; a.reason = reason; }
1025
- current() {
1026
- for (let i = 0; i < this.oneshots.length; i++) if (!this.oneshots[i].stopping) return this.oneshots[i].name;
1027
- for (let i = 0; i < this.postures.length; i++) if (this.postures[i].target > 0) return this.postures[i].name;
1028
- return this.standReq ? 'stand' : null;
1029
- }
1030
- clear() {
1031
- for (const a of [...this.postures, ...this.oneshots]) { a.resolveIn?.('stopped'); a.resolve?.('stopped'); }
1032
- this.postures.length = 0; this.oneshots.length = 0;
1033
- if (this.standReq) { this.standReq.resolve('stopped'); this.standReq = null; }
1034
- }
1035
-
1036
- play(name, opts = {}) {
1037
- const e = this.e;
1038
- if (name === 'idle') name = 'stand';
1039
- if (name === 'attack') name = 'strike';
1040
- if (name === 'stand') {
1041
- if (!this.postures.some((p) => p.target > 0) && !this.postures.length) return Promise.resolve('done');
1042
- for (const p of this.postures) if (p.target > 0) { p.target = 0; p.resolveIn?.('interrupted'); }
1043
- e.emit('actionStart', { name: 'stand' });
1044
- if (this.standReq) return this.standReq.promise;
1045
- let resolve;
1046
- const promise = new Promise((r) => { resolve = r; });
1047
- this.standReq = { promise, resolve };
1048
- return promise;
1049
- }
1050
- if (POSTURES.includes(name)) {
1051
- const cur = this.postures.find((p) => p.target > 0);
1052
- if (cur && cur.name === 'death' && name !== 'death') return Promise.resolve('refused');
1053
- if (cur && cur.name === name) return cur.promiseIn;
1054
- for (const p of this.postures) if (p.target > 0) { p.target = 0; p.resolveIn?.('interrupted'); }
1055
- if (this.standReq) { this.standReq.resolve('interrupted'); this.standReq = null; }
1056
- for (const a of this.oneshots) if (a.name !== 'hit') this.end(a, 'interrupted');
1057
- let side = opts.side ?? (e.rand() < 0.5 ? -1 : 1);
1058
- if (opts.direction && name === 'death') {
1059
- const d = opts.direction;
1060
- side = d.x * Math.cos(e.heading) - d.z * Math.sin(e.heading) > 0 ? 1 : -1;
1061
- }
1062
- const inst = { name, t: 0, w: 0, target: 1, side, opts, reached: false, baked: false, phase0: e.rand() * TAU };
1063
- inst.promiseIn = new Promise((r) => { inst.resolveIn = r; });
1064
- if (COILED[name]) {
1065
- // already lying in a coil (the body winds through more than ~300 degrees): stay put
1066
- const wound = Math.abs(e.byawU[0] - e.byawU[e.N - 1]) > 1.7 * Math.PI;
1067
- if (!this.coiled && !wound) this.startCoil(inst);
1068
- else { inst.coilDone = true; this.coiled = true; }
1069
- }
1070
- this.postures.push(inst);
1071
- e.emit('actionStart', { name });
1072
- if (name === 'death') e.emit('death', { position: e.pos.clone() });
1073
- return inst.promiseIn;
1074
- }
1075
- const def = ONESHOTS[name];
1076
- if (!def) return Promise.reject(new Error(`procedural-animals: unknown action "${name}"`));
1077
- if (this.dead() && name !== 'hit') return Promise.resolve('refused');
1078
- if (this.postures.some((p) => p.target > 0) && (name === 'jump' || name === 'strike') && !this.postures.some((p) => p.name === 'sit' && p.target > 0)) {
1079
- return this.play('stand').then((r) => (r === 'refused' ? r : this.play(name, opts)));
1080
- }
1081
- // a new one-shot interrupts the running ones (they fade out, their promises resolve)
1082
- for (const a of this.oneshots) this.end(a, 'interrupted');
1083
- const inst = { name, t: 0, w: 0, dur: 1, stopping: false, opts, fade: 0.15, still: false, side: e.rand() < 0.5 ? -1 : 1 };
1084
- def.start(inst, this);
1085
- inst.promise = new Promise((r) => { inst.resolve = r; });
1086
- this.oneshots.push(inst);
1087
- e.emit('actionStart', { name });
1088
- return inst.promise;
1089
- }
1090
-
1091
- stop(name) {
1092
- if (name === 'attack') name = 'strike';
1093
- for (const a of this.oneshots) if (!name || a.name === name) this.end(a, 'stopped');
1094
- if (!name || POSTURES.includes(name)) if (this.postures.some((p) => p.target > 0 && (name ? p.name === name : p.name !== 'death'))) this.play('stand');
1095
- }
1096
-
1097
- startCoil(inst) {
1098
- const e = this.e, tune = e.cfg.actions.coil || {};
1099
- // a real resting coil is not a machined spiral: the ring spacing is a little tighter than the body
1100
- // (inner turns ride up onto the outer ones), the curvature wanders (seeded), the centre is small
1101
- // and the whole body, tail tip included, ends up in the coil
1102
- const pitch = 2 * e.hwMax * (tune.pitch ?? 0.88);
1103
- const rin = Math.max(0.022 * e.TL, 1.8 * e.hw[0] + 0.008 * e.s);
1104
- const total = e.bodyLen * 1.08;
1105
- const dir = inst.side;
1106
- const f1 = 0.16 * e.TL * (0.8 + 0.4 * e.rand()), f2 = 0.07 * e.TL * (0.8 + 0.4 * e.rand()), p1 = e.rand() * TAU, p2 = e.rand() * TAU;
1107
- const sc = {
1108
- total, speed: (tune.speed ?? 0.34) * e.sq,
1109
- kappa: (u) => (dir * (1 + 0.13 * Math.sin((TAU * u) / f1 + p1) + 0.06 * Math.sin((TAU * u) / f2 + p2))) / Math.sqrt(rin * rin + (pitch * Math.max(0, total - u)) / Math.PI),
1110
- done: false,
1111
- };
1112
- e.startScript(sc);
1113
- inst.script = sc;
1114
- this.coiled = true;
1115
- }
1116
-
1117
- update(dt) {
1118
- const e = this.e, P = e.P, N = e.N;
1119
- resetPose(P, e.affect);
1120
- // ---- postures
1121
- for (const p of this.postures) {
1122
- p.t += dt;
1123
- const coilWait = p.script && !p.script.done;
1124
- if (p.target > 0) p.w = Math.min(1, p.w + dt / (p.name === 'death' ? 0.2 : 0.8));
1125
- else p.w = Math.max(0, p.w - dt / (p.name === 'death' ? 1.0 : 0.7));
1126
- if (!p.reached && p.target > 0 && p.w >= 1 && !coilWait && p.t > (p.name === 'death' ? 1.8 : p.name === 'lie' ? 1.6 : 0.6)) {
1127
- p.reached = true; p.resolveIn('done'); e.emit('actionEnd', { name: p.name });
1128
- }
1129
- // leaving the posture: once lifts and rolls are gone, bake the body shape into the trail
1130
- if (p.target === 0 && p.w <= 0 && !p.baked) {
1131
- if (p.script && !p.script.done) { e.script = null; e.reseed = true; }
1132
- p.baked = true;
1133
- p.bakeNow = true;
1134
- }
1135
- }
1136
- // any posture that changed the shape (death, lie) is baked when it ends
1137
- let bake = false;
1138
- for (let i = 0; i < this.postures.length; i++) { const p = this.postures[i]; if (p.bakeNow && (p.name === 'death' || p.name === 'lie')) bake = true; }
1139
- if (bake) this.coiled = false; // the body shape changed (death, lie)
1140
- compact(this.postures, notBaked);
1141
- if (bake) this.bake();
1142
- let active = null;
1143
- for (let i = 0; i < this.postures.length; i++) if (this.postures[i].target > 0) { active = this.postures[i]; break; }
1144
- this.posture = active ? (active.name === 'death' ? 'dead' : active.name) : this.postures.length ? 'standing-up' : 'stand';
1145
- if (this.standReq && !this.postures.length) { this.standReq.resolve('done'); this.standReq = null; e.emit('actionEnd', { name: 'stand' }); }
1146
- if (!active && e.speed > 0.01) this.coiled = false;
1147
-
1148
- // ---- idle and look-at
1149
- const still = e.speed < 0.01 * e.sq && !e.input.follow && !e.script;
1150
- let busy = false;
1151
- for (let i = 0; i < this.oneshots.length; i++) if (!this.oneshots[i].stopping) busy = true;
1152
- const idleT = still && !this.postures.length && !busy ? 1 : 0;
1153
- const id = this.idle;
1154
- id.w += clamp(idleT - id.w, -dt * 1.5, dt * 0.6);
1155
- this.applyIdle(dt, id.w);
1156
- this.applyLook(dt);
1157
- // rattlesnakes carry the rattle just off the ground while they travel
1158
- if (e.cfg.tailCarry > 0) {
1159
- this.carry = (this.carry || 0) + (smooth(0.003, 0.03, e.speed / e.sq) * (this.postures.length ? 0 : 1) - (this.carry || 0)) * (1 - Math.exp(-dt * 3));
1160
- if (this.carry > 1e-3) {
1161
- const Lt = 0.07 * e.TL;
1162
- for (let i = e.N; i >= 0 && e.bodyLen - e.sig[i] < Lt; i--) {
1163
- const q = 1 - (e.bodyLen - e.sig[i]) / Lt;
1164
- P.lift[i] += this.carry * e.cfg.tailCarry * q * q;
1165
- e.affect[i] += this.carry * q;
1166
- }
1167
- }
1168
- }
1169
- // ---- postures
1170
- for (const p of this.postures) {
1171
- const w = p.w * p.w * (3 - 2 * p.w);
1172
- if (w <= 0) continue;
1173
- POSES[p.name](P, w, p, this, dt);
1174
- }
1175
- // ---- one-shots
1176
- const wantsToMove = e.wantSpeed > 0.01 || !!e.input.follow;
1177
- for (const a of this.oneshots) {
1178
- if (wantsToMove && (a.name === 'eat' || a.name === 'drink')) this.end(a, 'interrupted');
1179
- a.t += dt;
1180
- const def = ONESHOTS[a.name];
1181
- if (!a.stopping && def.update(a, dt, this)) this.end(a, 'done');
1182
- if (a.stopping) a.w = Math.max(0, a.w - dt / a.fade);
1183
- else a.w = Math.min(1, a.w + dt / a.fade);
1184
- const w = a.w * a.w * (3 - 2 * a.w);
1185
- if (w > 0) def.apply(P, w, a, this);
1186
- }
1187
- for (const a of this.oneshots) if (a.stopping && a.w <= 0) { a.resolve(a.reason || 'done'); e.emit('actionEnd', { name: a.name, reason: a.reason || 'done' }); }
1188
- compact(this.oneshots, stillRunning);
1189
- // the neck never kinks: cap the per-segment yaw change of the posed body
1190
- void N;
1191
- }
1192
-
1193
- // the current body shape becomes the trail (the snake then crawls off along its own body)
1194
- bake() {
1195
- const e = this.e, N = e.N, tr = e.trail;
1196
- // current ground points, tail -> head, parameterised by the rest arc lengths
1197
- const sl = tr.lastS;
1198
- const tail = tr.lastS - e.sig[N];
1199
- // keep the trail behind the tail: extend straight from the posed tail
1200
- const tx = e.hx[N] - e.hx[N - 1], tz = e.hz[N] - e.hz[N - 1], tl = h2(tx, tz) || 1;
1201
- tr.clear();
1202
- const ext = 0.2 * e.TL;
1203
- for (let k = 4; k >= 1; k--) {
1204
- const d = (ext * k) / 4, x = e.hx[N] + (tx / tl) * d, z = e.hz[N] + (tz / tl) * d;
1205
- tr.push(x, e.terrainH(x, z), z, tail - d);
1206
- }
1207
- for (let i = N; i >= 0; i--) tr.push(e.hx[i], e.terrainH(e.hx[i], e.hz[i]), e.hz[i], sl - e.sig[i]);
1208
- e.reseed = true;
1209
- e.leader.set(e.hx[0], e.terrainH(e.hx[0], e.hz[0]), e.hz[0]);
1210
- e.C.copy(e.leader); e.pos.copy(e.C);
1211
- }
1212
-
1213
- applyIdle(dt, w) {
1214
- const e = this.e, P = e.P, id = this.idle, t = e.time, s = e.s;
1215
- if (w <= 0.001) { id.has = false; id.raiseS.step(0, 3, dt); id.yawS.step(0, 3, dt); return; }
1216
- id.t += dt;
1217
- // gentle head sway and breathing (a slow roll of the trunk)
1218
- P.headYaw += w * (0.05 * Math.sin(t * 0.7) + 0.03 * Math.sin(t * 1.9 + 1));
1219
- P.headPitch += w * 0.03 * Math.sin(t * 0.43 + 2);
1220
- const br = e.cfg.breath;
1221
- const b = Math.sin(t * TAU * br.rate) * br.roll * w;
1222
- for (let i = 0; i <= e.N; i++) { const q = e.sig[i] / e.bodyLen; P.roll[i] += b * smooth(0.1, 0.3, q) * (1 - smooth(0.6, 0.85, q)); }
1223
- // now and then: raise the head and look about
1224
- id.raiseT -= dt;
1225
- if (id.raiseT <= 0) {
1226
- id.raiseT = 2.5 + e.rand() * 4;
1227
- id.raise = e.rand() < 0.45 ? 0.02 + e.rand() * 0.035 : 0;
1228
- id.bend = (e.rand() - 0.5) * 1.1;
1229
- }
1230
- const rz = id.raiseS.step(id.raise * w, 1.6, dt), bd = id.yawS.step(id.bend * (id.raise > 0 ? 1 : 0.3) * w, 1.4, dt);
1231
- if (!e.input.look) this.raiseNeck(P, rz * e.TL, bd, e.cfg.body.neck * e.TL * 1.4, 1);
1232
- void s;
1233
- }
1234
-
1235
- // raise the forebody to height h (m) over arc length Ln, and bend the neck by yaw b (rad)
1236
- raiseNeck(P, h, b, Ln, w) {
1237
- const e = this.e, N = e.N;
1238
- Ln = Math.max(Ln, 2.3 * Math.abs(h));
1239
- for (let i = 0; i <= N; i++) {
1240
- const sg = e.sig[i];
1241
- if (sg > Ln) break;
1242
- const q = sg / Ln; // 0 head .. 1 base of the raised part
1243
- // the neck rises straight up to the head and flattens into the ground at its base
1244
- const f = Math.pow(1 - q, 1.7);
1245
- P.lift[i] += w * h * f;
1246
- if (i < N) {
1247
- const qm = (sg + e.len[i] * 0.5) / Ln;
1248
- P.dyaw[i] += w * b * (1 - smooth(0, 1, qm));
1249
- }
1250
- e.affect[i] += w * f;
1251
- }
1252
- }
1253
-
1254
- applyLook(dt) {
1255
- const e = this.e, P = e.P, L = this.lookS;
1256
- const look = e.input.look;
1257
- let posed = false;
1258
- for (let i = 0; i < this.postures.length; i++) { const p = this.postures[i]; if (p.target > 0 && (p.name === 'death' || p.name === 'sleep')) posed = true; }
1259
- for (let i = 0; i < this.oneshots.length; i++) { const a = this.oneshots[i]; if (!a.stopping && (a.name === 'strike' || a.name === 'eat' || a.name === 'jump')) posed = true; }
1260
- let raise = 0, bend = 0, wT = 0;
1261
- if (look && !posed) {
1262
- wT = 1;
1263
- const hx = e.hx[0], hz = e.hz[0];
1264
- const to = Math.atan2(look.x - hx, look.z - hz);
1265
- bend = clamp(angDiff(e.byaw[0], to), -1.5, 1.5) * (e.speed > 0.02 ? 0.3 : 1);
1266
- const d = h2(look.x - hx, look.z - hz);
1267
- const dy = look.y - e.terrainH(hx, hz);
1268
- raise = clamp(0.03 * e.TL + Math.max(0, dy) * 0.25 + 0.04 * e.TL * smooth(3, 0.5, d / e.TL), 0.02 * e.TL, 0.14 * e.TL) * (e.speed > 0.02 ? 0.3 : 1);
1269
- }
1270
- const w = L.w.step(wT, 3, dt);
1271
- const r = L.raise.step(raise, 2.5, dt), b = L.bend.step(bend, 2.5, dt);
1272
- if (w > 0.001) this.raiseNeck(P, r, b, e.cfg.body.neck * e.TL * (1.2 + Math.abs(b) * 0.6), w);
1273
- }
1274
- }
1275
-
1276
- // ---------------------------------------------------------------- forebody shapes (strike family)
1277
- // S-coil in the horizontal plane, raised at the front; extension e (0..1) straightens it toward the
1278
- // target. Writes yaw offsets and lifts with weight w.
1279
- function strikeShape(P, a, L, w, e, beta, hS, aimYaw, aimLift, curl) {
1280
- const E = L.e, N = E.N;
1281
- const Ls = a.Ls, ia = a.ia;
1282
- const baseA = E.byawU[Math.min(N - 1, ia)];
1283
- for (let i = 0; i < N && E.sig[i] <= Ls; i++) {
1284
- const q = (E.sig[i] + E.len[i] * 0.5) / Ls; // 0 head .. 1 anchor
1285
- const envS = Math.sin(Math.PI * Math.min(1, q * 1.05));
1286
- // S-coil yaw about the aim direction (two bends), aim ramp near the anchor
1287
- const aimRamp = 1 - smooth(0.55, 1, q);
1288
- const ySx = aimYaw * aimRamp + beta * Math.sin(TAU * q * curl) * envS;
1289
- const yE = aimYaw * (1 - smooth(0.55, 1, q));
1290
- const yaw = baseA + lerp(ySx, yE, e);
1291
- // hand over to the body's own curvature toward the anchor (no curvature step there)
1292
- P.dyaw[i] += w * (yaw - E.byawU[i]) * (1 - smooth(0.5, 1, q));
1293
- }
1294
- for (let i = 0; i <= N && E.sig[i] <= Ls; i++) {
1295
- const q = E.sig[i] / Ls;
1296
- const lS = hS * Math.pow(1 - smooth(0, 1, q), 1.2);
1297
- const lE = aimLift * (1 - smooth(0.05, 1, q));
1298
- P.lift[i] += w * lerp(lS, lE, e);
1299
- E.affect[i] += w * (1 - smooth(0.85, 1, q));
1300
- }
1301
- }
1302
-
1303
- function setupStrike(a, L, target, reachK) {
1304
- const E = L.e, N = E.N;
1305
- const t = target || L._t.set(E.hx[0] + Math.sin(E.byaw[0]) * 0.3 * E.TL, E.terrainH(E.hx[0], E.hz[0]) + 0.02 * E.TL, E.hz[0] + Math.cos(E.byaw[0]) * 0.3 * E.TL);
1306
- a.target = new THREE.Vector3().copy(t);
1307
- const maxL = reachK * E.TL;
1308
- // forebody length: enough to reach the target from the anchor, within limits
1309
- let Ls = maxL;
1310
- for (let it = 0; it < 3; it++) {
1311
- let ia = 0; while (ia < N && E.sig[ia] < Ls) ia++;
1312
- const d = h2(a.target.x - E.hx[ia], a.target.z - E.hz[ia]);
1313
- Ls = clamp(d * 1.04, 0.14 * E.TL, maxL);
1314
- }
1315
- let ia = 0; while (ia < N && E.sig[ia] < Ls) ia++;
1316
- a.Ls = E.sig[ia]; a.ia = ia;
1317
- const ax = E.hx[ia], az = E.hz[ia];
1318
- const to = Math.atan2(a.target.x - ax, a.target.z - az);
1319
- a.aimYaw = clamp(angDiff(E.byaw[Math.min(N - 1, ia)], to), -1.2, 1.2);
1320
- const dh = h2(a.target.x - ax, a.target.z - az);
1321
- const gy = E.terrainH(a.target.x, a.target.z);
1322
- a.aimLift = clamp(a.target.y - gy - E.r[0], 0, 0.6 * a.Ls) * smooth(0, 1, a.Ls / Math.max(dh, 1e-3));
1323
- }
1324
-
1325
- const ONESHOTS = {
1326
- // S-coil, strike at the target with the jaw wide open (attackHit), retract, relax
1327
- strike: {
1328
- start(a, L) {
1329
- const E = L.e, st = E.cfg.strike;
1330
- setupStrike(a, L, a.opts.target, st.reach);
1331
- a.still = true;
1332
- a.T = { cock: 0.5, hold: 0.62, hit: 0.62 + clamp(a.Ls / (st.speed * E.sq * 0.9), 0.14, 0.22) };
1333
- a.T.stay = a.T.hit + 0.14; a.T.back = a.T.stay + 0.32; a.T.end = a.T.back + 0.6;
1334
- a.dur = a.T.end;
1335
- a.fade = 0.2;
1336
- a.hitDone = false;
1337
- },
1338
- update(a, dt, L) {
1339
- const E = L.e, T = a.T;
1340
- if (!a.hitDone && a.t >= T.hit) {
1341
- a.hitDone = true;
1342
- _v.set(0, 0, 0);
1343
- const m = E.attachmentPoints.mouth;
1344
- const pos = new THREE.Vector3();
1345
- if (m) pos.setFromMatrixPosition(_m1.multiplyMatrices(E.bHead.matrixWorld, m.local)); else pos.set(E.px[0], E.py[0], E.pz[0]);
1346
- const dir = new THREE.Vector3(0, 0, 1).applyQuaternion(E.qHead);
1347
- E.emit('attackHit', { position: pos, direction: dir, style: 'strike', target: a.target.clone() });
1348
- }
1349
- return a.t >= a.dur;
1350
- },
1351
- apply(P, w, a, L) {
1352
- const E = L.e, T = a.T, t = a.t, st = E.cfg.strike;
1353
- const cock = smooth(0, T.cock, t) * (1 - smooth(T.back, T.end, t));
1354
- const ext = smootherstep(T.hold, T.hit, t) * (1 - smootherstep(T.stay, T.back, t));
1355
- const hS = st.coilHeight * E.TL;
1356
- strikeShape(P, a, L, w * cock, ext, 1.25, hS, a.aimYaw, a.aimLift, 1);
1357
- P.speedScale *= 1 - w;
1358
- const openT = smooth(0.45, 0.85, ext);
1359
- P.gape += w * openT * st.gape * Math.PI / 180;
1360
- P.gapeOmega = Math.max(P.gapeOmega, 40);
1361
- P.headPitch += w * (0.12 * cock * (1 - ext) + 0.25 * openT);
1362
- P.headOmega = Math.max(P.headOmega, lerp(9, 40, w * ext));
1363
- P.stabW *= 1 - w;
1364
- P.tongue *= 1 - w * cock;
1365
- P.lookW *= 1 - w;
1366
- P.stackW *= 1 - w;
1367
- },
1368
- },
1369
- // a forward lunge (snakes do not jump): a flat, closed-mouth strike straight ahead
1370
- jump: {
1371
- start(a, L) {
1372
- const E = L.e;
1373
- const h = E.byaw[0];
1374
- a.opts.target = new THREE.Vector3(E.hx[0] + Math.sin(h) * 0.22 * E.TL, E.terrainH(E.hx[0], E.hz[0]) + 0.03 * E.TL, E.hz[0] + Math.cos(h) * 0.22 * E.TL);
1375
- setupStrike(a, L, a.opts.target, 0.26);
1376
- a.still = true;
1377
- a.T = { cock: 0.4, hold: 0.45, hit: 0.7, stay: 0.8, back: 1.15, end: 1.6 };
1378
- a.dur = a.T.end; a.fade = 0.2;
1379
- },
1380
- update(a) { return a.t >= a.dur; },
1381
- apply(P, w, a, L) {
1382
- const E = L.e, T = a.T, t = a.t;
1383
- const cock = smooth(0, T.cock, t) * (1 - smooth(T.back, T.end, t));
1384
- const ext = smootherstep(T.hold, T.hit, t) * (1 - smootherstep(T.stay, T.back, t));
1385
- strikeShape(P, a, L, w * cock, ext, 0.5, 0.03 * E.TL, a.aimYaw, a.aimLift, 1);
1386
- P.speedScale *= 1 - w;
1387
- P.headPitch += w * 0.05 * cock;
1388
- P.headOmega = Math.max(P.headOmega, lerp(9, 30, w * ext));
1389
- P.stabW *= 1 - w; P.lookW *= 1 - w; P.tongue *= 1 - w * cock; P.stackW *= 1 - w;
1390
- },
1391
- },
1392
- // strike at prey on the ground ahead, then swallow: jaws walk left and right, the neck works
1393
- eat: {
1394
- start(a, L) {
1395
- const E = L.e, h = E.byaw[0];
1396
- a.opts.target = a.opts.target || new THREE.Vector3(E.hx[0] + Math.sin(h) * 0.12 * E.TL, 0, E.hz[0] + Math.cos(h) * 0.12 * E.TL);
1397
- a.opts.target.y = E.terrainH(a.opts.target.x, a.opts.target.z) + 0.005 * E.s;
1398
- setupStrike(a, L, a.opts.target, 0.22);
1399
- a.still = true;
1400
- const sw = E.cfg.actions.eat?.swallow ?? 3.2;
1401
- a.T = { cock: 0.35, hold: 0.4, hit: 0.62, stay: 0.9, back: 1.3, swallow: 1.3 + sw, yawn: 1.3 + sw + 0.7 };
1402
- a.dur = a.T.yawn + 0.3; a.fade = 0.25;
1403
- },
1404
- update(a) { return a.t >= a.dur; },
1405
- apply(P, w, a, L) {
1406
- const E = L.e, T = a.T, t = a.t;
1407
- const cock = smooth(0, T.cock, t) * (1 - smooth(T.stay, T.back, t));
1408
- const ext = smootherstep(T.hold, T.hit, t) * (1 - smootherstep(T.stay, T.back, t));
1409
- strikeShape(P, a, L, w * cock, ext, 0.3, 0.025 * E.TL, a.aimYaw, 0, 1);
1410
- P.gape += w * smooth(0.3, 0.8, ext) * 0.7;
1411
- P.gapeOmega = Math.max(P.gapeOmega, 25);
1412
- // swallowing: alternate jaw walks, a pulse down the neck
1413
- const sw = smooth(T.back - 0.2, T.back + 0.3, t) * (1 - smooth(T.swallow - 0.3, T.swallow, t));
1414
- const ph = (t - T.back) * TAU * 0.7;
1415
- P.headYaw += w * sw * 0.14 * Math.sin(ph);
1416
- P.headRoll += w * sw * 0.1 * Math.sin(ph);
1417
- P.gape += w * sw * (0.25 + 0.15 * Math.sin(ph * 2));
1418
- P.headPitch += w * sw * 0.06 * Math.sin(ph + 1);
1419
- const pulse = ((t - T.back) * 0.12 * E.TL) % (0.2 * E.TL);
1420
- for (let i = 0; i <= E.N && E.sig[i] < 0.25 * E.TL; i++) {
1421
- const d = (E.sig[i] - pulse) / (0.03 * E.TL);
1422
- P.lift[i] += w * sw * 0.004 * E.s * Math.exp(-d * d);
1423
- }
1424
- // yawn to reset the jaws
1425
- const yw = smooth(T.swallow, T.swallow + 0.3, t) * (1 - smooth(T.yawn - 0.3, T.yawn, t));
1426
- P.gape += w * yw * 1.1;
1427
- P.headPitch += w * yw * 0.25;
1428
- P.speedScale *= 1 - w; P.stabW *= 1 - w; P.lookW *= 1 - w; P.tongue *= 1 - w; void yw;
1429
- },
1430
- },
1431
- // head down, snout to the ground / water surface, buccal pumping
1432
- drink: {
1433
- start(a, L) { a.still = true; a.dur = 1.0 + (L.e.cfg.actions.drink?.pumps ?? 4) * 0.8; a.fade = 0.4; },
1434
- update(a) { return a.t >= a.dur; },
1435
- apply(P, w, a, L) {
1436
- const t = a.t;
1437
- const down = smooth(0, 0.6, t);
1438
- P.headPitch -= w * down * 0.35;
1439
- const pump = Math.max(0, Math.sin((t - 0.6) * TAU * 1.25)) * smooth(0.6, 0.9, t);
1440
- P.gape += w * down * (0.03 + 0.06 * pump);
1441
- P.gapeOmega = Math.max(P.gapeOmega, 20);
1442
- P.tongue *= 1 - w * down;
1443
- P.speedScale *= 1 - w; P.lookW *= 1 - w; P.stabW *= 1 - w;
1444
- },
1445
- },
1446
- // flinch into a curve away from the blow, head withdrawn, tail whipping, then settle back
1447
- hit: {
1448
- start(a, L) {
1449
- const E = L.e;
1450
- const d = a.opts.direction;
1451
- if (d) a.side = d.x * Math.cos(E.heading) - d.z * Math.sin(E.heading) > 0 ? 1 : -1;
1452
- a.dur = 1.4; a.fade = 0.2;
1453
- },
1454
- update(a) { return a.t >= a.dur; },
1455
- apply(P, w, a, L) {
1456
- const E = L.e, t = a.t, N = E.N;
1457
- const k = smootherstep(0, 0.3, t) * (1 - smootherstep(0.45, 1.35, t));
1458
- const ov = 0;
1459
- for (let i = 0; i < N; i++) {
1460
- const q = (E.sig[i] + E.len[i] * 0.5) / E.bodyLen;
1461
- // C-bend: forebody swings with the blow, the tail whips the other way
1462
- const prof = 1.0 * (1 - smooth(0, 0.45, q)) - 0.55 * smooth(0.55, 1, q);
1463
- P.dyaw[i] += w * a.side * (k * 0.75 + 0.2 * ov) * prof;
1464
- }
1465
- for (let i = 0; i <= N; i++) E.affect[i] += w * k;
1466
- L.raiseNeck(P, 0.035 * E.TL * k, -a.side * 0.3 * k, 0.12 * E.TL, w);
1467
- P.headYaw += w * a.side * 0.35 * k;
1468
- P.gape += w * k * (E.viper ? 0.45 : 0.2);
1469
- P.speedScale *= 1 - w * k;
1470
- P.lookW *= 1 - w * k;
1471
- P.stackW *= 1 - w * k;
1472
- if (E.cfg.rattle) rattle(P, E, w * smooth(0, 0.3, t), t);
1473
- },
1474
- },
1475
- };
1476
-
1477
- const notBaked = (p) => !p.baked;
1478
- const stillRunning = (a) => !(a.stopping && a.w <= 0);
1479
- function compact(arr, keep) {
1480
- let j = 0;
1481
- for (let i = 0; i < arr.length; i++) if (keep(arr[i])) arr[j++] = arr[i];
1482
- arr.length = j;
1483
- }
1484
-
1485
- function smootherstep(a, b, x) {
1486
- const t = clamp((x - a) / (b - a), 0, 1);
1487
- return t * t * t * (t * (t * 6 - 15) + 10);
1488
- }
1489
-
1490
- // tail tip raised and buzzing (rattlesnake)
1491
- function rattle(P, E, w, t) {
1492
- if (w <= 0) return;
1493
- const N = E.N, Lt = 0.11 * E.TL;
1494
- const buzz = Math.sin(t * TAU * 27);
1495
- for (let i = N; i >= 0 && E.bodyLen - E.sig[i] < Lt; i--) {
1496
- const q = 1 - (E.bodyLen - E.sig[i]) / Lt; // 0 .. 1 at the tip
1497
- P.lift[i] += w * 0.036 * E.TL * q * q;
1498
- E.affect[i] += w * q;
1499
- if (i < N) P.dyaw[i] += w * 0.03 * buzz * smooth(0.5, 1, q);
1500
- }
1501
- }
1502
-
1503
- // ---------------------------------------------------------------- posture poses
1504
- const POSES = {
1505
- coil(P, w, p, L) {
1506
- const E = L.e;
1507
- P.speedScale *= 1 - w;
1508
- P.tongueRate *= 1 - 0.4 * w;
1509
- // once coiled, the neck swings out over the inner ring and the head rests on top of the coils
1510
- const done = p.coilDone || (p.script && p.script.done);
1511
- if (done) {
1512
- if (p.restT === undefined) p.restT = p.t;
1513
- const k = w * smooth(0, 1.2, p.t - p.restT);
1514
- L.raiseNeck(P, 1.5 * E.hwMax * k, -p.side * 0.75 * k, 0.14 * E.TL, 1);
1515
- P.headPitch += k * 0.06;
1516
- }
1517
- },
1518
- sleep(P, w, p, L) {
1519
- POSES.coil(P, w, p, L);
1520
- const c = w * smooth(0.5, 2.5, p.t);
1521
- P.tongue *= 1 - c; P.lookW *= 1 - c; P.stabW *= 1 - c;
1522
- P.headPitch += c * 0.04;
1523
- P.headOmega = Math.min(P.headOmega, 4);
1524
- },
1525
- // alert / defensive coil: the forebody rises in an S facing the look target (rattlesnakes rattle)
1526
- sit(P, w, p, L) {
1527
- const E = L.e;
1528
- POSES.coil(P, w, p, L);
1529
- const up = w * (p.script ? smooth(0.6, 1, p.script.u / p.script.total) : 1);
1530
- if (up <= 0) return;
1531
- if (!p.S) p.S = { Ls: 0, ia: 0, aimYaw: 0, aimLift: 0 };
1532
- const S = p.S;
1533
- const Lf = (E.cfg.actions.sit?.raise ?? 0.1) * E.TL * 2.6;
1534
- let ia = 0; while (ia < E.N && E.sig[ia] < Lf) ia++;
1535
- S.Ls = E.sig[ia]; S.ia = ia; S.aimYaw = 0; S.aimLift = 0;
1536
- const look = E.input.look;
1537
- if (look) S.aimYaw = clamp(angDiff(E.byaw[Math.min(E.N - 1, ia)], Math.atan2(look.x - E.hx[ia], look.z - E.hz[ia])), -1.2, 1.2) * 0.6;
1538
- strikeShape(P, S, L, up, 0, 0.9, (E.cfg.actions.sit?.raise ?? 0.1) * E.TL, S.aimYaw, 0, 1);
1539
- P.stackW *= 1 - up;
1540
- P.tongueRate += up * 1.5;
1541
- if (E.cfg.rattle) rattle(P, E, up * smooth(0.5, 1.5, p.t), E.time);
1542
- },
1543
- // stretched out: the wave relaxes toward the body's mean line, head down
1544
- lie(P, w, p, L) {
1545
- const E = L.e, N = E.N;
1546
- P.speedScale *= 1 - w;
1547
- const k = w * smooth(0, 1.4, p.t) * 0.8;
1548
- const win = 0.18 * E.TL;
1549
- // mean line: yaw averaged over a window along the body (unwrapped relative to the local yaw)
1550
- for (let i = 0; i < N; i++) {
1551
- let sum = 0, n = 0;
1552
- for (let j = 0; j < N; j++) {
1553
- if (Math.abs(E.sig[j] - E.sig[i]) > win) continue;
1554
- sum += angDiff(E.byaw[i], E.byaw[j]); n++;
1555
- }
1556
- P.dyaw[i] += k * (sum / Math.max(1, n));
1557
- E.affect[i] += k;
1558
- }
1559
- E.affect[N] += k;
1560
- P.headPitch -= w * 0.02;
1561
- P.tongueRate *= 1 - 0.5 * w;
1562
- },
1563
- // writhe, go limp and roll half onto the back; stays down until 'stand'
1564
- death(P, w, p, L) {
1565
- const E = L.e, N = E.N, t = p.t;
1566
- P.speedScale = 0;
1567
- const writhe = 0.55 * smooth(0, 0.12, t) * Math.exp(-t / 0.55);
1568
- const slack = 0.32 * smooth(0.2, 1.4, t);
1569
- const rollK = smooth(0.35, 1.3, t);
1570
- const lam = 0.36 * E.TL;
1571
- for (let i = 0; i < N; i++) {
1572
- const sg = E.sig[i] + E.len[i] * 0.5;
1573
- const q = sg / E.bodyLen;
1574
- const env = smooth(0, 0.08, q) * (1 - 0.75 * smooth(0.78, 1, q));
1575
- const wv = writhe * Math.sin(TAU * (sg / lam - 1.7 * t) + p.phase0) + slack * Math.sin(TAU * sg / (0.55 * E.TL) + p.phase0 * 1.7) * 0.8;
1576
- P.dyaw[i] += w * wv * env;
1577
- }
1578
- for (let i = 0; i <= N; i++) {
1579
- const q = E.sig[i] / E.bodyLen;
1580
- E.affect[i] += w;
1581
- // only parts turn belly-up: neck upright, mid-body over ~160 deg, a second partial twist behind
1582
- const c1 = 0.4 + 0.1 * Math.sin(p.phase0), c2 = 0.76 + 0.06 * Math.cos(p.phase0 * 1.3);
1583
- const prof = 2.8 * Math.exp(-(((q - c1) / 0.15) ** 2)) + 1.05 * Math.exp(-(((q - c2) / 0.09) ** 2));
1584
- P.roll[i] += w * p.side * rollK * prof;
1585
- }
1586
- P.gape += w * 0.2 * smooth(0.4, 1.2, t);
1587
- P.gapeOmega = 6;
1588
- P.tongueOut += w * 0.45 * smooth(0.8, 1.6, t);
1589
- P.tongue = 0;
1590
- P.headRoll += w * p.side * 0.6 * rollK;
1591
- P.headOmega = lerp(P.headOmega, 5, w);
1592
- P.lookW = 0; P.stabW = 0; P.stackW *= 1 - w;
1593
- },
1594
- };