@bitefoundry/retrieviewer 0.0.3 → 0.0.4

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 (254) hide show
  1. package/ASSET-PROVENANCE.md +6 -0
  2. package/ASSETS-LICENSE.md +2 -1
  3. package/ATTRIBUTION.md +6 -1
  4. package/COMFORT-ART.md +65 -0
  5. package/DESKTOP-PLATFORMS.md +6 -1
  6. package/PLUGIN.md +13 -10
  7. package/PREREQUISITES.md +95 -0
  8. package/PUPPY-MEMORY.md +102 -1
  9. package/README.ko.md +110 -0
  10. package/README.md +117 -59
  11. package/RELEASING.md +23 -12
  12. package/THIRD-PARTY-NOTICES.md +5 -3
  13. package/braille.mjs +35 -9
  14. package/examples/live.mjs +2 -2
  15. package/integrations/linux-overlay/overlay.py +2 -2
  16. package/integrations/macos-overlay/overlay.jxa +8 -6
  17. package/integrations/opencode/runtime.mjs +4 -4
  18. package/integrations/opencode/tui.tsx +1 -1
  19. package/integrations/windows-overlay/Overlay.cs +4 -4
  20. package/integrations/windows-overlay/protocol.mjs +11 -7
  21. package/package.json +34 -13
  22. package/puppy-comfort-sprites.mjs +25 -0
  23. package/puppy-companion-comfort-q5.bin.gz +0 -0
  24. package/puppy-companion-comfort.json +77 -0
  25. package/puppy-companion-life.mjs +49 -16
  26. package/puppy-companion-objects.mjs +12 -0
  27. package/puppy-frame-predictive.mjs +15 -9
  28. package/puppy-frame-store.mjs +2 -2
  29. package/puppy-paper-world.mjs +14 -0
  30. package/puppy-paper.mjs +65 -14
  31. package/retrieviewer-plugin.d.mts +1 -1
  32. package/retrieviewer-plugin.mjs +1 -1
  33. package/retrieviewer.d.mts +10 -4
  34. package/retrieviewer.mjs +20 -14
  35. package/locomotion-core.mjs +0 -21
  36. package/locomotion-puppy.animal +0 -0
  37. package/vendor/procedural-animals/package.json +0 -40
  38. package/vendor/procedural-animals/src/animal.js +0 -146
  39. package/vendor/procedural-animals/src/bake.js +0 -51
  40. package/vendor/procedural-animals/src/build.js +0 -81
  41. package/vendor/procedural-animals/src/core/build/coatKit.js +0 -121
  42. package/vendor/procedural-animals/src/core/build/featherCards.js +0 -189
  43. package/vendor/procedural-animals/src/core/build/finEdges.js +0 -85
  44. package/vendor/procedural-animals/src/core/build/furSlope.js +0 -115
  45. package/vendor/procedural-animals/src/core/build/mesh.js +0 -67
  46. package/vendor/procedural-animals/src/core/build/pipeline.js +0 -272
  47. package/vendor/procedural-animals/src/core/build/seams.js +0 -183
  48. package/vendor/procedural-animals/src/core/build/warp.js +0 -112
  49. package/vendor/procedural-animals/src/core/build/weights.js +0 -393
  50. package/vendor/procedural-animals/src/core/math/vec.js +0 -81
  51. package/vendor/procedural-animals/src/core/motion/actions.js +0 -800
  52. package/vendor/procedural-animals/src/core/motion/bird.js +0 -3063
  53. package/vendor/procedural-animals/src/core/motion/birdActions.js +0 -1059
  54. package/vendor/procedural-animals/src/core/motion/common.js +0 -158
  55. package/vendor/procedural-animals/src/core/motion/index.js +0 -22
  56. package/vendor/procedural-animals/src/core/motion/quadruped.js +0 -2875
  57. package/vendor/procedural-animals/src/core/motion/snake.js +0 -1594
  58. package/vendor/procedural-animals/src/core/motion/spider.js +0 -1810
  59. package/vendor/procedural-animals/src/core/motion/swimmer.js +0 -1047
  60. package/vendor/procedural-animals/src/core/motion/swimmerActions.js +0 -613
  61. package/vendor/procedural-animals/src/core/render/animalObject.js +0 -262
  62. package/vendor/procedural-animals/src/core/render/coatMaterial.js +0 -1301
  63. package/vendor/procedural-animals/src/core/render/dqs.js +0 -159
  64. package/vendor/procedural-animals/src/core/render/eyes.js +0 -676
  65. package/vendor/procedural-animals/src/core/render/membranes.js +0 -77
  66. package/vendor/procedural-animals/src/core/render/skeleton.js +0 -41
  67. package/vendor/procedural-animals/src/core/rig/bird.js +0 -327
  68. package/vendor/procedural-animals/src/core/rig/quadruped.js +0 -76
  69. package/vendor/procedural-animals/src/core/rig/rig.js +0 -79
  70. package/vendor/procedural-animals/src/core/rig/snake.js +0 -76
  71. package/vendor/procedural-animals/src/core/rig/spider.js +0 -110
  72. package/vendor/procedural-animals/src/core/rig/swimmer.js +0 -181
  73. package/vendor/procedural-animals/src/core/sdf/eyeSocket.js +0 -53
  74. package/vendor/procedural-animals/src/core/sdf/mesher.js +0 -238
  75. package/vendor/procedural-animals/src/core/sdf/sdf.js +0 -263
  76. package/vendor/procedural-animals/src/index.d.ts +0 -98
  77. package/vendor/procedural-animals/src/index.js +0 -29
  78. package/vendor/procedural-animals/src/registry.js +0 -48
  79. package/vendor/procedural-animals/src/species/bear/behaviour.js +0 -463
  80. package/vendor/procedural-animals/src/species/bear/claws.js +0 -147
  81. package/vendor/procedural-animals/src/species/bear/coat.js +0 -726
  82. package/vendor/procedural-animals/src/species/bear/index.js +0 -139
  83. package/vendor/procedural-animals/src/species/bear/motion.js +0 -112
  84. package/vendor/procedural-animals/src/species/bear/regions.js +0 -78
  85. package/vendor/procedural-animals/src/species/bear/rig.js +0 -112
  86. package/vendor/procedural-animals/src/species/bear/sculpt.js +0 -272
  87. package/vendor/procedural-animals/src/species/boar/behaviour.js +0 -335
  88. package/vendor/procedural-animals/src/species/boar/coat.js +0 -372
  89. package/vendor/procedural-animals/src/species/boar/index.js +0 -110
  90. package/vendor/procedural-animals/src/species/boar/motion.js +0 -85
  91. package/vendor/procedural-animals/src/species/boar/regions.js +0 -88
  92. package/vendor/procedural-animals/src/species/boar/rig.js +0 -111
  93. package/vendor/procedural-animals/src/species/boar/sculpt.js +0 -285
  94. package/vendor/procedural-animals/src/species/cat/behaviour.js +0 -559
  95. package/vendor/procedural-animals/src/species/cat/coat.js +0 -841
  96. package/vendor/procedural-animals/src/species/cat/index.js +0 -211
  97. package/vendor/procedural-animals/src/species/cat/motion.js +0 -101
  98. package/vendor/procedural-animals/src/species/cat/regions.js +0 -82
  99. package/vendor/procedural-animals/src/species/cat/rig.js +0 -100
  100. package/vendor/procedural-animals/src/species/cat/sculpt.js +0 -266
  101. package/vendor/procedural-animals/src/species/cat/whiskers.js +0 -110
  102. package/vendor/procedural-animals/src/species/cheetah/coat.js +0 -427
  103. package/vendor/procedural-animals/src/species/cheetah/index.js +0 -51
  104. package/vendor/procedural-animals/src/species/cheetah/joints.js +0 -3
  105. package/vendor/procedural-animals/src/species/cheetah/motion.js +0 -77
  106. package/vendor/procedural-animals/src/species/cheetah/regions.js +0 -95
  107. package/vendor/procedural-animals/src/species/cheetah/rig.js +0 -66
  108. package/vendor/procedural-animals/src/species/cheetah/sculpt.js +0 -158
  109. package/vendor/procedural-animals/src/species/chicken/coat.js +0 -313
  110. package/vendor/procedural-animals/src/species/chicken/coverts.js +0 -320
  111. package/vendor/procedural-animals/src/species/chicken/index.js +0 -110
  112. package/vendor/procedural-animals/src/species/chicken/motion.js +0 -177
  113. package/vendor/procedural-animals/src/species/chicken/regions.js +0 -66
  114. package/vendor/procedural-animals/src/species/chicken/rig.js +0 -144
  115. package/vendor/procedural-animals/src/species/chicken/sculpt.js +0 -389
  116. package/vendor/procedural-animals/src/species/chicken/wingLook.js +0 -66
  117. package/vendor/procedural-animals/src/species/chicken/wingStack.js +0 -124
  118. package/vendor/procedural-animals/src/species/cow/behaviour.js +0 -521
  119. package/vendor/procedural-animals/src/species/cow/coat.js +0 -465
  120. package/vendor/procedural-animals/src/species/cow/index.js +0 -118
  121. package/vendor/procedural-animals/src/species/cow/motion.js +0 -81
  122. package/vendor/procedural-animals/src/species/cow/regions.js +0 -98
  123. package/vendor/procedural-animals/src/species/cow/rig.js +0 -116
  124. package/vendor/procedural-animals/src/species/cow/sculpt.js +0 -339
  125. package/vendor/procedural-animals/src/species/crow/coat.js +0 -145
  126. package/vendor/procedural-animals/src/species/crow/index.js +0 -70
  127. package/vendor/procedural-animals/src/species/crow/joints.js +0 -3
  128. package/vendor/procedural-animals/src/species/crow/motion.js +0 -101
  129. package/vendor/procedural-animals/src/species/crow/regions.js +0 -50
  130. package/vendor/procedural-animals/src/species/crow/rig.js +0 -83
  131. package/vendor/procedural-animals/src/species/crow/sculpt.js +0 -118
  132. package/vendor/procedural-animals/src/species/deer/behaviour.js +0 -513
  133. package/vendor/procedural-animals/src/species/deer/coat.js +0 -391
  134. package/vendor/procedural-animals/src/species/deer/index.js +0 -106
  135. package/vendor/procedural-animals/src/species/deer/motion.js +0 -97
  136. package/vendor/procedural-animals/src/species/deer/regions.js +0 -60
  137. package/vendor/procedural-animals/src/species/deer/rig.js +0 -124
  138. package/vendor/procedural-animals/src/species/deer/sculpt.js +0 -297
  139. package/vendor/procedural-animals/src/species/dog/behaviour.js +0 -521
  140. package/vendor/procedural-animals/src/species/dog/coat.js +0 -495
  141. package/vendor/procedural-animals/src/species/dog/index.js +0 -144
  142. package/vendor/procedural-animals/src/species/dog/motion.js +0 -118
  143. package/vendor/procedural-animals/src/species/dog/regions.js +0 -61
  144. package/vendor/procedural-animals/src/species/dog/rig.js +0 -129
  145. package/vendor/procedural-animals/src/species/dog/sculpt.js +0 -246
  146. package/vendor/procedural-animals/src/species/eagle/coat.js +0 -250
  147. package/vendor/procedural-animals/src/species/eagle/index.js +0 -76
  148. package/vendor/procedural-animals/src/species/eagle/joints.js +0 -3
  149. package/vendor/procedural-animals/src/species/eagle/motion.js +0 -137
  150. package/vendor/procedural-animals/src/species/eagle/regions.js +0 -57
  151. package/vendor/procedural-animals/src/species/eagle/rig.js +0 -107
  152. package/vendor/procedural-animals/src/species/eagle/sculpt.js +0 -137
  153. package/vendor/procedural-animals/src/species/fish/coat.js +0 -271
  154. package/vendor/procedural-animals/src/species/fish/index.js +0 -88
  155. package/vendor/procedural-animals/src/species/fish/motion.js +0 -92
  156. package/vendor/procedural-animals/src/species/fish/regions.js +0 -27
  157. package/vendor/procedural-animals/src/species/fish/rig.js +0 -143
  158. package/vendor/procedural-animals/src/species/fish/sculpt.js +0 -214
  159. package/vendor/procedural-animals/src/species/fish/variants.js +0 -126
  160. package/vendor/procedural-animals/src/species/fox/behaviour.js +0 -314
  161. package/vendor/procedural-animals/src/species/fox/coat.js +0 -444
  162. package/vendor/procedural-animals/src/species/fox/index.js +0 -120
  163. package/vendor/procedural-animals/src/species/fox/motion.js +0 -81
  164. package/vendor/procedural-animals/src/species/fox/regions.js +0 -59
  165. package/vendor/procedural-animals/src/species/fox/rig.js +0 -91
  166. package/vendor/procedural-animals/src/species/fox/sculpt.js +0 -202
  167. package/vendor/procedural-animals/src/species/frog/behaviour.js +0 -590
  168. package/vendor/procedural-animals/src/species/frog/coat.js +0 -305
  169. package/vendor/procedural-animals/src/species/frog/index.js +0 -97
  170. package/vendor/procedural-animals/src/species/frog/motion.js +0 -69
  171. package/vendor/procedural-animals/src/species/frog/regions.js +0 -46
  172. package/vendor/procedural-animals/src/species/frog/rig.js +0 -156
  173. package/vendor/procedural-animals/src/species/frog/sculpt.js +0 -240
  174. package/vendor/procedural-animals/src/species/goat/behaviour.js +0 -509
  175. package/vendor/procedural-animals/src/species/goat/coat.js +0 -590
  176. package/vendor/procedural-animals/src/species/goat/index.js +0 -149
  177. package/vendor/procedural-animals/src/species/goat/motion.js +0 -95
  178. package/vendor/procedural-animals/src/species/goat/regions.js +0 -98
  179. package/vendor/procedural-animals/src/species/goat/rig.js +0 -181
  180. package/vendor/procedural-animals/src/species/goat/sculpt.js +0 -547
  181. package/vendor/procedural-animals/src/species/horse/coat.js +0 -361
  182. package/vendor/procedural-animals/src/species/horse/coatSeam.js +0 -122
  183. package/vendor/procedural-animals/src/species/horse/index.js +0 -80
  184. package/vendor/procedural-animals/src/species/horse/motion.js +0 -245
  185. package/vendor/procedural-animals/src/species/horse/regions.js +0 -60
  186. package/vendor/procedural-animals/src/species/horse/rig.js +0 -117
  187. package/vendor/procedural-animals/src/species/horse/sculpt.js +0 -316
  188. package/vendor/procedural-animals/src/species/horse/seamWeights.js +0 -272
  189. package/vendor/procedural-animals/src/species/lion/behaviour.js +0 -509
  190. package/vendor/procedural-animals/src/species/lion/coat.js +0 -1016
  191. package/vendor/procedural-animals/src/species/lion/index.js +0 -111
  192. package/vendor/procedural-animals/src/species/lion/motion.js +0 -87
  193. package/vendor/procedural-animals/src/species/lion/regions.js +0 -106
  194. package/vendor/procedural-animals/src/species/lion/rig.js +0 -110
  195. package/vendor/procedural-animals/src/species/lion/sculpt.js +0 -384
  196. package/vendor/procedural-animals/src/species/lion/whiskers.js +0 -132
  197. package/vendor/procedural-animals/src/species/pig/behaviour.js +0 -575
  198. package/vendor/procedural-animals/src/species/pig/coat.js +0 -730
  199. package/vendor/procedural-animals/src/species/pig/ears.js +0 -242
  200. package/vendor/procedural-animals/src/species/pig/index.js +0 -161
  201. package/vendor/procedural-animals/src/species/pig/motion.js +0 -105
  202. package/vendor/procedural-animals/src/species/pig/regions.js +0 -100
  203. package/vendor/procedural-animals/src/species/pig/rig.js +0 -152
  204. package/vendor/procedural-animals/src/species/pig/sculpt.js +0 -382
  205. package/vendor/procedural-animals/src/species/rabbit/behaviour.js +0 -605
  206. package/vendor/procedural-animals/src/species/rabbit/coat.js +0 -285
  207. package/vendor/procedural-animals/src/species/rabbit/index.js +0 -85
  208. package/vendor/procedural-animals/src/species/rabbit/motion.js +0 -85
  209. package/vendor/procedural-animals/src/species/rabbit/regions.js +0 -48
  210. package/vendor/procedural-animals/src/species/rabbit/rig.js +0 -99
  211. package/vendor/procedural-animals/src/species/rabbit/sculpt.js +0 -148
  212. package/vendor/procedural-animals/src/species/rat/behaviour.js +0 -539
  213. package/vendor/procedural-animals/src/species/rat/coat.js +0 -341
  214. package/vendor/procedural-animals/src/species/rat/index.js +0 -111
  215. package/vendor/procedural-animals/src/species/rat/motion.js +0 -90
  216. package/vendor/procedural-animals/src/species/rat/regions.js +0 -92
  217. package/vendor/procedural-animals/src/species/rat/rig.js +0 -112
  218. package/vendor/procedural-animals/src/species/rat/sculpt.js +0 -163
  219. package/vendor/procedural-animals/src/species/rat/whiskers.js +0 -119
  220. package/vendor/procedural-animals/src/species/shark/coat.js +0 -383
  221. package/vendor/procedural-animals/src/species/shark/index.js +0 -104
  222. package/vendor/procedural-animals/src/species/shark/motion.js +0 -69
  223. package/vendor/procedural-animals/src/species/shark/regions.js +0 -34
  224. package/vendor/procedural-animals/src/species/shark/rig.js +0 -194
  225. package/vendor/procedural-animals/src/species/shark/sculpt.js +0 -343
  226. package/vendor/procedural-animals/src/species/shark/variants.js +0 -105
  227. package/vendor/procedural-animals/src/species/sheep/behaviour.js +0 -522
  228. package/vendor/procedural-animals/src/species/sheep/coat.js +0 -529
  229. package/vendor/procedural-animals/src/species/sheep/index.js +0 -152
  230. package/vendor/procedural-animals/src/species/sheep/motion.js +0 -86
  231. package/vendor/procedural-animals/src/species/sheep/neck.js +0 -41
  232. package/vendor/procedural-animals/src/species/sheep/regions.js +0 -62
  233. package/vendor/procedural-animals/src/species/sheep/rig.js +0 -163
  234. package/vendor/procedural-animals/src/species/sheep/sculpt.js +0 -542
  235. package/vendor/procedural-animals/src/species/snake/coat.js +0 -315
  236. package/vendor/procedural-animals/src/species/snake/index.js +0 -73
  237. package/vendor/procedural-animals/src/species/snake/motion.js +0 -51
  238. package/vendor/procedural-animals/src/species/snake/regions.js +0 -51
  239. package/vendor/procedural-animals/src/species/snake/rig.js +0 -108
  240. package/vendor/procedural-animals/src/species/snake/sculpt.js +0 -122
  241. package/vendor/procedural-animals/src/species/spider/coat.js +0 -279
  242. package/vendor/procedural-animals/src/species/spider/index.js +0 -78
  243. package/vendor/procedural-animals/src/species/spider/motion.js +0 -72
  244. package/vendor/procedural-animals/src/species/spider/regions.js +0 -87
  245. package/vendor/procedural-animals/src/species/spider/rig.js +0 -118
  246. package/vendor/procedural-animals/src/species/spider/sculpt.js +0 -182
  247. package/vendor/procedural-animals/src/species/wolf/behaviour.js +0 -160
  248. package/vendor/procedural-animals/src/species/wolf/coat.js +0 -749
  249. package/vendor/procedural-animals/src/species/wolf/index.js +0 -107
  250. package/vendor/procedural-animals/src/species/wolf/motion.js +0 -80
  251. package/vendor/procedural-animals/src/species/wolf/regions.js +0 -57
  252. package/vendor/procedural-animals/src/species/wolf/rig.js +0 -87
  253. package/vendor/procedural-animals/src/species/wolf/sculpt.js +0 -339
  254. package/vendor/procedural-animals/src/worker.js +0 -29
@@ -0,0 +1,25 @@
1
+ import {grayFrameReader,isGrayFrameSource} from './puppy-frame-store.mjs';
2
+
3
+ // A handful of authored keys, not an expanded 30fps film or a live image morph.
4
+ // First/last keys are the exact shared seated source; all keys use one scale.
5
+ export function comfortFrameIndex(progress,info){
6
+ if(!Number.isFinite(progress)||progress<0||progress>1)throw Error('Invalid comfort progress');
7
+ const seconds=progress*6;
8
+ if(seconds<.12||progress===1)return 0;
9
+ if(seconds<.65)return seconds<.3?1:2;
10
+ if(seconds>5.15)return seconds<5.45?7:seconds<5.8?8:0;
11
+ const loop=[3,4,3,5,3,6,3,5,3,4,3,6];
12
+ return loop[Math.floor((seconds-.65)/.28)%loop.length];
13
+ }
14
+
15
+ export function createComfortSpriteSampler(bytes,info){
16
+ if(!isGrayFrameSource(bytes)||info?.version!==1||info.width!==320||info.height!==320||info.frames!==9)throw Error('Invalid comfort sprites');
17
+ const frames=grayFrameReader(bytes,320,320,info.frames);let rgba=null;
18
+ return pose=>{
19
+ if(pose?.clip!=='tug')throw Error('Invalid comfort clip');
20
+ if(!rgba){rgba=new Uint8ClampedArray(320*320*4);for(let i=3;i<rgba.length;i+=4)rgba[i]=255;}
21
+ const index=comfortFrameIndex(pose.progress,info),frame=frames.getFrame(index);
22
+ for(let i=0,j=0;i<frame.length;i++,j+=4)rgba[j]=rgba[j+1]=rgba[j+2]=frame[i];
23
+ return rgba;
24
+ };
25
+ }
@@ -0,0 +1,77 @@
1
+ {
2
+ "version": 1,
3
+ "width": 320,
4
+ "height": 320,
5
+ "frames": 9,
6
+ "clips": [
7
+ "tug"
8
+ ],
9
+ "gzipBytes": 103549,
10
+ "residentPixelBytes": 291337,
11
+ "sourceSheetSha256": "10fde9dacca5cb45acd68f387e7ed3bcf88cd72cf0f9c4220d31d819db91731e",
12
+ "neutralSha256": "19f89a8bcb5a937e528593a8f36db846a0d45a5ee398f1b0873c056e5be2fa0a",
13
+ "encodedSha256": "e046e214da585ffda65e8d2c1a86c589af589325b792634f128ab8b71f6dbf3a",
14
+ "method": "Built-in imagegen authored tug keys. One fixed scale, ground/center translation only. Exact existing Q5 seat first/last. No expanded tween film or live body deformation.",
15
+ "registration": {
16
+ "sheetSize": [
17
+ 1774,
18
+ 887
19
+ ],
20
+ "targetBounds": [
21
+ 41,
22
+ 92,
23
+ 231,
24
+ 301
25
+ ],
26
+ "registration": [
27
+ {
28
+ "cell": 0,
29
+ "scale": 0.5663956639566395,
30
+ "dx": 32,
31
+ "dy": 69
32
+ },
33
+ {
34
+ "cell": 1,
35
+ "scale": 0.5663956639566395,
36
+ "dx": 39,
37
+ "dy": 69
38
+ },
39
+ {
40
+ "cell": 2,
41
+ "scale": 0.5663956639566395,
42
+ "dx": 39,
43
+ "dy": 69
44
+ },
45
+ {
46
+ "cell": 3,
47
+ "scale": 0.5663956639566395,
48
+ "dx": 35,
49
+ "dy": 68
50
+ },
51
+ {
52
+ "cell": 4,
53
+ "scale": 0.5663956639566395,
54
+ "dx": 33,
55
+ "dy": 74
56
+ },
57
+ {
58
+ "cell": 5,
59
+ "scale": 0.5663956639566395,
60
+ "dx": 39,
61
+ "dy": 74
62
+ },
63
+ {
64
+ "cell": 6,
65
+ "scale": 0.5663956639566395,
66
+ "dx": 39,
67
+ "dy": 74
68
+ },
69
+ {
70
+ "cell": 7,
71
+ "scale": 0.5663956639566395,
72
+ "dx": 36,
73
+ "dy": 74
74
+ }
75
+ ]
76
+ }
77
+ }
@@ -11,16 +11,18 @@ const BALL_TARGET={x:0,z:-4};
11
11
  const OFFER={x:0,z:.3};
12
12
  // Hand-fed treat marker, relative to the puppy's current root (no travel for treats).
13
13
  const TREAT_OFFSET={z:.08,height:.25};
14
- const COOLDOWN={ball:6,treat:15};
14
+ const COOLDOWN={ball:6,treat:15,tug:20};
15
15
  const FLIGHT=.65,BALL_PEAK=.75,CARRY_HEIGHT=.19,SETTLE_FALLBACK=.6,MAX_DT=.25,ARRIVE=.05;
16
16
  const LABEL={
17
17
  start:'기다리는 중',pause:'잠시 느긋하게 둘러보는 중',lie:'엎드리는 중',rest:'엎드려 쉬는 중',recover:'앉아서 기운 차리는 중',
18
18
  sit:'앉는 중',watch:'앉아서 지켜보는 중',look:'고개를 돌려 살피는 중',observe:'가만히 주변 관찰 중',
19
19
  stroll:'옆으로 살짝 걸어가는 중',sniff:'잠깐 멈춰 주변을 살피는 중',dash:'저 멀리 달려가는 중',far:'멀리서 둘러보는 중',home:'제자리로 돌아오는 중',
20
+ patrol:'주변을 왕복 산책하는 중',scan:'양쪽을 번갈아 살피는 중',roll:'느긋하게 뒹구는 중',play:'신나서 한 바퀴 도는 중',checkin:'가까이 와서 함께 쉬는 중',
21
+ belly:'배를 보이고 발라당 쉬는 중',sleep:'새근새근 잠자는 중',tug:'장난감을 물고 당기는 중',
20
22
  fetch:'공을 쫓아가는 중',pickup:'공을 무는 중',carry:'공을 물고 돌아오는 중',drop:'공을 내려놓는 중',offer:'공을 내려놓고 기다리는 중',
21
23
  treatSit:'간식 앞에 앉는 중',chew:'간식 먹는 중'
22
24
  };
23
- const PENDING_LABEL={treat:'곧 간식'};
25
+ const PENDING_LABEL={treat:'곧 간식',tug:'곧 터그놀이'};
24
26
  // Baked interaction clips, played only while holding at the shared seated endpoint.
25
27
  const GESTURE={'ball-pickup':1.4,'ball-drop':1.2,'treat-eat':2.4};
26
28
  const clamp=(v,a,b)=>Math.min(b,Math.max(a,v));
@@ -38,7 +40,8 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
38
40
 
39
41
  const rand=mulberry32(seed),range=(a,b)=>a+(b-a)*rand(),side=()=>rand()<.5?'left':'right';
40
42
  const needs={energy,social:.5,curiosity:.5};
41
- const cooldowns={ball:0,treat:0};
43
+ const cooldowns={ball:0,treat:0,tug:0};
44
+ const episodeCooldowns={roll:0,play:0,belly:0,sleep:0};
42
45
  let clock=0,decisions=0,lastEpisode=null;
43
46
  let queue=[],hold=0,holdIntent=null,waiting=null,gesture=null;
44
47
  let intent='start',label=LABEL.start,pending=null,active=null,ball=null,treat=null;
@@ -51,25 +54,44 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
51
54
  // Treats accept any seat variant at the front-seated endpoint: the hand comes to the puppy.
52
55
  const seatedAnyVariant=()=>{const s=actor.sample(clock);return actor.state==='idle'&&s.seatProgress===1&&s.view==='approach';};
53
56
  const fx=fn=>({fx:fn});
54
- const home=()=>cmd('go',[HOME.x,HOME.z],'home');
57
+ const home=()=>cmd('go',[HOME.x,HOME.z],'home',()=>{const s=actor.sample(clock);return Math.hypot(s.x-HOME.x,s.z-HOME.z)>ARRIVE;});
58
+ const sit=step=>cmd('sit',[],step,()=>!seatedAnyVariant());
59
+ const lie=()=>cmd('lie',[],'lie',()=>actor.state!=='down');
60
+ const walkTo=x=>cmd('walk',()=>{const dx=x-actor.sample(clock).x;return [dx<0?'left':'right',Math.abs(dx)];},'patrol',()=>Math.abs(x-actor.sample(clock).x)>.002);
55
61
 
56
62
  // Each episode is a short coherent plan; a pause always leads in so autonomy never stalls.
57
63
  const EPISODES={
58
- rest:()=>[cmd('lie',[],'lie'),wait(range(10,18),'rest'),cmd('sit',[],'recover'),wait(range(3,6),'recover')],
59
- watch:()=>[cmd('sit',[],'sit'),cmd('look',[side()],'look'),wait(range(3,9),'watch'),fx(()=>{needs.social-=.3;needs.curiosity+=.1;})],
60
- observe:()=>[cmd('look',[side()],'look'),wait(range(3,9),'observe'),fx(()=>{needs.curiosity-=.15;})],
64
+ rest:()=>[lie(),wait(range(10,18),'rest'),sit('recover'),wait(range(3,6),'recover')],
65
+ watch:()=>[sit('sit'),cmd('look',[side()],'look'),wait(range(3,9),'watch'),fx(()=>{needs.social-=.3;needs.curiosity+=.1;})],
66
+ observe:()=>[sit('sit'),cmd('look',[side()],'look'),wait(range(3,9),'observe'),fx(()=>{needs.curiosity-=.15;})],
61
67
  stroll:()=>[cmd('walk',[side(),range(.08,.18)],'stroll'),wait(range(2,4),'sniff'),home(),fx(()=>{needs.energy-=.08;needs.curiosity-=.2;})],
62
- dash:()=>[cmd('go',[(rand()*2-1)*.18,-range(3,8)],'dash'),wait(range(1.5,3),'far'),home(),fx(()=>{needs.energy-=.3;needs.curiosity-=.4;})]
68
+ dash:()=>[cmd('go',[(rand()*2-1)*.18,-range(3,8)],'dash'),wait(range(1.5,3),'far'),home(),fx(()=>{needs.energy-=.3;needs.curiosity-=.4;})],
69
+ scan:()=>[sit('sit'),cmd('look',['left'],'scan'),wait(range(.5,1.5),'scan'),cmd('look',['right'],'scan'),wait(range(2,4),'observe'),fx(()=>{needs.curiosity-=.25;})],
70
+ patrol:()=>{const x=actor.sample(clock).x,d=range(.16,.3)*(rand()<.5?-1:1);return [walkTo(x+d),wait(range(.6,1.2),'sniff'),walkTo(x-d),wait(range(.6,1.2),'sniff'),walkTo(x),wait(range(1,3),'observe'),fx(()=>{needs.energy-=.1;needs.curiosity-=.25;})];},
71
+ roll:()=>[lie(),wait(range(.7,1.5),'rest'),cmd('roll',[],'roll'),wait(range(2,5),'watch'),fx(()=>{needs.energy-=.08;needs.curiosity-=.15;})],
72
+ checkin:()=>[home(),sit('sit'),cmd('look',[side()],'look'),wait(range(5,10),'checkin'),fx(()=>{needs.social-=.4;})],
73
+ play:()=>[cmd('spin',[],'play'),wait(range(1,2),'pause'),sit('sit'),wait(range(2,4),'watch'),fx(()=>{needs.energy-=.12;needs.curiosity-=.2;})],
74
+ belly:()=>[cmd('belly',[range(5,9)],'belly'),wait(range(2,4),'watch'),fx(()=>{needs.energy+=.08;needs.curiosity-=.15;})],
75
+ sleep:()=>[cmd('sleep',[range(22,38)],'sleep'),wait(range(2,4),'recover'),fx(()=>{needs.energy+=.3;})],
63
76
  };
64
77
  const weights=()=>{
65
78
  const {energy:e,social:s,curiosity:c}=needs;
66
- return {rest:e<.35?6:(1-e)*1.2,watch:.8+s,observe:.6+c*.6,stroll:1.2*c*e,dash:e>.55?1.2*e*c:0};
79
+ return {rest:e<.35?6:(1-e)*1.2,watch:.8+s,observe:.6+c*.6,stroll:1.2*c*e,dash:e>.55?1.2*e*c:0,
80
+ scan:.35+c*.4,patrol:e>.25?.7*c*e:0,checkin:.25+s*.7,
81
+ roll:episodeCooldowns.roll===0&&e>.45?.35*e:0,
82
+ play:episodeCooldowns.play===0&&e>.6&&typeof actor.spin==='function'?.25*e*c:0,
83
+ belly:episodeCooldowns.belly===0&&typeof actor.belly==='function'?.35:0,
84
+ sleep:episodeCooldowns.sleep===0&&typeof actor.sleep==='function'?(e<.4?3:.2):0};
67
85
  };
68
86
  function decide(){
69
87
  const w=weights();if(lastEpisode)w[lastEpisode]=0;
70
88
  const kinds=Object.keys(w),total=kinds.reduce((sum,k)=>sum+w[k],0);
71
89
  let pick=rand()*total,kind=kinds.find(k=>(pick-=w[k])<0)??'observe';
72
90
  lastEpisode=kind;decisions++;
91
+ if(kind==='roll')episodeCooldowns.roll=90;
92
+ if(kind==='play')episodeCooldowns.play=120;
93
+ if(kind==='belly')episodeCooldowns.belly=80;
94
+ if(kind==='sleep')episodeCooldowns.sleep=100;
73
95
  queue=[wait(range(3,9),'pause'),...EPISODES[kind]()];
74
96
  }
75
97
 
@@ -88,15 +110,18 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
88
110
  fx(()=>{treat=true;}),cmd('sit',[],'treatSit',()=>!seatedAnyVariant()),
89
111
  play('treat-eat','chew'),fx(()=>{treat=null;needs.energy+=.25;needs.social-=.3;})
90
112
  ];
113
+ const TUG_STEPS=()=>[
114
+ cmd('sit',[],'tug',unseated),cmd('tug',[6],'tug'),wait(.5,'watch'),fx(()=>{needs.energy-=.12;needs.social-=.4;})
115
+ ];
91
116
 
92
117
  function startInteraction(){
93
118
  active=pending;pending=null;decisions++;hold=0;
94
- queue=active==='ball'?BALL_STEPS():TREAT_STEPS();
119
+ queue=active==='ball'?BALL_STEPS():active==='tug'?TUG_STEPS():TREAT_STEPS();
95
120
  }
96
121
  function finishInteraction(){cooldowns[active]=COOLDOWN[active];active=null;}
97
122
 
98
123
  function drift(dt){
99
- const resting=holdIntent==='rest'||holdIntent==='recover';
124
+ const resting=holdIntent==='rest'||holdIntent==='recover'||actor.state==='sleep'||intent==='belly';
100
125
  needs.energy+=resting?.04*dt:-.004*dt;
101
126
  if(!active){needs.curiosity+=.01*dt;needs.social+=.008*dt;}
102
127
  for(const k in needs)needs[k]=clamp(needs[k],0,1);
@@ -106,6 +131,7 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
106
131
  if(typeof dt!=='number'||!Number.isFinite(dt)||dt<0)throw RangeError('dt must be a finite non-negative number');
107
132
  dt=Math.min(dt,MAX_DT);clock+=dt;
108
133
  for(const k in cooldowns)cooldowns[k]=Math.max(0,cooldowns[k]-dt);
134
+ for(const k in episodeCooldowns)episodeCooldowns[k]=Math.max(0,episodeCooldowns[k]-dt);
109
135
  drift(dt);
110
136
  const safe=SAFE.has(actor.state);
111
137
  if(waiting){
@@ -133,15 +159,21 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
133
159
  if(step.when&&!step.when())continue;
134
160
  intent=step.intent;label=step.label;
135
161
  if(step.hold!==undefined){hold=step.hold;holdIntent=step.intent;if(step.gesture)gesture={clip:step.gesture,duration:step.hold,elapsed:0};return;}
136
- if(actor[step.cmd](...step.args)!==false)waiting={age:0,left:false};
162
+ if(actor[step.cmd](...(typeof step.args==='function'?step.args():step.args))!==false)waiting={age:0,left:false};
137
163
  return;
138
164
  }
139
165
  }
140
166
 
141
167
  function interact(kind){
142
- if(kind!=='ball'&&kind!=='treat')return false;
168
+ if(!['ball','treat','tug'].includes(kind))return false;
169
+ if(kind==='tug'&&typeof actor.tug!=='function')return false;
143
170
  if(pending||active||cooldowns[kind]>0)return false;
144
- if(kind==='treat'){pending=kind;return true;}
171
+ if(kind==='treat'||kind==='tug'){
172
+ pending=kind;
173
+ // The pet wakes through its authored recovery, not after a long sleep timer.
174
+ if(['sleep','belly'].includes(actor.sample(clock).state))actor.sit();
175
+ return true;
176
+ }
145
177
  // Ball: launch now and drop obsolete controller plans; the actor's own state is left to chase.
146
178
  active='ball';decisions++;ball={mode:'flight',start:clock};
147
179
  queue=BALL_STEPS();hold=0;holdIntent=null;gesture=null;waiting=null;
@@ -172,8 +204,9 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
172
204
  get report(){
173
205
  const interaction=active??pending;
174
206
  return {
175
- intent,label:pending?`${label} · ${PENDING_LABEL[pending]}`:label,interaction,busy:interaction!==null,
176
- ball:ballReport(),treat:treatReport(),energy:needs.energy,decisions,cooldowns:{...cooldowns},
207
+ intent,label:pending?`${label} · ${PENDING_LABEL[pending]}`:label,episode:lastEpisode,interaction,busy:interaction!==null,
208
+ ball:ballReport(),treat:treatReport(),tug:actor.state==='tug'?{progress:actor.sample(clock).comfortPose?.progress??0,grip:1}:null,
209
+ snore:actor.state==='sleep'?((clock%3.6)/3.6):null,energy:needs.energy,decisions,cooldowns:{...cooldowns},
177
210
  // Local hold clock only: progress freezes between updates; null outside the stationary hold.
178
211
  gesture:gesture&&SAFE.has(actor.state)?{clip:gesture.clip,progress:clamp(gesture.elapsed/gesture.duration,0,1)}:null
179
212
  };
@@ -7,6 +7,18 @@ export function paintCompanionObjects(pixels,width,height,projection,report,stat
7
7
  // Interaction drawings already include the toy/food. Never paint a second
8
8
  // floating ball over the baked mouth, or a biscuit through the puppy's chest.
9
9
  if(report.gesture)return pixels;
10
+ // A quiet, tiny rising "z" suggests snoring. The sleeping dog itself remains
11
+ // the original eyes-closed source pose: no chest/body zoom or squashing.
12
+ if(Number.isFinite(report.snore)&&state.state==='sleep'){
13
+ const placement=projection.placement(state.x,state.z),side=height*placement.size,sideX=side/placement.pixelAspect;
14
+ const left=width*placement.x-sideX/2,top=height*placement.y-300/320*side;
15
+ const cycle=report.snore,tone=Math.round(140*Math.sin(Math.PI*cycle)),unit=Math.max(1,Math.round(side/160));
16
+ const cx=Math.round(left+220/320*sideX),cy=Math.round(top+(210-16*cycle)/320*side);
17
+ if(tone>12)for(const [gx,gy] of [[0,0],[1,0],[2,0],[1,1],[0,2],[1,2],[2,2]])for(let dy=0;dy<unit;dy++)for(let dx=0;dx<unit;dx++){
18
+ const x=cx+gx*unit+dx,y=cy+gy*unit+dy;if(x<0||x>=width||y<0||y>=height)continue;
19
+ const j=(y*width+x)*4;pixels[j]=pixels[j+1]=pixels[j+2]=Math.max(pixels[j],tone);
20
+ }
21
+ }
10
22
  if(report.ball?.carried){if(mouth&&[mouth.x,mouth.y,mouth.rx,mouth.ry].every(Number.isFinite)&&mouth.rx>0&&mouth.ry>0)objects.push({kind:'ball',screen:mouth});}
11
23
  else if(report.ball)objects.push({...report.ball,radius:.035,kind:'ball'});
12
24
  if(report.treat)objects.push({...report.treat,height:report.treat.height??.018,radius:.022,kind:'treat'});
@@ -80,7 +80,7 @@ function unfilterRow(type,src,at,cur,prev,w){
80
80
 
81
81
  // Validate and unfilter. Only the returned typed arrays survive; `bytes` is
82
82
  // not referenced by the store built from them.
83
- function decode(bytes){
83
+ function decode(bytes,{frameIndices}={}){
84
84
  if(!(bytes instanceof Uint8Array))fail('bytes');
85
85
  if(!isPredictiveFrames(bytes)||bytes.length<HEADER)fail('magic');
86
86
  const view=new DataView(bytes.buffer,bytes.byteOffset,bytes.byteLength),u32=at=>view.getUint32(at,true);
@@ -89,8 +89,13 @@ function decode(bytes){
89
89
  if(unique<1||unique>frameCount)fail('unique count');
90
90
  const data=HEADER+4*frameCount+RECORD*unique;
91
91
  if(bytes.length<data)fail('truncated');
92
- const map=new Uint32Array(frameCount),used=new Uint8Array(unique);
93
- for(let f=0;f<frameCount;f++){const u=u32(HEADER+4*f);if(u>=unique)fail('map');map[f]=u;used[u]=1;}
92
+ const retained=frameIndices===undefined?null:new Uint8Array(frameCount);
93
+ if(retained){
94
+ if(!Array.isArray(frameIndices)||!frameIndices.length)fail('retained frames');
95
+ for(const f of frameIndices){if(!Number.isInteger(f)||f<0||f>=frameCount)fail('retained frame index');retained[f]=1;}
96
+ }
97
+ const map=new Uint32Array(frameCount),used=new Uint8Array(unique),needed=retained?new Uint8Array(unique):null;
98
+ for(let f=0;f<frameCount;f++){const u=u32(HEADER+4*f);if(u>=unique)fail('map');map[f]=u;used[u]=1;if(retained?.[f])needed[u]=1;}
94
99
  if(used.includes(0))fail('unreferenced frame');
95
100
  // x,y,w,h,first pixel index in the packed stream
96
101
  const rects=new Float64Array(unique*5);let total=0,pixels=0;
@@ -98,7 +103,7 @@ function decode(bytes){
98
103
  const at=HEADER+4*frameCount+RECORD*u,x=u32(at),y=u32(at+4),w=u32(at+8),h=u32(at+12),offset=u32(at+16),length=u32(at+20);
99
104
  if((w===0)!==(h===0)||(w===0&&(x||y))||x+w>width||y+h>height)fail('rectangle');
100
105
  if(length!==h*(w+1)||offset!==total)fail('offset');
101
- rects.set([x,y,w,h,pixels],u*5);total+=length;pixels+=w*h;
106
+ rects.set([x,y,w,h,pixels],u*5);total+=length;if(!needed||needed[u])pixels+=w*h;
102
107
  }
103
108
  if(bytes.length!==data+total)fail('pixel length');
104
109
  if(pixels*BITS>0xffffffff)fail('pixel count');
@@ -109,23 +114,24 @@ function decode(bytes){
109
114
  for(let y=0;y<h;y++){
110
115
  unfilterRow(bytes[at],bytes,at+1,cur,prev,w);at+=w+1;
111
116
  for(let x=0;x<w;x++){
112
- const q=cur[x];if(q>31)fail('quantized value');
117
+ const q=cur[x];if(q>31)fail('quantized value');if(needed&&!needed[u])continue;
113
118
  const i=pos>>>3,s=pos&7;packed[i]|=(q<<s)&255;if(s>3)packed[i+1]|=q>>(8-s);pos+=BITS;
114
119
  }
115
120
  const t=prev;prev=cur;cur=t;
116
121
  }
117
122
  }
118
- return{width,height,frameCount,unique,map,rects,packed};
123
+ return{width,height,frameCount,unique,map,rects,packed,retained};
119
124
  }
120
125
 
121
- function createStore({width,height,frameCount,unique,map,rects,packed}){
126
+ function createStore({width,height,frameCount,unique,map,rects,packed,retained}){
122
127
  const n=width*height,slots=[{unique:-1,frame:null},{unique:-1,frame:null}];let decodes=0,hits=0;
123
128
  return Object.freeze({
124
129
  width,height,frameCount,uniqueCount:unique,byteLength:n*frameCount,residentPixelBytes:packed.byteLength,
125
- get residentBytes(){return packed.byteLength+map.byteLength+rects.byteLength+slots.reduce((s,e)=>s+(e.frame?.byteLength??0),0);},
130
+ get residentBytes(){return packed.byteLength+map.byteLength+rects.byteLength+(retained?.byteLength??0)+slots.reduce((s,e)=>s+(e.frame?.byteLength??0),0);},
126
131
  stats(){return{decodes,hits,cachedFrames:slots.filter(e=>e.frame).length};},
127
132
  getFrame(index){
128
133
  if(!Number.isInteger(index)||index<0||index>=frameCount)fail('frame index');
134
+ if(retained&&!retained[index])fail('frame was not retained');
129
135
  const u=map[index];
130
136
  if(slots[0].unique===u){hits++;return slots[0].frame;}
131
137
  if(slots[1].unique===u){hits++;slots.reverse();return slots[0].frame;}
@@ -144,4 +150,4 @@ function createStore({width,height,frameCount,unique,map,rects,packed}){
144
150
 
145
151
  // Parse, validate and unpack an RVQ5 store. A returned frame is valid until two
146
152
  // other unique frames have been requested.
147
- export function readPredictiveFrames(bytes){return createStore(decode(bytes));}
153
+ export function readPredictiveFrames(bytes,options){return createStore(decode(bytes,options));}
@@ -52,9 +52,9 @@ function checkShape(width,height,frameCount){
52
52
  // resident (crops only) and reconstructs at most two full frames at a time.
53
53
  // A returned frame is valid until two other unique frames have been requested.
54
54
  // An 'RVQ5' store (lossy 32-level comparison codec) is delegated and branded.
55
- export function readPackedFrames(bytes){
55
+ export function readPackedFrames(bytes,options){
56
56
  if(!(bytes instanceof Uint8Array))throw Error('Invalid packed frames: bytes');
57
- if(isPredictiveFrames(bytes)){const store=readPredictiveFrames(bytes);stores.add(store);return store;}
57
+ if(isPredictiveFrames(bytes)){const store=readPredictiveFrames(bytes,options);stores.add(store);return store;}
58
58
  if(bytes.length<HEADER||MAGIC.some((v,i)=>bytes[i]!==v))throw Error('Invalid packed frames: magic');
59
59
  const view=new DataView(bytes.buffer,bytes.byteOffset,bytes.byteLength),u32=at=>view.getUint32(at,true);
60
60
  const width=u32(4),height=u32(8),frameCount=u32(12),unique=u32(16);
@@ -1,6 +1,20 @@
1
1
  // Design calibration, not a measurement of the user's current head position.
2
2
  // Sources: OpenCV pinhole model; PsychoPy viewtools physical-screen frustums.
3
3
  const diagonal=.0254*32;
4
+ // The paper actor already integrates its own calibrated travel profile and foot
5
+ // contacts. Its old 3D Animal.follow() merely copied that root's x/z, then also
6
+ // updated an invisible skinned mesh, IK, fur and materials. Keep just the follow
7
+ // contract here; speed/stride/projection remain owned by the existing planner.
8
+ export function createPaperBody(){
9
+ const position={x:0,y:0,z:0};let target=null,disposed=false;
10
+ const alive=()=>{if(disposed)throw Error('Paper body is disposed');};
11
+ return{
12
+ position,
13
+ follow(point){alive();if(!point||![point.x,point.y,point.z].every(Number.isFinite))throw Error('Invalid paper body position');target={x:point.x,y:point.y,z:point.z};},
14
+ update(dt){alive();if(!Number.isFinite(dt)||dt<0||dt>.1)throw Error('Invalid paper body step');if(target){position.x=target.x;position.z=target.z;}},
15
+ dispose(){disposed=true;target=null;},
16
+ };
17
+ }
4
18
  export const PAPER_SCREEN=Object.freeze({widthMeters:diagonal*16/Math.hypot(16,9),heightMeters:diagonal*9/Math.hypot(16,9),viewDistance:.70});
5
19
  const eyeHeight=.45,eyeZ=1.65,pitch=9*Math.PI/180;
6
20
  export const PAPER_CAMERA=Object.freeze({fov:2*Math.atan(PAPER_SCREEN.heightMeters/(2*PAPER_SCREEN.viewDistance))*180/Math.PI,aspect:16/9,position:Object.freeze([0,eyeHeight,eyeZ]),target:Object.freeze([0,.12,eyeZ-(eyeHeight-.12)/Math.tan(pitch)]),canvasMeters:.64});
package/puppy-paper.mjs CHANGED
@@ -8,13 +8,31 @@ import {createPaperAreaSampler} from './puppy-paper-area.mjs';
8
8
  import {PAPER_ACTION_DURATIONS,createPaperActionSampler,paperActionPose} from './puppy-paper-actions.mjs';
9
9
  import {createPawTransport} from './puppy-paper-paw-transport.mjs';
10
10
  import {createCompanionSpriteSampler} from './puppy-companion-sprites.mjs';
11
+ import {createComfortSpriteSampler} from './puppy-comfort-sprites.mjs';
11
12
  import {mouthSourceFrame,mouthAnchor,projectMouth} from './puppy-mouth-anchor.mjs';
13
+ import {trickClipEdges} from './puppy-paper-trick-timing.mjs';
12
14
 
13
15
  // Fixed viewer: slightly closer/wider lens gives depth without changing the art.
14
16
  export {PAPER_CAMERA} from './puppy-paper-world.mjs';
15
17
 
18
+ // Comfort poses reuse existing clips only. Belly holds the calibrated roll-down
19
+ // source exactly at the start of the edge leaving `key` (paws-up belly), easing
20
+ // in/out with zero speed at the hold. `hold` defaults to the v9 keyOrder/edge
21
+ // boundary of roll7; createPaperComfortTimings re-derives it from action info.
22
+ export const PAPER_COMFORT_LIMITS=Object.freeze({belly:30,sleep:120,tug:30});
23
+ export const PAPER_COMFORT_TIMINGS=Object.freeze({belly:Object.freeze({key:'roll7',hold:.4449823183425605,enter:1.3,exit:1.7}),tugExit:.35});
24
+ export function createPaperComfortTimings(actionInfo,{key=PAPER_COMFORT_TIMINGS.belly.key}={}){
25
+ const {keys,bounds}=trickClipEdges('roll-down',actionInfo),i=keys.findIndex(k=>k[0]===key);
26
+ if(i<=0)throw Error(`Invalid comfort timing: no roll-down edge leaves ${key}`);
27
+ return Object.freeze({...PAPER_COMFORT_TIMINGS,belly:Object.freeze({...PAPER_COMFORT_TIMINGS.belly,key,hold:bounds[i]})});
28
+ }
29
+ const comfortDuration=(name,seconds)=>{if(typeof seconds!=='number'||!Number.isFinite(seconds)||seconds<=0||seconds>PAPER_COMFORT_LIMITS[name])throw Error(`Invalid ${name} duration`);};
30
+ const comfortEase=u=>u<=0?0:u>=1?1:u*u*(3-2*u);
31
+
16
32
  // Original artwork only. Physics supplies ground travel; no procedural dog is drawn.
17
- export function createPaperActor(animal,info,{manualTricks=false,trickTimings=null,walkTransitions=false}={}){
33
+ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=null,walkTransitions=false,comfortTimings=PAPER_COMFORT_TIMINGS}={}){
34
+ const bellyTiming=comfortTimings?.belly;
35
+ if(!bellyTiming||![bellyTiming.hold,bellyTiming.enter,bellyTiming.exit,comfortTimings.tugExit].every(Number.isFinite)||bellyTiming.hold<=0||bellyTiming.hold>=1||bellyTiming.enter<=0||bellyTiming.exit<=0||comfortTimings.tugExit<=0)throw Error('Invalid comfort timing');
18
36
  let state='idle',age=0,phase=0,flip=false,nextFlip=false,target=null,route=false,routeIndex=0,view='front',nextView='front',turnView='front',turnFlip=false;
19
37
  let held=null,waypoints=[];
20
38
  let preparationRate=1;
@@ -22,6 +40,7 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
22
40
  let straight=false,seatProgress=0,seatVariant='default',travel=null,approachPlan=null,travelAge=0,travelDistance=0,motionSpeed=0,motionStage='rest',completed=false;
23
41
  let watchSide='left',restProgress=0,idleHold=2.5;
24
42
  let pendingAction=null,activeAction=null,actionDone=null,deferred=null,movementMode='run',stayDown=false,manualDown=false,spinReady=false;
43
+ let comfort=null,comfortSeconds=0;
25
44
  const director=createPaperDirector();
26
45
  const points=[[.12,PAPER_GROUND.away]];
27
46
  const enter=s=>{state=s;age=0;};
@@ -33,13 +52,31 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
33
52
  const turnSeconds=()=>snappyPreparation()?.22:.85;
34
53
  const launchSeconds=()=>snappyPreparation()?.10:.25;
35
54
  const dispatch=(fn,{keepDown=false}={})=>{
36
- if(activeAction||(movementMode==='walk'&&state==='run')){route=false;deferred=()=>dispatch(fn,{keepDown});return true;}
55
+ if(activeAction||state==='tug'||(movementMode==='walk'&&state==='run')){route=false;deferred=()=>dispatch(fn,{keepDown});hurryComfort();return true;}
37
56
  // The calibrated trick down pose has its own byte-shared recovery; never
38
57
  // jump back into the older/larger rest illustration on another command.
39
58
  if(manualDown&&!keepDown){route=false;deferred=fn;playAction('lie-up',()=>{manualDown=false;view='approach';seatProgress=1;seatVariant='default';enter('idle');drain();},.55);return true;}
40
59
  return fn();
41
60
  };
42
61
  const drain=()=>{const fn=deferred;deferred=null;if(fn){fn();return true;}return false;};
62
+ // Belly: 0 -> hold eased to rest, held exact source sample, eased hold -> 1.
63
+ // Queued commands cut the remaining hold only; the roll always finishes at seat.
64
+ const bellyAction=seconds=>{
65
+ const {hold:H,enter:a,exit:b}=bellyTiming,t={comfort:'belly',hold:[a,a+seconds],duration:a+seconds+b};
66
+ t.progressAt=age=>{if(!Number.isFinite(age))throw Error('Invalid comfort age');return age<t.hold[0]?H*comfortEase(age/a):age<t.hold[1]?H:H+(1-H)*comfortEase((age-t.hold[1])/b);};
67
+ return t;
68
+ };
69
+ const startBelly=seconds=>{
70
+ manualDown=false;stayDown=false;restProgress=0;comfort=null;
71
+ const timing=bellyAction(seconds);activeAction={name:'roll-down',duration:timing.duration,timing,comfort:'belly'};actionDone=()=>finishTrick('roll');stop();enter('action');
72
+ if(deferred)hurryComfort();
73
+ };
74
+ const tugProgress=()=>{if(!comfort||comfort.kind!=='tug')return 0;const h=comfort.hurry;return h?(h.length>0?Math.min(1,h.progress+(1-h.progress)*(age-h.age)/h.length):1):Math.min(1,age/comfort.seconds);};
75
+ function hurryComfort(){
76
+ const t=activeAction?.timing;
77
+ if(t?.comfort==='belly'&&age<t.hold[1]){t.hold[1]=Math.max(t.hold[0],age);t.duration=activeAction.duration=t.hold[1]+bellyTiming.exit;}
78
+ else if(state==='tug'&&comfort&&!comfort.hurry){const progress=tugProgress();comfort.hurry={progress,age,length:Math.min(comfortTimings.tugExit,(1-progress)*comfort.seconds)};}
79
+ }
43
80
  const playAction=(name,done,duration=PAPER_ACTION_DURATIONS[name])=>{const timing=trickTimings?.[name];duration=timing?.duration??duration;if(!Number.isFinite(duration)||duration<=0)throw Error('Invalid paper action duration');activeAction={name,duration,timing};actionDone=done;stop();enter('action');};
44
81
  const finishTrick=name=>{if(name!=='spin'){view='approach';flip=false;seatProgress=1;seatVariant='default';enter('idle');}else{view='front';flip=false;seatProgress=0;enter('settle');}drain();};
45
82
  const issue=(skipEntry=false)=>{
@@ -61,6 +98,9 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
61
98
  if(walkTransitions&&name==='spin'&&!spinReady){spinReady=true;enter('launch');return;}
62
99
  pendingAction=null;
63
100
  if(name==='sit'){enter('idle');return;}
101
+ if(name==='belly'){const seconds=comfortSeconds;playAction('lie-down',()=>startBelly(seconds),.65);return;}
102
+ if(name==='sleep'){stayDown=false;comfort={kind:'sleep',seconds:comfortSeconds};enter('recline');return;}
103
+ if(name==='tug'){stop();comfort={kind:'tug',seconds:comfortSeconds};enter('tug');return;}
64
104
  if(name==='lie'){
65
105
  if(manualTricks){playAction('lie-down',()=>{manualDown=true;enter('down');drain();},.65);}
66
106
  else{stayDown=true;enter('recline');}
@@ -73,7 +113,9 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
73
113
  preparationRate=1;
74
114
  if(name==='roll'&&manualDown&&state==='down'){route=false;target=null;waypoints=[];pendingAction=null;stayDown=false;manualDown=false;restProgress=0;playAction('roll-down',()=>finishTrick('roll'));return true;}
75
115
  if(name==='roll'&&state==='down'&&restProgress===.25){route=false;target=null;waypoints=[];pendingAction=null;stayDown=false;restProgress=0;playAction('roll-down',()=>finishTrick('roll'));return true;}
76
- route=false;target=null;waypoints=[];pendingAction=name;spinReady=false;completed=false;stayDown=false;movementMode='run';stop();
116
+ if(name==='belly'&&state==='down'&&(manualDown||restProgress===.25)){route=false;target=null;waypoints=[];pendingAction=null;startBelly(comfortSeconds);return true;}
117
+ if(name==='sleep'&&state==='sleep'&&comfort?.kind==='sleep'){comfort.seconds=age+comfortSeconds;return true;}
118
+ comfort=null;route=false;target=null;waypoints=[];pendingAction=name;spinReady=false;completed=false;stayDown=false;movementMode='run';stop();
77
119
  if(restProgress>0||state==='recline'||state==='wake')enter('wake');
78
120
  else if(['watch','view-turn','turn','arrival-exit'].includes(state)){/* reach a shared endpoint */}
79
121
  else if(['run','finish','launch','settle'].includes(state))enter('finish');
@@ -87,7 +129,7 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
87
129
  if(![x,z].every(Number.isFinite))throw Error('Invalid paper target');
88
130
  preparationRate=urgent?3:1;
89
131
  target=paperGroundTarget(x,z);
90
- pendingAction=null;stayDown=false;movementMode=walking?'walk':'run';
132
+ pendingAction=null;stayDown=false;comfort=null;movementMode=walking?'walk':'run';
91
133
  const dz=target[1]-animal.position.z,dx=target[0]-animal.position.x;
92
134
  waypoints=[];
93
135
  // Do not drag a straight rear/front drawing diagonally across the floor.
@@ -116,10 +158,14 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
116
158
  walk(side,distance=1.05){if(!['left','right'].includes(side)||!Number.isFinite(distance)||distance<=0)throw Error('Invalid walk command');return dispatch(()=>{const [x,z]=paperWalkTarget(animal.position.x,animal.position.z,side==='left'?-1:1,distance);if(Math.abs(x-animal.position.x)<1e-6)return false;route=false;go(x,z,{walking:true});return true;});},
117
159
  look(side='left'){if(state!=='idle'||seatProgress!==1)return false;route=false;watchSide=side==='right'?'right':'left';enter('watch');return true;},
118
160
  lie(){const name=manualDown||activeAction?.name==='lie-down'||restProgress>0?'sit':'lie';return dispatch(()=>requestAction(name));},
119
- rest(){return dispatch(()=>{route=false;target=null;waypoints=[];pendingAction=null;stayDown=false;stop();if(restProgress>0){enter('wake');}else if(state==='watch'||state==='arrival-exit'){/* finish shared return */}else if(seatProgress>0||state==='rise'||state==='sit-down')enter('sit-down');else if(!['idle','view-turn','turn'].includes(state))enter('finish');});},
161
+ // Comfort poses: bounded finite seconds; same dispatch safety as tricks.
162
+ belly(seconds=6){comfortDuration('belly',seconds);return dispatch(()=>{comfortSeconds=seconds;return requestAction('belly');},{keepDown:true});},
163
+ sleep(seconds=25){comfortDuration('sleep',seconds);return dispatch(()=>{comfortSeconds=seconds;return requestAction('sleep');});},
164
+ tug(seconds=6){comfortDuration('tug',seconds);return dispatch(()=>{comfortSeconds=seconds;return requestAction('tug');});},
165
+ rest(){return dispatch(()=>{route=false;target=null;waypoints=[];pendingAction=null;stayDown=false;comfort=null;stop();if(restProgress>0){enter('wake');}else if(state==='watch'||state==='arrival-exit'){/* finish shared return */}else if(seatProgress>0||state==='rise'||state==='sit-down')enter('sit-down');else if(!['idle','view-turn','turn'].includes(state))enter('finish');});},
120
166
  update(dt){
121
167
  if(!Number.isFinite(dt)||dt<0||dt>.1)throw Error('Invalid paper step');
122
- const before=animal.position.clone();
168
+ const beforeX=animal.position.x,beforeZ=animal.position.z;
123
169
  if(state==='run'&&travel&&target){
124
170
  travelAge+=dt;const p=travel.plan.at(travelAge),ratio=travel.plan.distance?p.distance/travel.plan.distance:1;
125
171
  travelDistance=p.distance;motionSpeed=p.speed;motionStage=p.stage;
@@ -130,7 +176,7 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
130
176
  animal.update(dt);
131
177
  const prepareDt=dt*preparationRate;
132
178
  age+=['view-turn','turn','rise','watch','arrival-exit','launch','wake'].includes(state)?prepareDt:dt;
133
- const distance=Math.hypot(animal.position.x-before.x,animal.position.z-before.z);
179
+ const distance=Math.hypot(animal.position.x-beforeX,animal.position.z-beforeZ);
134
180
  director.update(dt,state==='run',distance);
135
181
  if(state==='run'){
136
182
  phase+=distance/travel.stride;
@@ -151,7 +197,9 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
151
197
  else if(state==='rise'){seatProgress=Math.max(0,seatProgress-prepareDt/(snappyPreparation()?.28:.65));if(seatProgress===0){if(seatVariant==='arrival')enter('arrival-exit');else prepareDirection();}}
152
198
  else if(state==='arrival-exit'&&age>=.22){seatVariant='default';prepareDirection();}
153
199
  else if(state==='watch'&&age>=1.4){if(pendingAction)prepareDirection();else if(target)enter('rise');else enter('idle');}
154
- else if(state==='recline'){const end=stayDown?.25:1;restProgress=Math.min(end,restProgress+dt/1.8);if(restProgress===end)enter(stayDown?'down':'nap');}
200
+ else if(state==='recline'){const end=stayDown?.25:1;restProgress=Math.min(end,restProgress+dt/1.8);if(restProgress===end)enter(stayDown?'down':comfort?.kind==='sleep'?'sleep':'nap');}
201
+ else if(state==='sleep'&&age>=comfort.seconds){comfort=null;enter('wake');}
202
+ else if(state==='tug'&&tugProgress()>=1){comfort=null;enter('idle');drain();}
155
203
  else if(state==='nap'&&age>=5&&!stayDown){enter('wake');}
156
204
  else if(state==='wake'){restProgress=Math.max(0,restProgress-prepareDt/1.2);if(restProgress===0){if(pendingAction)prepareDirection();else if(target)enter('rise');else enter('idle');}}
157
205
  else if(state==='idle'&&route&&age>=idleHold)nextRoute();
@@ -161,7 +209,8 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
161
209
  if(![12,24,30].includes(poseFps))throw Error('Invalid pose cadence');
162
210
  let pair=20,blend=0,kind=state,rearPair=null,approachPair=null;
163
211
  let seat=state==='sit-down'||state==='rise'||seatProgress>0?Math.round(seatProgress*48)/48:null;
164
- let behavior=null,behaviorProgress=0;
212
+ let behavior=null,behaviorProgress=0,comfortPose=state==='tug'?{clip:'tug',progress:tugProgress()}:null;
213
+ if(activeAction?.comfort)kind=activeAction.comfort;
165
214
  let actionName=activeAction?.name??(state==='run'&&movementMode==='walk'?'walk':null),actionProgress=activeAction?(activeAction.timing?activeAction.timing.progressAt(age):Math.min(1,age/activeAction.duration)):phase;
166
215
  if(manualDown&&state==='down'){actionName='lie-down';actionProgress=1;}
167
216
  if(walkTransitions&&state==='view-turn'&&turnView==='front'){
@@ -192,13 +241,13 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
192
241
  // Deliberate held source poses: no live frame blending or face/body deformation.
193
242
  blend=Math.round(blend*16)/16;
194
243
  const key=`${state}/${actionName}/${view}/${turnView}/${poseFps}/${Math.floor(clock*poseFps)}`;
195
- if(held?.key===key){pair=held.pair;blend=held.blend;rearPair=held.rearPair;approachPair=held.approachPair;seat=held.seat;behavior=held.behavior;behaviorProgress=held.behaviorProgress;actionName=held.actionName;actionProgress=held.actionProgress;}else held={key,pair,blend,rearPair,approachPair,seat,behavior,behaviorProgress,actionName,actionProgress,posePhase:phase};
196
- return{state:state==='run'||state==='finish'?'run':kind,pair,blend,rearPair,approachPair,seatProgress:seat,seatVariant,behavior,behaviorProgress,actionName,actionProgress,queued:!!deferred,view,straight,flip:state==='turn'&&!walkTransitions?false:flip,phase,posePhase:held.posePhase,stride:travel?.stride??PAPER_GAITS.front.stride,heading:travel?.heading??0,speed:motionSpeed,motionStage,travelDistance,energy:director.report.energy,x:animal.position.x,z:animal.position.z};
244
+ if(held?.key===key){pair=held.pair;blend=held.blend;rearPair=held.rearPair;approachPair=held.approachPair;seat=held.seat;behavior=held.behavior;behaviorProgress=held.behaviorProgress;actionName=held.actionName;actionProgress=held.actionProgress;comfortPose=held.comfortPose;}else held={key,pair,blend,rearPair,approachPair,seat,behavior,behaviorProgress,actionName,actionProgress,comfortPose,posePhase:phase};
245
+ return{state:state==='run'||state==='finish'?'run':kind,pair,blend,rearPair,approachPair,seatProgress:seat,seatVariant,behavior,behaviorProgress,actionName,actionProgress,queued:!!deferred,view,straight,flip:state==='turn'&&!walkTransitions?false:flip,phase,posePhase:held.posePhase,stride:travel?.stride??PAPER_GAITS.front.stride,heading:travel?.heading??0,speed:motionSpeed,motionStage,travelDistance,energy:director.report.energy,x:animal.position.x,z:animal.position.z,...(comfortPose?{comfortPose:{...comfortPose}}:{})};
197
246
  },get state(){return state;}
198
247
  };
199
248
  }
200
249
 
201
- export function createPaperRenderer(cache,info,meta,width,height,{rear,approach,seat,arrivalSeat,behaviors,actions,interactions,contacts=true,pawTransport=false}={}){
250
+ export function createPaperRenderer(cache,info,meta,width,height,{rear,approach,seat,arrivalSeat,behaviors,actions,interactions,comfort,contacts=true,pawTransport=false}={}){
202
251
  if(![width,height].every(Number.isInteger)||width<2||height<2||width*height>1_000_000)throw Error('Invalid paper grid');
203
252
  const tween=createCachedTween(cache,info),renderMeta={...meta,pairs:info.pair_map,run_sequence:info.run_sequence};
204
253
  const out=new Uint8ClampedArray(width*height*4),area=createPaperAreaSampler();
@@ -208,6 +257,7 @@ export function createPaperRenderer(cache,info,meta,width,height,{rear,approach,
208
257
  const arrivalSeatSample=arrivalSeat?createPaperSeatSampler(arrivalSeat.bytes,arrivalSeat.info):null;
209
258
  const behaviorSample=behaviors?createPaperBehaviorSampler(behaviors.bytes,behaviors.info):null;
210
259
  const interactionSample=interactions?createCompanionSpriteSampler(interactions.bytes,interactions.info):null;
260
+ const comfortSample=comfort?createComfortSpriteSampler(comfort.bytes,comfort.info):null;
211
261
  const actionSample=actions?createPaperActionSampler(actions.bytes,actions.info):null,actionArea=actions?createPaperAreaSampler(actions.info.width):null;
212
262
  const walkEpisodes=actions?createPaperSupportEpisodes(actions.info.feet,actions.info.clips.walk,9):null;
213
263
  const sourceEpisodes=rear?{
@@ -222,10 +272,11 @@ export function createPaperRenderer(cache,info,meta,width,height,{rear,approach,
222
272
  const isApproach=state.approachPair!==null&&state.approachPair!==undefined&&approachSample;
223
273
  const isSeat=state.seatProgress!==null&&state.seatProgress!==undefined&&seatSample;
224
274
  if(!Number.isFinite(pixelAspect)||pixelAspect<.1||pixelAspect>10)throw Error('Invalid dot pixel aspect');
225
- const isInteraction=state.interactionPose&&interactionSample;
275
+ const isComfort=state.comfortPose&&comfortSample;
276
+ const isInteraction=isComfort||state.interactionPose&&interactionSample;
226
277
  const isBehavior=!isInteraction&&state.behavior&&behaviorSample;
227
278
  const isAction=!isInteraction&&state.actionName&&actionSample,sourceN=isAction?actions.info.width:320,originX=(sourceN-320)/2,originY=(sourceN/320-1)*300;
228
- const image=isInteraction?interactionSample(state.interactionPose):isAction?actionSample(state):isBehavior?behaviorSample(state):isSeat?(state.seatVariant==='arrival'&&arrivalSeatSample?arrivalSeatSample:seatSample)(state.seatProgress):isApproach?approachSample(state.approachPair,state.blend):isRear?rearSample(state.rearPair,state.blend):tween(state),side=height*size,sideX=side/pixelAspect;
279
+ const image=isComfort?comfortSample(state.comfortPose):isInteraction?interactionSample(state.interactionPose):isAction?actionSample(state):isBehavior?behaviorSample(state):isSeat?(state.seatVariant==='arrival'&&arrivalSeatSample?arrivalSeatSample:seatSample)(state.seatProgress):isApproach?approachSample(state.approachPair,state.blend):isRear?rearSample(state.rearPair,state.blend):tween(state),side=height*size,sideX=side/pixelAspect;
229
280
  // Quantize source lookup only, never the projected rectangle. Rounding side,
230
281
  // left and top first introduced whole-body scale/position jumps at thresholds.
231
282
  const left=width*x-sideX/2,floor=isInteraction||isAction||isBehavior||isSeat||isRear||isApproach?0:cleanGroundOffset(state,renderMeta),top=height*y-(300-floor)/320*side;
@@ -36,7 +36,7 @@ export interface PetPlugin {
36
36
  /** Host-owned loop: advance seconds then render. False while hidden. Throws while running. */
37
37
  tick(dtSeconds: number): boolean;
38
38
  /** False when busy or cooling down. */
39
- interact(kind: 'ball' | 'treat'): boolean;
39
+ interact(kind: 'ball' | 'treat' | 'tug'): boolean;
40
40
  readonly running: boolean;
41
41
  readonly disposed: boolean;
42
42
  dispose(): void;
@@ -82,7 +82,7 @@ export async function createPetPlugin({host,petOptions,fps=30,scheduler}={}){
82
82
  if(typeof dtSeconds!=='number'||!Number.isFinite(dtSeconds)||dtSeconds<0)throw RangeError('dtSeconds must be finite non-negative seconds');
83
83
  try{return draw(dtSeconds);}catch(error){halt();throw error;}
84
84
  },
85
- // 'ball' | 'treat'. False when busy or cooling down; shows on the next frame.
85
+ // 'ball' | 'treat' | 'tug'. False when busy or cooling down; shows on the next frame.
86
86
  interact(kind){alive();return pet.interact(kind);},
87
87
  get running(){return running;},
88
88
  get disposed(){return disposed;},