@bitefoundry/retrieviewer 0.0.17 → 0.0.19

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 (62) hide show
  1. package/ASSET-PROVENANCE.md +11 -0
  2. package/ASSETS-LICENSE.md +6 -0
  3. package/ATTRIBUTION.md +8 -0
  4. package/DESKTOP-PLATFORMS.md +1 -1
  5. package/INSTALLING.md +2 -1
  6. package/LICENSE +16 -16
  7. package/NATIVE-RUNTIME.md +95 -0
  8. package/PREREQUISITES.md +7 -0
  9. package/PUPPY-MEMORY.md +22 -0
  10. package/README.ko.md +17 -1
  11. package/README.md +19 -1
  12. package/RELEASING.md +35 -0
  13. package/SPARSE-PAINT.md +66 -0
  14. package/THIRD-PARTY-NOTICES.md +8 -0
  15. package/WINDOWS-COMPANION.md +15 -6
  16. package/braille.mjs +2 -2
  17. package/integrations/native-windows/CMakeLists.txt +15 -0
  18. package/integrations/native-windows/THIRD-PARTY-LICENSES.txt +77 -0
  19. package/integrations/native-windows/application.manifest +8 -0
  20. package/integrations/native-windows/bin/win32-x64/retrieviewer-native.exe +0 -0
  21. package/integrations/native-windows/desktop.mjs +48 -0
  22. package/integrations/native-windows/host.cpp +85 -0
  23. package/integrations/native-windows/manifest.json +14 -0
  24. package/integrations/native-windows/native-control.hpp +41 -0
  25. package/integrations/native-windows/native-paint.hpp +86 -0
  26. package/integrations/native-windows/native-window.hpp +107 -0
  27. package/integrations/native-windows/paint-regions.hpp +55 -0
  28. package/integrations/native-windows/runtime.bundle.mjs +4694 -0
  29. package/integrations/native-windows/runtime.mjs +55 -0
  30. package/integrations/native-windows/sources.json +23 -0
  31. package/integrations/purrviewer/ART.md +289 -0
  32. package/integrations/purrviewer/ASSETS-LICENSE.md +21 -0
  33. package/integrations/purrviewer/LICENSE +24 -0
  34. package/integrations/purrviewer/art/ginger.bin.gz +0 -0
  35. package/integrations/purrviewer/art/ginger.json +1707 -0
  36. package/integrations/purrviewer/art/ginger.rvz +0 -0
  37. package/integrations/purrviewer/src/contact.mjs +63 -0
  38. package/integrations/purrviewer/src/habitat.mjs +41 -0
  39. package/integrations/purrviewer/src/life.mjs +280 -0
  40. package/integrations/purrviewer/src/purrviewer-core.mjs +130 -0
  41. package/integrations/purrviewer/src/purrviewer.mjs +37 -0
  42. package/integrations/purrviewer/src/walk-cycle.mjs +3 -0
  43. package/integrations/purrviewer/src/world.mjs +216 -0
  44. package/integrations/purrviewer/vendor/retrieviewer/LICENSE-CODE +21 -0
  45. package/integrations/purrviewer/vendor/retrieviewer/braille.mjs +73 -0
  46. package/integrations/purrviewer/vendor/retrieviewer/puppy-frame-file.mjs +15 -0
  47. package/integrations/purrviewer/vendor/retrieviewer/puppy-frame-indexed.mjs +78 -0
  48. package/integrations/purrviewer/vendor/retrieviewer/puppy-frame-predictive.mjs +170 -0
  49. package/integrations/purrviewer/vendor/retrieviewer/puppy-frame-store.mjs +114 -0
  50. package/integrations/windows-overlay/desktop.mjs +11 -3
  51. package/integrations/windows-overlay/worker.mjs +9 -2
  52. package/package.json +42 -4
  53. package/puppy-clean.mjs +2 -2
  54. package/puppy-companion-sprites.mjs +2 -2
  55. package/puppy-frame-indexed.mjs +2 -2
  56. package/puppy-paper-actions.mjs +2 -2
  57. package/puppy-paper-behaviors.mjs +1 -1
  58. package/puppy-paper-contact.mjs +2 -2
  59. package/puppy-paper-seat.mjs +2 -2
  60. package/puppy-paper.mjs +12 -12
  61. package/retrieviewer-cli.mjs +10 -6
  62. package/retrieviewer.mjs +13 -4
@@ -0,0 +1,4694 @@
1
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined")
6
+ return require.apply(this, arguments);
7
+ throw Error('Dynamic require of "' + x + '" is not supported');
8
+ });
9
+
10
+ // puppy-frame-predictive.mjs
11
+ function unfilterRow(type, src, at, cur, prev, w) {
12
+ switch (type) {
13
+ case 0:
14
+ for (let x = 0;x < w; x++)
15
+ cur[x] = src[at + x];
16
+ return;
17
+ case 1:
18
+ for (let x = 0;x < w; x++)
19
+ cur[x] = src[at + x] + (x ? cur[x - 1] : 0) & 255;
20
+ return;
21
+ case 2:
22
+ for (let x = 0;x < w; x++)
23
+ cur[x] = src[at + x] + prev[x] & 255;
24
+ return;
25
+ case 3:
26
+ for (let x = 0;x < w; x++)
27
+ cur[x] = src[at + x] + ((x ? cur[x - 1] : 0) + prev[x] >> 1) & 255;
28
+ return;
29
+ case 4:
30
+ for (let x = 0;x < w; x++)
31
+ cur[x] = src[at + x] + paeth(x ? cur[x - 1] : 0, prev[x], x ? prev[x - 1] : 0) & 255;
32
+ return;
33
+ }
34
+ fail("filter");
35
+ }
36
+ var quantize5 = (v) => Math.round(v * 31 / 255), expand5 = (q) => Math.round(q * 255 / 31), QUANT, DISPLAY, quantizedGray, fail = (what) => {
37
+ throw Error(`Invalid predictive frames: ${what}`);
38
+ }, paeth = (a, b, c) => {
39
+ const p = a + b - c, pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c);
40
+ return pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
41
+ };
42
+ var init_puppy_frame_predictive = __esm(() => {
43
+ QUANT = Uint8Array.from({ length: 256 }, (_, v) => quantize5(v));
44
+ DISPLAY = Uint8Array.from({ length: 32 }, (_, q) => expand5(q));
45
+ quantizedGray = Uint8Array.from({ length: 256 }, (_, v) => DISPLAY[QUANT[v]]);
46
+ });
47
+
48
+ // puppy-frame-store.mjs
49
+ function registerFrameStore(store) {
50
+ if (!store || typeof store.getFrame !== "function" || typeof store.stats !== "function" || !Number.isInteger(store.width) || !Number.isInteger(store.height) || !Number.isInteger(store.frameCount))
51
+ throw Error("Invalid frame store");
52
+ stores.add(store);
53
+ return store;
54
+ }
55
+ function isGrayFrameSource(value) {
56
+ return value instanceof Uint8Array || stores.has(value);
57
+ }
58
+ function checkShape(width, height, frameCount) {
59
+ if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1 || width > MAX_SIDE || height > MAX_SIDE)
60
+ throw Error("Invalid gray frames: dimensions");
61
+ if (!Number.isInteger(frameCount) || frameCount < 1 || frameCount > 4294967295 || !Number.isSafeInteger(width * height * frameCount))
62
+ throw Error("Invalid gray frames: frame count");
63
+ }
64
+ function grayFrameReader(source, width, height, frameCount) {
65
+ checkShape(width, height, frameCount);
66
+ const n = width * height, byteLength = n * frameCount;
67
+ const check = (index) => {
68
+ if (!Number.isInteger(index) || index < 0 || index >= frameCount)
69
+ throw Error("Invalid gray frames: frame index");
70
+ };
71
+ if (stores.has(source)) {
72
+ if (source.width !== width || source.height !== height || source.frameCount !== frameCount)
73
+ throw Error("Invalid gray frames: store shape");
74
+ return { width, height, frameCount, byteLength, getFrame(index) {
75
+ check(index);
76
+ return source.getFrame(index);
77
+ } };
78
+ }
79
+ if (!(source instanceof Uint8Array))
80
+ throw Error("Invalid gray frames: source");
81
+ if (source.length !== byteLength)
82
+ throw Error("Invalid gray frames: byte length");
83
+ return { width, height, frameCount, byteLength, getFrame(index) {
84
+ check(index);
85
+ return source.subarray(index * n, (index + 1) * n);
86
+ } };
87
+ }
88
+ var MAX_SIDE = 16384, stores;
89
+ var init_puppy_frame_store = __esm(() => {
90
+ init_puppy_frame_predictive();
91
+ stores = new WeakSet;
92
+ });
93
+
94
+ // puppy-frame-indexed.mjs
95
+ function indexedIndexLength(header) {
96
+ if (!(header instanceof Uint8Array) || header.length < HEADER || String.fromCharCode(...header.subarray(0, 4)) !== "RVZ5")
97
+ fail3("header");
98
+ const v = viewOf(header), width = v.getUint32(4, true), height = v.getUint32(8, true), frames = v.getUint32(12, true), unique = v.getUint32(16, true);
99
+ if (!width || !height || width > 16384 || height > 16384 || width * height > MAX_PIXELS || !frames || !unique || unique > frames)
100
+ fail3("shape");
101
+ const length2 = HEADER + 4 * frames + RECORD * unique;
102
+ if (length2 > MAX_INDEX)
103
+ fail3("index size");
104
+ return length2;
105
+ }
106
+ function readIndexedIndex(bytes, fileSize = bytes.length) {
107
+ const length2 = indexedIndexLength(bytes);
108
+ if (bytes.length < length2 || !Number.isSafeInteger(fileSize) || fileSize < length2)
109
+ fail3("truncated index");
110
+ const v = viewOf(bytes), width = v.getUint32(4, true), height = v.getUint32(8, true), frameCount = v.getUint32(12, true), uniqueCount = v.getUint32(16, true);
111
+ const map = new Uint32Array(frameCount), used = new Uint8Array(uniqueCount), rects = new Uint32Array(uniqueCount * 6);
112
+ let payload = 0, expanded = 0;
113
+ for (let i = 0;i < frameCount; i++) {
114
+ const u = v.getUint32(HEADER + i * 4, true);
115
+ if (u >= uniqueCount)
116
+ fail3("map");
117
+ map[i] = u;
118
+ used[u] = 1;
119
+ }
120
+ if (used.includes(0))
121
+ fail3("unreferenced frame");
122
+ for (let u = 0;u < uniqueCount; u++) {
123
+ const at = HEADER + frameCount * 4 + u * RECORD, r = Array.from({ length: 6 }, (_, i) => v.getUint32(at + i * 4, true)), [x, y, w, h, offset, size] = r;
124
+ if (w === 0 !== (h === 0) || !w && (x || y) || x + w > width || y + h > height)
125
+ fail3("rectangle");
126
+ if (offset !== payload || size < 20)
127
+ fail3("offset");
128
+ payload += size;
129
+ expanded += h * (w + 1);
130
+ if (expanded > MAX_EXPANDED)
131
+ fail3("expanded size");
132
+ rects.set(r, u * 6);
133
+ }
134
+ if (length2 + payload !== fileSize)
135
+ fail3("file size");
136
+ return { width, height, frameCount, uniqueCount, map, rects, indexBytes: length2, expandedBytes: expanded, fileSize };
137
+ }
138
+ function createIndexedFrameStore(index, { read, expand, close = () => {}, frameIndices, kernel = null } = {}) {
139
+ if (typeof read !== "function" || typeof expand !== "function")
140
+ fail3("reader");
141
+ const { width, height, frameCount, uniqueCount, map, rects, indexBytes } = index, n = width * height;
142
+ const retained = frameIndices === undefined ? null : new Uint8Array(frameCount);
143
+ if (retained) {
144
+ if (!Array.isArray(frameIndices) || !frameIndices.length)
145
+ fail3("retained frames");
146
+ for (const i of frameIndices) {
147
+ if (!Number.isInteger(i) || i < 0 || i >= frameCount)
148
+ fail3("retained frame index");
149
+ retained[i] = 1;
150
+ }
151
+ }
152
+ const slots = [{ unique: -1, frame: null }, { unique: -1, frame: null }], cur = new Uint8Array(width), prev = new Uint8Array(width);
153
+ let closed = false, decodes = 0, hits = 0, diskBytes = 0;
154
+ return Object.freeze({
155
+ width,
156
+ height,
157
+ frameCount,
158
+ uniqueCount,
159
+ byteLength: n * frameCount,
160
+ residentPixelBytes: 0,
161
+ get residentBytes() {
162
+ return closed ? 0 : map.byteLength + rects.byteLength + cur.byteLength + prev.byteLength + (retained?.byteLength ?? 0) + slots.reduce((s2, e) => s2 + (e.frame?.byteLength ?? 0), 0);
163
+ },
164
+ stats() {
165
+ return { decodes, hits, cachedFrames: slots.filter((s2) => s2.frame).length, diskReads: decodes, diskBytes, closed };
166
+ },
167
+ getFrame(i) {
168
+ if (closed)
169
+ throw Error("Frame store is disposed");
170
+ if (!Number.isInteger(i) || i < 0 || i >= frameCount)
171
+ fail3("frame index");
172
+ if (retained && !retained[i])
173
+ fail3("frame was not retained");
174
+ const u = map[i];
175
+ if (slots[0].unique === u) {
176
+ hits++;
177
+ return slots[0].frame;
178
+ }
179
+ if (slots[1].unique === u) {
180
+ hits++;
181
+ slots.reverse();
182
+ return slots[0].frame;
183
+ }
184
+ const k = u * 6, x = rects[k], y = rects[k + 1], w = rects[k + 2], h = rects[k + 3], offset = rects[k + 4], size = rects[k + 5], expected = h * (w + 1);
185
+ const compressed = read(indexBytes + offset, size);
186
+ if (!(compressed instanceof Uint8Array) || compressed.length !== size)
187
+ fail3("short read");
188
+ const rows = expand(compressed, expected);
189
+ if (!(rows instanceof Uint8Array) || rows.length !== expected)
190
+ fail3("expanded length");
191
+ const slot = slots.pop();
192
+ slots.unshift(slot);
193
+ slot.unique = -1;
194
+ const frame = slot.frame ??= new Uint8Array(n);
195
+ frame.fill(0);
196
+ prev.fill(0);
197
+ if (kernel?.decodeRows)
198
+ kernel.decodeRows(frame, rows, x, y, w, h, width);
199
+ else
200
+ for (let r = 0, at = 0;r < h; r++) {
201
+ const type = rows[at++];
202
+ if (type > 4)
203
+ fail3("filter");
204
+ unfilterRow(type, rows, at, cur, prev, w);
205
+ at += w;
206
+ for (let c2 = 0;c2 < w; c2++) {
207
+ const q = cur[c2];
208
+ if (q > 31)
209
+ fail3("quantized value");
210
+ frame[(y + r) * width + x + c2] = display[q];
211
+ }
212
+ prev.set(cur.subarray(0, w));
213
+ }
214
+ slot.unique = u;
215
+ decodes++;
216
+ diskBytes += size;
217
+ return frame;
218
+ },
219
+ dispose() {
220
+ if (closed)
221
+ return;
222
+ closed = true;
223
+ for (const slot of slots) {
224
+ slot.frame = null;
225
+ slot.unique = -1;
226
+ }
227
+ close();
228
+ }
229
+ });
230
+ }
231
+ var HEADER = 20, RECORD = 24, MAX_PIXELS = 16777216, MAX_INDEX, MAX_EXPANDED, fail3 = (why) => {
232
+ throw Error("Invalid indexed frames: " + why);
233
+ }, viewOf = (b) => new DataView(b.buffer, b.byteOffset, b.byteLength), display;
234
+ var init_puppy_frame_indexed = __esm(() => {
235
+ init_puppy_frame_predictive();
236
+ MAX_INDEX = 64 * 1024 * 1024;
237
+ MAX_EXPANDED = 512 * 1024 * 1024;
238
+ display = Uint8Array.from({ length: 32 }, (_, q) => Math.round(q * 255 / 31));
239
+ });
240
+
241
+ // integrations/native-windows/runtime.mjs
242
+ import * as std from "qjs:std";
243
+
244
+ // puppy-continuous.mjs
245
+ var endpoints = Array.from({ length: 34 }, (_, i) => i < 20 ? [i, (i + 1) % 20, false, false] : i < 30 ? [i, 20 + (i - 19) % 10, false, false] : [[0, 20, false, false], [20, 0, false, true], [0, 20, true, false], [20, 0, false, false]][i - 30]);
246
+
247
+ // puppy-stable.mjs
248
+ var mix = (a, b, p) => a + (b - a) * p;
249
+ function groundOffset(state, meta) {
250
+ const [a, b] = meta.pairs[state.pair], p = state.blend;
251
+ const v = meta.ground_offsets;
252
+ if (state.pair < 30) {
253
+ const start = state.pair < 20 ? 0 : 20, n = start ? 10 : 20, index = a - start;
254
+ const before = v[start + (index + n - 1) % n], after = v[start + (index + 2) % n];
255
+ return 0.5 * (2 * v[a] + (-before + v[b]) * p + (2 * before - 5 * v[a] + 4 * v[b] - after) * p * p + (-before + 3 * v[a] - 3 * v[b] + after) * p * p * p);
256
+ }
257
+ return mix(v[a], v[b], p);
258
+ }
259
+
260
+ // puppy-clean.mjs
261
+ init_puppy_frame_store();
262
+ var CLEAN_BYTES = 47 * 17 * 320 * 320;
263
+ function retimeRun(phase, knots) {
264
+ if (!knots)
265
+ return phase;
266
+ const count = knots.length - 1;
267
+ if (count < 2 || count > 20 || knots[0] !== 0 || knots[count] !== 20 || knots.some((x, i) => !Number.isFinite(x) || i && x <= knots[i - 1]))
268
+ throw Error("Invalid gait timing");
269
+ const tangent = (i) => {
270
+ i %= count;
271
+ const before = i ? knots[i] - knots[i - 1] : 20 - knots[count - 1], after = knots[i + 1] - knots[i], w1 = 2 * after + before, w2 = after + 2 * before;
272
+ return (w1 + w2) / (w1 * before + w2 * after);
273
+ };
274
+ let index = 0;
275
+ while (index < count - 1 && phase > knots[index + 1])
276
+ index++;
277
+ const h = knots[index + 1] - knots[index], p = Math.max(0, Math.min(1, (phase - knots[index]) / h)), p2 = p * p, p3 = p2 * p;
278
+ return (2 * p3 - 3 * p2 + 1) * index + (p3 - 2 * p2 + p) * h * tangent(index) + (-2 * p3 + 3 * p2) * (index + 1) + (p3 - p2) * h * tangent(index + 1);
279
+ }
280
+ function cleanGroundOffset(state, meta) {
281
+ if (state.state === "run" && meta.run_sequence) {
282
+ const seq = meta.run_sequence, n = seq.length, i2 = state.pair, p2 = state.blend, v2 = meta.ground_offsets;
283
+ const a2 = v2[seq[(i2 + n - 1) % n]], b2 = v2[seq[i2]], c2 = v2[seq[(i2 + 1) % n]], d2 = v2[seq[(i2 + 2) % n]];
284
+ return 0.5 * (2 * b2 + (-a2 + c2) * p2 + (2 * a2 - 5 * b2 + 4 * c2 - d2) * p2 * p2 + (-a2 + 3 * b2 - 3 * c2 + d2) * p2 * p2 * p2);
285
+ }
286
+ if (state.pair < 30 || state.pair > 40)
287
+ return groundOffset(state, meta);
288
+ const { pair: i, blend: p } = state, v = meta.ground_offsets;
289
+ const a = v[Math.max(30, i - 1)], b = v[i], c = v[i + 1], d = v[Math.min(41, i + 2)];
290
+ return 0.5 * (2 * b + (-a + c) * p + (2 * a - 5 * b + 4 * c - d) * p * p + (-a + 3 * b - 3 * c + d) * p * p * p);
291
+ }
292
+ function createCachedTween(cache, info, kernel = null) {
293
+ if (!isGrayFrameSource(cache) || cache.byteLength !== CLEAN_BYTES || info.pairs !== 47 || info.samples !== 17 || info.width !== 320 || info.height !== 320 || info.channels !== 1 || info.bytes !== CLEAN_BYTES)
294
+ throw Error("Invalid baked tween cache");
295
+ const frames = grayFrameReader(cache, 320, 320, 47 * 17), output = new Uint8ClampedArray(320 * 320 * 4), stride = 320 * 320;
296
+ for (let i = 3;i < output.length; i += 4)
297
+ output[i] = 255;
298
+ return (state) => {
299
+ if (!Number.isInteger(state.pair) || state.pair < 0 || state.pair >= 47 || !Number.isFinite(state.blend))
300
+ throw Error("Invalid baked tween state");
301
+ const phase = Math.max(0, Math.min(1, state.blend)) * 16, index = Math.min(15, Math.floor(phase)), alpha = phase - index;
302
+ const a = frames.getFrame(state.pair * 17 + index), b = frames.getFrame(state.pair * 17 + index + 1);
303
+ if (kernel?.blendGray)
304
+ kernel.blendGray(output, a, b, alpha);
305
+ else
306
+ for (let i = 0, j = 0;i < stride; i++, j += 4) {
307
+ const value = a[i] * (1 - alpha) + b[i] * alpha;
308
+ output[j] = output[j + 1] = output[j + 2] = value;
309
+ }
310
+ return output;
311
+ };
312
+ }
313
+
314
+ // puppy-paper-world.mjs
315
+ var diagonal = 0.0254 * 32;
316
+ function createPaperBody() {
317
+ const position = { x: 0, y: 0, z: 0 };
318
+ let target = null, disposed = false;
319
+ const alive = () => {
320
+ if (disposed)
321
+ throw Error("Paper body is disposed");
322
+ };
323
+ return {
324
+ position,
325
+ follow(point) {
326
+ alive();
327
+ if (!point || ![point.x, point.y, point.z].every(Number.isFinite))
328
+ throw Error("Invalid paper body position");
329
+ target = { x: point.x, y: point.y, z: point.z };
330
+ },
331
+ update(dt) {
332
+ alive();
333
+ if (!Number.isFinite(dt) || dt < 0 || dt > 0.1)
334
+ throw Error("Invalid paper body step");
335
+ if (target) {
336
+ position.x = target.x;
337
+ position.z = target.z;
338
+ }
339
+ },
340
+ dispose() {
341
+ disposed = true;
342
+ target = null;
343
+ }
344
+ };
345
+ }
346
+ var PAPER_SCREEN = Object.freeze({ widthMeters: diagonal * 16 / Math.hypot(16, 9), heightMeters: diagonal * 9 / Math.hypot(16, 9), viewDistance: 0.7 });
347
+ var eyeHeight = 0.45;
348
+ var eyeZ = 1.65;
349
+ var pitch = 9 * Math.PI / 180;
350
+ var 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, 0.12, eyeZ - (eyeHeight - 0.12) / Math.tan(pitch)]), canvasMeters: 0.64 });
351
+ var PAPER_BODY = Object.freeze({ shoulderMeters: 0.35, referenceShoulderMeters: 0.6 });
352
+ var PAPER_LIMB = Object.freeze({ length: PAPER_BODY.shoulderMeters, stanceHeight: PAPER_BODY.shoulderMeters * 5 / 7 });
353
+ var bodyRatio = PAPER_BODY.shoulderMeters / PAPER_BODY.referenceShoulderMeters;
354
+ var gait = (stride, frequency) => Object.freeze({ stride: stride * bodyRatio, frequency: frequency / Math.sqrt(bodyRatio), speed: stride * frequency * Math.sqrt(bodyRatio) });
355
+ var run = gait(2.26, 2.64);
356
+ var PAPER_GAITS = Object.freeze({ front: run, rear: run, approach: run, trot: gait(1.36, 2.07), walk: gait(0.9, 1.38) });
357
+ var PAPER_DYNAMICS = Object.freeze({ acceleration: 4, deceleration: 5, walkAcceleration: 2, walkDeceleration: 3 });
358
+ var PAPER_GROUND = Object.freeze({ near: 0.2, far: -200, away: -8 });
359
+ var dz = PAPER_CAMERA.position[2] - PAPER_CAMERA.target[2];
360
+ var dy = PAPER_CAMERA.position[1] - PAPER_CAMERA.target[1];
361
+ var length = Math.hypot(dz, dy);
362
+ var c = dz / length;
363
+ var s = dy / length;
364
+ function paperCameraPoint(x, y, z) {
365
+ return { x, y: c * (y - eyeHeight) - s * (z - eyeZ), depth: -s * (y - eyeHeight) - c * (z - eyeZ) };
366
+ }
367
+ function paperGroundTarget(x, z) {
368
+ if (![x, z].every(Number.isFinite))
369
+ throw Error("Invalid paper target");
370
+ z = Math.max(PAPER_GROUND.far, Math.min(PAPER_GROUND.near, z));
371
+ const halfWidth = Math.max(0.72, paperCameraPoint(0, 0, z).depth * PAPER_SCREEN.widthMeters / (2 * PAPER_SCREEN.viewDistance) - PAPER_CAMERA.canvasMeters / 2);
372
+ return [Math.max(-halfWidth, Math.min(halfWidth, x)), z];
373
+ }
374
+ function createPaperProjection({ viewportWidth, viewportHeight, rect, width, height }) {
375
+ if (![viewportWidth, viewportHeight, rect?.left, rect?.top, rect?.width, rect?.height, width, height].every(Number.isFinite) || Math.min(viewportWidth, viewportHeight, rect.width, rect.height, width, height) <= 0)
376
+ throw Error("Invalid paper viewport");
377
+ const pixelsPerMeter = Math.min(rect.width / PAPER_SCREEN.widthMeters, rect.height / PAPER_SCREEN.heightMeters), f = PAPER_SCREEN.viewDistance * pixelsPerMeter;
378
+ const cx = rect.width / 2, cy = rect.height / 2;
379
+ return {
380
+ project(x, y, z) {
381
+ const p = paperCameraPoint(x, y, z);
382
+ if (p.depth <= 0.02)
383
+ throw Error("Paper point behind camera");
384
+ return { x: (cx + f * p.x / p.depth) / rect.width, y: (cy - f * p.y / p.depth) / rect.height, depth: p.depth };
385
+ },
386
+ placement(x, z, lift = 0) {
387
+ const p = this.project(x, lift, z), sourceSize = f * PAPER_CAMERA.canvasMeters / p.depth;
388
+ return {
389
+ x: p.x,
390
+ y: p.y,
391
+ size: sourceSize / rect.height,
392
+ pixelAspect: rect.width / width / (rect.height / height),
393
+ depth: p.depth,
394
+ groundFromPixel: (px, py) => this.ground(px / width, py / height),
395
+ pixelFromGround: (point) => {
396
+ const q = this.project(point.x, 0, point.z);
397
+ return { x: q.x * width, y: q.y * height };
398
+ }
399
+ };
400
+ },
401
+ ground(u, v) {
402
+ if (![u, v].every(Number.isFinite))
403
+ return null;
404
+ const rx = (u * rect.width - cx) / f, ry = -(v * rect.height - cy) / f;
405
+ const vy = c * ry - s, vz = -s * ry - c;
406
+ if (vy >= -0.00000001)
407
+ return null;
408
+ const t = -PAPER_CAMERA.position[1] / vy;
409
+ return { x: rx * t, z: eyeZ + vz * t };
410
+ },
411
+ focalPixels: f,
412
+ pixelsPerMeter,
413
+ principalPoint: { x: cx, y: cy }
414
+ };
415
+ }
416
+ function planPaperTravel(distance, { speed = 0.35, acceleration = 0.7, deceleration = 0.5, startSpeed = 0, endSpeed = 0 } = {}) {
417
+ if (![distance, speed, acceleration, deceleration, startSpeed, endSpeed].every(Number.isFinite) || distance < 0 || Math.min(speed, acceleration, deceleration) <= 0 || Math.min(startSpeed, endSpeed) < 0 || Math.max(startSpeed, endSpeed) > speed)
418
+ throw Error("Invalid paper travel");
419
+ const peak = Math.min(speed, Math.sqrt((2 * distance + startSpeed ** 2 / acceleration + endSpeed ** 2 / deceleration) / (1 / acceleration + 1 / deceleration)));
420
+ if (peak + 0.000000001 < Math.max(startSpeed, endSpeed))
421
+ throw Error("Insufficient braking distance");
422
+ const ta = (peak - startSpeed) / acceleration, td = (peak - endSpeed) / deceleration, da = (peak + startSpeed) * ta / 2, dd = (peak + endSpeed) * td / 2, dc = Math.max(0, distance - da - dd), tc = peak ? dc / peak : 0, duration = ta + tc + td;
423
+ return {
424
+ distance,
425
+ duration,
426
+ peak,
427
+ brakingDistance: dd,
428
+ acceleration,
429
+ deceleration,
430
+ at(time) {
431
+ if (!Number.isFinite(time))
432
+ throw Error("Invalid travel time");
433
+ const t = Math.max(0, time);
434
+ if (t >= duration)
435
+ return { distance, speed: endSpeed, stage: "arrived", done: true };
436
+ if (t < ta)
437
+ return { distance: startSpeed * t + 0.5 * acceleration * t * t, speed: startSpeed + acceleration * t, stage: "accelerating", done: false };
438
+ if (t < ta + tc)
439
+ return { distance: da + peak * (t - ta), speed: peak, stage: "cruise", done: false };
440
+ const r = duration - t;
441
+ return { distance: distance - endSpeed * r - 0.5 * deceleration * r * r, speed: endSpeed + deceleration * r, stage: "braking", done: false };
442
+ }
443
+ };
444
+ }
445
+ function planPaperApproach(distance) {
446
+ const runStride = PAPER_GAITS.approach.stride, walkStride = PAPER_GAITS.walk.stride, runDistance = Math.max(0, Math.floor((distance - 1.1) / runStride)) * runStride, entry = Math.min(0.12, distance * 0.2), walkCycles = Math.max(0, Math.floor((distance - runDistance - entry - 0.25) / walkStride)), walkDistance = walkCycles * walkStride, brakeStart = runDistance + entry + walkDistance;
447
+ return { runStride, walkStride, runDistance, entry, walkDistance, brakeStart, at(d) {
448
+ if (runDistance > 0 && d < runDistance)
449
+ return { clip: "run", progress: d / runStride };
450
+ if (d < runDistance + entry)
451
+ return { clip: "walk-enter", progress: (d - runDistance) / (entry || 1) };
452
+ if (d < brakeStart)
453
+ return { clip: "walk", progress: (d - runDistance - entry) / walkStride };
454
+ return { clip: "brake", progress: Math.max(0, Math.min(1, (d - brakeStart) / Math.max(0.000000001, distance - brakeStart))) };
455
+ } };
456
+ }
457
+ function planPaperApproachTravel(distance) {
458
+ const geometry = planPaperApproach(distance), walkSpeed = PAPER_GAITS.walk.speed;
459
+ if (!distance)
460
+ return planPaperTravel(0, { speed: walkSpeed });
461
+ const preDistance = geometry.brakeStart - geometry.runDistance, brakeDistance = distance - geometry.brakeStart;
462
+ const entrySpeed = geometry.runDistance ? walkSpeed : Math.min(walkSpeed, Math.sqrt(2 * PAPER_DYNAMICS.walkAcceleration * preDistance));
463
+ const segments = [];
464
+ if (geometry.runDistance)
465
+ segments.push(planPaperTravel(geometry.runDistance, { speed: PAPER_GAITS.approach.speed, ...PAPER_DYNAMICS, endSpeed: walkSpeed }));
466
+ segments.push(planPaperTravel(preDistance, { speed: walkSpeed, startSpeed: geometry.runDistance ? walkSpeed : 0, endSpeed: entrySpeed, acceleration: PAPER_DYNAMICS.walkAcceleration, deceleration: PAPER_DYNAMICS.walkDeceleration }));
467
+ segments.push(planPaperTravel(brakeDistance, { speed: entrySpeed, startSpeed: entrySpeed, acceleration: PAPER_DYNAMICS.walkAcceleration, deceleration: entrySpeed ** 2 / (2 * brakeDistance) }));
468
+ const duration = segments.reduce((a, p) => a + p.duration, 0);
469
+ return { distance, duration, peak: Math.max(...segments.map((p) => p.peak)), brakingDistance: brakeDistance, at(t) {
470
+ if (!Number.isFinite(t))
471
+ throw Error("Invalid approach time");
472
+ if (t >= duration)
473
+ return { distance, speed: 0, stage: "arrived", done: true };
474
+ let offset = 0;
475
+ for (let i = 0;i < segments.length; i++) {
476
+ const p = segments[i];
477
+ if (t < p.duration || i === segments.length - 1) {
478
+ const s2 = p.at(t);
479
+ return { ...s2, distance: offset + s2.distance };
480
+ }
481
+ t -= p.duration;
482
+ offset += p.distance;
483
+ }
484
+ } };
485
+ }
486
+ function planPaperCarry(distance, { mixed = false, settleSeconds = 0.22 } = {}) {
487
+ if (!Number.isFinite(distance) || distance <= 0 || typeof mixed !== "boolean" || !Number.isFinite(settleSeconds) || settleSeconds <= 0 || settleSeconds > 1)
488
+ throw Error("Invalid carry travel");
489
+ const gait2 = PAPER_GAITS.trot, runDistance = mixed ? Math.max(0, Math.floor((distance - 1.5) / run.stride)) * run.stride : 0;
490
+ const remaining = distance - runDistance, gatherDistance = Math.min(0.088, remaining * 0.2), trotDistance = remaining - gatherDistance;
491
+ const cycles = Math.max(1, Math.ceil(trotDistance / gait2.stride)), stride = trotDistance / cycles;
492
+ const speed = Math.min(gait2.speed, stride * gait2.frequency), entrySpeed = Math.min(speed, 2 * gatherDistance / settleSeconds, Math.sqrt(2 * 2.5 * trotDistance));
493
+ const gatherSeconds = 2 * gatherDistance / entrySpeed;
494
+ const segments = [];
495
+ if (runDistance)
496
+ segments.push(planPaperTravel(runDistance, { speed: run.speed, ...PAPER_DYNAMICS, endSpeed: speed }));
497
+ segments.push(planPaperTravel(trotDistance, { speed, acceleration: 2.5, deceleration: 4, startSpeed: runDistance ? speed : 0, endSpeed: entrySpeed }));
498
+ segments.push(planPaperTravel(gatherDistance, { speed: entrySpeed, startSpeed: entrySpeed, deceleration: entrySpeed / gatherSeconds }));
499
+ const duration = segments.reduce((a, p) => a + p.duration, 0), gatherStart = runDistance + trotDistance;
500
+ return {
501
+ distance,
502
+ duration,
503
+ peak: Math.max(...segments.map((p) => p.peak)),
504
+ brakingDistance: gatherDistance,
505
+ stride,
506
+ runDistance,
507
+ trotDistance,
508
+ gatherStart,
509
+ gatherSeconds,
510
+ poseAt(d) {
511
+ const phase = (d - runDistance) / stride;
512
+ return d < runDistance ? { clip: "run", progress: d / run.stride } : d < gatherStart ? { clip: "carry-trot", progress: phase % 1, cycle: Math.floor(phase) } : { clip: "carry-settle", progress: 1 - Math.sqrt(Math.max(0, (distance - d) / gatherDistance)) };
513
+ },
514
+ at(t) {
515
+ if (!Number.isFinite(t))
516
+ throw Error("Invalid carry time");
517
+ if (t >= duration)
518
+ return { distance, speed: 0, stage: "arrived", done: true };
519
+ let offset = 0;
520
+ for (let i = 0;i < segments.length; i++) {
521
+ const p = segments[i];
522
+ if (t < p.duration || i === segments.length - 1) {
523
+ const s2 = p.at(t);
524
+ return { ...s2, distance: offset + s2.distance };
525
+ }
526
+ t -= p.duration;
527
+ offset += p.distance;
528
+ }
529
+ }
530
+ };
531
+ }
532
+ function planPaperRun(distance) {
533
+ const cycles = Math.max(1, Math.ceil(distance / run.stride)), stride = distance / cycles || run.stride;
534
+ return { stride, cycles, plan: planPaperTravel(distance, { speed: Math.min(run.speed, stride * run.frequency), ...PAPER_DYNAMICS }) };
535
+ }
536
+ function planPaperStumble(distance, { entryPhase = 0.25, brakeSeconds = 0.27, catchSeconds = 0.36, catchSpeedRatio = 0.18, duration = 0.82 } = {}) {
537
+ if (![distance, entryPhase, brakeSeconds, catchSeconds, catchSpeedRatio, duration].every(Number.isFinite) || distance <= 0 || entryPhase <= 0 || entryPhase >= 1 || brakeSeconds <= 0 || catchSeconds <= 0 || catchSpeedRatio <= 0 || catchSpeedRatio >= 1 || duration < brakeSeconds + catchSeconds || duration > 3)
538
+ throw Error("Invalid stumble travel");
539
+ const cycles = Math.max(0, Math.ceil(distance / run.stride - entryPhase)), contacts = cycles + entryPhase, a = PAPER_DYNAMICS.acceleration;
540
+ const coefficient = ((1 + catchSpeedRatio) * brakeSeconds + catchSpeedRatio * catchSeconds) / 2;
541
+ const speedAt = (d) => Math.min(run.speed, d / contacts * run.frequency, Math.sqrt(2 * a * d));
542
+ let lo = 0, hi = distance;
543
+ for (let i = 0;i < 60; i++) {
544
+ const d = (lo + hi) / 2;
545
+ if (d + coefficient * speedAt(d) < distance)
546
+ lo = d;
547
+ else
548
+ hi = d;
549
+ }
550
+ const entryDistance = (lo + hi) / 2, entrySpeed = speedAt(entryDistance), stride = entryDistance / contacts, catchSpeed = entrySpeed * catchSpeedRatio;
551
+ const entry = planPaperTravel(entryDistance, { speed: entrySpeed, acceleration: a, deceleration: PAPER_DYNAMICS.deceleration, endSpeed: entrySpeed });
552
+ const plan = planPaperTravel(distance, { speed: entrySpeed, acceleration: a, deceleration: PAPER_DYNAMICS.deceleration, endSpeed: entrySpeed });
553
+ const brakeDistance = (entrySpeed + catchSpeed) * brakeSeconds / 2, impactDistance = distance - entryDistance;
554
+ return {
555
+ plan,
556
+ stride,
557
+ entryDistance,
558
+ entrySpeed,
559
+ entryTime: entry.duration,
560
+ entryPhase,
561
+ duration,
562
+ impactDistance,
563
+ impactAt(age) {
564
+ if (!Number.isFinite(age))
565
+ throw Error("Invalid stumble age");
566
+ const t = Math.max(0, age);
567
+ if (t >= brakeSeconds + catchSeconds)
568
+ return { distance: impactDistance, speed: 0, stage: "rest" };
569
+ if (t < brakeSeconds) {
570
+ const deceleration2 = (entrySpeed - catchSpeed) / brakeSeconds;
571
+ return { distance: entrySpeed * t - deceleration2 * t * t / 2, speed: entrySpeed - deceleration2 * t, stage: "bracing" };
572
+ }
573
+ const u = t - brakeSeconds, deceleration = catchSpeed / catchSeconds;
574
+ return { distance: brakeDistance + catchSpeed * u - deceleration * u * u / 2, speed: catchSpeed - deceleration * u, stage: "catching" };
575
+ }
576
+ };
577
+ }
578
+ function planPaperWalk(distance) {
579
+ const gait2 = PAPER_GAITS.walk, cycles = Math.max(1, Math.ceil(distance / gait2.stride)), stride = distance / cycles || gait2.stride;
580
+ return { stride, cycles, plan: planPaperTravel(distance, { speed: Math.min(gait2.speed, stride * gait2.frequency), acceleration: PAPER_DYNAMICS.walkAcceleration, deceleration: PAPER_DYNAMICS.walkDeceleration }) };
581
+ }
582
+ function paperWalkTarget(x, z, direction, distance = 1.05) {
583
+ if (![x, z, direction, distance].every(Number.isFinite) || ![-1, 1].includes(direction) || distance <= 0)
584
+ throw Error("Invalid walk target");
585
+ const radius = Math.max(0.1, paperCameraPoint(0, 0, z).depth * PAPER_SCREEN.widthMeters / (2 * PAPER_SCREEN.viewDistance) - 0.44);
586
+ const next = direction > 0 ? Math.max(x, Math.min(radius, x + distance)) : Math.min(x, Math.max(-radius, x - distance));
587
+ return paperGroundTarget(next, z);
588
+ }
589
+
590
+ // puppy-paper-contact.mjs
591
+ init_puppy_frame_store();
592
+ var smooth = (a, b, x) => {
593
+ const t = Math.max(0, Math.min(1, (x - a) / (b - a)));
594
+ return t * t * (3 - 2 * t);
595
+ };
596
+ function createPaperFootLock({ maxImageStretch = Infinity } = {}) {
597
+ if (typeof maxImageStretch !== "number" || !(maxImageStretch > 1.08))
598
+ throw Error("Invalid image leg stretch");
599
+ const anchors = new Map;
600
+ let identity = "", samples = 0, episodes = 0, rawMax = 0, max = 0, limited = 0;
601
+ const blocked = new Map, releasing = new Set;
602
+ let releaseEvents = 0, releasingFrames = 0, releasedMax = 0;
603
+ return {
604
+ update(feet, placement, key) {
605
+ if (key !== identity) {
606
+ anchors.clear();
607
+ blocked.clear();
608
+ releasing.clear();
609
+ identity = key;
610
+ }
611
+ const { left, top, side } = placement, scale = side / 320, scaleX = (placement.sideX ?? side) / 320, pins = [], active = new Set;
612
+ for (const foot of feet) {
613
+ if (!foot.stance || foot.y < 245)
614
+ continue;
615
+ if (blocked.has(foot.id)) {
616
+ if (blocked.get(foot.id) === foot.episode)
617
+ continue;
618
+ blocked.delete(foot.id);
619
+ }
620
+ const source = { x: foot.x + 0.5, y: foot.y + 0.5 };
621
+ const x = left + source.x * scaleX, y = top + source.y * scale;
622
+ active.add(foot.id);
623
+ let anchor = anchors.get(foot.id);
624
+ if (!anchor || anchor.episode !== foot.episode) {
625
+ anchor = { x, y, episode: foot.episode, world: placement.groundFromPixel?.(x, y) ?? null, root: placement.root ? { ...placement.root } : null };
626
+ anchors.set(foot.id, anchor);
627
+ episodes++;
628
+ }
629
+ if (anchor.world && placement.pixelFromGround)
630
+ Object.assign(anchor, placement.pixelFromGround(anchor.world));
631
+ let desired = { x: anchor.x, y: anchor.y }, gain = 1;
632
+ if (anchor.world && placement.root && placement.pixelFromGround) {
633
+ const root = placement.root, contact = anchor.root, forward = { x: Math.sin(contact.heading), z: Math.cos(contact.heading) };
634
+ const reach = Math.sqrt(PAPER_LIMB.length ** 2 - PAPER_LIMB.stanceHeight ** 2);
635
+ const hip = { x: anchor.world.x - forward.x * reach + root.x - contact.x, z: anchor.world.z - forward.z * reach + root.z - contact.z }, dx2 = anchor.world.x - hip.x, dz2 = anchor.world.z - hip.z;
636
+ const radius = Math.hypot(dx2, dz2);
637
+ if (radius > reach + 0.000000001) {
638
+ gain = reach / radius;
639
+ desired = placement.pixelFromGround({ x: hip.x + dx2 * gain, z: hip.z + dz2 * gain });
640
+ }
641
+ }
642
+ const dx = (desired.x - x) / scaleX, dy2 = (desired.y - y) / scale, length2 = Math.hypot(dx, dy2), sourceGain = anchor.world && placement.root ? 1 : length2 > 56 ? 56 / length2 : 1;
643
+ const rest = Math.max(16, source.y - 225), strain = Math.hypot(dx, rest + dy2) / rest;
644
+ const strength = Number.isFinite(maxImageStretch) ? 1 - smooth(1.08, maxImageStretch, strain) : 1;
645
+ const isReleasing = strength < 1;
646
+ if (isReleasing && !releasing.has(foot.id)) {
647
+ releaseEvents++;
648
+ releasing.add(foot.id);
649
+ }
650
+ if (!strength) {
651
+ anchors.delete(foot.id);
652
+ blocked.set(foot.id, foot.episode);
653
+ continue;
654
+ }
655
+ const target = { x: source.x + dx * sourceGain * strength, y: source.y + dy2 * sourceGain * strength };
656
+ const actual = { x: Math.floor(left + target.x * scaleX) + 0.5, y: Math.floor(top + target.y * scale) + 0.5 };
657
+ rawMax = Math.max(rawMax, Math.hypot(x - anchor.x, y - anchor.y));
658
+ const residual = Math.hypot(actual.x - anchor.x, actual.y - anchor.y);
659
+ if (isReleasing) {
660
+ releasingFrames++;
661
+ releasedMax = Math.max(releasedMax, residual);
662
+ } else
663
+ max = Math.max(max, residual);
664
+ samples++;
665
+ if (gain < 1 || sourceGain < 1)
666
+ limited++;
667
+ pins.push({ id: foot.id, source, target, anchor: { ...anchor }, actual, limited: gain < 1 || sourceGain < 1, releasing: isReleasing });
668
+ }
669
+ for (const id of anchors.keys())
670
+ if (!active.has(id))
671
+ anchors.delete(id);
672
+ const supported = new Set(feet.filter((f) => f.stance).map((f) => f.id));
673
+ for (const id of blocked.keys())
674
+ if (!supported.has(id))
675
+ blocked.delete(id);
676
+ for (const id of releasing)
677
+ if (!supported.has(id))
678
+ releasing.delete(id);
679
+ return pins;
680
+ },
681
+ reset() {
682
+ anchors.clear();
683
+ blocked.clear();
684
+ releasing.clear();
685
+ identity = "";
686
+ },
687
+ report() {
688
+ return { samples, episodes, rawMax, max, limited, releaseEvents, releasingFrames, releasedMax };
689
+ }
690
+ };
691
+ }
692
+ function createPaperSupportEpisodes(labels, indices, samples, { cyclic = true } = {}) {
693
+ const step = samples - 1, period = indices.length * step, ids = new Set(indices.flatMap((p) => labels[p].flatMap((f) => f.map((x) => x.id)))), starts = new Map;
694
+ for (const id of ids) {
695
+ const active = Array.from({ length: period + (cyclic ? 0 : 1) }, (_, i) => {
696
+ const p = Math.min(indices.length - 1, Math.floor(i / step)), s2 = i - p * step;
697
+ return labels[indices[p]][s2].some((f) => f.id === id && f.stance);
698
+ });
699
+ starts.set(id, active.map((v, i) => {
700
+ if (!v)
701
+ return -1;
702
+ let start = i;
703
+ while (start > 0 && active[start - 1])
704
+ start--;
705
+ if (cyclic && start === 0 && active.at(-1)) {
706
+ let k = active.length - 1;
707
+ while (k > 0 && active[k - 1])
708
+ k--;
709
+ return k;
710
+ }
711
+ return start;
712
+ }));
713
+ }
714
+ return (feet, pair, blend, phase) => {
715
+ const p = indices.indexOf(pair);
716
+ if (p < 0)
717
+ return feet;
718
+ const q = p * step + Math.round(blend * step), index = cyclic ? q % period : Math.min(period, q), cycle = cyclic ? Math.floor(phase) + Math.floor(q / period) : 0;
719
+ return feet.map((foot) => {
720
+ const start = starts.get(foot.id)?.[index] ?? -1;
721
+ return { ...foot, episode: `${start}/${cycle - (cyclic && start > index ? 1 : 0)}` };
722
+ });
723
+ };
724
+ }
725
+ function createBakedSupportEpisodes(track) {
726
+ return (feet, pair, blend, phase) => {
727
+ const offset = track.offsets[pair];
728
+ if (offset === undefined)
729
+ return feet;
730
+ const q = offset + Math.round(blend * track.step), index = track.cyclic ? q % track.period : Math.min(track.period, q), cycle = track.cyclic ? Math.floor(phase) + Math.floor(q / track.period) : 0;
731
+ return feet.map((foot) => {
732
+ const start = track.starts[foot.id]?.[index] ?? -1;
733
+ return { ...foot, episode: `${start}/${cycle - (track.cyclic && start > index ? 1 : 0)}` };
734
+ });
735
+ };
736
+ }
737
+ function footSourcePoint(x, y, pins) {
738
+ if (y <= 225 || !pins.length)
739
+ return { x, y };
740
+ let best = 0, chosen = null;
741
+ for (const pin of pins) {
742
+ const dx = pin.target.x - pin.source.x, dy2 = pin.target.y - pin.source.y;
743
+ const rx = 28 + 2 * Math.abs(dx), ry = 28 + 2 * Math.abs(dy2), r2 = ((x - pin.target.x) / rx) ** 2 + ((y - pin.target.y) / ry) ** 2;
744
+ const weight = r2 < 1 ? (1 - r2) ** 2 * smooth(225, Math.min(245, Math.max(226, pin.target.y)), y) : 0;
745
+ if (weight > best) {
746
+ best = weight;
747
+ chosen = { dx, dy: dy2 };
748
+ }
749
+ }
750
+ return chosen ? { x: x - chosen.dx * best, y: y - chosen.dy * best } : { x, y };
751
+ }
752
+ function createRearSampler(bytes, info, output, kernel = null) {
753
+ if (!isGrayFrameSource(bytes) || info.pairs !== 18 || info.samples !== 9 || bytes.byteLength !== 18 * 9 * 320 * 320)
754
+ throw Error("Invalid rear sprite cache");
755
+ const frames = grayFrameReader(bytes, 320, 320, 18 * 9), out = output ?? new Uint8ClampedArray(320 * 320 * 4);
756
+ if (!(out instanceof Uint8ClampedArray) || out.length !== 320 * 320 * 4)
757
+ throw Error("Invalid rear output");
758
+ for (let i = 3;i < out.length; i += 4)
759
+ out[i] = 255;
760
+ return (pair, blend) => {
761
+ if (!Number.isInteger(pair) || pair < 0 || pair >= 18 || !Number.isFinite(blend) || blend < 0 || blend > 1)
762
+ throw Error("Invalid rear pose");
763
+ const frame = frames.getFrame(pair * 9 + Math.round(blend * 8));
764
+ if (kernel?.copyGray)
765
+ kernel.copyGray(out, frame);
766
+ else
767
+ for (let j = 0, i = 0;i < 320 * 320; i++, j += 4)
768
+ out[j] = out[j + 1] = out[j + 2] = frame[i];
769
+ return out;
770
+ };
771
+ }
772
+
773
+ // puppy-motion-lookup.mjs
774
+ var views = Object.freeze({ front: 0, rear: 1, approach: 2 });
775
+ function preparationRoute(tables, kind, { view, flip, seatProgress = 0, desired, nextFlip = false, speed = 0 }) {
776
+ if (!Number.isFinite(speed) || Math.abs(speed) > tables.routes.maxPreparationSpeed)
777
+ throw Error("Motion preparation requires a stopped root");
778
+ const viewIndex = views[view], seat = seatProgress === 0 ? 0 : seatProgress === 1 ? 2 : 1;
779
+ const target = kind === "action" ? desired === "stand" ? 0 : desired === "seat" ? 1 : undefined : views[desired];
780
+ if (viewIndex === undefined || target === undefined || typeof flip !== "boolean" || typeof nextFlip !== "boolean")
781
+ throw Error("Unknown baked motion route");
782
+ const index = kind === "action" ? ((viewIndex * 2 + +flip) * 3 + seat) * 2 + target : ((viewIndex * 2 + +flip) * 3 + target) * 2 + +nextFlip;
783
+ const code = tables.routes[kind]?.[index], route = tables.routes.steps[code];
784
+ if (!route)
785
+ throw Error("Unknown baked motion route");
786
+ return route;
787
+ }
788
+ function canJoinMotion(tables, name, { view, speed, phase = 0, nextSpeed = speed }) {
789
+ const join = tables.joins[name], epsilon = 0.0000001;
790
+ if (!join || join.view !== view || ![speed, phase, nextSpeed].every(Number.isFinite))
791
+ return false;
792
+ if (speed < join.speed[0] - epsilon || speed > join.speed[1] + epsilon || nextSpeed < join.speed[0] - epsilon || nextSpeed > join.speed[1] + epsilon || join.positiveSpeed && (speed <= 0 || nextSpeed <= 0))
793
+ return false;
794
+ if (join.phase !== undefined) {
795
+ const d = phase - join.phase;
796
+ if (Math.abs(d - Math.round(d)) > epsilon)
797
+ return false;
798
+ }
799
+ if (join.matchVelocity && Math.abs(speed - nextSpeed) > epsilon)
800
+ return false;
801
+ return true;
802
+ }
803
+
804
+ // puppy-paper-seat.mjs
805
+ init_puppy_frame_store();
806
+ function createPaperSeatSampler(bytes, info, output, kernel = null) {
807
+ const stride = 320 * 320;
808
+ if (!isGrayFrameSource(bytes) || info.width !== 320 || info.height !== 320 || info.pairs !== 3 || info.samples !== 17 || bytes.byteLength !== 3 * 17 * stride)
809
+ throw Error("Invalid paper sitting cache");
810
+ const frames = grayFrameReader(bytes, 320, 320, 3 * 17), out = output ?? new Uint8ClampedArray(stride * 4);
811
+ if (!(out instanceof Uint8ClampedArray) || out.length !== stride * 4)
812
+ throw Error("Invalid seat output");
813
+ for (let i = 3;i < out.length; i += 4)
814
+ out[i] = 255;
815
+ return (progress) => {
816
+ if (!Number.isFinite(progress) || progress < 0 || progress > 1)
817
+ throw Error("Invalid paper sitting progress");
818
+ const q = progress * 3, pair = Math.min(2, Math.floor(q)), sample = Math.round((q - pair) * 16), frame = frames.getFrame(pair * 17 + sample);
819
+ if (kernel?.copyGray)
820
+ kernel.copyGray(out, frame);
821
+ else
822
+ for (let i = 0, j = 0;i < stride; i++, j += 4)
823
+ out[j] = out[j + 1] = out[j + 2] = frame[i];
824
+ return out;
825
+ };
826
+ }
827
+
828
+ // puppy-paper-director.mjs
829
+ function createPaperDirector(seed = 731) {
830
+ let value = seed >>> 0, energy = 0.85, last = "", choices = 0, excursions = 0;
831
+ const random = () => {
832
+ value = Math.imul(value, 1664525) + 1013904223 >>> 0;
833
+ return value / 4294967296;
834
+ };
835
+ return {
836
+ update(dt, moving, distance = 0) {
837
+ energy = Math.max(0, Math.min(1, energy + dt * (moving ? -0.05 : 0.025) - (moving ? distance * 0.025 : 0)));
838
+ },
839
+ choose({ x, z, seated }) {
840
+ choices++;
841
+ let candidates = seated ? [{ kind: "watch", weight: 2 }, { kind: "rest", weight: energy < 0.45 ? 5 : 0.5 }, { kind: "wander", weight: energy * 5 }] : [{ kind: "wander", weight: 3 }, { kind: "come", weight: 2 }];
842
+ candidates = candidates.filter((a) => a.kind !== last);
843
+ const total = candidates.reduce((n, a) => n + a.weight, 0);
844
+ let pick = random() * total, selected = candidates.at(-1);
845
+ for (const candidate of candidates) {
846
+ pick -= candidate.weight;
847
+ if (pick <= 0) {
848
+ selected = candidate;
849
+ break;
850
+ }
851
+ }
852
+ last = selected.kind;
853
+ const depths = [-1.6, -8, -3.2];
854
+ const target = last === "come" ? [x, 0.2] : [x > 0 ? -0.5 : 0.5, z < -1 ? -0.15 : depths[excursions++ % depths.length]];
855
+ return { kind: last, target, holdSeconds: 2 + random() * 2, side: random() < 0.5 ? "left" : "right", energy };
856
+ },
857
+ get report() {
858
+ return { energy, last, choices };
859
+ }
860
+ };
861
+ }
862
+
863
+ // puppy-paper-behaviors.mjs
864
+ function paperBehaviorPose(state, info) {
865
+ const list = info.clips[state.behavior];
866
+ if (!list)
867
+ throw Error("Unknown paper behavior");
868
+ const progress = state.behavior === "walk" ? state.behaviorProgress % 1 : Math.max(0, Math.min(1, state.behaviorProgress));
869
+ const q = Math.min(list.length - 0.000000001, progress * list.length), i = Math.floor(q), blend = Math.round((q - i) * 8) / 8;
870
+ return { pair: list[i], blend };
871
+ }
872
+ function createPaperBehaviorSampler(bytes, info, output, kernel = null) {
873
+ const sample = createRearSampler(bytes, info, output, kernel);
874
+ return (state) => {
875
+ const p = paperBehaviorPose(state, info);
876
+ return sample(p.pair, p.blend);
877
+ };
878
+ }
879
+
880
+ // puppy-paper-area.mjs
881
+ function createPaperAreaSampler(n = 320) {
882
+ if (!Number.isInteger(n) || n < 2 || n > 640)
883
+ throw Error("Invalid area source size");
884
+ const pitch2 = n + 1;
885
+ let sums = null;
886
+ const integral = (x, y) => {
887
+ x = Math.max(0, Math.min(n, x));
888
+ y = Math.max(0, Math.min(n, y));
889
+ const ix = Math.min(n - 1, Math.floor(x)), iy = Math.min(n - 1, Math.floor(y)), fx = x - ix, fy = y - iy, k = iy * pitch2 + ix;
890
+ return (sums[k] * (1 - fx) + sums[k + 1] * fx) * (1 - fy) + (sums[k + pitch2] * (1 - fx) + sums[k + pitch2 + 1] * fx) * fy;
891
+ };
892
+ return {
893
+ prepare(image) {
894
+ if (image.length !== n * n * 4)
895
+ throw Error("Invalid area source");
896
+ const Type = image instanceof Uint8Array || image instanceof Uint8ClampedArray ? Uint32Array : Float64Array;
897
+ if (!(sums instanceof Type))
898
+ sums = new Type(pitch2 * pitch2);
899
+ else
900
+ sums.fill(0);
901
+ for (let y = 0;y < n; y++) {
902
+ let row = 0;
903
+ for (let x = 0;x < n; x++) {
904
+ row += image[(y * n + x) * 4];
905
+ sums[(y + 1) * pitch2 + x + 1] = sums[y * pitch2 + x + 1] + row;
906
+ }
907
+ }
908
+ },
909
+ sample(x, y, dx, dy2) {
910
+ if (![x, y, dx, dy2].every(Number.isFinite) || Math.min(dx, dy2) <= 0)
911
+ throw Error("Invalid area footprint");
912
+ if (!sums)
913
+ return 0;
914
+ const l = x - dx / 2, r = x + dx / 2, t = y - dy2 / 2, b = y + dy2 / 2;
915
+ return Math.max(0, (integral(r, b) - integral(l, b) - integral(r, t) + integral(l, t)) / (dx * dy2));
916
+ }
917
+ };
918
+ }
919
+
920
+ // puppy-paper-actions.mjs
921
+ init_puppy_frame_store();
922
+ var PAPER_ACTION_DURATIONS = Object.freeze({ spin: 2.8, roll: 4.6, "roll-down": 4.6, "walk-enter": 0.28, "walk-exit": 0.28 });
923
+ var PAPER_ACTION_FORMAT = Object.freeze({ width: 480, height: 480, samples: 9, maxPairs: 256 });
924
+ var own = (o, k) => o !== null && typeof o === "object" && !Array.isArray(o) && Object.prototype.hasOwnProperty.call(o, k);
925
+ var isPairCount = (n) => Number.isInteger(n) && n > 0 && n <= PAPER_ACTION_FORMAT.maxPairs;
926
+ function actionClip(name, info) {
927
+ if (typeof name !== "string" || !own(info?.clips, name))
928
+ throw Error(`Invalid paper action: unknown clip ${JSON.stringify(name)}`);
929
+ if (!isPairCount(info.pairs))
930
+ throw Error("Invalid paper action: pair count");
931
+ const list = info.clips[name];
932
+ if (!Array.isArray(list) || !list.length)
933
+ throw Error(`Invalid paper action: empty clip ${name}`);
934
+ for (const p of list)
935
+ if (!Number.isInteger(p) || p < 0 || p >= info.pairs)
936
+ throw Error(`Invalid paper action: pair ${p} out of range in ${name}`);
937
+ let weights = list.map(() => 1);
938
+ if (info.weights !== undefined) {
939
+ if (info.weights === null || typeof info.weights !== "object" || Array.isArray(info.weights))
940
+ throw Error("Invalid paper action: weights");
941
+ if (own(info.weights, name)) {
942
+ weights = info.weights[name];
943
+ if (!Array.isArray(weights) || weights.length !== list.length)
944
+ throw Error(`Invalid paper action: weights length for ${name}`);
945
+ for (const w of weights)
946
+ if (typeof w !== "number" || !Number.isFinite(w) || w <= 0)
947
+ throw Error(`Invalid paper action: weight ${w} in ${name}`);
948
+ }
949
+ }
950
+ return { list, weights };
951
+ }
952
+ function validatePaperActionInfo(info, byteLength) {
953
+ const f = PAPER_ACTION_FORMAT;
954
+ if (info === null || typeof info !== "object" || info.width !== f.width || info.height !== f.height || info.samples !== f.samples)
955
+ throw Error("Invalid action cache: format");
956
+ if (!isPairCount(info.pairs))
957
+ throw Error("Invalid action cache: pair count");
958
+ if (info.clips === null || typeof info.clips !== "object" || Array.isArray(info.clips) || !Object.keys(info.clips).length)
959
+ throw Error("Invalid action cache: clips");
960
+ if (info.weights !== undefined) {
961
+ for (const k of Object.keys(info.weights ?? {}))
962
+ if (!own(info.clips, k))
963
+ throw Error(`Invalid action cache: weights for unknown clip ${k}`);
964
+ }
965
+ for (const name of Object.keys(info.clips))
966
+ actionClip(name, info);
967
+ if (byteLength !== undefined && byteLength !== f.width * f.height * f.samples * info.pairs)
968
+ throw Error("Invalid action cache: byte length");
969
+ return info;
970
+ }
971
+ function paperActionPose(name, progress, info, compiledClips) {
972
+ if (typeof progress !== "number" || !Number.isFinite(progress))
973
+ throw Error("Invalid paper action: progress");
974
+ if (compiledClips && !own(compiledClips, name))
975
+ throw Error(`Invalid paper action: unknown compiled clip ${JSON.stringify(name)}`);
976
+ const { list, weights, total = weights.reduce((a, b) => a + b, 0) } = compiledClips?.[name] ?? actionClip(name, info);
977
+ let t = (name === "walk" ? (progress % 1 + 1) % 1 : Math.max(0, Math.min(1, progress))) * total, index = 0;
978
+ while (index < list.length - 1 && t > weights[index]) {
979
+ t -= weights[index++];
980
+ }
981
+ const blend = Math.round(Math.max(0, Math.min(1, t / weights[index])) * 8) / 8;
982
+ if (!Number.isFinite(blend))
983
+ throw Error("Invalid paper action: blend");
984
+ return { pair: list[index], blend };
985
+ }
986
+ function createPaperActionSampler(bytes, info, output, compiledClips, kernel = null) {
987
+ if (!isGrayFrameSource(bytes))
988
+ throw Error("Invalid action cache: bytes");
989
+ validatePaperActionInfo(info, bytes.byteLength);
990
+ const stride = info.width * info.height, pairs = info.pairs, frames = grayFrameReader(bytes, info.width, info.height, pairs * 9);
991
+ const out = output ?? new Uint8ClampedArray(stride * 4);
992
+ if (!(out instanceof Uint8ClampedArray) || out.length !== stride * 4)
993
+ throw Error("Invalid action output");
994
+ for (let i = 3;i < out.length; i += 4)
995
+ out[i] = 255;
996
+ return (state) => {
997
+ const p = paperActionPose(state.actionName, state.actionProgress, info, compiledClips), sample = Math.round(p.blend * 8);
998
+ if (p.pair >= pairs || sample < 0 || sample > 8)
999
+ throw Error("Invalid paper action: sample out of range");
1000
+ const frame = frames.getFrame(p.pair * 9 + sample);
1001
+ if (kernel?.copyGray)
1002
+ kernel.copyGray(out, frame);
1003
+ else
1004
+ for (let i = 0, j = 0;i < stride; i++, j += 4)
1005
+ out[j] = out[j + 1] = out[j + 2] = frame[i];
1006
+ return out;
1007
+ };
1008
+ }
1009
+
1010
+ // puppy-paper-paw-transport.mjs
1011
+ var TORSO = 226;
1012
+ var TOE_RIGID = 8;
1013
+ var MIN_SPAN = 4;
1014
+ var FOLD_MARGIN = 0.9;
1015
+ var smooth2 = (t) => t <= 0 ? 0 : t >= 1 ? 1 : t * t * (3 - 2 * t);
1016
+ var finite = (v) => typeof v === "number" && Number.isFinite(v);
1017
+ var depthRank = (id) => /near/.test(id) ? 0 : /far/.test(id) ? 2 : 1;
1018
+ function createPawTransport(feet, pins, { opaque = null } = {}) {
1019
+ const list = [], seen = new Set;
1020
+ for (const foot of Array.isArray(feet) ? feet : []) {
1021
+ if (!foot || typeof foot.id !== "string" || seen.has(foot.id) || !finite(foot.x) || !finite(foot.y))
1022
+ continue;
1023
+ seen.add(foot.id);
1024
+ list.push(foot);
1025
+ }
1026
+ const pinById = new Map;
1027
+ for (const pin of Array.isArray(pins) ? pins : []) {
1028
+ if (!pin || !seen.has(pin.id) || pinById.has(pin.id))
1029
+ continue;
1030
+ const { source: s2, target: t } = pin;
1031
+ if (!s2 || !t || ![s2.x, s2.y, t.x, t.y].every(finite))
1032
+ continue;
1033
+ pinById.set(pin.id, pin);
1034
+ }
1035
+ const limited = [], degenerate = [];
1036
+ const identity = (x, y) => finite(x) && finite(y) ? { x, y } : null;
1037
+ if (!pinById.size || !list.length)
1038
+ return Object.assign(identity, { layers: [], limited, degenerate });
1039
+ const byX = [...list].sort((a, b) => a.x - b.x || (a.id < b.id ? -1 : 1));
1040
+ const layers = byX.map((foot, i) => {
1041
+ const cx = foot.x + 0.5, lo = i ? (byX[i - 1].x + 0.5 + cx) / 2 : -Infinity, hi = i < byX.length - 1 ? (cx + byX[i + 1].x + 0.5) / 2 : Infinity;
1042
+ if (!(cx >= lo && cx < hi))
1043
+ degenerate.push(foot.id);
1044
+ const pin = pinById.get(foot.id), dx = pin ? pin.target.x - pin.source.x : 0;
1045
+ let dy2 = pin ? pin.target.y - pin.source.y : 0;
1046
+ const toeTop = Math.max(TORSO + MIN_SPAN, (pin ? pin.source.y : foot.y + 0.5) - TOE_RIGID), span = toeTop - TORSO;
1047
+ let a = 0;
1048
+ if (dy2 < 0) {
1049
+ const k = FOLD_MARGIN * span / -dy2;
1050
+ if (k < 1) {
1051
+ a = 1;
1052
+ dy2 = -FOLD_MARGIN * span;
1053
+ limited.push(foot.id);
1054
+ } else if (k < 1.5)
1055
+ a = Math.min(1, 3 - 2 * k);
1056
+ }
1057
+ return { id: foot.id, lo, hi, dx, dy: dy2, toeTop, span, a, active: !!pin, rank: depthRank(foot.id), y: foot.y, lastY: NaN, lastSy: NaN };
1058
+ });
1059
+ const gx = (L, s2) => smooth2((s2 - TORSO) / L.span);
1060
+ const gy = (L, s2) => {
1061
+ const t = (s2 - TORSO) / L.span;
1062
+ return t <= 0 ? 0 : t >= 1 ? 1 : (1 - L.a) * smooth2(t) + L.a * t;
1063
+ };
1064
+ const invY = (L, y) => {
1065
+ if (L.dy === 0)
1066
+ return y;
1067
+ if (y >= L.toeTop + L.dy)
1068
+ return y - L.dy;
1069
+ if (y === L.lastY)
1070
+ return L.lastSy;
1071
+ let lo = TORSO, hi = L.toeTop;
1072
+ for (let i = 0;i < 48 && hi - lo > 0.000000001; i++) {
1073
+ const m = (lo + hi) / 2;
1074
+ if (m + L.dy * gy(L, m) < y)
1075
+ lo = m;
1076
+ else
1077
+ hi = m;
1078
+ }
1079
+ L.lastY = y;
1080
+ L.lastSy = (lo + hi) / 2;
1081
+ return L.lastSy;
1082
+ };
1083
+ const order = [...layers].sort((p, q) => (opaque ? 0 : q.active - p.active) || p.rank - q.rank || q.y - p.y || (p.id < q.id ? -1 : 1));
1084
+ const sourcePoint = (x, y) => {
1085
+ if (!finite(x) || !finite(y))
1086
+ return null;
1087
+ if (y < TORSO)
1088
+ return { x, y };
1089
+ let fallback = null;
1090
+ for (const L of order) {
1091
+ let sx, sy;
1092
+ if (L.dx === 0 && L.dy === 0) {
1093
+ sx = x;
1094
+ sy = y;
1095
+ } else {
1096
+ sy = invY(L, y);
1097
+ sx = sy >= L.toeTop ? x - L.dx : x - L.dx * gx(L, sy);
1098
+ }
1099
+ if (!(sx >= L.lo && sx < L.hi))
1100
+ continue;
1101
+ if (!opaque || opaque(sx, sy))
1102
+ return { x: sx, y: sy };
1103
+ fallback ??= { x: sx, y: sy };
1104
+ }
1105
+ return fallback;
1106
+ };
1107
+ return Object.assign(sourcePoint, { layers: order.map(({ id, lo, hi, dx, dy: dy2, toeTop, active, rank }) => ({ id, lo, hi, dx, dy: dy2, toeTop, active, rank })), limited, degenerate });
1108
+ }
1109
+
1110
+ // puppy-companion-sprites.mjs
1111
+ init_puppy_frame_store();
1112
+ function createCompanionSpriteSampler(bytes, info, output, kernel = null) {
1113
+ const n = 320 * 320;
1114
+ if (!isGrayFrameSource(bytes) || info?.width !== 320 || info.height !== 320 || info.samples !== 9 || !Number.isInteger(info.pairs) || info.pairs < 1 || info.pairs > 128 || bytes.byteLength !== info.pairs * 9 * n)
1115
+ throw Error("Invalid companion sprite cache");
1116
+ for (const key of ["treat-eat", "ball-pickup", "ball-drop"])
1117
+ if (!Array.isArray(info.clips?.[key]) || !info.clips[key].length || !info.clips[key].every((p) => Number.isInteger(p) && p >= 0 && p < info.pairs))
1118
+ throw Error("Invalid companion sprite clip");
1119
+ const frames = grayFrameReader(bytes, 320, 320, info.pairs * 9), rgba = output ?? new Uint8ClampedArray(n * 4);
1120
+ if (!(rgba instanceof Uint8ClampedArray) || rgba.length !== n * 4)
1121
+ throw Error("Invalid sprite output");
1122
+ for (let i = 3;i < rgba.length; i += 4)
1123
+ rgba[i] = 255;
1124
+ return ({ clip, progress }) => {
1125
+ const list = info.clips[clip];
1126
+ if (!list || !Number.isFinite(progress) || progress < 0 || progress > 1)
1127
+ throw Error("Invalid companion sprite pose");
1128
+ const q = progress * list.length, index = Math.min(list.length - 1, Math.floor(q)), s2 = Math.round((q - index) * 8), frame = frames.getFrame(list[index] * 9 + s2);
1129
+ if (kernel?.copyGray)
1130
+ kernel.copyGray(rgba, frame);
1131
+ else
1132
+ for (let i = 0, j = 0;i < n; i++, j += 4)
1133
+ rgba[j] = rgba[j + 1] = rgba[j + 2] = frame[i];
1134
+ return rgba;
1135
+ };
1136
+ }
1137
+
1138
+ // puppy-comfort-sprites.mjs
1139
+ init_puppy_frame_store();
1140
+ function comfortMotionKey(progress, motion) {
1141
+ if (!Number.isFinite(progress) || progress < 0 || progress > 1)
1142
+ throw Error("Invalid motion progress");
1143
+ const age = progress * motion.duration;
1144
+ let key = 0;
1145
+ while (key < motion.times.length - 1 && age >= motion.times[key + 1])
1146
+ key++;
1147
+ return key;
1148
+ }
1149
+ function comfortMotion(pose, info) {
1150
+ const attention = pose?.clip === "attention-paw" ? info.attentionPaw : pose?.clip === "attention-wall" ? info.attentionWall : null;
1151
+ return attention ? pose.release ? attention.releases?.[pose.release.from] : attention : pose?.clip === "scramble" ? info.scramble : pose?.clip === "pickup" ? info.pickups?.[pose.view] : pose?.clip === "carry-trot" ? info.carryTrot : pose?.clip === "carry-settle" ? info.carryTrot?.settle : null;
1152
+ }
1153
+ var comfortProgress = (pose) => pose.release?.progress ?? pose.progress;
1154
+ function validateMotion(motion, count) {
1155
+ if (!motion || !Array.isArray(motion.frames) || !Array.isArray(motion.times) || motion.frames.length < 2 || motion.frames.length > 32 || motion.frames.length !== motion.times.length || motion.times[0] !== 0 || !Number.isFinite(motion.duration) || motion.duration <= 0 || motion.duration > 3 || (motion.cyclic ? motion.times.at(-1) >= motion.duration : motion.times.at(-1) !== motion.duration) || motion.frames.some((i) => !Number.isInteger(i) || i < 0 || i >= count) || motion.times.some((t, i) => !Number.isFinite(t) || i && t <= motion.times[i - 1]))
1156
+ throw Error("Invalid fetch motion metadata");
1157
+ if (!Array.isArray(motion.floorOffsets) || motion.floorOffsets.length !== motion.frames.length || motion.floorOffsets.some((v) => !Number.isFinite(v) || Math.abs(v) > 64))
1158
+ throw Error("Invalid fetch floor metadata");
1159
+ if (motion.mouth && (!Array.isArray(motion.mouth) || motion.mouth.length !== motion.frames.length || motion.mouth.some((p) => !Array.isArray(p) || p.length !== 2 || p.some((v) => !Number.isFinite(v) || v < 0 || v > 320))))
1160
+ throw Error("Invalid fetch mouth metadata");
1161
+ }
1162
+ function comfortFrameIndex(progress, info) {
1163
+ if (!Number.isFinite(progress) || progress < 0 || progress > 1)
1164
+ throw Error("Invalid comfort progress");
1165
+ const seconds = progress * 6;
1166
+ if (seconds < 0.12 || progress === 1)
1167
+ return 0;
1168
+ if (seconds < 0.65)
1169
+ return seconds < 0.3 ? 1 : 2;
1170
+ if (seconds > 5.15)
1171
+ return seconds < 5.45 ? 7 : seconds < 5.8 ? 8 : 0;
1172
+ const loop = [3, 4, 3, 5, 3, 6, 3, 5, 3, 4, 3, 6];
1173
+ return loop[Math.floor((seconds - 0.65) / 0.28) % loop.length];
1174
+ }
1175
+ function createComfortSpriteSampler(bytes, info, output) {
1176
+ if (!isGrayFrameSource(bytes) || ![1, 2, 3, 4, 5, 6].includes(info?.version) || info.width !== 320 || info.height !== 320 || (info.version >= 4 ? !Number.isInteger(info.frames) || info.frames < 19 || info.frames > 64 : info.frames !== { 1: 9, 2: 13, 3: 19 }[info.version]))
1177
+ throw Error("Invalid comfort sprites");
1178
+ if (info.version === 3) {
1179
+ const s2 = info.scramble;
1180
+ if (!s2 || !Array.isArray(s2.frames) || !Array.isArray(s2.times) || s2.frames.length !== 7 || s2.times.length !== 7 || s2.times[0] !== 0 || !Number.isFinite(s2.duration) || s2.duration <= 0 || s2.duration > 3 || s2.times.at(-1) !== s2.duration || s2.frames[0] !== s2.frames.at(-1) || s2.frames.some((i) => !Number.isInteger(i) || i < 0 || i >= info.frames) || s2.times.some((t, i) => !Number.isFinite(t) || i && t <= s2.times[i - 1]))
1181
+ throw Error("Invalid scramble metadata");
1182
+ }
1183
+ if (info.version >= 4) {
1184
+ validateMotion(info.scramble, info.frames);
1185
+ for (const view of ["rear", "front"])
1186
+ validateMotion(info.pickups?.[view], info.frames);
1187
+ }
1188
+ if (info.version >= 6) {
1189
+ validateMotion(info.carryTrot, info.frames);
1190
+ validateMotion(info.carryTrot?.settle, info.frames);
1191
+ if (info.carryTrot.cyclic !== true || !info.carryTrot.mouth || !info.carryTrot.settle.mouth || !Array.isArray(info.carryTrot.feet) || info.carryTrot.feet.length !== info.carryTrot.frames.length || info.carryTrot.feet.some((list) => !Array.isArray(list) || list.some((f) => typeof f.id !== "string" || typeof f.stance !== "boolean" || ![f.x, f.y].every((v) => Number.isFinite(v) && v >= 0 && v < 320))))
1192
+ throw Error("Invalid carry metadata");
1193
+ }
1194
+ for (const [motion, maxKeys, maxRecovery] of [[info.attentionPaw, 8, 0.3], [info.attentionWall, 12, 0.4]])
1195
+ if (motion) {
1196
+ validateMotion(motion, info.frames);
1197
+ if (motion.frames[0] !== 0 || motion.frames.at(-1) !== 0 || !motion.releases || Object.keys(motion.releases).length > maxKeys)
1198
+ throw Error("Invalid attention endpoint");
1199
+ for (const frame of new Set(motion.frames)) {
1200
+ const exit = motion.releases[frame];
1201
+ validateMotion(exit, info.frames);
1202
+ if (exit.frames[0] !== frame || exit.frames.at(-1) !== 0 || exit.duration > maxRecovery)
1203
+ throw Error("Invalid attention recovery");
1204
+ }
1205
+ }
1206
+ if (output !== undefined && (!(output instanceof Uint8ClampedArray) || output.length !== 320 * 320 * 4))
1207
+ throw Error("Invalid comfort output");
1208
+ const frames = grayFrameReader(bytes, 320, 320, info.frames);
1209
+ let rgba = output ?? null;
1210
+ if (rgba)
1211
+ for (let i = 3;i < rgba.length; i += 4)
1212
+ rgba[i] = 255;
1213
+ return (pose) => {
1214
+ if (!["tug", "sleep", "scramble", "pickup", "carry-trot", "carry-settle", "attention-paw", "attention-wall"].includes(pose?.clip))
1215
+ throw Error("Invalid comfort clip");
1216
+ if (!rgba) {
1217
+ rgba = new Uint8ClampedArray(320 * 320 * 4);
1218
+ for (let i = 3;i < rgba.length; i += 4)
1219
+ rgba[i] = 255;
1220
+ }
1221
+ let index;
1222
+ if (["scramble", "pickup", "carry-trot", "carry-settle", "attention-paw", "attention-wall"].includes(pose.clip)) {
1223
+ const motion = comfortMotion(pose, info);
1224
+ if (!motion)
1225
+ throw Error("Invalid scramble/pickup pose");
1226
+ index = motion.frames[comfortMotionKey(comfortProgress(pose), motion)];
1227
+ } else
1228
+ index = pose.clip === "sleep" ? info.sleep?.[pose.variant] : comfortFrameIndex(pose.progress, info);
1229
+ if (!Number.isInteger(index))
1230
+ throw Error("Invalid sleep variant");
1231
+ const frame = frames.getFrame(index), base = pose.base, blend = pose.blend ?? 1;
1232
+ if (!Number.isFinite(blend) || blend < 0 || blend > 1)
1233
+ throw Error("Invalid sleep blend");
1234
+ for (let i = 0, j = 0;i < frame.length; i++, j += 4) {
1235
+ const v = frame[pose.right ? Math.floor(i / 320) * 320 + 319 - i % 320 : i];
1236
+ rgba[j] = rgba[j + 1] = rgba[j + 2] = base && blend < 1 ? Math.round(base[j] + (v - base[j]) * blend) : v;
1237
+ }
1238
+ return rgba;
1239
+ };
1240
+ }
1241
+
1242
+ // puppy-mouth-anchor.mjs
1243
+ var MOUTH_ANCHORS = { version: 1, ballRadiusPx: 17.5, families: { approach: { sha256: "f5558c57d5a445d396e06c060f9f8d64e6ce0ff5b9b92fa6d77ef9bb2984a2fe", frames: [null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, [224.66, 147.5, 0.9, 0.904], [222.44, 147.33, 0.9, 0.915], [219.17, 148.34, 0.9, 0.934], [215.72, 149.59, 0.92, 0.945], [212.89, 150.36, 0.92, 0.957], [209.34, 151.71, 0.94, 0.966], [206.61, 152.52, 0.94, 0.972], [202.85, 153.49, 0.94, 0.965], [202.85, 153.49, 0.94, 0.965], [198.71, 154.18, 0.96, 0.966], [195.56, 154.22, 0.96, 0.97], [191.82, 155.11, 0.98, 0.967], [188.11, 155.2, 0.98, 0.966], [184.12, 154.72, 0.96, 0.966], [180.5, 154.77, 0.96, 0.968], [177.45, 154.9, 0.96, 0.97], [173.13, 154.91, 0.96, 0.965], [173.13, 154.91, 0.96, 0.965], [175.53, 155.91, 0.96, 0.974], [177.69, 157.9, 0.98, 0.976], [179.97, 159.24, 0.98, 0.983], [182.41, 161.16, 0.98, 0.988], [184.55, 162.2, 0.98, 0.987], [186.96, 165.39, 1, 0.991], [189.54, 166, 0.98, 0.986], [191.7, 168.3, 1, 1], [191.7, 168.3, 1, 1], [190.78, 168.33, 1, 0.999], [190.26, 168.08, 0.98, 0.983], [189.46, 169.24, 1, 0.994], [188.66, 169.45, 1, 0.988], [187.9, 169.62, 1, 0.977], [187.07, 168.92, 0.98, 0.971], [186.15, 171.37, 1.02, 0.964], [185.12, 171.44, 1.02, 0.959], [185.12, 171.44, 1.02, 0.959], [183.01, 167.4, 0.98, 0.967], [180.53, 166.1, 1, 0.965], [178.7, 163.98, 0.98, 0.962], [176.6, 160.9, 0.96, 0.958], [174.17, 158.89, 0.96, 0.958], [172.42, 158.03, 0.98, 0.948], [170.58, 155.84, 0.96, 0.945], [168.27, 154.8, 0.98, 0.932], [168.27, 154.8, 0.98, 0.932], [168.28, 154.25, 0.98, 0.941], [167.82, 153.76, 0.98, 0.938], [167.36, 153.07, 0.98, 0.941], [167.1, 151.76, 0.96, 0.941], [166.97, 152.2, 0.98, 0.947], [166.89, 151.46, 0.98, 0.944], [166.21, 151.32, 0.98, 0.944], [166.05, 150.38, 0.98, 0.943], [166.05, 150.38, 0.98, 0.943], [168.13, 148.79, 0.96, 0.945], [169.85, 149.25, 0.98, 0.951], [172.04, 147.99, 0.98, 0.948], [174.87, 146.91, 0.98, 0.947], [176.38, 147.11, 1, 0.947], [179.35, 146.95, 1.02, 0.941], [181.18, 146.19, 1.02, 0.943], [183.86, 145.25, 1.02, 0.942], [183.86, 145.25, 1.02, 0.942], [182.95, 145.34, 1.02, 0.946], [182.65, 145.38, 1.02, 0.953], [181.7, 145.7, 1.02, 0.948], [181.06, 145.96, 1.02, 0.949], [180.46, 145.26, 1, 0.954], [179.53, 146.46, 1.02, 0.949], [179.36, 146.51, 1.02, 0.948], [178.41, 146.53, 1.02, 0.947], [178.41, 146.53, 1.02, 0.947], [180.31, 147.47, 1.02, 0.952], [181.26, 147.72, 1.02, 0.954], [183.14, 147.04, 0.98, 0.957], [184.8, 149.17, 1, 0.967], [186.42, 150.19, 1, 0.966], [188.41, 150.46, 1, 0.966], [189.95, 151.41, 1, 0.971], [191.61, 152.4, 1, 0.971], [191.61, 152.4, 1, 0.971], [189.01, 151.48, 1, 0.971], [186.85, 151.25, 1, 0.979], [184.64, 150.57, 1, 0.975], [182.06, 149.13, 0.98, 0.974], [179.48, 149.48, 1, 0.976], [177.17, 149.61, 1.02, 0.976], [175.2, 149.27, 1.02, 0.979], [172.3, 148.6, 1.02, 0.975], [172.3, 148.6, 1.02, 0.975], [175.21, 151.35, 1.02, 0.979], [177.27, 153.34, 1.02, 0.982], [179.97, 155.12, 1, 0.983], [181.35, 158.65, 1.02, 0.988], [184.96, 160.94, 1, 0.987], [187.54, 163.29, 1, 0.996], [189.22, 166.33, 1.02, 0.992], [191.7, 168.3, 1, 1], [191.7, 168.3, 1, 1], [189.18, 166.28, 1, 0.99], [186.95, 164.07, 0.98, 0.991], [184.75, 163.57, 1, 0.987], [182.21, 160.91, 0.98, 0.986], [180.15, 158.8, 0.96, 0.98], [177.73, 156.71, 0.96, 0.974], [175.67, 155.87, 0.96, 0.97], [173.13, 154.91, 0.96, 0.965], [173.13, 154.91, 0.96, 0.965], [176.75, 154.83, 0.96, 0.966], [180.19, 154.77, 0.96, 0.966], [183.7, 154.69, 0.96, 0.966], [187.78, 153.74, 0.94, 0.965], [191.91, 155.07, 0.98, 0.968], [195.35, 154.27, 0.96, 0.969], [199.09, 154.17, 0.96, 0.968], [202.85, 153.49, 0.94, 0.965], [202.85, 153.49, 0.94, 0.965], [206.07, 152.72, 0.94, 0.968], [209.04, 152.89, 0.96, 0.962], [211.97, 151.51, 0.94, 0.959], [215.45, 150.92, 0.94, 0.941], [218.71, 149.41, 0.92, 0.933], [221.75, 148.5, 0.9, 0.92], [224.95, 147.58, 0.9, 0.903], null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null] }, behaviors: { sha256: "7918261fb64c57a91739729841fd84c6dd53891196c7fb76ee9c27712ab6ef5d", frames: [[201.21, 166.15, 0.98, 0.959], [200.4, 166.14, 0.98, 0.961], [200.13, 166.16, 0.98, 0.963], [199.21, 166.15, 0.98, 0.964], [198.65, 166.13, 0.98, 0.964], [197.98, 166.12, 0.98, 0.967], [197.08, 166.1, 0.98, 0.966], [196.87, 166.13, 0.98, 0.966], [195.93, 166.1, 0.98, 0.965], [195.93, 166.1, 0.98, 0.965], [196.01, 166.1, 0.98, 0.967], [196.35, 166.05, 0.98, 0.96], [196.85, 166.1, 0.98, 0.966], [197.08, 166.07, 0.98, 0.964], [197.25, 166.07, 0.98, 0.96], [197.72, 166.12, 0.98, 0.963], [198.13, 166.05, 0.98, 0.957], [198.18, 166.04, 0.98, 0.954], [198.18, 166.04, 0.98, 0.954], [198.23, 166.06, 0.98, 0.957], [198.26, 166.09, 0.98, 0.957], [198.28, 166.12, 0.98, 0.958], [198.81, 167.29, 1, 0.968], [198.92, 167.34, 1, 0.966], [198.98, 167.37, 1, 0.964], [199.06, 167.41, 1, 0.962], [199.07, 167.42, 1, 0.96], [199.07, 167.42, 1, 0.96], [199.16, 167.39, 1, 0.96], [199.43, 167.35, 1, 0.96], [199.96, 167.33, 1, 0.965], [200.14, 166.34, 0.98, 0.965], [200.28, 166.27, 0.98, 0.963], [200.84, 166.24, 0.98, 0.965], [201.14, 166.17, 0.98, 0.962], [201.21, 166.15, 0.98, 0.959], [191.7, 168.3, 1, 1], [192.77, 168.21, 1, 0.999], [194.11, 167.32, 0.98, 0.992], [195.54, 167.92, 1, 0.986], [196.71, 166.87, 0.98, 0.982], [197.88, 167.48, 1, 0.978], [199.08, 166.37, 0.98, 0.973], [200.32, 166.2, 0.98, 0.965], [201.21, 166.15, 0.98, 0.959], [201.21, 166.15, 0.98, 0.959], [201.8, 170.56, 1, 0.964], [202.18, 172.74, 0.98, 0.965], [202.46, 176.13, 0.98, 0.966], [203.09, 179.85, 0.98, 0.968], [203.28, 183.55, 0.98, 0.961], [203.98, 187.56, 1, 0.963], [204.72, 190.66, 1, 0.967], [204.91, 194.05, 1, 0.954], [204.91, 194.05, 1, 0.954], [203.9, 194.08, 1, 0.956], [202.88, 194.15, 0.98, 0.958], [201.8, 195, 1, 0.958], [200.65, 195.45, 1, 0.961], [199.66, 196.08, 1, 0.957], [198.46, 196.15, 1, 0.953], [197.37, 196.24, 0.98, 0.95], [196.35, 196.3, 0.98, 0.947], [196.35, 196.3, 0.98, 0.947], [192.86, 196.11, 1, 0.951], [190.53, 196.01, 1, 0.951], [187.51, 196, 1, 0.949], [184.08, 194.32, 0.98, 0.952], [180.99, 193.88, 0.98, 0.949], [177.81, 194.62, 1, 0.949], [175.36, 194.61, 1.02, 0.95], [171.67, 193.62, 1, 0.944], [171.67, 193.62, 1, 0.944], [174.59, 193.5, 1.02, 0.952], [177.38, 192.46, 1.02, 0.95], [180.43, 190.38, 1, 0.957], [183.32, 188.01, 0.98, 0.95], [186.05, 187, 0.98, 0.954], [189.53, 187.6, 1.02, 0.95], [192.59, 185.84, 1, 0.945], [195.46, 183.93, 0.98, 0.942], [195.46, 183.93, 0.98, 0.942], [195.23, 182.08, 0.98, 0.958], [194.56, 180.73, 1, 0.963], [194.34, 179.26, 1, 0.974], [193.93, 177.12, 1, 0.982], [193.33, 175.54, 1.02, 0.983], [192.7, 173.2, 1, 0.996], [192.66, 170.55, 1, 0.992], [191.7, 168.3, 1, 1], null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null] }, seat: { sha256: "613dbeeeaec11804e80db0216922c8f62294745514ee6283bdce61a6d784dd5e", frames: [[191.7, 168.3, 1, 1], [187.28, 167.83, 0.98, 0.992], [186.01, 168.11, 0.98, 0.997], [184.36, 167.85, 0.96, 0.997], [182.14, 168.27, 0.96, 0.99], [179.4, 167.97, 0.94, 0.989], [177.05, 167.47, 0.92, 0.991], [174.39, 167.11, 0.9, 0.982], [171.52, 167.84, 0.9, 0.977], [169.74, 168.03, 0.9, 0.978], [166.37, 168.27, 0.9, 0.98], [163.95, 166.65, 0.86, 0.975], [161.1, 167.16, 0.86, 0.973], [158.82, 167.1, 0.86, 0.97], [157.06, 167.85, 0.86, 0.97], [155.39, 167.09, 0.84, 0.966], [152.19, 167.86, 0.84, 0.951], [152.19, 167.86, 0.84, 0.951], [152.24, 167.87, 0.84, 0.952], [152.26, 167.86, 0.84, 0.953], [152.26, 167.85, 0.84, 0.953], [152.27, 167.85, 0.84, 0.953], [152.3, 167.84, 0.84, 0.954], [152.33, 167.83, 0.84, 0.954], [152.69, 166.08, 0.8, 0.956], [152.75, 166.06, 0.8, 0.956], [152.77, 166.04, 0.8, 0.957], [153.16, 166.03, 0.8, 0.957], [153.19, 166.03, 0.8, 0.956], [153.21, 166.02, 0.8, 0.955], [153.23, 166.02, 0.8, 0.955], [153.27, 166.02, 0.8, 0.953], [153.37, 166.03, 0.8, 0.952], [153.28, 166.02, 0.8, 0.952], [153.28, 166.02, 0.8, 0.952], [154.28, 166.02, 0.8, 0.954], [154.49, 166.87, 0.82, 0.951], [155.31, 165.99, 0.8, 0.953], [156.08, 165.96, 0.8, 0.956], [156.67, 166.83, 0.82, 0.958], [157.34, 166.82, 0.82, 0.955], [158.31, 166.73, 0.82, 0.956], [158.83, 166.62, 0.82, 0.96], [159.42, 166.54, 0.82, 0.958], [160.23, 166.48, 0.82, 0.952], [160.85, 166.5, 0.82, 0.954], [161.4, 166.49, 0.82, 0.954], [162.04, 166.45, 0.82, 0.947], [162.98, 166.47, 0.82, 0.947], [163.19, 166.46, 0.82, 0.946], [164.02, 166.47, 0.82, 0.945]] }, arrivalSeat: { sha256: "13acb62fd573ff7ef97bcf59be80757897fe9d1784f187ab278990ff94a643b9", frames: [[195.46, 183.93, 0.98, 0.942], [191.05, 182.87, 0.98, 0.955], [189.92, 182.76, 0.98, 0.95], [187.17, 181.29, 0.96, 0.954], [184.77, 179.89, 0.94, 0.954], [182.36, 179.93, 0.96, 0.955], [179.54, 177.85, 0.94, 0.96], [176.3, 176.4, 0.92, 0.959], [174.08, 175.36, 0.9, 0.966], [171.91, 174.25, 0.9, 0.958], [168.15, 172.23, 0.88, 0.953], [165.17, 170.82, 0.86, 0.963], [162.45, 171.28, 0.88, 0.959], [160.06, 170.01, 0.86, 0.965], [157.6, 169.78, 0.86, 0.959], [155.67, 167.31, 0.82, 0.96], [152.19, 167.86, 0.84, 0.951], [152.19, 167.86, 0.84, 0.951], [152.24, 167.87, 0.84, 0.952], [152.26, 167.86, 0.84, 0.953], [152.26, 167.85, 0.84, 0.953], [152.27, 167.85, 0.84, 0.953], [152.3, 167.84, 0.84, 0.954], [152.33, 167.83, 0.84, 0.954], [152.69, 166.08, 0.8, 0.956], [152.75, 166.06, 0.8, 0.956], [152.77, 166.04, 0.8, 0.957], [153.16, 166.03, 0.8, 0.957], [153.19, 166.03, 0.8, 0.956], [153.21, 166.02, 0.8, 0.955], [153.23, 166.02, 0.8, 0.955], [153.27, 166.02, 0.8, 0.953], [153.37, 166.03, 0.8, 0.952], [153.28, 166.02, 0.8, 0.952], [153.28, 166.02, 0.8, 0.952], [154.28, 166.02, 0.8, 0.954], [154.49, 166.87, 0.82, 0.951], [155.31, 165.99, 0.8, 0.953], [156.08, 165.96, 0.8, 0.956], [156.67, 166.83, 0.82, 0.958], [157.34, 166.82, 0.82, 0.955], [158.31, 166.73, 0.82, 0.956], [158.83, 166.62, 0.82, 0.96], [159.42, 166.54, 0.82, 0.958], [160.23, 166.48, 0.82, 0.952], [160.85, 166.5, 0.82, 0.954], [161.4, 166.49, 0.82, 0.954], [162.04, 166.45, 0.82, 0.947], [162.98, 166.47, 0.82, 0.947], [163.19, 166.46, 0.82, 0.946], [164.02, 166.47, 0.82, 0.945]] } } };
1244
+ function mouthSourceFrame(family, state, info) {
1245
+ if (family === "approach")
1246
+ return state.approachPair * 9 + Math.round(state.blend * 8);
1247
+ if (family === "behaviors") {
1248
+ const p = paperBehaviorPose(state, info);
1249
+ return p.pair * 9 + Math.round(p.blend * 8);
1250
+ }
1251
+ if (family === "seat" || family === "arrivalSeat") {
1252
+ const q = state.seatProgress * 3, pair = Math.min(2, Math.floor(q));
1253
+ return pair * 17 + Math.round((q - pair) * 16);
1254
+ }
1255
+ return null;
1256
+ }
1257
+ function mouthAnchor(family, frame, sha256) {
1258
+ const f = MOUTH_ANCHORS.families[family];
1259
+ if (!f || !Number.isInteger(frame) || sha256 && f.sha256 !== sha256)
1260
+ return null;
1261
+ const a = f.frames[frame];
1262
+ return a ? { x: a[0], y: a[1], scale: a[2], score: a[3] } : null;
1263
+ }
1264
+ function projectMouth(anchor, { left, top, side, sideX }, flip = false, { family = null, frame = null } = {}) {
1265
+ if (!anchor)
1266
+ return null;
1267
+ const sx = flip ? 320 - anchor.x : anchor.x, r = MOUTH_ANCHORS.ballRadiusPx;
1268
+ const m = { x: left + sx / 320 * sideX, y: top + anchor.y / 320 * side, rx: r / 320 * sideX, ry: r / 320 * side, family, frame, flip: !!flip, scale: anchor.scale, score: anchor.score };
1269
+ return [m.x, m.y, m.rx, m.ry].every(Number.isFinite) && m.rx > 0 && m.ry > 0 ? m : null;
1270
+ }
1271
+
1272
+ // puppy-paper-trick-timing.mjs
1273
+ var SNAPPY_TRICK_PROFILES = Object.freeze({
1274
+ spin: Object.freeze({ enter: "spin0", exit: "spin15", seconds: Object.freeze([0.1, 0.72, 0.14]) }),
1275
+ roll: Object.freeze({ enter: "bridge6", exit: "bridge6", seconds: Object.freeze([0.56, 0.86, 0.62]) }),
1276
+ "roll-down": Object.freeze({ enter: "bridge6", exit: "bridge6", seconds: Object.freeze([0.32, 0.86, 0.62]) })
1277
+ });
1278
+ var TRICK_PHASES = Object.freeze(["windup", "active", "settle"]);
1279
+ var fail2 = (m) => {
1280
+ throw Error(`Invalid trick timing: ${m}`);
1281
+ };
1282
+ function trickClipEdges(name, info) {
1283
+ validatePaperActionInfo(info);
1284
+ const list = info.clips[name], weights = info.weights?.[name] ?? list.map(() => 1), { keyOrder, edges } = info;
1285
+ if (!Array.isArray(keyOrder) || !Array.isArray(edges) || edges.length < info.pairs)
1286
+ fail2("missing keyOrder/edges");
1287
+ const keys = list.map((p) => {
1288
+ const e = edges[p];
1289
+ if (!Array.isArray(e) || e.length !== 2 || !e.every((k) => Number.isInteger(k) && typeof keyOrder[k] === "string"))
1290
+ fail2(`edge ${p}`);
1291
+ return [keyOrder[e[0]], keyOrder[e[1]]];
1292
+ });
1293
+ for (let i = 1;i < keys.length; i++)
1294
+ if (edges[list[i - 1]][1] !== edges[list[i]][0])
1295
+ fail2(`${name} chain breaks at edge ${i}`);
1296
+ const total = weights.reduce((a, b) => a + b, 0), bounds = [0];
1297
+ let sum = 0;
1298
+ for (let i = 0;i < weights.length - 1; i++) {
1299
+ sum += weights[i];
1300
+ bounds.push(sum / total);
1301
+ }
1302
+ bounds.push(1);
1303
+ return { list, keys, bounds };
1304
+ }
1305
+ function createTrickTiming(name, info, profile = SNAPPY_TRICK_PROFILES[name]) {
1306
+ if (!profile || typeof profile.enter !== "string" || typeof profile.exit !== "string")
1307
+ fail2(`profile for ${name}`);
1308
+ const s2 = profile.seconds;
1309
+ if (!Array.isArray(s2) || s2.length !== 3 || !s2.every((v) => typeof v === "number" && Number.isFinite(v) && v > 0))
1310
+ fail2("seconds");
1311
+ const { list, keys, bounds } = trickClipEdges(name, info);
1312
+ const first = keys.findIndex((k) => k[1] === profile.enter), last = keys.findLastIndex((k) => k[0] === profile.exit);
1313
+ if (first < 0)
1314
+ fail2(`no edge arrives at ${profile.enter}`);
1315
+ if (last < 0)
1316
+ fail2(`no edge leaves ${profile.exit}`);
1317
+ const ranges = [[0, first], [first + 1, last - 1], [last, list.length - 1]];
1318
+ if (!ranges.every(([a, b]) => a <= b))
1319
+ fail2(`${name} phases need at least one edge each`);
1320
+ const T = [0, s2[0], s2[0] + s2[1], s2[0] + s2[1] + s2[2]], P = [0, bounds[first + 1], bounds[last], 1];
1321
+ const d = [0, 1, 2].map((i) => (P[i + 1] - P[i]) / (T[i + 1] - T[i])), m = [0, 2 * d[0] * d[1] / (d[0] + d[1]), 2 * d[1] * d[2] / (d[1] + d[2]), 0];
1322
+ const duration = T[3];
1323
+ const progressAt = (age) => {
1324
+ if (typeof age !== "number" || !Number.isFinite(age))
1325
+ fail2("age");
1326
+ if (age <= 0)
1327
+ return 0;
1328
+ if (age >= duration)
1329
+ return 1;
1330
+ const i = age < T[1] ? 0 : age < T[2] ? 1 : 2, h = T[i + 1] - T[i], u = (age - T[i]) / h, u2 = u * u, u3 = u2 * u;
1331
+ const p = (2 * u3 - 3 * u2 + 1) * P[i] + (u3 - 2 * u2 + u) * h * m[i] + (-2 * u3 + 3 * u2) * P[i + 1] + (u3 - u2) * h * m[i + 1];
1332
+ return Math.max(P[i], Math.min(P[i + 1], p));
1333
+ };
1334
+ const phases = Object.freeze(TRICK_PHASES.map((phase, i) => Object.freeze({
1335
+ phase,
1336
+ start: T[i],
1337
+ end: T[i + 1],
1338
+ progressStart: P[i],
1339
+ progressEnd: P[i + 1],
1340
+ firstEdge: ranges[i][0],
1341
+ lastEdge: ranges[i][1],
1342
+ fromKey: keys[ranges[i][0]][0],
1343
+ toKey: keys[ranges[i][1]][1]
1344
+ })));
1345
+ return Object.freeze({ name, duration, phases, progressAt });
1346
+ }
1347
+ function createSnappyTrickTimings(info, profiles = SNAPPY_TRICK_PROFILES) {
1348
+ return Object.freeze(Object.fromEntries(Object.keys(profiles).filter((n) => Object.prototype.hasOwnProperty.call(info?.clips ?? {}, n)).map((n) => [n, createTrickTiming(n, info, profiles[n])])));
1349
+ }
1350
+
1351
+ // puppy-paper.mjs
1352
+ var PAPER_COMFORT_LIMITS = Object.freeze({ belly: 30, sleep: 120, tug: 30 });
1353
+ var PAPER_COMFORT_TIMINGS = Object.freeze({ belly: Object.freeze({ key: "roll7", hold: 0.4449823183425605, enter: 1.3, exit: 1.7 }), tugExit: 0.35 });
1354
+ function createPaperComfortTimings(actionInfo, { key = PAPER_COMFORT_TIMINGS.belly.key } = {}) {
1355
+ const { keys, bounds } = trickClipEdges("roll-down", actionInfo), i = keys.findIndex((k) => k[0] === key);
1356
+ if (i <= 0)
1357
+ throw Error(`Invalid comfort timing: no roll-down edge leaves ${key}`);
1358
+ return Object.freeze({ ...PAPER_COMFORT_TIMINGS, belly: Object.freeze({ ...PAPER_COMFORT_TIMINGS.belly, key, hold: bounds[i] }) });
1359
+ }
1360
+ var comfortDuration = (name, seconds) => {
1361
+ if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds <= 0 || seconds > PAPER_COMFORT_LIMITS[name])
1362
+ throw Error(`Invalid ${name} duration`);
1363
+ };
1364
+ var comfortEase = (u) => u <= 0 ? 0 : u >= 1 ? 1 : u * u * (3 - 2 * u);
1365
+ var PAPER_WATCH_TIMING = Object.freeze({ turn: 0.6, hold: 0.65, back: 0.65, total: 1.9 });
1366
+ function paperWatchProgress(age) {
1367
+ const { turn, hold, back } = PAPER_WATCH_TIMING;
1368
+ return age < turn ? 0.5 * comfortEase(age / turn) : age < turn + hold ? 0.5 : 0.5 + 0.5 * comfortEase((age - turn - hold) / back);
1369
+ }
1370
+ function createPaperActor(animal, info, { manualTricks = false, trickTimings = null, walkTransitions = false, comfortTimings = PAPER_COMFORT_TIMINGS, scrambleSeconds = 0, fetchMotions = null, motionTables = null } = {}) {
1371
+ if (!Number.isFinite(scrambleSeconds) || scrambleSeconds < 0 || scrambleSeconds > 3)
1372
+ throw Error("Invalid scramble duration");
1373
+ if (fetchMotions && (!Number.isFinite(fetchMotions.scramble?.entryPhase) || fetchMotions.scramble.entryPhase <= 0 || fetchMotions.scramble.entryPhase >= 1 || !Number.isFinite(fetchMotions.scramble.brakeSeconds) || fetchMotions.scramble.brakeSeconds <= 0 || fetchMotions.scramble.brakeSeconds > scrambleSeconds || !["front", "rear"].every((v) => Number.isFinite(fetchMotions.pickups?.[v]?.duration) && fetchMotions.pickups[v].duration > 0 && fetchMotions.pickups[v].duration <= 3)))
1374
+ throw Error("Invalid fetch actor metadata");
1375
+ if (fetchMotions?.scramble.trajectory === "single-pass-target") {
1376
+ if (fetchMotions.scramble.entryView !== "rear" || fetchMotions.scramble.duration !== scrambleSeconds)
1377
+ throw Error("Invalid single-pass view/duration");
1378
+ planPaperStumble(1, fetchMotions.scramble);
1379
+ }
1380
+ const recovery = fetchMotions?.scramble?.recovery;
1381
+ if (recovery && (![recovery.start, recovery.end, recovery.distance, recovery.height].every(Number.isFinite) || recovery.start < fetchMotions.scramble.brakeSeconds || recovery.end <= recovery.start || recovery.end > scrambleSeconds || recovery.distance < 0 || recovery.distance > 0.2 || recovery.height < 0 || recovery.height > 0.1))
1382
+ throw Error("Invalid fetch recovery metadata");
1383
+ const bellyTiming = comfortTimings?.belly;
1384
+ 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)
1385
+ throw Error("Invalid comfort timing");
1386
+ 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;
1387
+ let held = null, waypoints = [];
1388
+ let preparationRate = 1;
1389
+ let scrambleArmed = false, movingScramble = null, carryMode = null;
1390
+ let arrived = false;
1391
+ let straight = false, seatProgress = 0, seatVariant = "default", travel = null, approachPlan = null, travelAge = 0, travelDistance = 0, motionSpeed = 0, motionStage = "rest", completed = false;
1392
+ let watchSide = "left", restProgress = 0, idleHold = 2.5;
1393
+ let pendingAction = null, activeAction = null, actionDone = null, deferred = null, movementMode = "run", stayDown = false, manualDown = false, spinReady = false;
1394
+ let comfort = null, comfortSeconds = 0, sleepLook = null, nextSleepLook = { variant: "side", right: false };
1395
+ const director = createPaperDirector();
1396
+ const points = [[0.12, PAPER_GROUND.away]];
1397
+ const enter = (s2) => {
1398
+ state = s2;
1399
+ age = 0;
1400
+ if (s2 === "idle")
1401
+ preparationRate = 1;
1402
+ };
1403
+ const nextRoute = () => {
1404
+ const choice = director.choose({ x: animal.position.x, z: animal.position.z, seated: view === "approach" && seatProgress === 1 });
1405
+ idleHold = choice.holdSeconds;
1406
+ if (choice.kind === "watch") {
1407
+ watchSide = choice.side;
1408
+ enter("watch");
1409
+ } else if (choice.kind === "rest")
1410
+ enter("recline");
1411
+ else if (choice.kind === "come")
1412
+ go(animal.position.x, 0.2, { direct: true });
1413
+ else
1414
+ go(...choice.target);
1415
+ };
1416
+ const settle = () => {
1417
+ if (view === "approach" && completed)
1418
+ seatVariant = "arrival";
1419
+ enter(view === "approach" ? "sit-down" : "settle");
1420
+ };
1421
+ const stop = () => {
1422
+ animal.follow({ x: animal.position.x, y: 0, z: animal.position.z }, { x: 0, y: 0, z: 0 }, { heading: 0 });
1423
+ motionSpeed = 0;
1424
+ };
1425
+ const snappyPreparation = () => !!trickTimings?.[pendingAction];
1426
+ const viewTurnSeconds = () => snappyPreparation() ? 0.24 : walkTransitions && movementMode === "walk" ? 0.4 : 1;
1427
+ const turnSeconds = () => snappyPreparation() ? 0.22 : 0.85;
1428
+ const launchSeconds = () => snappyPreparation() ? 0.1 : 0.25;
1429
+ const dispatch = (fn, { keepDown = false, urgent = false } = {}) => {
1430
+ if (urgent)
1431
+ preparationRate = 3;
1432
+ if (activeAction || state === "tug" || state === "attention-paw" || state === "attention-wall" || state === "scramble" || state === "pickup" || movementMode === "walk" && state === "run") {
1433
+ route = false;
1434
+ deferred = () => dispatch(fn, { keepDown, urgent });
1435
+ hurryComfort();
1436
+ return true;
1437
+ }
1438
+ if (manualDown && !keepDown) {
1439
+ route = false;
1440
+ deferred = fn;
1441
+ playAction("lie-up", () => {
1442
+ manualDown = false;
1443
+ view = "approach";
1444
+ seatProgress = 1;
1445
+ seatVariant = "default";
1446
+ enter("idle");
1447
+ drain();
1448
+ }, 0.55);
1449
+ return true;
1450
+ }
1451
+ return fn();
1452
+ };
1453
+ const drain = () => {
1454
+ const fn = deferred;
1455
+ deferred = null;
1456
+ if (fn) {
1457
+ fn();
1458
+ return true;
1459
+ }
1460
+ return false;
1461
+ };
1462
+ const bellyAction = (seconds) => {
1463
+ const { hold: H, enter: a, exit: b } = bellyTiming, t = { comfort: "belly", hold: [a, a + seconds], duration: a + seconds + b };
1464
+ t.progressAt = (age2) => {
1465
+ if (!Number.isFinite(age2))
1466
+ throw Error("Invalid comfort age");
1467
+ return age2 < t.hold[0] ? H * comfortEase(age2 / a) : age2 < t.hold[1] ? H : H + (1 - H) * comfortEase((age2 - t.hold[1]) / b);
1468
+ };
1469
+ return t;
1470
+ };
1471
+ const startBelly = (seconds) => {
1472
+ manualDown = false;
1473
+ stayDown = false;
1474
+ restProgress = 0;
1475
+ comfort = null;
1476
+ const timing = bellyAction(seconds);
1477
+ activeAction = { name: "roll-down", duration: timing.duration, timing, comfort: "belly" };
1478
+ actionDone = () => finishTrick("roll");
1479
+ stop();
1480
+ enter("action");
1481
+ if (deferred)
1482
+ hurryComfort();
1483
+ };
1484
+ const tugProgress = () => {
1485
+ if (!comfort || comfort.kind !== "tug")
1486
+ return 0;
1487
+ const h = comfort.hurry;
1488
+ 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);
1489
+ };
1490
+ function hurryComfort() {
1491
+ const t = activeAction?.timing;
1492
+ if (t?.comfort === "belly" && age < t.hold[1]) {
1493
+ t.hold[1] = Math.max(t.hold[0], age);
1494
+ t.duration = activeAction.duration = t.hold[1] + bellyTiming.exit;
1495
+ } else if (state === "tug" && comfort && !comfort.hurry) {
1496
+ const progress = tugProgress();
1497
+ comfort.hurry = { progress, age, length: Math.min(comfortTimings.tugExit, (1 - progress) * comfort.seconds) };
1498
+ } else if ((state === "attention-paw" || state === "attention-wall") && comfort && !comfort.hurry) {
1499
+ const motion = state === "attention-wall" ? fetchMotions.attentionWall : fetchMotions.attentionPaw, progress = held?.comfortPose?.clip === state ? held.comfortPose.progress : Math.min(1, age / comfort.seconds), from = motion.frames[comfortMotionKey(progress, motion)];
1500
+ comfort.hurry = { from, age, length: motion.releases[from].duration };
1501
+ }
1502
+ }
1503
+ const playAction = (name, done, duration = PAPER_ACTION_DURATIONS[name]) => {
1504
+ const timing = trickTimings?.[name];
1505
+ duration = timing?.duration ?? duration;
1506
+ if (!Number.isFinite(duration) || duration <= 0)
1507
+ throw Error("Invalid paper action duration");
1508
+ activeAction = { name, duration, timing };
1509
+ actionDone = done;
1510
+ stop();
1511
+ enter("action");
1512
+ };
1513
+ const finishTrick = (name) => {
1514
+ if (name !== "spin") {
1515
+ view = "approach";
1516
+ flip = false;
1517
+ seatProgress = 1;
1518
+ seatVariant = "default";
1519
+ enter("idle");
1520
+ } else {
1521
+ view = "front";
1522
+ flip = false;
1523
+ seatProgress = 0;
1524
+ enter("settle");
1525
+ }
1526
+ drain();
1527
+ };
1528
+ const issue = (skipEntry = false) => {
1529
+ if (skipEntry && deferred) {
1530
+ drain();
1531
+ return;
1532
+ }
1533
+ if (movementMode === "walk" && !skipEntry) {
1534
+ playAction("walk-enter", () => issue(true));
1535
+ return;
1536
+ }
1537
+ arrived = false;
1538
+ completed = false;
1539
+ travelAge = 0;
1540
+ travelDistance = 0;
1541
+ const x = animal.position.x, z = animal.position.z;
1542
+ if (straight && view === "approach")
1543
+ target[0] = x;
1544
+ const dx = target[0] - x, dz2 = target[1] - z, distance = Math.hypot(dx, dz2), run2 = movementMode === "walk" ? planPaperWalk(distance) : planPaperRun(distance);
1545
+ phase = Math.ceil(phase);
1546
+ travel = { x, z, dx, dz: dz2, heading: Math.atan2(dx, dz2), stride: view === "approach" ? PAPER_GAITS.approach.stride : run2.stride, plan: view === "approach" ? planPaperApproachTravel(distance) : run2.plan };
1547
+ approachPlan = view === "approach" ? planPaperApproach(distance) : null;
1548
+ stop();
1549
+ enter("run");
1550
+ if (["trot", "run-trot"].includes(carryMode) && fetchMotions?.carryTrot && view === "approach" && distance > 0) {
1551
+ const carry = planPaperCarry(distance, { mixed: carryMode === "run-trot", settleSeconds: fetchMotions.carryTrot.settle.duration });
1552
+ Object.assign(travel, { plan: carry, carry, stride: carry.stride });
1553
+ approachPlan = null;
1554
+ }
1555
+ if (scrambleArmed && fetchMotions?.scramble.trajectory === "single-pass-target" && view === fetchMotions.scramble.entryView && !waypoints.length) {
1556
+ const stumble = planPaperStumble(distance, fetchMotions.scramble);
1557
+ if (motionTables && !canJoinMotion(motionTables, "rear-run/scramble", { view, phase: stumble.entryPhase, speed: stumble.entrySpeed, nextSpeed: stumble.impactAt(0).speed }))
1558
+ throw Error("Unsafe baked run/impact connection");
1559
+ Object.assign(travel, { plan: stumble.plan, stride: stumble.stride, stumble, scrambleAt: stumble.entryTime });
1560
+ } else if (scrambleArmed && fetchMotions && view === "front" && !waypoints.length && fetchMotions.scramble.trajectory !== "single-pass-target") {
1561
+ const contact = fetchMotions.scramble.entryPhase, cycle = Math.max(0, Math.floor(distance / travel.stride - contact - 0.15)), d = (cycle + contact) * travel.stride;
1562
+ let lo = 0, hi = travel.plan.duration;
1563
+ for (let i = 0;i < 40; i++) {
1564
+ const mid = (lo + hi) / 2;
1565
+ if (travel.plan.at(mid).distance < d)
1566
+ lo = mid;
1567
+ else
1568
+ hi = mid;
1569
+ }
1570
+ travel.scrambleAt = (lo + hi) / 2;
1571
+ }
1572
+ };
1573
+ const prepareAction = () => {
1574
+ const name = pendingAction, desired = name === "spin" ? "front" : "approach";
1575
+ if (restProgress > 0) {
1576
+ enter("wake");
1577
+ return;
1578
+ }
1579
+ if (motionTables) {
1580
+ const [step, target2] = preparationRoute(motionTables, "action", { view, flip, seatProgress, desired: name === "spin" ? "stand" : "seat", speed: motionSpeed });
1581
+ if (step !== "ready") {
1582
+ if (step === "view-turn")
1583
+ turnView = target2;
1584
+ else if (step === "turn")
1585
+ turnFlip = target2;
1586
+ enter(step);
1587
+ return;
1588
+ }
1589
+ } else {
1590
+ if (name === "spin" && seatProgress > 0) {
1591
+ enter("rise");
1592
+ return;
1593
+ }
1594
+ if (flip && view !== "front") {
1595
+ turnView = "front";
1596
+ enter("view-turn");
1597
+ return;
1598
+ }
1599
+ if (flip) {
1600
+ turnFlip = false;
1601
+ enter("turn");
1602
+ return;
1603
+ }
1604
+ if (view !== desired) {
1605
+ turnView = view === "front" ? desired : "front";
1606
+ enter("view-turn");
1607
+ return;
1608
+ }
1609
+ if (name !== "spin" && seatProgress < 1) {
1610
+ enter("sit-down");
1611
+ return;
1612
+ }
1613
+ }
1614
+ if (walkTransitions && name === "spin" && !spinReady) {
1615
+ spinReady = true;
1616
+ enter("launch");
1617
+ return;
1618
+ }
1619
+ pendingAction = null;
1620
+ if (name === "sit") {
1621
+ enter("idle");
1622
+ return;
1623
+ }
1624
+ if (name === "belly") {
1625
+ const seconds = comfortSeconds;
1626
+ playAction("lie-down", () => startBelly(seconds), 0.65);
1627
+ return;
1628
+ }
1629
+ if (name === "sleep") {
1630
+ stayDown = false;
1631
+ sleepLook = { ...nextSleepLook };
1632
+ comfort = { kind: "sleep", seconds: comfortSeconds };
1633
+ enter("recline");
1634
+ return;
1635
+ }
1636
+ if (name === "tug") {
1637
+ stop();
1638
+ comfort = { kind: "tug", seconds: comfortSeconds };
1639
+ enter("tug");
1640
+ return;
1641
+ }
1642
+ if (name === "attention-paw" || name === "attention-wall") {
1643
+ stop();
1644
+ held = null;
1645
+ comfort = { kind: name, seconds: (name === "attention-wall" ? fetchMotions.attentionWall : fetchMotions.attentionPaw).duration };
1646
+ enter(name);
1647
+ return;
1648
+ }
1649
+ if (name === "lie") {
1650
+ if (manualTricks) {
1651
+ playAction("lie-down", () => {
1652
+ manualDown = true;
1653
+ enter("down");
1654
+ drain();
1655
+ }, 0.65);
1656
+ } else {
1657
+ stayDown = true;
1658
+ enter("recline");
1659
+ }
1660
+ return;
1661
+ }
1662
+ playAction(name, () => finishTrick(name));
1663
+ };
1664
+ const prepareDirection = () => {
1665
+ if (pendingAction) {
1666
+ prepareAction();
1667
+ return;
1668
+ }
1669
+ if (!target) {
1670
+ settle();
1671
+ return;
1672
+ }
1673
+ if (motionTables) {
1674
+ const [step, targetView] = preparationRoute(motionTables, "direction", { view, flip, desired: nextView, nextFlip, speed: motionSpeed });
1675
+ if (step === "view-turn") {
1676
+ turnView = targetView;
1677
+ enter(step);
1678
+ } else if (step === "turn") {
1679
+ turnFlip = targetView;
1680
+ enter(step);
1681
+ } else
1682
+ issue();
1683
+ } else if (nextView !== view) {
1684
+ turnView = view === "front" ? nextView : "front";
1685
+ enter("view-turn");
1686
+ } else if (view === "front" && nextFlip !== flip) {
1687
+ turnFlip = nextFlip;
1688
+ enter("turn");
1689
+ } else
1690
+ issue();
1691
+ };
1692
+ const requestAction = (name, { urgent = false } = {}) => {
1693
+ preparationRate = urgent ? 3 : 1;
1694
+ if (name === "roll" && manualDown && state === "down") {
1695
+ route = false;
1696
+ target = null;
1697
+ waypoints = [];
1698
+ pendingAction = null;
1699
+ stayDown = false;
1700
+ manualDown = false;
1701
+ restProgress = 0;
1702
+ playAction("roll-down", () => finishTrick("roll"));
1703
+ return true;
1704
+ }
1705
+ if (name === "roll" && state === "down" && restProgress === 0.25) {
1706
+ route = false;
1707
+ target = null;
1708
+ waypoints = [];
1709
+ pendingAction = null;
1710
+ stayDown = false;
1711
+ restProgress = 0;
1712
+ playAction("roll-down", () => finishTrick("roll"));
1713
+ return true;
1714
+ }
1715
+ if (name === "belly" && state === "down" && (manualDown || restProgress === 0.25)) {
1716
+ route = false;
1717
+ target = null;
1718
+ waypoints = [];
1719
+ pendingAction = null;
1720
+ startBelly(comfortSeconds);
1721
+ return true;
1722
+ }
1723
+ if (name === "sleep" && state === "sleep" && comfort?.kind === "sleep") {
1724
+ comfort.seconds = age + comfortSeconds;
1725
+ return true;
1726
+ }
1727
+ comfort = null;
1728
+ route = false;
1729
+ target = null;
1730
+ waypoints = [];
1731
+ pendingAction = name;
1732
+ spinReady = false;
1733
+ completed = false;
1734
+ stayDown = false;
1735
+ movementMode = "run";
1736
+ stop();
1737
+ if (restProgress > 0 || state === "recline" || state === "wake")
1738
+ enter("wake");
1739
+ else if (["watch", "view-turn", "turn", "arrival-exit"].includes(state)) {} else if (["run", "finish", "launch", "settle"].includes(state))
1740
+ enter("finish");
1741
+ else if (state === "idle" && seatProgress === 0 && view === "front" && !walkTransitions)
1742
+ enter("launch");
1743
+ else
1744
+ prepareDirection();
1745
+ return true;
1746
+ };
1747
+ const faceTarget = (direct) => {
1748
+ nextFlip = target[0] < animal.position.x;
1749
+ const dz2 = target[1] - animal.position.z, dx = target[0] - animal.position.x;
1750
+ straight = direct;
1751
+ nextView = direct ? "approach" : Math.abs(dz2) > 0.16 && Math.abs(dz2) > Math.abs(dx) * 0.7 ? dz2 < 0 ? "rear" : "approach" : "front";
1752
+ };
1753
+ function go(x, z, { direct = false, walking = false, urgent = false, scramble = false, returnMode = null } = {}) {
1754
+ if (![x, z].every(Number.isFinite))
1755
+ throw Error("Invalid paper target");
1756
+ preparationRate = urgent ? 3 : 1;
1757
+ carryMode = returnMode;
1758
+ scrambleArmed = !!(scramble && fetchMotions);
1759
+ movingScramble = null;
1760
+ target = paperGroundTarget(x, z);
1761
+ pendingAction = null;
1762
+ stayDown = false;
1763
+ comfort = null;
1764
+ movementMode = walking ? "walk" : "run";
1765
+ const dz2 = target[1] - animal.position.z, dx = target[0] - animal.position.x;
1766
+ waypoints = [];
1767
+ if (!direct && Math.abs(dx) > 0.1 && Math.abs(dz2) > 0.16) {
1768
+ waypoints = [target];
1769
+ target = dz2 < 0 ? [animal.position.x, target[1]] : [target[0], animal.position.z];
1770
+ }
1771
+ faceTarget(direct);
1772
+ completed = false;
1773
+ stop();
1774
+ if (restProgress > 0 || state === "recline" || state === "wake")
1775
+ enter("wake");
1776
+ else if (state === "watch") {} else if (state === "arrival-exit") {} else if (seatProgress > 0 || state === "sit-down" || state === "rise")
1777
+ enter("rise");
1778
+ else if (state !== "view-turn" && state !== "turn") {
1779
+ if (walkTransitions && state === "idle")
1780
+ prepareDirection();
1781
+ else
1782
+ enter(state === "idle" ? "launch" : "finish");
1783
+ }
1784
+ }
1785
+ return {
1786
+ attend() {
1787
+ if (state !== "run" || !travel || !target || motionSpeed < 0.00000001)
1788
+ return dispatch(() => requestAction("sit", { urgent: true }), { urgent: true });
1789
+ const n = Math.hypot(travel.dx, travel.dz), remaining = Math.max(0, travel.plan.distance - (travelDistance - (travel.distanceBase ?? 0)));
1790
+ const a = movementMode === "walk" ? PAPER_DYNAMICS.walkDeceleration : PAPER_DYNAMICS.deceleration;
1791
+ const distance = Math.min(remaining, motionSpeed ** 2 / (2 * a));
1792
+ if (!n || distance < 0.0000000001)
1793
+ return dispatch(() => requestAction("sit", { urgent: true }), { urgent: true });
1794
+ const x = animal.position.x, z = animal.position.z, dx = travel.dx / n * distance, dz2 = travel.dz / n * distance;
1795
+ const plan = planPaperTravel(distance, { speed: motionSpeed, startSpeed: motionSpeed, deceleration: motionSpeed ** 2 / (2 * distance) });
1796
+ travel = { ...travel, x, z, dx, dz: dz2, plan, distanceBase: travelDistance, attending: true };
1797
+ travelAge = 0;
1798
+ target = [x + dx, z + dz2];
1799
+ waypoints = [];
1800
+ route = false;
1801
+ deferred = null;
1802
+ pendingAction = "sit";
1803
+ preparationRate = 3;
1804
+ scrambleArmed = false;
1805
+ completed = false;
1806
+ return true;
1807
+ },
1808
+ go(x, z) {
1809
+ if (![x, z].every(Number.isFinite))
1810
+ throw Error("Invalid paper target");
1811
+ return dispatch(() => {
1812
+ route = false;
1813
+ go(x, z);
1814
+ });
1815
+ },
1816
+ chase(x, z, { scramble = false, returnMode = null } = {}) {
1817
+ if (![x, z].every(Number.isFinite) || ![null, "brake", "trot", "run-trot"].includes(returnMode))
1818
+ throw Error("Invalid paper target/return mode");
1819
+ return dispatch(() => {
1820
+ route = false;
1821
+ go(x, z, { urgent: true, scramble, returnMode });
1822
+ }, { urgent: true });
1823
+ },
1824
+ come() {
1825
+ return dispatch(() => {
1826
+ route = false;
1827
+ if (state === "idle" && seatProgress === 1 && Math.abs(animal.position.z - 0.2) < 0.002)
1828
+ return;
1829
+ go(animal.position.x, 0.2, { direct: true });
1830
+ });
1831
+ },
1832
+ roam() {
1833
+ return dispatch(() => {
1834
+ route = true;
1835
+ routeIndex = 0;
1836
+ go(...points[routeIndex]);
1837
+ });
1838
+ },
1839
+ sit({ urgent = false } = {}) {
1840
+ return dispatch(() => requestAction("sit", { urgent }), { urgent });
1841
+ },
1842
+ spin() {
1843
+ return dispatch(() => requestAction("spin"));
1844
+ },
1845
+ roll() {
1846
+ return dispatch(() => requestAction("roll"), { keepDown: true });
1847
+ },
1848
+ walk(side, distance = 1.05) {
1849
+ if (!["left", "right"].includes(side) || !Number.isFinite(distance) || distance <= 0)
1850
+ throw Error("Invalid walk command");
1851
+ return dispatch(() => {
1852
+ const [x, z] = paperWalkTarget(animal.position.x, animal.position.z, side === "left" ? -1 : 1, distance);
1853
+ if (Math.abs(x - animal.position.x) < 0.000001)
1854
+ return false;
1855
+ route = false;
1856
+ go(x, z, { walking: true });
1857
+ return true;
1858
+ });
1859
+ },
1860
+ look(side = "left") {
1861
+ if (state !== "idle" || seatProgress !== 1)
1862
+ return false;
1863
+ route = false;
1864
+ watchSide = side === "right" ? "right" : "left";
1865
+ enter("watch");
1866
+ return true;
1867
+ },
1868
+ lie() {
1869
+ const name = manualDown || activeAction?.name === "lie-down" || restProgress > 0 ? "sit" : "lie";
1870
+ return dispatch(() => requestAction(name));
1871
+ },
1872
+ belly(seconds = 6) {
1873
+ comfortDuration("belly", seconds);
1874
+ return dispatch(() => {
1875
+ comfortSeconds = seconds;
1876
+ return requestAction("belly");
1877
+ }, { keepDown: true });
1878
+ },
1879
+ sleep(seconds = 25, variant = "side", right = false) {
1880
+ comfortDuration("sleep", seconds);
1881
+ if (!["side", "belly", "curl", "chin"].includes(variant) || typeof right !== "boolean")
1882
+ throw Error("Invalid sleep pose");
1883
+ return dispatch(() => {
1884
+ comfortSeconds = seconds;
1885
+ nextSleepLook = { variant, right };
1886
+ return requestAction("sleep");
1887
+ });
1888
+ },
1889
+ tug(seconds = 6) {
1890
+ comfortDuration("tug", seconds);
1891
+ return dispatch(() => {
1892
+ comfortSeconds = seconds;
1893
+ return requestAction("tug");
1894
+ });
1895
+ },
1896
+ attentionPaw() {
1897
+ if (!fetchMotions?.attentionPaw)
1898
+ return false;
1899
+ return dispatch(() => requestAction("attention-paw"));
1900
+ },
1901
+ get canAttentionPaw() {
1902
+ return !!fetchMotions?.attentionPaw;
1903
+ },
1904
+ attentionWall() {
1905
+ if (!fetchMotions?.attentionWall)
1906
+ return false;
1907
+ return dispatch(() => requestAction("attention-wall"));
1908
+ },
1909
+ get canAttentionWall() {
1910
+ return !!fetchMotions?.attentionWall;
1911
+ },
1912
+ get canScramble() {
1913
+ return scrambleSeconds > 0;
1914
+ },
1915
+ get canMovingScramble() {
1916
+ return !!fetchMotions;
1917
+ },
1918
+ get canSinglePassScramble() {
1919
+ return fetchMotions?.scramble.trajectory === "single-pass-target";
1920
+ },
1921
+ get canPickup() {
1922
+ return !!fetchMotions;
1923
+ },
1924
+ get canCarryTrot() {
1925
+ return !!fetchMotions?.carryTrot;
1926
+ },
1927
+ pickup() {
1928
+ if (!fetchMotions || state !== "idle" || seatProgress > 0 || !["rear", "front"].includes(view))
1929
+ return false;
1930
+ route = false;
1931
+ target = null;
1932
+ waypoints = [];
1933
+ pendingAction = null;
1934
+ stop();
1935
+ comfort = { kind: "pickup", seconds: fetchMotions.pickups[view].duration };
1936
+ enter("pickup");
1937
+ return true;
1938
+ },
1939
+ scramble() {
1940
+ if (!scrambleSeconds || state !== "idle" || view !== "front" || seatProgress > 0)
1941
+ return false;
1942
+ route = false;
1943
+ target = null;
1944
+ waypoints = [];
1945
+ pendingAction = null;
1946
+ stop();
1947
+ comfort = { kind: "scramble", seconds: scrambleSeconds };
1948
+ enter("scramble");
1949
+ return true;
1950
+ },
1951
+ rest() {
1952
+ return dispatch(() => {
1953
+ route = false;
1954
+ target = null;
1955
+ waypoints = [];
1956
+ pendingAction = null;
1957
+ stayDown = false;
1958
+ comfort = null;
1959
+ stop();
1960
+ if (restProgress > 0) {
1961
+ enter("wake");
1962
+ } else if (state === "watch" || state === "arrival-exit") {} else if (seatProgress > 0 || state === "rise" || state === "sit-down")
1963
+ enter("sit-down");
1964
+ else if (!["idle", "view-turn", "turn"].includes(state))
1965
+ enter("finish");
1966
+ });
1967
+ },
1968
+ update(dt) {
1969
+ if (!Number.isFinite(dt) || dt < 0 || dt > 0.1)
1970
+ throw Error("Invalid paper step");
1971
+ const beforeX = animal.position.x, beforeZ = animal.position.z;
1972
+ let enteredScramble = false;
1973
+ if (state === "run" && travel && target) {
1974
+ travelAge += dt;
1975
+ const p = travel.plan.at(travelAge), ratio = travel.plan.distance ? p.distance / travel.plan.distance : 1;
1976
+ travelDistance = (travel.distanceBase ?? 0) + p.distance;
1977
+ motionSpeed = p.speed;
1978
+ motionStage = p.stage;
1979
+ const gain = travel.plan.distance ? p.speed / travel.plan.distance : 0;
1980
+ animal.follow({ x: travel.x + travel.dx * ratio, y: 0, z: travel.z + travel.dz * ratio }, { x: travel.dx * gain, y: 0, z: travel.dz * gain }, { heading: travel.heading });
1981
+ arrived = p.done;
1982
+ if (scrambleArmed && travel.scrambleAt !== undefined && travelAge >= travel.scrambleAt) {
1983
+ const entry = travel.plan.at(travel.scrambleAt), ratio2 = entry.distance / travel.plan.distance, n = travel.plan.distance;
1984
+ const x = travel.x + travel.dx * ratio2, z = travel.z + travel.dz * ratio2, ux = travel.dx / n, uz = travel.dz / n;
1985
+ const end = paperGroundTarget(x + ux * entry.speed * fetchMotions.scramble.brakeSeconds / 2, z + uz * entry.speed * fetchMotions.scramble.brakeSeconds / 2);
1986
+ const distance2 = Math.hypot(end[0] - x, end[1] - z), brake = entry.speed ? 2 * distance2 / entry.speed : 0;
1987
+ const fullEnd = paperGroundTarget(end[0] + ux * (recovery?.distance ?? 0), end[1] + uz * (recovery?.distance ?? 0));
1988
+ movingScramble = { x, z, ux, uz, speed: entry.speed, brake, distance: distance2, travelBase: entry.distance, hop: Math.hypot(fullEnd[0] - end[0], fullEnd[1] - end[1]), profile: travel.stumble ?? null };
1989
+ scrambleArmed = false;
1990
+ target = null;
1991
+ waypoints = [];
1992
+ comfort = { kind: "scramble", seconds: scrambleSeconds };
1993
+ enter("scramble");
1994
+ age = travelAge - travel.scrambleAt;
1995
+ enteredScramble = true;
1996
+ }
1997
+ }
1998
+ if (state === "scramble" && movingScramble) {
1999
+ const m = movingScramble, time = age + (enteredScramble ? 0 : dt), t = Math.min(m.brake, time), p = recovery ? Math.max(0, Math.min(1, (time - recovery.start) / (recovery.end - recovery.start))) : 0;
2000
+ const hop = m.hop * (1 - Math.cos(Math.PI * p)) / 2, hopSpeed = recovery && p > 0 && p < 1 ? m.hop * Math.PI / (2 * (recovery.end - recovery.start)) * Math.sin(Math.PI * p) : 0;
2001
+ const kinematic = m.profile?.impactAt(time), speed = kinematic?.speed ?? (m.brake ? m.speed * (1 - t / m.brake) : 0) + hopSpeed, d = kinematic?.distance ?? (m.brake ? m.speed * t - 0.5 * m.speed / m.brake * t * t : 0) + hop;
2002
+ animal.follow({ x: m.x + m.ux * d, y: 0, z: m.z + m.uz * d }, { x: m.ux * speed, y: 0, z: m.uz * speed }, { heading: travel.heading });
2003
+ travelDistance = m.travelBase + d;
2004
+ motionSpeed = speed;
2005
+ motionStage = kinematic?.stage ?? (hopSpeed > 0.00000001 ? "recovery" : speed ? "braking" : "rest");
2006
+ }
2007
+ animal.update(dt);
2008
+ const prepareDt = dt * preparationRate;
2009
+ if (!enteredScramble)
2010
+ age += ["view-turn", "turn", "rise", "watch", "arrival-exit", "launch", "wake", "finish", "settle"].includes(state) || state === "action" && deferred ? prepareDt : dt;
2011
+ const distance = Math.hypot(animal.position.x - beforeX, animal.position.z - beforeZ);
2012
+ director.update(dt, state === "run", distance);
2013
+ if (state === "run") {
2014
+ phase += distance / (travel.carry && travelDistance < travel.carry.runDistance ? PAPER_GAITS.approach.stride : travel.stride);
2015
+ if (target && arrived) {
2016
+ completed = true;
2017
+ stop();
2018
+ target = null;
2019
+ if (movementMode === "walk")
2020
+ playAction("walk-exit", () => {
2021
+ movementMode = "run";
2022
+ enter(walkTransitions ? "idle" : "settle");
2023
+ if (pendingAction)
2024
+ prepareDirection();
2025
+ else
2026
+ drain();
2027
+ });
2028
+ else
2029
+ enter("finish");
2030
+ }
2031
+ } else if (state === "action" && age >= activeAction.duration) {
2032
+ const done = actionDone;
2033
+ activeAction = null;
2034
+ actionDone = null;
2035
+ done();
2036
+ } else if (state === "launch" && age >= launchSeconds()) {
2037
+ phase = Math.ceil(phase);
2038
+ prepareDirection();
2039
+ } else if (state === "finish" && age >= 0.22) {
2040
+ phase = Math.ceil(phase);
2041
+ if (!target && waypoints.length) {
2042
+ target = waypoints.shift();
2043
+ faceTarget(false);
2044
+ }
2045
+ if (target && Math.hypot(target[0] - animal.position.x, target[1] - animal.position.z) > 0.12) {
2046
+ prepareDirection();
2047
+ } else if (pendingAction)
2048
+ prepareDirection();
2049
+ else
2050
+ settle();
2051
+ } else if (state === "view-turn" && age >= viewTurnSeconds()) {
2052
+ view = turnView;
2053
+ prepareDirection();
2054
+ } else if (state === "turn" && age >= turnSeconds()) {
2055
+ flip = turnFlip;
2056
+ prepareDirection();
2057
+ } else if (state === "sit-down") {
2058
+ seatProgress = Math.min(1, seatProgress + prepareDt / (snappyPreparation() ? 0.35 : 0.95));
2059
+ if (seatProgress === 1) {
2060
+ seatVariant = "default";
2061
+ enter("idle");
2062
+ if (pendingAction)
2063
+ prepareDirection();
2064
+ }
2065
+ } else if (state === "rise") {
2066
+ seatProgress = Math.max(0, seatProgress - prepareDt / (snappyPreparation() ? 0.28 : 0.65));
2067
+ if (seatProgress === 0) {
2068
+ if (seatVariant === "arrival")
2069
+ enter("arrival-exit");
2070
+ else
2071
+ prepareDirection();
2072
+ }
2073
+ } else if (state === "arrival-exit" && age >= 0.22) {
2074
+ seatVariant = "default";
2075
+ prepareDirection();
2076
+ } else if (state === "watch" && age >= PAPER_WATCH_TIMING.total) {
2077
+ if (pendingAction)
2078
+ prepareDirection();
2079
+ else if (target)
2080
+ enter("rise");
2081
+ else
2082
+ enter("idle");
2083
+ } else if (state === "recline") {
2084
+ const end = stayDown ? 0.25 : 1;
2085
+ restProgress = Math.min(end, restProgress + dt / 1.8);
2086
+ if (restProgress === end)
2087
+ enter(stayDown ? "down" : comfort?.kind === "sleep" ? "sleep" : "nap");
2088
+ } else if (state === "sleep" && age >= comfort.seconds) {
2089
+ comfort = null;
2090
+ enter("wake");
2091
+ } else if (state === "tug" && tugProgress() >= 1) {
2092
+ comfort = null;
2093
+ enter("idle");
2094
+ drain();
2095
+ } else if ((state === "attention-paw" || state === "attention-wall") && (comfort.hurry ? age - comfort.hurry.age >= comfort.hurry.length : age >= comfort.seconds)) {
2096
+ comfort = null;
2097
+ enter("idle");
2098
+ drain();
2099
+ } else if ((state === "scramble" || state === "pickup") && age >= comfort.seconds) {
2100
+ comfort = null;
2101
+ movingScramble = null;
2102
+ stop();
2103
+ enter("idle");
2104
+ drain();
2105
+ } else if (state === "nap" && age >= 5 && !stayDown) {
2106
+ enter("wake");
2107
+ } else if (state === "wake") {
2108
+ restProgress = Math.max(0, restProgress - prepareDt / 1.2);
2109
+ if (restProgress === 0) {
2110
+ if (pendingAction)
2111
+ prepareDirection();
2112
+ else if (target)
2113
+ enter("rise");
2114
+ else
2115
+ enter("idle");
2116
+ }
2117
+ } else if (state === "idle" && route && age >= idleHold)
2118
+ nextRoute();
2119
+ else if (state === "settle" && age >= 0.35)
2120
+ enter("idle");
2121
+ },
2122
+ sample(clock, { poseFps = 24 } = {}) {
2123
+ if (![12, 24, 30].includes(poseFps))
2124
+ throw Error("Invalid pose cadence");
2125
+ let pair = 20, blend = 0, kind = state, rearPair = null, approachPair = null;
2126
+ let seat = state === "sit-down" || state === "rise" || seatProgress > 0 ? Math.round(seatProgress * 48) / 48 : null;
2127
+ if (restProgress === 0 && state !== "recline")
2128
+ sleepLook = null;
2129
+ let behavior = null, behaviorProgress = 0, comfortPose = state === "scramble" ? { clip: "scramble", view, progress: Math.min(1, age / comfort.seconds) } : state === "pickup" ? { clip: "pickup", view, progress: Math.min(1, age / comfort.seconds) } : state === "tug" ? { clip: "tug", progress: tugProgress() } : sleepLook && restProgress > 0.72 ? { clip: "sleep", ...sleepLook, blend: comfortEase((restProgress - 0.72) / 0.28) } : null;
2130
+ if (state === "attention-paw" || state === "attention-wall")
2131
+ comfortPose = { clip: state, progress: Math.min(1, age / comfort.seconds), ...comfort.hurry ? { release: { from: comfort.hurry.from, progress: Math.min(1, (age - comfort.hurry.age) / comfort.hurry.length) } } : {} };
2132
+ if (activeAction?.comfort)
2133
+ kind = activeAction.comfort;
2134
+ 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;
2135
+ if (manualDown && state === "down") {
2136
+ actionName = "lie-down";
2137
+ actionProgress = 1;
2138
+ }
2139
+ if (walkTransitions && state === "view-turn" && turnView === "front") {
2140
+ actionName = view === "approach" ? "walk-face-front" : "walk-face-rear";
2141
+ actionProgress = Math.min(1, age / viewTurnSeconds());
2142
+ }
2143
+ if (walkTransitions && state === "turn") {
2144
+ actionName = "walk-turn";
2145
+ actionProgress = Math.min(1, age / turnSeconds());
2146
+ }
2147
+ if (view === "approach" && approachPlan && (state === "run" || state === "finish" && completed)) {
2148
+ const p = approachPlan.at(travelDistance);
2149
+ if (state === "finish" && !travel?.attending) {
2150
+ behavior = "brake";
2151
+ behaviorProgress = 1;
2152
+ } else if (p.clip !== "run") {
2153
+ behavior = p.clip;
2154
+ behaviorProgress = p.progress;
2155
+ }
2156
+ }
2157
+ if (view === "approach" && travel?.carry && (state === "run" || state === "finish")) {
2158
+ const p = state === "finish" ? { clip: "carry-settle", progress: 1 } : travel.carry.poseAt(travelDistance);
2159
+ if (p.clip !== "run") {
2160
+ comfortPose = p;
2161
+ behavior = null;
2162
+ actionName = null;
2163
+ }
2164
+ }
2165
+ if (state === "arrival-exit") {
2166
+ behavior = "finish";
2167
+ behaviorProgress = Math.min(1, age / 0.22);
2168
+ }
2169
+ if (state === "watch") {
2170
+ behavior = `watch-${watchSide}`;
2171
+ behaviorProgress = paperWatchProgress(age);
2172
+ }
2173
+ if (restProgress > 0 || state === "recline" || state === "wake" || state === "nap") {
2174
+ behavior = "rest";
2175
+ behaviorProgress = restProgress;
2176
+ }
2177
+ if (state === "run" || state === "finish") {
2178
+ const p = state === "finish" ? phase + (Math.ceil(phase) - phase) * Math.min(1, age / 0.22) : phase;
2179
+ const q = view !== "front" ? p % 1 * 8 : retimeRun(p % 1 * 20, info.run_timing);
2180
+ pair = Math.floor(q) % info.run_sequence.length;
2181
+ blend = q - Math.floor(q);
2182
+ if (view === "rear")
2183
+ rearPair = 5 + pair;
2184
+ if (view === "approach")
2185
+ approachPair = 5 + pair;
2186
+ } else if (state === "view-turn") {
2187
+ const q = Math.min(4.999999, age / viewTurnSeconds() * 5), i = Math.floor(q);
2188
+ if (view === "approach" || turnView === "approach")
2189
+ approachPair = (view === "front" ? 0 : 13) + i;
2190
+ else
2191
+ rearPair = (view === "front" ? 0 : 13) + i;
2192
+ blend = q - i;
2193
+ } else if (state === "turn") {
2194
+ const t = Math.min(1, age / turnSeconds());
2195
+ if (t < 0.15) {
2196
+ pair = flip ? 43 : 41;
2197
+ blend = t / 0.15;
2198
+ } else if (t < 0.85) {
2199
+ const q = (t - 0.15) / 0.7 * 11, i = Math.min(10, Math.floor(q));
2200
+ pair = flip ? 40 - i : 30 + i;
2201
+ blend = flip ? 1 - (q - i) : q - i;
2202
+ } else {
2203
+ pair = flip ? 44 : 42;
2204
+ blend = (t - 0.85) / 0.15;
2205
+ }
2206
+ } else if (state === "launch") {
2207
+ pair = 45;
2208
+ blend = Math.min(1, age / launchSeconds());
2209
+ if (view === "rear") {
2210
+ rearPair = 5;
2211
+ blend = 0;
2212
+ }
2213
+ } else if (state === "settle") {
2214
+ pair = 46;
2215
+ blend = Math.min(1, age / 0.35);
2216
+ } else {
2217
+ const q = Math.floor((walkTransitions && state === "idle" ? age : clock) * poseFps) / poseFps * 6;
2218
+ pair = 20 + Math.floor(q) % 10;
2219
+ blend = q - Math.floor(q);
2220
+ }
2221
+ if (view === "rear" && (state === "idle" || state === "settle" || state === "turn")) {
2222
+ rearPair = 5;
2223
+ blend = 0;
2224
+ }
2225
+ if (view === "approach" && (state === "idle" || state === "settle" || state === "turn" || state === "launch")) {
2226
+ approachPair = 5;
2227
+ blend = 0;
2228
+ }
2229
+ blend = Math.round(blend * 16) / 16;
2230
+ const key = `${state}/${actionName}/${view}/${turnView}/${poseFps}/${Math.floor(clock * poseFps)}`;
2231
+ if (held?.key === key) {
2232
+ pair = held.pair;
2233
+ blend = held.blend;
2234
+ rearPair = held.rearPair;
2235
+ approachPair = held.approachPair;
2236
+ seat = held.seat;
2237
+ behavior = held.behavior;
2238
+ behaviorProgress = held.behaviorProgress;
2239
+ actionName = held.actionName;
2240
+ actionProgress = held.actionProgress;
2241
+ comfortPose = held.comfortPose;
2242
+ } else
2243
+ held = { key, pair, blend, rearPair, approachPair, seat, behavior, behaviorProgress, actionName, actionProgress, comfortPose, posePhase: phase };
2244
+ const hopProgress = recovery && movingScramble ? Math.max(0, Math.min(1, (age - recovery.start) / (recovery.end - recovery.start))) : 0;
2245
+ 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?.carry && travelDistance < travel.carry.runDistance ? PAPER_GAITS.approach.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, ...movingScramble ? { bodyLift: recovery && hopProgress > 0 && hopProgress < 1 ? recovery.height * Math.sin(Math.PI * hopProgress) : 0 } : {}, ...comfortPose ? { comfortPose: { ...comfortPose } } : {} };
2246
+ },
2247
+ get state() {
2248
+ return state;
2249
+ }
2250
+ };
2251
+ }
2252
+ function createPaperRenderer(cache, info, meta, width, height, { rear, approach, seat, arrivalSeat, behaviors, actions, interactions, comfort, contacts = true, pawTransport = false, motionTables = null, kernel = null } = {}) {
2253
+ if (![width, height].every(Number.isInteger) || width < 2 || height < 2 || width * height > 1e6)
2254
+ throw Error("Invalid paper grid");
2255
+ const tween = createCachedTween(cache, info, kernel), renderMeta = { ...meta, pairs: info.pair_map, run_sequence: info.run_sequence };
2256
+ const out = new Uint8ClampedArray(width * height * 4), area = createPaperAreaSampler();
2257
+ const actionScratch = new Uint8ClampedArray(Math.max(320 * 320, (actions?.info.width ?? 320) * (actions?.info.height ?? 320)) * 4);
2258
+ const spriteScratch = actionScratch.subarray(0, 320 * 320 * 4);
2259
+ for (let i = 3;i < actionScratch.length; i += 4)
2260
+ actionScratch[i] = 255;
2261
+ const rearSample = rear ? createRearSampler(rear.bytes, rear.info, spriteScratch, kernel) : null, lock = createPaperFootLock({ maxImageStretch: pawTransport ? 1.45 : Infinity });
2262
+ const approachSample = approach ? createRearSampler(approach.bytes, approach.info, spriteScratch, kernel) : null;
2263
+ const seatSample = seat ? createPaperSeatSampler(seat.bytes, seat.info, spriteScratch, kernel) : null;
2264
+ const arrivalSeatSample = arrivalSeat ? createPaperSeatSampler(arrivalSeat.bytes, arrivalSeat.info, spriteScratch, kernel) : null;
2265
+ const behaviorSample = behaviors ? createPaperBehaviorSampler(behaviors.bytes, behaviors.info, spriteScratch, kernel) : null;
2266
+ const interactionSample = interactions ? createCompanionSpriteSampler(interactions.bytes, interactions.info, spriteScratch, kernel) : null;
2267
+ const comfortSample = comfort ? createComfortSpriteSampler(comfort.bytes, comfort.info, spriteScratch) : null;
2268
+ const actionSample = actions ? createPaperActionSampler(actions.bytes, actions.info, actionScratch.subarray(0, actions.info.width * actions.info.height * 4), motionTables?.actionClips, kernel) : null, actionArea = actions ? createPaperAreaSampler(actions.info.width) : null;
2269
+ const episodes = (key, labels, indices, samples, options) => motionTables?.support[key] ? createBakedSupportEpisodes(motionTables.support[key]) : createPaperSupportEpisodes(labels, indices, samples, options);
2270
+ const walkEpisodes = actions ? episodes("walk", actions.info.feet, actions.info.clips.walk, 9) : null;
2271
+ const sourceEpisodes = rear ? {
2272
+ front: episodes("front", rear.info.frontFeet, [0, 1, 2, 3, 4, 5, 6, 7], 17),
2273
+ rear: episodes("rear", rear.info.rearFeet, [5, 6, 7, 8, 9, 10, 11, 12], 9),
2274
+ approach: approach ? episodes("approach", approach.info.feet, [5, 6, 7, 8, 9, 10, 11, 12], 9) : null
2275
+ } : null;
2276
+ const behaviorEpisodes = behaviors ? Object.fromEntries(Object.entries(behaviors.info.clips).map(([name, indices]) => [name, episodes("behavior/" + name, behaviors.info.feet, indices, 9, { cyclic: name === "walk" })])) : {};
2277
+ const render = (state, { x = 0.5, y = 0.85, size = 0.85, pixelAspect = 1, contactEnabled = contacts, groundFromPixel, pixelFromGround } = {}) => {
2278
+ if (![x, y, size].every(Number.isFinite) || size <= 0 || size > 3)
2279
+ throw Error("Invalid paper placement");
2280
+ const isRear = state.rearPair !== null && state.rearPair !== undefined && rearSample;
2281
+ const isApproach = state.approachPair !== null && state.approachPair !== undefined && approachSample;
2282
+ const isSeat = state.seatProgress !== null && state.seatProgress !== undefined && seatSample;
2283
+ if (!Number.isFinite(pixelAspect) || pixelAspect < 0.1 || pixelAspect > 10)
2284
+ throw Error("Invalid dot pixel aspect");
2285
+ const isComfort = state.comfortPose && comfortSample;
2286
+ const isInteraction = isComfort || state.interactionPose && interactionSample;
2287
+ const isBehavior = !isInteraction && state.behavior && behaviorSample;
2288
+ const isAction = !isInteraction && state.actionName && actionSample, sourceN = isAction ? actions.info.width : 320, originX = (sourceN - 320) / 2, originY = (sourceN / 320 - 1) * 300;
2289
+ const image = isComfort ? comfortSample({ ...state.comfortPose, ...state.comfortPose.clip === "sleep" && state.comfortPose.blend < 1 && behaviorSample ? { base: behaviorSample({ behavior: "rest", behaviorProgress: state.behaviorProgress }) } : {} }) : 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;
2290
+ const motion = isComfort ? comfortMotion(state.comfortPose, comfort.info) : null;
2291
+ const left = width * x - sideX / 2, floor = motion ? motion.floorOffsets?.[comfortMotionKey(comfortProgress(state.comfortPose), motion)] ?? motion.floorOffset ?? 0 : isInteraction || isAction || isBehavior || isSeat || isRear || isApproach ? 0 : cleanGroundOffset(state, renderMeta), top = height * y - (300 - floor) / 320 * side;
2292
+ let feet = [];
2293
+ if (contactEnabled && isComfort && state.comfortPose.clip === "carry-trot") {
2294
+ const k = comfortMotionKey(state.comfortPose.progress, motion);
2295
+ feet = motion.feet[k].map((f) => ({ ...f, episode: `carry/${state.comfortPose.cycle}/${f.id}` }));
2296
+ if (state.flip)
2297
+ feet = feet.map((p) => ({ ...p, x: 319 - p.x }));
2298
+ } else if (contactEnabled && isAction && state.actionName === "walk") {
2299
+ const p = paperActionPose("walk", state.actionProgress, actions.info, motionTables?.actionClips);
2300
+ feet = walkEpisodes(actions.info.feet[p.pair][Math.round(p.blend * 8)], p.pair, p.blend, state.actionProgress);
2301
+ if (state.flip)
2302
+ feet = feet.map((p2) => ({ ...p2, x: 319 - p2.x }));
2303
+ } else if (contactEnabled && rear && state.state === "run" && !isAction && !isComfort) {
2304
+ const b = isBehavior ? paperBehaviorPose(state, behaviors.info) : null;
2305
+ feet = isBehavior ? behaviors.info.feet[b.pair][Math.round(b.blend * 8)] : isApproach ? approach.info.feet[state.approachPair][Math.round(state.blend * 8)] : isRear ? rear.info.rearFeet[state.rearPair][Math.round(state.blend * 8)] : rear.info.frontFeet[state.pair]?.[Math.round(state.blend * 16)] ?? [];
2306
+ feet = isBehavior ? behaviorEpisodes[state.behavior](feet, b.pair, b.blend, state.behaviorProgress) : isApproach ? sourceEpisodes.approach(feet, state.approachPair, state.blend, state.posePhase ?? state.phase ?? 0) : isRear ? sourceEpisodes.rear(feet, state.rearPair, state.blend, state.posePhase ?? state.phase ?? 0) : sourceEpisodes.front(feet, state.pair, state.blend, state.posePhase ?? state.phase ?? 0);
2307
+ if (state.flip)
2308
+ feet = feet.map((p) => ({ ...p, x: 319 - p.x }));
2309
+ }
2310
+ const pins = lock.update(feet, { left, top, side, sideX, groundFromPixel, pixelFromGround, root: { x: state.x, z: state.z, heading: state.heading ?? 0 } }, `${state.view ?? "front"}/${state.flip}/${state.state === "run"}/${state.comfortPose?.clip ?? state.actionName ?? state.behavior ?? "gallop"}`);
2311
+ render.lastPins = pins;
2312
+ const transport = pawTransport ? createPawTransport(feet, pins, { opaque: (x2, y2) => {
2313
+ const rawX = x2 + originX, rawY = y2 + originY;
2314
+ if (rawX < 0 || rawX >= sourceN || rawY < 0 || rawY >= sourceN)
2315
+ return false;
2316
+ let sx = Math.floor(rawX);
2317
+ if (state.flip)
2318
+ sx = sourceN - 1 - sx;
2319
+ return image[(Math.floor(rawY) * sourceN + sx) * 4] > 16;
2320
+ } }) : null;
2321
+ render.lastTransport = transport;
2322
+ const minify = side < 320 || sideX < 320, filter = isAction ? actionArea : area;
2323
+ if (minify && !(kernel?.blit && transport))
2324
+ filter.prepare(image);
2325
+ if (kernel?.clear)
2326
+ kernel.clear(out);
2327
+ else {
2328
+ out.fill(0);
2329
+ for (let i = 3;i < out.length; i += 4)
2330
+ out[i] = 255;
2331
+ }
2332
+ render.lastPlacement = { left, top, side, sideX };
2333
+ const mouthFamily = isInteraction || isAction ? null : isBehavior ? "behaviors" : isSeat ? state.seatVariant === "arrival" && arrivalSeatSample ? "arrivalSeat" : "seat" : isApproach ? "approach" : null;
2334
+ const mouthInfo = { behaviors: behaviors?.info, seat: seat?.info, arrivalSeat: arrivalSeat?.info, approach: approach?.info }[mouthFamily];
2335
+ const mouthFrame = mouthFamily ? mouthSourceFrame(mouthFamily, state, mouthInfo) : null;
2336
+ render.lastMouth = projectMouth(mouthFamily ? mouthAnchor(mouthFamily, mouthFrame, mouthInfo?.sha256) : null, { left, top, side, sideX }, state.flip, { family: mouthFamily, frame: mouthFrame });
2337
+ if (motion?.mouth) {
2338
+ const k = comfortMotionKey(state.comfortPose.progress, motion), p = motion.mouth[k];
2339
+ render.lastMouth = projectMouth({ x: p[0], y: p[1], scale: 1, score: null }, { left, top, side, sideX }, state.flip, { family: "comfort", frame: motion.frames[k] });
2340
+ }
2341
+ const drawTop = top - originY / 320 * side, bottom = top + Math.max(sourceN - originY, ...pins.map((p) => p.target.y + 20)) / 320 * side;
2342
+ const drawLeft = left + Math.min(-originX, ...pins.map((p) => p.target.x - 20)) / 320 * sideX, drawRight = left + Math.max(sourceN - originX, ...pins.map((p) => p.target.x + 20)) / 320 * sideX;
2343
+ if (kernel?.blit && transport)
2344
+ kernel.blit(out, image, { width, height, sourceN, left, top, side, sideX, originX, originY, drawTop, bottom, drawLeft, drawRight, flip: state.flip, layers: transport.layers });
2345
+ else
2346
+ for (let dy2 = Math.max(0, minify ? Math.floor(drawTop) : Math.ceil(drawTop - 0.5));dy2 < Math.min(height, minify ? Math.ceil(bottom) : Math.ceil(bottom - 0.5)); dy2++)
2347
+ for (let dx = Math.max(0, minify ? Math.floor(drawLeft) : Math.ceil(drawLeft - 0.5));dx < Math.min(width, minify ? Math.ceil(drawRight) : Math.ceil(drawRight - 0.5)); dx++) {
2348
+ const px = (dx + 0.5 - left) / sideX * 320, py = (dy2 + 0.5 - top) / side * 320;
2349
+ const warped = transport ? transport(px, py) : footSourcePoint(px, py, pins);
2350
+ if (!warped)
2351
+ continue;
2352
+ const rawX = warped.x + originX, rawY = warped.y + originY;
2353
+ let sx = Math.max(0, Math.min(sourceN - 1, Math.floor(rawX)));
2354
+ if (state.flip)
2355
+ sx = sourceN - 1 - sx;
2356
+ const sy = Math.max(0, Math.min(sourceN - 1, Math.floor(rawY))), j = (dy2 * width + dx) * 4, k = (sy * sourceN + sx) * 4;
2357
+ out[j] = out[j + 1] = out[j + 2] = minify ? filter.sample(state.flip ? sourceN - rawX : rawX, rawY, 320 / sideX, 320 / side) : rawY < 0 || rawY >= sourceN || rawX < 0 || rawX >= sourceN ? 0 : image[k];
2358
+ }
2359
+ return out;
2360
+ };
2361
+ render.report = () => lock.report();
2362
+ render.lastPins = [];
2363
+ render.lastMouth = null;
2364
+ return render;
2365
+ }
2366
+
2367
+ // braille.mjs
2368
+ var BRAILLE_BITS = [[1, 8], [2, 16], [4, 32], [64, 128]];
2369
+ var BAYER = [[0, 8, 2, 10], [12, 4, 14, 6], [3, 11, 1, 9], [15, 7, 13, 5]];
2370
+ var clamp = (v) => Math.max(0, Math.min(1, v));
2371
+ var GLYPHS = Array.from({ length: 256 }, (_, i) => String.fromCharCode(10240 + i));
2372
+ var grayTable = null;
2373
+ var grayBrightness = 0;
2374
+ var grayContrast = 0;
2375
+ function stableBraille(rgba, width, height, brightness, contrast) {
2376
+ if (grayBrightness !== brightness || grayContrast !== contrast) {
2377
+ grayTable = Float32Array.from({ length: 256 }, (_, v) => {
2378
+ const luma = (0.2126 * v + 0.7152 * v + 0.0722 * v) / 255;
2379
+ return luma === 0 ? 0 : clamp((Math.pow(luma, 0.9) * brightness - 0.5) * contrast + 0.5);
2380
+ });
2381
+ grayBrightness = brightness;
2382
+ grayContrast = contrast;
2383
+ }
2384
+ const table = grayTable, bytePixels = rgba instanceof Uint8Array || rgba instanceof Uint8ClampedArray, columns = Math.ceil(width / 2), rows = Math.ceil(height / 4), masks = new Uint8Array(columns * rows);
2385
+ let dots = 0;
2386
+ for (let y = 0;y < height; y++)
2387
+ for (let x = 0, p = y * width * 4;x < width; x++, p += 4) {
2388
+ const r = rgba[p], g = rgba[p + 1], b = rgba[p + 2], a = rgba[p + 3];
2389
+ if (a === 0 || r === 0 && g === 0 && b === 0)
2390
+ continue;
2391
+ let value;
2392
+ if (bytePixels && a === 255 && r === g && g === b)
2393
+ value = table[r];
2394
+ else {
2395
+ const luma = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255 * (a / 255);
2396
+ value = Math.fround(luma === 0 ? 0 : clamp((Math.pow(luma, 0.9) * brightness - 0.5) * contrast + 0.5));
2397
+ }
2398
+ if (value <= 0)
2399
+ continue;
2400
+ let n = Math.imul(x + 1, 521288629) ^ Math.imul(y + 1, 1597334677);
2401
+ n = Math.imul(n ^ n >>> 16, 73244475);
2402
+ n = Math.imul(n ^ n >>> 16, 73244475);
2403
+ n = (n ^ n >>> 16) >>> 0;
2404
+ if (value > (n + 0.5) / 4294967296) {
2405
+ masks[(y >> 2) * columns + (x >> 1)] |= BRAILLE_BITS[y & 3][x & 1];
2406
+ dots++;
2407
+ }
2408
+ }
2409
+ const lines = new Array(rows), cells = new Array(columns);
2410
+ for (let y = 0;y < rows; y++) {
2411
+ for (let x = 0;x < columns; x++)
2412
+ cells[x] = GLYPHS[masks[y * columns + x]];
2413
+ lines[y] = cells.join("");
2414
+ }
2415
+ return { text: lines.join(`
2416
+ `), columns, rows, dots };
2417
+ }
2418
+ function rgbaToBraille(rgba, width, height, { brightness = 1, contrast = 1, method = "floyd", kernel = null } = {}) {
2419
+ if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1 || width * height > 1e6 || rgba.length !== width * height * 4)
2420
+ throw Error("Invalid RGBA image");
2421
+ if (!Number.isFinite(brightness) || brightness <= 0 || brightness > 4 || !Number.isFinite(contrast) || contrast <= 0 || contrast > 4 || !["floyd", "bayer", "stable"].includes(method))
2422
+ throw Error("Invalid dithering options");
2423
+ if (method === "stable")
2424
+ return kernel?.braille ? kernel.braille(rgba, width, height, brightness, contrast) : stableBraille(rgba, width, height, brightness, contrast);
2425
+ const values = new Float32Array(width * height), bits = new Uint8Array(width * height);
2426
+ for (let i = 0;i < values.length; i++) {
2427
+ const p = i * 4, luma = (0.2126 * rgba[p] + 0.7152 * rgba[p + 1] + 0.0722 * rgba[p + 2]) / 255 * (rgba[p + 3] / 255);
2428
+ values[i] = luma === 0 ? 0 : clamp((Math.pow(luma, 0.9) * brightness - 0.5) * contrast + 0.5);
2429
+ }
2430
+ if (method === "bayer") {
2431
+ for (let y = 0;y < height; y++)
2432
+ for (let x = 0;x < width; x++)
2433
+ bits[y * width + x] = values[y * width + x] > (BAYER[y % 4][x % 4] + 0.5) / 16 ? 1 : 0;
2434
+ } else {
2435
+ const add = (x, y, error, weight) => {
2436
+ if (x >= 0 && x < width && y < height)
2437
+ values[y * width + x] += error * weight / 16;
2438
+ };
2439
+ for (let y = 0;y < height; y++) {
2440
+ const direction = y % 2 ? -1 : 1;
2441
+ for (let k = 0;k < width; k++) {
2442
+ const x = direction === 1 ? k : width - 1 - k, i = y * width + x, old = values[i], value = old >= 0.5 ? 1 : 0, error = old - value;
2443
+ bits[i] = value;
2444
+ add(x + direction, y, error, 7);
2445
+ add(x - direction, y + 1, error, 3);
2446
+ add(x, y + 1, error, 5);
2447
+ add(x + direction, y + 1, error, 1);
2448
+ }
2449
+ }
2450
+ }
2451
+ const columns = Math.ceil(width / 2), rows = Math.ceil(height / 4), lines = [];
2452
+ for (let cy = 0;cy < rows; cy++) {
2453
+ let line = "";
2454
+ for (let cx = 0;cx < columns; cx++) {
2455
+ let mask = 0;
2456
+ for (let dy2 = 0;dy2 < 4; dy2++)
2457
+ for (let dx = 0;dx < 2; dx++) {
2458
+ const x = cx * 2 + dx, y = cy * 4 + dy2;
2459
+ if (x < width && y < height && bits[y * width + x])
2460
+ mask |= BRAILLE_BITS[dy2][dx];
2461
+ }
2462
+ line += String.fromCodePoint(10240 + mask);
2463
+ }
2464
+ lines.push(line);
2465
+ }
2466
+ return { text: lines.join(`
2467
+ `), columns, rows, dots: bits.reduce((a, b) => a + b, 0) };
2468
+ }
2469
+
2470
+ // puppy-companion-ball.mjs
2471
+ function createBallPath({ from = { x: 0, z: 0.2 }, landing = { x: 0, z: -4 }, stop = landing, flight = 0.65, peak = 0.75, deceleration = 0.6 } = {}) {
2472
+ if (![from.x, from.z, landing.x, landing.z, stop.x, stop.z, flight, peak, deceleration].every(Number.isFinite) || flight < 0 || peak < 0 || deceleration <= 0)
2473
+ throw Error("Invalid ball path");
2474
+ from = Object.freeze({ ...from });
2475
+ landing = Object.freeze({ ...landing });
2476
+ stop = Object.freeze({ ...stop });
2477
+ const dx = stop.x - landing.x, dz2 = stop.z - landing.z, distance = Math.hypot(dx, dz2), speed = Math.sqrt(2 * deceleration * distance);
2478
+ const roll = distance ? planPaperTravel(distance, { speed, startSpeed: speed, acceleration: deceleration, deceleration }) : null;
2479
+ const duration = flight + (roll?.duration ?? 0);
2480
+ return Object.freeze({
2481
+ duration,
2482
+ from,
2483
+ landing,
2484
+ stop,
2485
+ at(age) {
2486
+ if (!Number.isFinite(age))
2487
+ throw Error("Invalid ball age");
2488
+ const t = Math.max(0, age);
2489
+ if (t < flight) {
2490
+ const p = t / flight;
2491
+ return { x: from.x + (landing.x - from.x) * p, z: from.z + (landing.z - from.z) * p, height: 4 * peak * p * (1 - p), spin: 0, speed: 0, phase: "flight", settled: false };
2492
+ }
2493
+ const state = roll?.at(t - flight), ratio = distance ? state.distance / distance : 1;
2494
+ return { x: landing.x + dx * ratio, z: landing.z + dz2 * ratio, height: 0, spin: (state?.distance ?? 0) / 0.035, speed: state?.speed ?? 0, phase: t >= duration ? "rest" : "roll", settled: t >= duration };
2495
+ }
2496
+ });
2497
+ }
2498
+ function createFetchPatterns(random, { canTumble = false } = {}) {
2499
+ if (typeof random !== "function")
2500
+ throw Error("Fetch pattern RNG is required");
2501
+ let count = 0, last = null, lastTumble = -4;
2502
+ return (allowTumble = canTumble) => {
2503
+ if (typeof allowTumble !== "boolean")
2504
+ throw Error("Invalid tumble availability");
2505
+ count++;
2506
+ const weights = { clean: 4, rolling: 3, miss: 2, tumble: canTumble && allowTumble && count >= 4 && count - lastTumble >= 4 ? 0.6 : 0 };
2507
+ if (last)
2508
+ weights[last] = 0;
2509
+ let pattern = "clean";
2510
+ if (count > 1) {
2511
+ let pick = random() * Object.values(weights).reduce((a, b) => a + b, 0);
2512
+ pattern = Object.keys(weights).find((k) => (pick -= weights[k]) < 0) ?? "clean";
2513
+ }
2514
+ if (pattern === "tumble")
2515
+ lastTumble = count;
2516
+ last = pattern;
2517
+ return pattern;
2518
+ };
2519
+ }
2520
+
2521
+ // puppy-companion-life.mjs
2522
+ var SAFE = new Set(["idle", "down"]);
2523
+ var FETCH_RETURN_STYLES = Object.freeze(["auto", "brake", "trot", "run-trot"]);
2524
+ var HOME = { x: 0, z: 0.2 };
2525
+ var BALL_TARGET = { x: 0, z: -4 };
2526
+ var OFFER = { x: 0, z: 0.3 };
2527
+ var TREAT_OFFSET = { z: 0.08, height: 0.25 };
2528
+ var COOLDOWN = { ball: 6, treat: 15, tug: 20 };
2529
+ var CARRY_HEIGHT = 0.19;
2530
+ var SETTLE_FALLBACK = 0.6;
2531
+ var MAX_DT = 0.25;
2532
+ var ARRIVE = 0.05;
2533
+ var BUTTERFLY_RULES = Object.freeze({ firstMin: 240, firstSpread: 240, cooldownMin: 720, cooldownSpread: 480, maxVisible: 18 });
2534
+ var LABEL = {
2535
+ notifyWall: "뒷발로 서서 벽을 박박 긁는 중",
2536
+ start: "기다리는 중",
2537
+ pause: "잠시 느긋하게 둘러보는 중",
2538
+ lie: "엎드리는 중",
2539
+ rest: "엎드려 쉬는 중",
2540
+ recover: "앉아서 기운 차리는 중",
2541
+ sit: "앉는 중",
2542
+ watch: "앉아서 지켜보는 중",
2543
+ look: "고개를 돌려 살피는 중",
2544
+ observe: "가만히 주변 관찰 중",
2545
+ stroll: "옆으로 살짝 걸어가는 중",
2546
+ sniff: "잠깐 멈춰 주변을 살피는 중",
2547
+ dash: "저 멀리 달려가는 중",
2548
+ far: "멀리서 둘러보는 중",
2549
+ home: "제자리로 돌아오는 중",
2550
+ nest: "쉴 자리를 찾아가는 중",
2551
+ patrol: "주변을 왕복 산책하는 중",
2552
+ scan: "양쪽을 번갈아 살피는 중",
2553
+ roll: "느긋하게 뒹구는 중",
2554
+ play: "신나서 한 바퀴 도는 중",
2555
+ checkin: "가까이 와서 함께 쉬는 중",
2556
+ belly: "배를 보이고 발라당 쉬는 중",
2557
+ sleep: "새근새근 잠자는 중",
2558
+ tug: "장난감을 물고 당기는 중",
2559
+ butterflyNotice: "나비를 발견하고 귀를 쫑긋하는 중",
2560
+ butterfly: "나비를 폴짝폴짝 쫓는 중",
2561
+ butterflyBye: "날아가는 나비를 바라보는 중",
2562
+ fetch: "공을 쫓아가는 중",
2563
+ miss: "굴러가는 공을 한 번 놓친 중",
2564
+ reChase: "공을 다시 쫓아가는 중",
2565
+ tumble: "우당탕 균형을 되찾는 중",
2566
+ pickup: "공을 무는 중",
2567
+ carry: "공을 물고 돌아오는 중",
2568
+ drop: "공을 내려놓는 중",
2569
+ offer: "공을 내려놓고 기다리는 중",
2570
+ treatSit: "간식 앞에 앉는 중",
2571
+ chew: "간식 먹는 중",
2572
+ notifySit: "확인할 일을 발견하고 앉는 중",
2573
+ notifyContact: "확인할 일이 있어 사용자를 바라보는 중",
2574
+ notifyCome: "확인할 일을 알려주러 가까이 오는 중",
2575
+ notifyPaw: "앞발로 살살 관심을 끄는 중",
2576
+ notify: "확인할 일이 있는 쪽을 바라보는 중"
2577
+ };
2578
+ var PENDING_LABEL = { treat: "곧 간식", tug: "곧 터그놀이" };
2579
+ var GESTURE = { "ball-pickup": 0.65, "ball-drop": 0.65, "treat-eat": 2.4 };
2580
+ var clamp2 = (v, a, b) => Math.min(b, Math.max(a, v));
2581
+ function mulberry32(seed) {
2582
+ let a = seed >>> 0;
2583
+ return () => {
2584
+ a = a + 1831565813 >>> 0;
2585
+ let t = a;
2586
+ t = Math.imul(t ^ t >>> 15, t | 1);
2587
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
2588
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
2589
+ };
2590
+ }
2591
+ function createCompanionLife(actor, { seed = 731, energy = 0.8, returnStyle = "auto" } = {}) {
2592
+ for (const name of ["update", "sample", "go", "come", "sit", "lie", "look", "walk", "roll", "rest"])
2593
+ if (!actor || typeof actor[name] !== "function")
2594
+ throw TypeError(`actor.${name} must be a function`);
2595
+ if (!Number.isInteger(seed))
2596
+ throw TypeError("seed must be an integer");
2597
+ if (!Number.isFinite(energy) || energy < 0 || energy > 1)
2598
+ throw RangeError("energy must be within 0..1");
2599
+ const validateReturnStyle = (style) => {
2600
+ if (!FETCH_RETURN_STYLES.includes(style))
2601
+ throw RangeError("returnStyle must be auto, brake, trot or run-trot");
2602
+ if (["trot", "run-trot"].includes(style) && !actor.canCarryTrot)
2603
+ throw Error("Carry-trot artwork is not available");
2604
+ };
2605
+ validateReturnStyle(returnStyle);
2606
+ const rand = mulberry32(seed), range = (a, b) => a + (b - a) * rand(), side = () => rand() < 0.5 ? "left" : "right";
2607
+ const spaceRand = mulberry32(seed ^ 682529223), spaceRange = (a, b) => a + (b - a) * spaceRand();
2608
+ const toyRand = mulberry32(seed ^ 1820504397), chooseFetch = createFetchPatterns(toyRand, { canTumble: actor.canScramble === true });
2609
+ const returnRand = mulberry32(seed ^ 1941741925);
2610
+ let lastReturnMode = null;
2611
+ let placeSide = spaceRand() < 0.5 ? -1 : 1, outings = 0, nearSeconds = 0, lastCheckin = -Infinity;
2612
+ const encounterRand = mulberry32(seed ^ 1274116755), encounterRange = (a, b) => a + (b - a) * encounterRand();
2613
+ let nextButterfly = BUTTERFLY_RULES.firstMin + encounterRange(0, BUTTERFLY_RULES.firstSpread), butterfly = null;
2614
+ const needs = { energy, social: 0.5, curiosity: 0.5, drowsiness: 0 };
2615
+ const cooldowns = { ball: 0, treat: 0, tug: 0 };
2616
+ const episodeCooldowns = { roll: 0, play: 0, belly: 0, sleep: 0 };
2617
+ let clock = 0, decisions = 0, lastEpisode = null, quiet = 0;
2618
+ let queue = [], hold = 0, holdIntent = null, waiting = null, gesture = null;
2619
+ let intent = "start", label = LABEL.start, pending = null, active = null, ball = null, treat = null;
2620
+ let fetchPlan = null, notice = null, noticeCount = 0;
2621
+ const cmd = (name, args, step, when) => ({ cmd: name, args, intent: step, label: LABEL[step], when });
2622
+ const wait = (seconds, step) => ({ hold: seconds, intent: step, label: LABEL[step] });
2623
+ const play = (clip, step) => ({ hold: GESTURE[clip], gesture: clip, intent: step, label: LABEL[step] });
2624
+ const unseated = () => {
2625
+ const s2 = actor.sample(clock);
2626
+ return !(actor.state === "idle" && s2.seatProgress === 1 && s2.view === "approach" && (s2.seatVariant ?? "default") === "default");
2627
+ };
2628
+ const seatedAnyVariant = () => {
2629
+ const s2 = actor.sample(clock);
2630
+ return actor.state === "idle" && s2.seatProgress === 1 && s2.view === "approach";
2631
+ };
2632
+ const fx = (fn) => ({ fx: fn });
2633
+ const home = () => cmd("go", [HOME.x, HOME.z], "home", () => {
2634
+ const s2 = actor.sample(clock);
2635
+ return Math.hypot(s2.x - HOME.x, s2.z - HOME.z) > ARRIVE;
2636
+ });
2637
+ const sit = (step) => cmd("sit", [], step, () => !seatedAnyVariant());
2638
+ const lie = () => cmd("lie", [], "lie", () => actor.state !== "down");
2639
+ const walkTo = (x) => cmd("walk", () => {
2640
+ const dx = x - actor.sample(clock).x;
2641
+ return [dx < 0 ? "left" : "right", Math.abs(dx)];
2642
+ }, "patrol", () => Math.abs(x - actor.sample(clock).x) > 0.002);
2643
+ const roomRadius = (z) => Math.max(0.12, paperCameraPoint(0, 0, z).depth * PAPER_SCREEN.widthMeters / (2 * PAPER_SCREEN.viewDistance) - PAPER_CAMERA.canvasMeters / 2);
2644
+ const place = (nap) => {
2645
+ const bands = [[1.4, 3], [4, 8], [0.7, 1.4], [9, 15]], band = bands[outings++ % bands.length];
2646
+ const z = -(nap ? spaceRange(0.7, 2.6) : spaceRange(...band));
2647
+ placeSide *= -1;
2648
+ return paperGroundTarget(placeSide * roomRadius(z) * spaceRange(0.5, 0.8), z);
2649
+ };
2650
+ const travelTo = (target, step) => cmd("go", target, step, () => {
2651
+ const s2 = actor.sample(clock);
2652
+ return Math.hypot(s2.x - target[0], s2.z - target[1]) > 0.12;
2653
+ });
2654
+ const quietPlace = () => {
2655
+ const s2 = actor.sample(clock);
2656
+ if (s2.z <= -0.5 && s2.z >= -3.5 && Math.abs(s2.x) > roomRadius(s2.z) * 0.3 && spaceRand() < 0.65)
2657
+ return [];
2658
+ return [travelTo(place(true), "nest")];
2659
+ };
2660
+ const butterflyPoint = () => {
2661
+ if (!butterfly)
2662
+ return null;
2663
+ const elapsed = clock - butterfly.since, p = clamp2(elapsed / 0.65, 0, 1), ease = p * p * (3 - 2 * p);
2664
+ const leaving = butterfly.leaving ? clamp2((clock - butterfly.leaving) / 1.1, 0, 1) : 0;
2665
+ return {
2666
+ x: butterfly.from.x + (butterfly.to.x - butterfly.from.x) * ease + 0.025 * Math.sin(elapsed * 5) + leaving * 0.2 * butterfly.side,
2667
+ z: butterfly.from.z + (butterfly.to.z - butterfly.from.z) * ease,
2668
+ height: 0.45 + 0.045 * Math.sin((clock - butterfly.start) * 6) + leaving * 0.35,
2669
+ phase: (clock - butterfly.start) * 5 % 1,
2670
+ opacity: 1 - leaving
2671
+ };
2672
+ };
2673
+ const butterflyEpisode = () => {
2674
+ const s2 = actor.sample(clock), radius = roomRadius(s2.z);
2675
+ const direction = s2.x > radius - 0.3 ? -1 : s2.x < -radius + 0.3 ? 1 : encounterRand() < 0.5 ? -1 : 1;
2676
+ const targets = [clamp2(s2.x + direction * 0.36, -radius, radius), clamp2(s2.x - direction * 0.38, -radius, radius), clamp2(s2.x + direction * 0.24, -radius, radius)];
2677
+ const z = s2.z;
2678
+ const moveFly = (x) => fx(() => {
2679
+ const from = butterflyPoint() ?? { x: s2.x - direction * 0.28, z };
2680
+ const heading = x < actor.sample(clock).x ? -1 : 1;
2681
+ butterfly = { start: butterfly?.start ?? clock, since: clock, from: { x: from.x, z: from.z }, to: { x: x + heading * 0.23, z }, side: heading, leaving: 0 };
2682
+ });
2683
+ return [
2684
+ sit("butterflyNotice"),
2685
+ moveFly(targets[0]),
2686
+ cmd("look", [direction < 0 ? "right" : "left"], "butterflyNotice"),
2687
+ wait(0.8, "butterflyNotice"),
2688
+ ...targets.flatMap((x, i) => [...i ? [moveFly(x)] : [], cmd(typeof actor.chase === "function" ? "chase" : "go", [x, z], "butterfly"), wait(0.3, "butterfly")]),
2689
+ fx(() => {
2690
+ if (butterfly)
2691
+ butterfly.leaving = clock;
2692
+ }),
2693
+ wait(1.1, "butterflyBye"),
2694
+ fx(() => {
2695
+ butterfly = null;
2696
+ needs.energy -= 0.12;
2697
+ needs.curiosity -= 0.25;
2698
+ }),
2699
+ sit("recover"),
2700
+ wait(range(3, 6), "recover")
2701
+ ];
2702
+ };
2703
+ const EPISODES = {
2704
+ rest: () => [...quietPlace(), lie(), wait(range(10, 18), "rest"), sit("recover"), wait(range(3, 6), "recover")],
2705
+ watch: () => [sit("sit"), cmd("look", [side()], "look"), wait(range(3, 9), "watch"), fx(() => {
2706
+ needs.social -= 0.3;
2707
+ needs.curiosity += 0.1;
2708
+ })],
2709
+ observe: () => [sit("sit"), cmd("look", [side()], "look"), wait(range(3, 9), "observe"), fx(() => {
2710
+ needs.curiosity -= 0.15;
2711
+ })],
2712
+ stroll: () => [cmd("walk", [side(), range(0.22, 0.5)], "stroll"), wait(range(4, 9), "sniff"), fx(() => {
2713
+ needs.energy -= 0.08;
2714
+ needs.curiosity -= 0.2;
2715
+ })],
2716
+ dash: () => [travelTo(place(false), "dash"), sit("far"), wait(range(8, 16), "far"), fx(() => {
2717
+ needs.energy -= 0.22;
2718
+ needs.curiosity -= 0.3;
2719
+ })],
2720
+ scan: () => [sit("sit"), cmd("look", ["left"], "scan"), wait(range(0.5, 1.5), "scan"), cmd("look", ["right"], "scan"), wait(range(2, 4), "observe"), fx(() => {
2721
+ needs.curiosity -= 0.25;
2722
+ })],
2723
+ patrol: () => {
2724
+ const x = actor.sample(clock).x, d = range(0.16, 0.3) * (rand() < 0.5 ? -1 : 1);
2725
+ return [walkTo(x + d), wait(range(0.6, 1.2), "sniff"), walkTo(x - d), wait(range(0.6, 1.2), "sniff"), walkTo(x), wait(range(1, 3), "observe"), fx(() => {
2726
+ needs.energy -= 0.1;
2727
+ needs.curiosity -= 0.25;
2728
+ })];
2729
+ },
2730
+ roll: () => [lie(), wait(range(0.7, 1.5), "rest"), cmd("roll", [], "roll"), wait(range(2, 5), "watch"), fx(() => {
2731
+ needs.energy -= 0.08;
2732
+ needs.curiosity -= 0.15;
2733
+ })],
2734
+ checkin: () => [home(), sit("sit"), cmd("look", [side()], "look"), wait(range(5, 10), "checkin"), fx(() => {
2735
+ needs.social -= 0.4;
2736
+ })],
2737
+ play: () => [cmd("spin", [], "play"), wait(range(1, 2), "pause"), sit("sit"), wait(range(2, 4), "watch"), fx(() => {
2738
+ needs.energy -= 0.12;
2739
+ needs.curiosity -= 0.2;
2740
+ })],
2741
+ belly: () => [cmd("belly", [range(5, 9)], "belly"), wait(range(2, 4), "watch"), fx(() => {
2742
+ needs.energy += 0.08;
2743
+ needs.curiosity -= 0.15;
2744
+ })],
2745
+ sleep: () => [...quietPlace(), cmd("sleep", [range(65, 110), ["side", "curl", "chin", "belly"][Math.floor(rand() * 4)], rand() < 0.5], "sleep"), wait(range(2, 4), "recover"), fx(() => {
2746
+ needs.energy += 0.3;
2747
+ })],
2748
+ butterfly: butterflyEpisode
2749
+ };
2750
+ const weights = () => {
2751
+ const { energy: e, social: s2, curiosity: c2 } = needs;
2752
+ return {
2753
+ rest: e < 0.35 ? 6 : (1 - e) * 1.2,
2754
+ watch: 0.8 + s2,
2755
+ observe: 0.6 + c2 * 0.6,
2756
+ stroll: 1.2 * c2 * e,
2757
+ dash: e > 0.55 ? 1.2 * e * c2 : 0,
2758
+ scan: 0.35 + c2 * 0.4,
2759
+ patrol: e > 0.25 ? 0.7 * c2 * e : 0,
2760
+ checkin: clock - lastCheckin >= 90 ? 0.25 + s2 * 0.7 : 0,
2761
+ roll: episodeCooldowns.roll === 0 && e > 0.45 ? 0.35 * e : 0,
2762
+ play: episodeCooldowns.play === 0 && e > 0.6 && typeof actor.spin === "function" ? 0.25 * e * c2 : 0,
2763
+ belly: episodeCooldowns.belly === 0 && typeof actor.belly === "function" ? 0.35 : 0,
2764
+ sleep: episodeCooldowns.sleep === 0 && typeof actor.sleep === "function" ? e < 0.4 ? 3 : 0.2 : 0
2765
+ };
2766
+ };
2767
+ function decide() {
2768
+ const w = weights();
2769
+ if (lastEpisode)
2770
+ w[lastEpisode] = 0;
2771
+ const kinds = Object.keys(w), total = kinds.reduce((sum, k) => sum + w[k], 0);
2772
+ let pick = rand() * total, kind = kinds.find((k) => (pick -= w[k]) < 0) ?? "observe";
2773
+ if (nearSeconds >= 40 && needs.energy > 0.4 && lastEpisode !== "dash")
2774
+ kind = "dash";
2775
+ if (quiet >= 120 && needs.drowsiness >= 0.35 && episodeCooldowns.sleep === 0 && lastEpisode !== "sleep" && typeof actor.sleep === "function")
2776
+ kind = "sleep";
2777
+ const position = actor.sample(clock);
2778
+ if (kind !== "sleep" && clock >= nextButterfly && quiet >= 15 && needs.energy > 0.6 && needs.drowsiness < 0.35 && position.z > -1.2) {
2779
+ kind = "butterfly";
2780
+ nextButterfly = clock + BUTTERFLY_RULES.cooldownMin + encounterRange(0, BUTTERFLY_RULES.cooldownSpread);
2781
+ }
2782
+ lastEpisode = kind;
2783
+ decisions++;
2784
+ if (kind === "checkin")
2785
+ lastCheckin = clock;
2786
+ if (kind === "roll")
2787
+ episodeCooldowns.roll = 90;
2788
+ if (kind === "play")
2789
+ episodeCooldowns.play = 120;
2790
+ if (kind === "belly")
2791
+ episodeCooldowns.belly = 80;
2792
+ if (kind === "sleep")
2793
+ episodeCooldowns.sleep = 100;
2794
+ queue = [wait(range(3, 9), "pause"), ...EPISODES[kind]()];
2795
+ }
2796
+ const landed = () => ball?.mode === "flight" && ball.path.at(clock - ball.start).settled;
2797
+ const arrived = () => {
2798
+ const s2 = actor.sample(clock), target = fetchPlan.pickupRoot;
2799
+ return SAFE.has(actor.state) && Math.hypot(s2.x - target.x, s2.z - target.z) < ARRIVE;
2800
+ };
2801
+ const fetchGo = typeof actor.chase === "function" ? "chase" : "go";
2802
+ const directionalPickup = () => {
2803
+ const s2 = actor.sample(clock);
2804
+ return actor.canPickup === true && (s2.seatProgress ?? 0) === 0 && ["rear", "front"].includes(s2.view);
2805
+ };
2806
+ const BALL_STEPS = () => [
2807
+ cmd(fetchGo, [fetchPlan.first.x, fetchPlan.first.z, { scramble: fetchPlan.pattern === "tumble" }], "fetch"),
2808
+ ...fetchPlan.pattern === "miss" ? [wait(0.12, "miss"), cmd(fetchGo, [fetchPlan.pickupRoot.x, fetchPlan.pickupRoot.z], "reChase")] : [],
2809
+ ...fetchPlan.pattern === "tumble" && !actor.canSinglePassScramble ? [...actor.canMovingScramble ? [] : [cmd("scramble", [], "tumble")], cmd(fetchGo, [fetchPlan.pickupRoot.x, fetchPlan.pickupRoot.z], "reChase")] : [],
2810
+ { until: () => landed() && arrived() },
2811
+ fx(() => {
2812
+ ball = { mode: "ground", ...fetchPlan.stop };
2813
+ }),
2814
+ cmd("pickup", [], "pickup", directionalPickup),
2815
+ cmd("sit", [{ urgent: true }], "pickup", () => !directionalPickup() && unseated()),
2816
+ { ...play("ball-pickup", "pickup"), when: () => !directionalPickup() },
2817
+ fx(() => {
2818
+ ball = { mode: "carried" };
2819
+ }),
2820
+ cmd(fetchGo, [HOME.x, HOME.z, { returnMode: fetchPlan.returnMode }], "carry"),
2821
+ cmd("sit", [{ urgent: true }], "drop", unseated),
2822
+ play("ball-drop", "drop"),
2823
+ fx(() => {
2824
+ ball = { mode: "dropped", start: clock, path: createBallPath({ from: OFFER, landing: OFFER, stop: { x: OFFER.x + 0.035, z: OFFER.z + 0.06 }, flight: 0, peak: 0, deceleration: 0.2 }) };
2825
+ needs.social -= 0.4;
2826
+ }),
2827
+ wait(3, "offer"),
2828
+ fx(() => {
2829
+ ball = null;
2830
+ fetchPlan = null;
2831
+ })
2832
+ ];
2833
+ const TREAT_STEPS = () => [
2834
+ fx(() => {
2835
+ treat = true;
2836
+ }),
2837
+ cmd("sit", [], "treatSit", () => !seatedAnyVariant()),
2838
+ play("treat-eat", "chew"),
2839
+ fx(() => {
2840
+ treat = null;
2841
+ needs.energy += 0.25;
2842
+ needs.social -= 0.3;
2843
+ })
2844
+ ];
2845
+ const TUG_STEPS = () => [
2846
+ cmd("sit", [], "tug", unseated),
2847
+ cmd("tug", [6], "tug"),
2848
+ wait(0.5, "watch"),
2849
+ fx(() => {
2850
+ needs.energy -= 0.12;
2851
+ needs.social -= 0.4;
2852
+ })
2853
+ ];
2854
+ function startInteraction() {
2855
+ active = pending;
2856
+ pending = null;
2857
+ decisions++;
2858
+ hold = 0;
2859
+ queue = active === "ball" ? BALL_STEPS() : active === "tug" ? TUG_STEPS() : TREAT_STEPS();
2860
+ }
2861
+ function finishInteraction() {
2862
+ cooldowns[active] = COOLDOWN[active];
2863
+ if (active === "ball")
2864
+ lastCheckin = clock;
2865
+ active = null;
2866
+ }
2867
+ function drift(dt) {
2868
+ const resting = holdIntent === "rest" || holdIntent === "recover" || actor.state === "sleep" || intent === "belly";
2869
+ needs.energy += resting ? 0.04 * dt : -0.004 * dt;
2870
+ const asleep = actor.state === "sleep";
2871
+ if (!active && !pending && !asleep) {
2872
+ const position = actor.sample(clock);
2873
+ nearSeconds = position.z > -0.5 && Math.abs(position.x) < 0.32 ? nearSeconds + dt : 0;
2874
+ } else
2875
+ nearSeconds = 0;
2876
+ needs.drowsiness += asleep ? -dt / 60 : dt / 240;
2877
+ quiet = active || pending || asleep ? 0 : quiet + dt;
2878
+ if (!active) {
2879
+ needs.curiosity += 0.01 * dt;
2880
+ needs.social += 0.008 * dt;
2881
+ }
2882
+ for (const k in needs)
2883
+ needs[k] = clamp2(needs[k], 0, 1);
2884
+ }
2885
+ function update(dt) {
2886
+ if (typeof dt !== "number" || !Number.isFinite(dt) || dt < 0)
2887
+ throw RangeError("dt must be a finite non-negative number");
2888
+ dt = Math.min(dt, MAX_DT);
2889
+ clock += dt;
2890
+ for (const k in cooldowns)
2891
+ cooldowns[k] = Math.max(0, cooldowns[k] - dt);
2892
+ for (const k in episodeCooldowns)
2893
+ episodeCooldowns[k] = Math.max(0, episodeCooldowns[k] - dt);
2894
+ drift(dt);
2895
+ if (butterfly && clock - butterfly.start > BUTTERFLY_RULES.maxVisible) {
2896
+ butterfly = null;
2897
+ queue = [sit("recover"), wait(3, "recover")];
2898
+ hold = 0;
2899
+ holdIntent = null;
2900
+ gesture = null;
2901
+ waiting = null;
2902
+ actor.rest();
2903
+ }
2904
+ const safe = SAFE.has(actor.state);
2905
+ if (waiting) {
2906
+ waiting.age += dt;
2907
+ if (!safe)
2908
+ waiting.left = true;
2909
+ if (!safe || !waiting.left && waiting.age < SETTLE_FALLBACK)
2910
+ return;
2911
+ waiting = null;
2912
+ }
2913
+ if (hold > 0) {
2914
+ hold -= dt;
2915
+ if (gesture)
2916
+ gesture.elapsed += dt;
2917
+ if (pending && !active && safe)
2918
+ hold = 0;
2919
+ if (hold > 0)
2920
+ return;
2921
+ }
2922
+ holdIntent = null;
2923
+ gesture = null;
2924
+ if (!safe)
2925
+ return;
2926
+ if (pending && !active)
2927
+ startInteraction();
2928
+ if (notice?.phase === "queued" && !active && !pending) {
2929
+ notice.phase = "showing";
2930
+ const far = () => {
2931
+ const s2 = actor.sample(clock);
2932
+ return Math.hypot(s2.x - HOME.x, s2.z - HOME.z) > 0.8;
2933
+ };
2934
+ queue = [
2935
+ sit("notifySit"),
2936
+ wait(0.45, "notifyContact"),
2937
+ fx(() => {
2938
+ if (far())
2939
+ notice.phase = "approaching";
2940
+ }),
2941
+ cmd("go", [HOME.x, HOME.z], "notifyCome", far),
2942
+ sit("notifySit"),
2943
+ fx(() => {
2944
+ notice.phase = "showing";
2945
+ }),
2946
+ wait(0.6, "notifyContact"),
2947
+ ...notice.gesture === "wall" ? [cmd("attentionWall", [], "notifyWall"), wait(0.3, "notifyContact")] : actor.canAttentionPaw ? [cmd("attentionPaw", [], "notifyPaw"), wait(0.3, "notifyContact")] : [],
2948
+ cmd("look", [notice.side], "notify"),
2949
+ wait(2.5, "notify"),
2950
+ fx(() => {
2951
+ notice = null;
2952
+ })
2953
+ ];
2954
+ }
2955
+ for (;; ) {
2956
+ if (!queue.length) {
2957
+ if (active) {
2958
+ finishInteraction();
2959
+ if (pending)
2960
+ startInteraction();
2961
+ else
2962
+ decide();
2963
+ } else
2964
+ decide();
2965
+ }
2966
+ const step = queue.shift();
2967
+ if (step.fx) {
2968
+ step.fx();
2969
+ for (const k in needs)
2970
+ needs[k] = clamp2(needs[k], 0, 1);
2971
+ continue;
2972
+ }
2973
+ if (step.until) {
2974
+ if (step.until())
2975
+ continue;
2976
+ queue.unshift(step);
2977
+ return;
2978
+ }
2979
+ if (step.when && !step.when())
2980
+ continue;
2981
+ intent = step.intent;
2982
+ label = step.label;
2983
+ if (step.hold !== undefined) {
2984
+ hold = step.hold;
2985
+ holdIntent = step.intent;
2986
+ if (step.gesture)
2987
+ gesture = { clip: step.gesture, duration: step.hold, elapsed: 0 };
2988
+ return;
2989
+ }
2990
+ if (actor[step.cmd](...typeof step.args === "function" ? step.args() : step.args) !== false)
2991
+ waiting = { age: 0, left: false };
2992
+ return;
2993
+ }
2994
+ }
2995
+ function interact(kind) {
2996
+ if (!["ball", "treat", "tug"].includes(kind))
2997
+ return false;
2998
+ if (kind === "tug" && typeof actor.tug !== "function")
2999
+ return false;
3000
+ if (pending || active || cooldowns[kind] > 0)
3001
+ return false;
3002
+ clearNotice();
3003
+ quiet = 0;
3004
+ if (butterfly || lastEpisode === "butterfly") {
3005
+ butterfly = null;
3006
+ queue = [];
3007
+ hold = 0;
3008
+ holdIntent = null;
3009
+ gesture = null;
3010
+ waiting = null;
3011
+ }
3012
+ if (kind === "treat" || kind === "tug") {
3013
+ pending = kind;
3014
+ queue = [];
3015
+ hold = 0;
3016
+ holdIntent = null;
3017
+ gesture = null;
3018
+ waiting = null;
3019
+ if (kind === "treat")
3020
+ treat = true;
3021
+ if (typeof actor.attend === "function")
3022
+ actor.attend();
3023
+ else if (["sleep", "belly"].includes(actor.sample(clock).state))
3024
+ actor.sit();
3025
+ return true;
3026
+ }
3027
+ const throwPosition = actor.sample(clock), pattern = chooseFetch(throwPosition.z >= -1.09);
3028
+ const landing = { ...BALL_TARGET }, stop = { ...BALL_TARGET };
3029
+ if (pattern !== "clean")
3030
+ stop.z -= 2.5 + toyRand() * 2;
3031
+ let first = pattern === "miss" ? { ...landing } : { ...stop };
3032
+ if (pattern === "tumble") {
3033
+ if (actor.canSinglePassScramble) {
3034
+ Object.assign(landing, { x: throwPosition.x, z: throwPosition.z - 1.15 });
3035
+ Object.assign(stop, { x: throwPosition.x, z: landing.z - 1.1 });
3036
+ first = { ...stop };
3037
+ } else {
3038
+ const sign = throwPosition.x > 0.1 ? -1 : throwPosition.x < -0.1 ? 1 : toyRand() < 0.5 ? -1 : 1, z = -1.25, radius = roomRadius(z);
3039
+ Object.assign(landing, { x: sign * radius * 0.1, z });
3040
+ Object.assign(stop, { x: sign * radius * 0.28, z });
3041
+ first = { x: sign * radius * 0.75, z };
3042
+ }
3043
+ }
3044
+ let pickupRoot = { ...stop };
3045
+ if (actor.canPickup === true) {
3046
+ const from = Math.hypot(first.x - stop.x, first.z - stop.z) < 0.000001 ? throwPosition : first, dx = stop.x - from.x, dz2 = stop.z - from.z, d = Math.hypot(dx, dz2);
3047
+ if (d > 0.001) {
3048
+ const target = paperGroundTarget(stop.x - dx / d * 0.17, stop.z - dz2 / d * 0.17);
3049
+ pickupRoot = { x: target[0], z: target[1] };
3050
+ }
3051
+ if (pattern === "clean" || pattern === "rolling" || pattern === "tumble" && actor.canSinglePassScramble)
3052
+ first = { ...pickupRoot };
3053
+ }
3054
+ const returns = actor.canCarryTrot ? FETCH_RETURN_STYLES.slice(1).filter((mode) => mode !== lastReturnMode) : ["brake"];
3055
+ const returnMode = returnStyle === "auto" ? returns[Math.floor(returnRand() * returns.length)] : returnStyle;
3056
+ lastReturnMode = returnMode;
3057
+ fetchPlan = { pattern, stop, first, pickupRoot, returnMode };
3058
+ active = "ball";
3059
+ decisions++;
3060
+ ball = { mode: "flight", start: clock, path: createBallPath({ landing, stop }) };
3061
+ queue = BALL_STEPS();
3062
+ hold = 0;
3063
+ holdIntent = null;
3064
+ gesture = null;
3065
+ waiting = null;
3066
+ intent = "fetch";
3067
+ label = LABEL.fetch;
3068
+ const urgent = typeof actor.chase === "function";
3069
+ if ((urgent || SAFE.has(actor.state)) && actor[urgent ? "chase" : "go"](first.x, first.z, { scramble: pattern === "tumble" }) !== false)
3070
+ queue.shift();
3071
+ return true;
3072
+ }
3073
+ function clearNotice() {
3074
+ if (!notice)
3075
+ return;
3076
+ const approaching = notice.phase === "approaching";
3077
+ notice = null;
3078
+ queue = [];
3079
+ hold = 0;
3080
+ holdIntent = null;
3081
+ gesture = null;
3082
+ waiting = null;
3083
+ intent = "recover";
3084
+ label = LABEL.recover;
3085
+ if (approaching || actor.state === "attention-paw" || actor.state === "attention-wall")
3086
+ actor.attend?.();
3087
+ }
3088
+ function notify({ kind, side: side2 = "left", gesture: gesture2 = "auto" } = {}) {
3089
+ if (!["approval", "input"].includes(kind) || !["left", "right"].includes(side2) || !["auto", "paw", "wall"].includes(gesture2))
3090
+ throw Error("Invalid pet notice");
3091
+ if (active || pending || notice)
3092
+ return false;
3093
+ if (gesture2 === "wall" && !actor.canAttentionWall)
3094
+ return false;
3095
+ const selected = gesture2 === "auto" ? actor.canAttentionWall && noticeCount % 2 === 1 ? "wall" : "paw" : gesture2;
3096
+ noticeCount++;
3097
+ notice = { kind, side: side2, gesture: selected, phase: "queued" };
3098
+ butterfly = null;
3099
+ queue = [];
3100
+ hold = 0;
3101
+ holdIntent = null;
3102
+ gesture2 = null;
3103
+ waiting = null;
3104
+ actor.attend?.();
3105
+ return true;
3106
+ }
3107
+ function treatReport() {
3108
+ if (!treat)
3109
+ return null;
3110
+ const s2 = actor.sample(clock);
3111
+ return { x: s2.x, z: s2.z + TREAT_OFFSET.z, height: TREAT_OFFSET.height };
3112
+ }
3113
+ function ballReport() {
3114
+ if (!ball)
3115
+ return null;
3116
+ const pose = actor.sample(clock);
3117
+ if (pose.state === "pickup" && pose.view === "rear")
3118
+ return null;
3119
+ if (pose.state === "pickup" && pose.comfortPose?.progress >= 0.5)
3120
+ return { x: pose.x, z: pose.z, height: CARRY_HEIGHT, carried: true };
3121
+ if (ball.mode === "flight" || ball.mode === "dropped")
3122
+ return { ...ball.path.at(clock - ball.start), carried: false };
3123
+ if (ball.mode === "carried") {
3124
+ const s2 = actor.sample(clock);
3125
+ return { x: s2.x, z: s2.z, height: CARRY_HEIGHT, carried: true };
3126
+ }
3127
+ return { x: ball.x, z: ball.z, height: 0, carried: false };
3128
+ }
3129
+ return {
3130
+ update,
3131
+ interact,
3132
+ notify,
3133
+ clearNotice,
3134
+ setReturnStyle(style) {
3135
+ validateReturnStyle(style);
3136
+ returnStyle = style;
3137
+ return style;
3138
+ },
3139
+ get report() {
3140
+ const interaction = active ?? pending;
3141
+ return {
3142
+ intent: actor.state === "scramble" && fetchPlan ? "tumble" : intent,
3143
+ label: pending ? `${label} · ${PENDING_LABEL[pending]}` : actor.state === "scramble" && fetchPlan ? LABEL.tumble : label,
3144
+ episode: lastEpisode,
3145
+ interaction,
3146
+ busy: interaction !== null,
3147
+ ball: ballReport(),
3148
+ fetchPattern: fetchPlan?.pattern ?? null,
3149
+ returnStyle,
3150
+ returnMode: fetchPlan?.returnMode ?? null,
3151
+ treat: treatReport(),
3152
+ butterfly: butterflyPoint(),
3153
+ tug: actor.state === "tug" ? { progress: actor.sample(clock).comfortPose?.progress ?? 0, grip: 1 } : null,
3154
+ attention: notice ? { ...notice } : null,
3155
+ snore: actor.state === "sleep" ? clock % 3.6 / 3.6 : null,
3156
+ energy: needs.energy,
3157
+ drowsiness: needs.drowsiness,
3158
+ decisions,
3159
+ cooldowns: { ...cooldowns },
3160
+ gesture: gesture && SAFE.has(actor.state) ? { clip: gesture.clip, progress: clamp2(gesture.elapsed / gesture.duration, 0, 1) } : null
3161
+ };
3162
+ }
3163
+ };
3164
+ }
3165
+
3166
+ // puppy-companion-objects.mjs
3167
+ function paintCompanionObjects(pixels, width, height, projection, report, state, mouth = null) {
3168
+ const objects = [];
3169
+ if (report.gesture)
3170
+ return pixels;
3171
+ if (report.butterfly) {
3172
+ const b = report.butterfly, p = projection.project(b.x, b.height, b.z);
3173
+ const edge = projection.project(b.x + 0.04, b.height, b.z), up = projection.project(b.x, b.height + 0.035, b.z);
3174
+ const cx = p.x * width, cy = p.y * height, rx = Math.abs(edge.x - p.x) * width, ry = Math.abs(up.y - p.y) * height;
3175
+ const opening = 0.22 + 0.78 * Math.abs(Math.sin(b.phase * Math.PI)), tone = Math.round(235 * b.opacity);
3176
+ if ([cx, cy, rx, ry, opening, tone].every(Number.isFinite) && rx > 0 && ry > 0 && tone > 0)
3177
+ for (let y = Math.max(0, Math.floor(cy - ry));y < Math.min(height, Math.ceil(cy + ry)); y++)
3178
+ for (let x = Math.max(0, Math.floor(cx - rx));x < Math.min(width, Math.ceil(cx + rx)); x++) {
3179
+ const u = (x + 0.5 - cx) / rx, v = (y + 0.5 - cy) / ry, a = Math.abs(u) / opening;
3180
+ const wing = ((a - 0.5) / 0.44) ** 2 + ((v + 0.28) / 0.56) ** 2 < 1 || ((a - 0.36) / 0.29) ** 2 + ((v - 0.43) / 0.37) ** 2 < 1;
3181
+ const body = Math.abs(u) < 0.1 && Math.abs(v) < 0.7;
3182
+ if (wing || body) {
3183
+ const j = (y * width + x) * 4;
3184
+ pixels[j] = pixels[j + 1] = pixels[j + 2] = Math.max(pixels[j], body ? tone : Math.round(tone * (0.65 + 0.35 * a)));
3185
+ }
3186
+ }
3187
+ }
3188
+ if (Number.isFinite(report.snore) && state.state === "sleep") {
3189
+ const placement = projection.placement(state.x, state.z), side = height * placement.size, sideX = side / placement.pixelAspect;
3190
+ const left = width * placement.x - sideX / 2, top = height * placement.y - 300 / 320 * side;
3191
+ const cycle = report.snore, tone = Math.round(140 * Math.sin(Math.PI * cycle)), unit = Math.max(1, Math.round(side / 160));
3192
+ const cx = Math.round(left + 220 / 320 * sideX), cy = Math.round(top + (210 - 16 * cycle) / 320 * side);
3193
+ if (tone > 12)
3194
+ for (const [gx, gy] of [[0, 0], [1, 0], [2, 0], [1, 1], [0, 2], [1, 2], [2, 2]])
3195
+ for (let dy2 = 0;dy2 < unit; dy2++)
3196
+ for (let dx = 0;dx < unit; dx++) {
3197
+ const x = cx + gx * unit + dx, y = cy + gy * unit + dy2;
3198
+ if (x < 0 || x >= width || y < 0 || y >= height)
3199
+ continue;
3200
+ const j = (y * width + x) * 4;
3201
+ pixels[j] = pixels[j + 1] = pixels[j + 2] = Math.max(pixels[j], tone);
3202
+ }
3203
+ }
3204
+ if (report.ball?.carried) {
3205
+ if (mouth && [mouth.x, mouth.y, mouth.rx, mouth.ry].every(Number.isFinite) && mouth.rx > 0 && mouth.ry > 0)
3206
+ objects.push({ kind: "ball", screen: mouth });
3207
+ } else if (report.ball)
3208
+ objects.push({ ...report.ball, radius: 0.035, kind: "ball" });
3209
+ if (report.treat)
3210
+ objects.push({ ...report.treat, height: report.treat.height ?? 0.018, radius: 0.022, kind: "treat" });
3211
+ for (const o of objects) {
3212
+ let cx, cy, rx, ry;
3213
+ if (o.screen)
3214
+ ({ x: cx, y: cy, rx, ry } = o.screen);
3215
+ else {
3216
+ const p = projection.project(o.x, o.height ?? 0, o.z), edge = projection.project(o.x + o.radius, o.height ?? 0, o.z);
3217
+ cx = p.x * width;
3218
+ cy = p.y * height;
3219
+ rx = Math.abs(edge.x - p.x) * width;
3220
+ const up = projection.project(o.x, (o.height ?? 0) + o.radius, o.z);
3221
+ ry = Math.abs(up.y - p.y) * height;
3222
+ }
3223
+ if (![cx, cy, rx, ry].every(Number.isFinite) || rx <= 0 || ry <= 0)
3224
+ continue;
3225
+ const spin = o.spin ?? 0, cos = Math.cos(spin), sin = Math.sin(spin);
3226
+ for (let y = Math.max(0, Math.floor(cy - ry));y < Math.min(height, Math.ceil(cy + ry)); y++)
3227
+ for (let x = Math.max(0, Math.floor(cx - rx));x < Math.min(width, Math.ceil(cx + rx)); x++) {
3228
+ const u = (x + 0.5 - cx) / rx, v = (y + 0.5 - cy) / ry;
3229
+ if (u * u + v * v > 1)
3230
+ continue;
3231
+ const stripe = v * cos + u * sin + 0.25 * (u * cos - v * sin);
3232
+ const tone = o.kind === "treat" ? (x + y) % 3 ? 190 : 85 : Math.abs(stripe) < 0.18 ? 85 : 225;
3233
+ const j = (y * width + x) * 4;
3234
+ pixels[j] = pixels[j + 1] = pixels[j + 2] = tone;
3235
+ }
3236
+ }
3237
+ return pixels;
3238
+ }
3239
+
3240
+ // retrieviewer.mjs
3241
+ init_puppy_frame_store();
3242
+ init_puppy_frame_indexed();
3243
+
3244
+ // puppy-motion-tables.generated.mjs
3245
+ var freeze = (v) => {
3246
+ if (v && typeof v === "object") {
3247
+ for (const x of Object.values(v))
3248
+ freeze(x);
3249
+ Object.freeze(v);
3250
+ }
3251
+ return v;
3252
+ };
3253
+ var BAKED_MOTION = freeze({ version: 1, sources: { "puppy-stride-meta.json": "30fd474f3ed9337851b2071037d88d60b37cb2bb1c12d48b28c34ebc126b7e3a", "puppy-paper-rear-gallop.json": "fe9b1bf0b88f2a366f28deee6cb63964b8a94d6faa99b111524112832e80705d", "puppy-paper-approach.json": "e7e02589622cecde679de79bb79c71b77494fd2fd850793c2fba9754b46aa474", "puppy-paper-actions-v9.json": "38b35496c552443c7ea99beceac3cd25c670bc91836b22509b1cc97da3d0a9de", "puppy-paper-behaviors.json": "67d8633bc61a2d1c46206969b9aeaaf30073fcc0f0ae7dc9954957d5522edb32", "puppy-companion-comfort.json": "23ed137983ce5a653936a829932f41a16099378d7c0ad99b3f51a2e6091fc6bf", "puppy-paper-actions-v9-q5.bin.gz": "abb4677468206c62937c0247dc4c5823e0a7c4a3a197da40194a08dd594aeef9", "puppy-paper-rear-gallop-q5.bin.gz": "ca731e1979a37ec2e005a80365e3b58358f640b8b26e7036b0e931940a7b1264", "puppy-companion-comfort-q5.bin.gz": "57942c55e9c467d973a08f1e23804b0f756eaed46dddfe3aba7e7c3621996236", "puppy-paper-arrival-seat-q5.bin.gz": "7effa0a2150e8ef9ed37d5b6cb10089672e0ce32599c06db21ad4769144bf246", "puppy-paper-seat-q5.bin.gz": "5480c994de0d034d70eff6ea86095f3722cf87bde72f364dd369b94b1ad66f84", "puppy-paper-behaviors-q5.bin.gz": "e4b79387aca974ceec74a02dc3f02780eb53d38af6ec9ebd69994de744b39716" }, support: { front: { step: 16, period: 128, cyclic: true, offsets: { "0": 0, "1": 16, "2": 32, "3": 48, "4": 64, "5": 80, "6": 96, "7": 112 }, starts: { "hind-near": [123, 123, 123, 123, 123, 123, 123, 123, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 123, 123, 123, 123, 123], "fore-far": [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], "fore-near": [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], "hind-far": [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1] } }, rear: { step: 8, period: 64, cyclic: true, offsets: { "5": 0, "6": 8, "7": 16, "8": 24, "9": 32, "10": 40, "11": 48, "12": 56 }, starts: { "hind-far": [60, 60, 60, 60, 60, 60, 60, 60, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 60, 60, 60, 60], "hind-near": [62, 62, 62, 62, 62, 62, 62, 62, 62, 62, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, 62] } }, approach: { step: 8, period: 64, cyclic: true, offsets: { "5": 0, "6": 8, "7": 16, "8": 24, "9": 32, "10": 40, "11": 48, "12": 56 }, starts: { "fore-left": [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 39, 39, 39, 39, 39, 39, -1, -1, -1, -1, -1, -1, -1, -1, -1, 54, 54, 54, 54, 54, -1, -1, -1, -1, -1], "fore-right": [61, 61, 61, 61, 61, 61, 61, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, -1, -1, -1, -1, 55, 55, 55, 55, 55, -1, 61, 61, 61] } }, walk: { step: 8, period: 64, cyclic: true, offsets: { "42": 0, "43": 8, "44": 16, "45": 24, "46": 32, "47": 40, "48": 48, "49": 56 }, starts: { "hind-far": [52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52], "hind-near": [58, 58, 58, 58, 58, 58, 58, 58, 58, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 58, 58, 58, 58, 58, 58], "fore-far": [-1, -1, -1, 3, 3, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 21, 21, 21, 21, 21, -1, -1, -1, -1, -1, 31, 31, 31, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 47, 47, 47, 47, 47, -1, -1, -1, -1, -1, -1, -1, 59, 59, 59, -1, -1], "fore-near": [-1, -1, -1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, -1, -1, -1, -1, -1, -1, -1, 33, -1, -1, -1, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, -1, -1] } }, "behavior/walk": { step: 8, period: 32, cyclic: true, offsets: { "0": 0, "1": 8, "2": 16, "3": 24 }, starts: { "fore-left": [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24, 24, -1, -1, -1, -1, -1, -1], "fore-right": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1] } }, "behavior/walk-enter": { step: 8, period: 8, cyclic: false, offsets: { "4": 0 }, starts: { "fore-left": [-1, -1, -1, -1, -1, -1, -1, -1, -1], "fore-right": [-1, -1, -1, -1, 4, 4, 4, 4, 4] } }, "behavior/brake": { step: 8, period: 32, cyclic: false, offsets: { "5": 0, "6": 8, "7": 16, "8": 24 }, starts: { "fore-left": [-1, -1, -1, -1, -1, -1, -1, -1, -1, 9, 9, 9, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23], "fore-right": [0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -1, 15, 15, 15, 15, 15, 15, 15, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1] } }, "behavior/finish": { step: 8, period: 8, cyclic: false, offsets: { "9": 0 }, starts: {} }, "behavior/watch-left": { step: 8, period: 16, cyclic: false, offsets: { "10": 0, "11": 8 }, starts: {} }, "behavior/watch-right": { step: 8, period: 16, cyclic: false, offsets: { "12": 0, "13": 8 }, starts: {} }, "behavior/rest": { step: 8, period: 32, cyclic: false, offsets: { "14": 0, "15": 8, "16": 16, "17": 24 }, starts: {} } }, actionClips: { spin: { list: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], weights: [1.1168, 0.8052, 0.827, 0.7936, 1.3421, 1.4204, 0.9015, 0.5275, 1.1081, 0.7701, 0.723, 1.0494, 1.4422, 1.3325, 1.0787, 0.8964, 0.8655], total: 17 }, roll: { list: [17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40], weights: [1.122, 1.4089, 1.6479, 0.6752, 0.753, 0.8756, 0.9957, 0.9586, 0.7325, 0.8218, 1.0968, 0.9962, 0.8959, 0.6589, 1.0191, 0.8352, 0.4557, 1.1818, 1.1101, 0.905, 0.6752, 1.6479, 1.4089, 1.122], total: 23.9999 }, "roll-down": { list: [19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40], weights: [1.6886, 0.6919, 0.7717, 0.8973, 1.0203, 0.9823, 0.7506, 0.8422, 1.124, 1.0208, 0.918, 0.6752, 1.0443, 0.8559, 0.467, 1.211, 1.1376, 0.9274, 0.6919, 1.6886, 1.4438, 1.1498], total: 22.0002 }, "lie-down": { list: [17, 18], weights: [0.8866, 1.1134], total: 2 }, "lie-up": { list: [39, 40], weights: [1.1134, 0.8866], total: 2 }, "walk-enter": { list: [41], weights: [1], total: 1 }, walk: { list: [42, 43, 44, 45, 46, 47, 48, 49], weights: [1, 1, 1, 1, 1, 1, 1, 1], total: 8 }, "walk-exit": { list: [50], weights: [1], total: 1 }, "walk-face-front": { list: [51], weights: [1], total: 1 }, "walk-face-rear": { list: [52], weights: [1], total: 1 }, "walk-turn": { list: [53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65], weights: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], total: 13 } }, routes: { maxPreparationSpeed: 0.00000001, steps: [["ready"], ["turn", true], ["view-turn", "rear"], ["view-turn", "approach"], ["turn", false], ["view-turn", "front"], ["rise"], ["sit-down"]], direction: [0, 1, 2, 2, 3, 3, 4, 0, 2, 2, 3, 3, 5, 5, 0, 0, 5, 5, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 5, 0, 0], action: [0, 3, 6, 3, 6, 3, 4, 4, 6, 4, 6, 4, 5, 5, 6, 5, 6, 5, 5, 5, 6, 5, 6, 5, 5, 7, 6, 7, 6, 0, 5, 5, 6, 5, 6, 5] }, joins: { "walk-enter/walk": { from: ["puppy-paper-actions-v9", 377], to: ["puppy-paper-actions-v9", 378], poseSha256: "81361614df56226b64be437f189c2e135c13498324286880db0fc06c5df3e86a", view: "front", speed: [0, 0], supportEvidence: "shared-pixels-with-labeled-walk-pose", supports: ["hind-far", "hind-near"], phase: 0 }, "walk/walk-exit": { from: ["puppy-paper-actions-v9", 449], to: ["puppy-paper-actions-v9", 450], poseSha256: "81361614df56226b64be437f189c2e135c13498324286880db0fc06c5df3e86a", view: "front", speed: [0, 0], supportEvidence: "shared-pixels-with-labeled-walk-pose", supports: ["hind-far", "hind-near"], phase: 0 }, "rear-run/scramble": { from: ["puppy-paper-rear-gallop", 63], to: ["puppy-companion-comfort", 25], poseSha256: "fa79a8762b57c0d5b70586a687f10e4a313e227e11e2435f6d7af0cd0866cc83", view: "rear", phase: 0.25, speed: [0, 4.556913271064087], positiveSpeed: true, matchVelocity: true, supports: [], supportEvidence: "source-paw-proxies", airborne: true }, "scramble/pickup-rear": { from: ["puppy-companion-comfort", 36], to: ["puppy-companion-comfort", 13], poseSha256: "8192c504552a3984c80e1c3216e134605d4a930ec14cd7f778036e629e334601", view: "rear", speed: [0, 0], supports: ["hind-far", "hind-near"], supportEvidence: "shared-pixels-with-labeled-rear-pose" }, "carry-settle/arrival-seat": { from: ["puppy-companion-comfort", 44], to: ["puppy-paper-arrival-seat", 0], poseSha256: "5ccf5acd9ca9ea81a0c9fc59d1ab2a2ddab148c942e31009f1a4c18de347fae8", view: "approach", speed: [0, 0], supportEvidence: "shared-pose-only" }, "tug/seat": { from: ["puppy-companion-comfort", 0], to: ["puppy-paper-seat", 50], poseSha256: "19f89a8bcb5a937e528593a8f36db846a0d45a5ee398f1b0873c056e5be2fa0a", view: "approach", speed: [0, 0], supportEvidence: "shared-pose-only" }, "watch-90/seat": { from: ["puppy-paper-behaviors", 90], to: ["puppy-paper-seat", 50], poseSha256: "19f89a8bcb5a937e528593a8f36db846a0d45a5ee398f1b0873c056e5be2fa0a", view: "approach", speed: [0, 0], supportEvidence: "shared-pose-only" }, "watch-107/seat": { from: ["puppy-paper-behaviors", 107], to: ["puppy-paper-seat", 50], poseSha256: "19f89a8bcb5a937e528593a8f36db846a0d45a5ee398f1b0873c056e5be2fa0a", view: "approach", speed: [0, 0], supportEvidence: "shared-pose-only" }, "watch-108/seat": { from: ["puppy-paper-behaviors", 108], to: ["puppy-paper-seat", 50], poseSha256: "19f89a8bcb5a937e528593a8f36db846a0d45a5ee398f1b0873c056e5be2fa0a", view: "approach", speed: [0, 0], supportEvidence: "shared-pose-only" }, "watch-125/seat": { from: ["puppy-paper-behaviors", 125], to: ["puppy-paper-seat", 50], poseSha256: "19f89a8bcb5a937e528593a8f36db846a0d45a5ee398f1b0873c056e5be2fa0a", view: "approach", speed: [0, 0], supportEvidence: "shared-pose-only" } } });
3254
+
3255
+ // retrieviewer.mjs
3256
+ var STEP_SECONDS = 1 / 60;
3257
+ var MAX_CATCH_UP_SECONDS = 0.1;
3258
+ var INTERACTIONS = Object.freeze(["ball", "treat", "tug"]);
3259
+ var DEFAULT_ART_PROFILE = "q5";
3260
+ var ART_PROFILES = Object.freeze(["original", "q5"]);
3261
+ var PET_ASSETS = Object.freeze({
3262
+ animal: "locomotion-puppy.animal",
3263
+ stride: "puppy-stride-tweens-q5.rvz",
3264
+ strideInfo: "puppy-stride-meta.json",
3265
+ stableInfo: "puppy-stable-meta.json",
3266
+ rear: "puppy-paper-rear-gallop",
3267
+ approach: "puppy-paper-approach",
3268
+ seat: "puppy-paper-seat",
3269
+ arrivalSeat: "puppy-paper-arrival-seat",
3270
+ behaviors: "puppy-paper-behaviors",
3271
+ actions: "puppy-paper-actions-v9",
3272
+ interactions: "puppy-companion-interactions-v3",
3273
+ comfort: "puppy-companion-comfort"
3274
+ });
3275
+ var BUNDLES = ["rear", "approach", "seat", "arrivalSeat", "behaviors", "actions", "interactions", "comfort"];
3276
+ async function defaultLoadAsset(name) {
3277
+ const url = new URL(name, import.meta.url);
3278
+ if (url.protocol === "file:") {
3279
+ const { readFile } = await import("node:fs/promises");
3280
+ try {
3281
+ return await readFile(url);
3282
+ } catch (error) {
3283
+ if (error.code === "ENOENT" && name.endsWith("-compact.bin.gz"))
3284
+ throw Error("Original art is optional and not bundled. Supply the preserved original assets with loadAsset, or use the default q5 profile.", { cause: error });
3285
+ throw error;
3286
+ }
3287
+ }
3288
+ const r = await fetch(url);
3289
+ if (!r.ok)
3290
+ throw Error(`${name} HTTP ${r.status}`);
3291
+ return r.arrayBuffer();
3292
+ }
3293
+ var sharedAssets = new Map;
3294
+ function heldFrameKey(state, report, { columns, rows, cellAspect = 0.5, camera, poseFps = 24, contactEnabled = true } = {}) {
3295
+ const seated = state.state === "idle" && state.seatProgress === 1 && !state.behavior && !state.actionName && !state.comfortPose;
3296
+ const down = state.state === "down" && (state.actionName === "lie-down" && state.actionProgress === 1 || state.behavior === "rest");
3297
+ if (!seated && !down || report.gesture || report.butterfly || report.snore !== null && report.snore !== undefined)
3298
+ return null;
3299
+ return JSON.stringify([
3300
+ columns,
3301
+ rows,
3302
+ cellAspect,
3303
+ poseFps,
3304
+ !!contactEnabled,
3305
+ camera?.viewportWidth,
3306
+ camera?.viewportHeight,
3307
+ camera?.rect?.left,
3308
+ camera?.rect?.top,
3309
+ camera?.rect?.width,
3310
+ camera?.rect?.height,
3311
+ state.state,
3312
+ state.x,
3313
+ state.z,
3314
+ state.bodyLift ?? 0,
3315
+ state.view,
3316
+ state.flip,
3317
+ state.seatProgress,
3318
+ state.seatVariant,
3319
+ state.actionName,
3320
+ down ? state.actionProgress : 0,
3321
+ state.behavior,
3322
+ state.behaviorProgress,
3323
+ report.ball,
3324
+ report.treat
3325
+ ]);
3326
+ }
3327
+ function petOptions({ loadAsset = defaultLoadAsset, seed, energy, pawTransport = true, artProfile = DEFAULT_ART_PROFILE, returnStyle = "auto" } = {}) {
3328
+ if (typeof loadAsset !== "function")
3329
+ throw TypeError("loadAsset must be a function");
3330
+ if (seed === undefined)
3331
+ seed = globalThis.crypto?.getRandomValues ? globalThis.crypto.getRandomValues(new Uint32Array(1))[0] : 731;
3332
+ if (!Number.isInteger(seed) || seed < 0 || seed > 4294967295)
3333
+ throw RangeError("seed must be a uint32");
3334
+ if (energy !== undefined && (!Number.isFinite(energy) || energy < 0 || energy > 1))
3335
+ throw RangeError("energy must be within 0..1");
3336
+ if (typeof pawTransport !== "boolean")
3337
+ throw TypeError("pawTransport must be boolean");
3338
+ if (!ART_PROFILES.includes(artProfile))
3339
+ throw RangeError("artProfile must be original or q5");
3340
+ if (!FETCH_RETURN_STYLES.includes(returnStyle))
3341
+ throw RangeError("returnStyle must be auto, brake, trot or run-trot");
3342
+ return { loadAsset, seed, energy, pawTransport, artProfile, returnStyle };
3343
+ }
3344
+ function createPetFromAssets(options, lease, kernel = null) {
3345
+ const { loadAsset, seed, energy, pawTransport, artProfile, returnStyle } = petOptions(options);
3346
+ let data = lease.data;
3347
+ const release = lease.release;
3348
+ lease = null;
3349
+ let { cache, info, meta } = data;
3350
+ const assets = { ...data.assets };
3351
+ data = null;
3352
+ const motionTables = loadAsset === defaultLoadAsset && artProfile === "q5" ? BAKED_MOTION : null;
3353
+ let animal, actor, life;
3354
+ try {
3355
+ if (!assets.actions.info.clips?.["lie-down"] || !assets.actions.info.clips?.["lie-up"])
3356
+ throw Error("v9 action cache lacks lie-down/lie-up clips");
3357
+ animal = createPaperBody();
3358
+ actor = createPaperActor(animal, info, { manualTricks: true, trickTimings: createSnappyTrickTimings(assets.actions.info), comfortTimings: createPaperComfortTimings(assets.actions.info), walkTransitions: true, scrambleSeconds: assets.comfort.info.scramble?.duration ?? 0, fetchMotions: assets.comfort.info.version >= 4 ? assets.comfort.info : null, motionTables });
3359
+ life = createCompanionLife(actor, { seed, returnStyle, ...energy === undefined ? {} : { energy } });
3360
+ } catch (e) {
3361
+ animal?.dispose?.();
3362
+ release();
3363
+ throw e;
3364
+ }
3365
+ let paint = null, paintKey = "", time = 0, acc = 0, disposed = false, lastHeldFrameKey = null;
3366
+ const alive = () => {
3367
+ if (disposed)
3368
+ throw Error("Pet is disposed");
3369
+ };
3370
+ return {
3371
+ step(dt) {
3372
+ alive();
3373
+ if (typeof dt !== "number" || !Number.isFinite(dt) || dt < 0)
3374
+ throw RangeError("dt must be finite non-negative seconds");
3375
+ acc += Math.min(dt, MAX_CATCH_UP_SECONDS);
3376
+ let n = 0;
3377
+ while (acc >= STEP_SECONDS - 0.000000001) {
3378
+ actor.update(STEP_SECONDS);
3379
+ life.update(STEP_SECONDS);
3380
+ time += STEP_SECONDS;
3381
+ acc = Math.max(0, acc - STEP_SECONDS);
3382
+ n++;
3383
+ }
3384
+ return n;
3385
+ },
3386
+ needsFrame(options2) {
3387
+ alive();
3388
+ const key = heldFrameKey(actor.sample(time, { poseFps: options2?.poseFps ?? 24 }), life.report, options2);
3389
+ return key === null || key !== lastHeldFrameKey;
3390
+ },
3391
+ frame({ columns, rows, cellAspect = 0.5, camera, poseFps = 24, contactEnabled = true } = {}) {
3392
+ alive();
3393
+ if (!Number.isInteger(columns) || !Number.isInteger(rows) || columns < 1 || rows < 1 || columns * rows * 8 > 1e6)
3394
+ throw RangeError("columns/rows must be positive integers within 125000 cells");
3395
+ if (typeof cellAspect !== "number" || !Number.isFinite(cellAspect) || cellAspect <= 0)
3396
+ throw RangeError("cellAspect must be a positive number");
3397
+ if (![12, 24, 30].includes(poseFps))
3398
+ throw RangeError("poseFps must be 12, 24 or 30");
3399
+ const width = columns * 2, height = rows * 4, key = `${width}x${height}`;
3400
+ if (paintKey !== key) {
3401
+ paint = createPaperRenderer(cache, info, meta, width, height, { ...assets, pawTransport, motionTables, kernel });
3402
+ paintKey = key;
3403
+ }
3404
+ const viewportWidth = camera?.viewportWidth ?? columns * cellAspect, viewportHeight = camera?.viewportHeight ?? rows;
3405
+ const projection = createPaperProjection({ viewportWidth, viewportHeight, rect: camera?.rect ?? { left: 0, top: 0, width: viewportWidth, height: viewportHeight }, width, height });
3406
+ const state = actor.sample(time, { poseFps }), report = life.report;
3407
+ if (report.gesture)
3408
+ state.interactionPose = report.gesture;
3409
+ const placement = projection.placement(state.x, state.z, state.bodyLift ?? 0), pixels = paint(state, { ...placement, contactEnabled: !!contactEnabled });
3410
+ paintCompanionObjects(pixels, width, height, projection, report, state, paint.lastMouth);
3411
+ const { text } = rgbaToBraille(pixels, width, height, { method: "stable", brightness: 1.18, contrast: 1.1, kernel });
3412
+ lastHeldFrameKey = heldFrameKey(state, report, { columns, rows, cellAspect, camera, poseFps, contactEnabled });
3413
+ return { text, columns, rows, pixels, state, report, placement, projection, artProfile, mouth: paint.lastMouth, contacts: paint.report() };
3414
+ },
3415
+ interact(kind) {
3416
+ alive();
3417
+ if (!INTERACTIONS.includes(kind))
3418
+ throw TypeError(`Unknown interaction: ${kind}`);
3419
+ return life.interact(kind);
3420
+ },
3421
+ notify(notice) {
3422
+ alive();
3423
+ return life.notify(notice);
3424
+ },
3425
+ clearNotice() {
3426
+ alive();
3427
+ life.clearNotice();
3428
+ },
3429
+ setReturnStyle(style) {
3430
+ alive();
3431
+ return life.setReturnStyle(style);
3432
+ },
3433
+ get disposed() {
3434
+ return disposed;
3435
+ },
3436
+ dispose() {
3437
+ if (disposed)
3438
+ return;
3439
+ disposed = true;
3440
+ try {
3441
+ animal.dispose?.();
3442
+ } finally {
3443
+ animal = actor = life = paint = cache = info = meta = null;
3444
+ paintKey = "";
3445
+ lastHeldFrameKey = null;
3446
+ for (const k of BUNDLES)
3447
+ assets[k] = null;
3448
+ release();
3449
+ }
3450
+ }
3451
+ };
3452
+ }
3453
+
3454
+ // integrations/purrviewer/vendor/retrieviewer/braille.mjs
3455
+ var BRAILLE_BITS2 = [[1, 8], [2, 16], [4, 32], [64, 128]];
3456
+ var BAYER2 = [[0, 8, 2, 10], [12, 4, 14, 6], [3, 11, 1, 9], [15, 7, 13, 5]];
3457
+ var clamp3 = (v) => Math.max(0, Math.min(1, v));
3458
+ var GLYPHS2 = Array.from({ length: 256 }, (_, i) => String.fromCharCode(10240 + i));
3459
+ var grayTable2 = null;
3460
+ var grayBrightness2 = 0;
3461
+ var grayContrast2 = 0;
3462
+ function stableBraille2(rgba, width, height, brightness, contrast) {
3463
+ if (grayBrightness2 !== brightness || grayContrast2 !== contrast) {
3464
+ grayTable2 = Float32Array.from({ length: 256 }, (_, v) => {
3465
+ const luma = (0.2126 * v + 0.7152 * v + 0.0722 * v) / 255;
3466
+ return luma === 0 ? 0 : clamp3((Math.pow(luma, 0.9) * brightness - 0.5) * contrast + 0.5);
3467
+ });
3468
+ grayBrightness2 = brightness;
3469
+ grayContrast2 = contrast;
3470
+ }
3471
+ const table = grayTable2, bytePixels = rgba instanceof Uint8Array || rgba instanceof Uint8ClampedArray, columns = Math.ceil(width / 2), rows = Math.ceil(height / 4), masks = new Uint8Array(columns * rows);
3472
+ let dots = 0;
3473
+ for (let y = 0;y < height; y++)
3474
+ for (let x = 0, p = y * width * 4;x < width; x++, p += 4) {
3475
+ const r = rgba[p], g = rgba[p + 1], b = rgba[p + 2], a = rgba[p + 3];
3476
+ if (a === 0 || r === 0 && g === 0 && b === 0)
3477
+ continue;
3478
+ let value;
3479
+ if (bytePixels && a === 255 && r === g && g === b)
3480
+ value = table[r];
3481
+ else {
3482
+ const luma = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255 * (a / 255);
3483
+ value = Math.fround(luma === 0 ? 0 : clamp3((Math.pow(luma, 0.9) * brightness - 0.5) * contrast + 0.5));
3484
+ }
3485
+ if (value <= 0)
3486
+ continue;
3487
+ let n = Math.imul(x + 1, 521288629) ^ Math.imul(y + 1, 1597334677);
3488
+ n = Math.imul(n ^ n >>> 16, 73244475);
3489
+ n = Math.imul(n ^ n >>> 16, 73244475);
3490
+ n = (n ^ n >>> 16) >>> 0;
3491
+ if (value > (n + 0.5) / 4294967296) {
3492
+ masks[(y >> 2) * columns + (x >> 1)] |= BRAILLE_BITS2[y & 3][x & 1];
3493
+ dots++;
3494
+ }
3495
+ }
3496
+ const lines = new Array(rows), cells = new Array(columns);
3497
+ for (let y = 0;y < rows; y++) {
3498
+ for (let x = 0;x < columns; x++)
3499
+ cells[x] = GLYPHS2[masks[y * columns + x]];
3500
+ lines[y] = cells.join("");
3501
+ }
3502
+ return { text: lines.join(`
3503
+ `), columns, rows, dots };
3504
+ }
3505
+ function rgbaToBraille2(rgba, width, height, { brightness = 1, contrast = 1, method = "floyd" } = {}) {
3506
+ if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1 || width * height > 1e6 || rgba.length !== width * height * 4)
3507
+ throw Error("Invalid RGBA image");
3508
+ if (!Number.isFinite(brightness) || brightness <= 0 || brightness > 4 || !Number.isFinite(contrast) || contrast <= 0 || contrast > 4 || !["floyd", "bayer", "stable"].includes(method))
3509
+ throw Error("Invalid dithering options");
3510
+ if (method === "stable")
3511
+ return stableBraille2(rgba, width, height, brightness, contrast);
3512
+ const values = new Float32Array(width * height), bits = new Uint8Array(width * height);
3513
+ for (let i = 0;i < values.length; i++) {
3514
+ const p = i * 4, luma = (0.2126 * rgba[p] + 0.7152 * rgba[p + 1] + 0.0722 * rgba[p + 2]) / 255 * (rgba[p + 3] / 255);
3515
+ values[i] = luma === 0 ? 0 : clamp3((Math.pow(luma, 0.9) * brightness - 0.5) * contrast + 0.5);
3516
+ }
3517
+ if (method === "bayer") {
3518
+ for (let y = 0;y < height; y++)
3519
+ for (let x = 0;x < width; x++)
3520
+ bits[y * width + x] = values[y * width + x] > (BAYER2[y % 4][x % 4] + 0.5) / 16 ? 1 : 0;
3521
+ } else {
3522
+ const add = (x, y, error, weight) => {
3523
+ if (x >= 0 && x < width && y < height)
3524
+ values[y * width + x] += error * weight / 16;
3525
+ };
3526
+ for (let y = 0;y < height; y++) {
3527
+ const direction = y % 2 ? -1 : 1;
3528
+ for (let k = 0;k < width; k++) {
3529
+ const x = direction === 1 ? k : width - 1 - k, i = y * width + x, old = values[i], value = old >= 0.5 ? 1 : 0, error = old - value;
3530
+ bits[i] = value;
3531
+ add(x + direction, y, error, 7);
3532
+ add(x - direction, y + 1, error, 3);
3533
+ add(x, y + 1, error, 5);
3534
+ add(x + direction, y + 1, error, 1);
3535
+ }
3536
+ }
3537
+ }
3538
+ const columns = Math.ceil(width / 2), rows = Math.ceil(height / 4), lines = [];
3539
+ for (let cy = 0;cy < rows; cy++) {
3540
+ let line = "";
3541
+ for (let cx = 0;cx < columns; cx++) {
3542
+ let mask = 0;
3543
+ for (let dy2 = 0;dy2 < 4; dy2++)
3544
+ for (let dx = 0;dx < 2; dx++) {
3545
+ const x = cx * 2 + dx, y = cy * 4 + dy2;
3546
+ if (x < width && y < height && bits[y * width + x])
3547
+ mask |= BRAILLE_BITS2[dy2][dx];
3548
+ }
3549
+ line += String.fromCodePoint(10240 + mask);
3550
+ }
3551
+ lines.push(line);
3552
+ }
3553
+ return { text: lines.join(`
3554
+ `), columns, rows, dots: bits.reduce((a, b) => a + b, 0) };
3555
+ }
3556
+
3557
+ // integrations/purrviewer/src/walk-cycle.mjs
3558
+ var WALK_STRIDE = 0.18141;
3559
+
3560
+ // integrations/purrviewer/src/world.mjs
3561
+ var gait2 = (stride, frequency) => Object.freeze({ stride, frequency, speed: stride * frequency });
3562
+ var CAT_GAITS = Object.freeze({
3563
+ walk: gait2(WALK_STRIDE, 0.27 / WALK_STRIDE),
3564
+ trot: gait2(0.62, 2),
3565
+ creep: gait2(WALK_STRIDE, 0.13 / WALK_STRIDE)
3566
+ });
3567
+ var GRAVITY = 9.81;
3568
+ var DECELERATION = 1.8;
3569
+ var ACCELERATION = Object.freeze({ walk: 0.65, trot: 2, creep: 0.3 });
3570
+ var coordinate = (n) => Number.isFinite(n) && Math.abs(n) <= 1e6;
3571
+ function point(x, z) {
3572
+ if (!coordinate(x) || !coordinate(z))
3573
+ throw new RangeError("Invalid cat target");
3574
+ }
3575
+ function options(value) {
3576
+ if (!value || typeof value !== "object" || Array.isArray(value))
3577
+ throw new TypeError("Invalid cat options");
3578
+ }
3579
+ function travel(length2, speed, acceleration) {
3580
+ const peak = Math.min(speed, Math.sqrt(2 * length2 / (1 / acceleration + 1 / DECELERATION)));
3581
+ const ta = peak / acceleration, td = peak / DECELERATION;
3582
+ const da = peak * ta / 2, dd = peak * td / 2;
3583
+ const tc = Math.max(0, length2 - da - dd) / peak;
3584
+ const duration = ta + tc + td;
3585
+ return { duration, at(t) {
3586
+ if (t >= duration)
3587
+ return { distance: length2, speed: 0 };
3588
+ if (t < ta)
3589
+ return { distance: acceleration * t * t / 2, speed: acceleration * t };
3590
+ if (t < ta + tc)
3591
+ return { distance: da + peak * (t - ta), speed: peak };
3592
+ const remaining = duration - t;
3593
+ return { distance: length2 - DECELERATION * remaining * remaining / 2, speed: DECELERATION * remaining };
3594
+ } };
3595
+ }
3596
+ function createCatMotion(config = {}) {
3597
+ options(config);
3598
+ let { x = 0, z = 0 } = config;
3599
+ point(x, z);
3600
+ let y = 0, speed = 0, distance = 0, phase = 0, heading = 0, currentGait = "walk";
3601
+ let active = null, pending = null, supportY = 0, beltSpeed = 0, beltDistance = 0, wheelRadius = 0.6, wheelPhase = 0;
3602
+ function advance(ds) {
3603
+ distance += ds;
3604
+ phase += ds / CAT_GAITS[currentGait].stride;
3605
+ }
3606
+ function launch(command) {
3607
+ if (command.kind === "wheel") {
3608
+ wheelRadius = command.radius;
3609
+ wheelPhase = 0;
3610
+ active = { ...command, elapsed: 0, covered: 0 };
3611
+ return;
3612
+ }
3613
+ const dx = command.x - x, dz2 = command.z - z, length2 = Math.hypot(dx, dz2);
3614
+ if (command.kind === "go")
3615
+ currentGait = command.gait;
3616
+ if (length2 > 0)
3617
+ heading = Math.atan2(dx, dz2);
3618
+ if (command.kind === "go" && length2 === 0)
3619
+ return;
3620
+ active = {
3621
+ kind: command.kind,
3622
+ x,
3623
+ z,
3624
+ targetX: command.x,
3625
+ targetZ: command.z,
3626
+ ux: length2 ? dx / length2 : 0,
3627
+ uz: length2 ? dz2 / length2 : 0,
3628
+ length: length2,
3629
+ elapsed: 0,
3630
+ covered: 0,
3631
+ ...command.kind === "pounce" ? (() => {
3632
+ const apex = Math.max(y, command.landingY) + command.height;
3633
+ const vy = Math.sqrt(2 * GRAVITY * (apex - y));
3634
+ return {
3635
+ startY: y,
3636
+ targetY: command.landingY,
3637
+ vy,
3638
+ duration: (vy + Math.sqrt(2 * GRAVITY * (apex - command.landingY))) / GRAVITY
3639
+ };
3640
+ })() : { plan: travel(length2, CAT_GAITS[currentGait].speed, ACCELERATION[currentGait]) }
3641
+ };
3642
+ if (command.kind === "pounce")
3643
+ speed = length2 / active.duration;
3644
+ }
3645
+ function brake() {
3646
+ if (!active || active.kind === "pounce" || ["brake", "wheel-brake"].includes(active.kind))
3647
+ return;
3648
+ if (active.kind === "wheel") {
3649
+ const stride = active.stride, minDistance = beltSpeed * beltSpeed / 2.4;
3650
+ const distance2 = beltSpeed > 0.15 ? (Math.ceil(wheelPhase + minDistance / stride) - wheelPhase) * stride : minDistance;
3651
+ const deceleration = distance2 > 0 ? beltSpeed * beltSpeed / (2 * distance2) : 1.2;
3652
+ active = beltSpeed > 0 ? {
3653
+ kind: "wheel-brake",
3654
+ initialSpeed: beltSpeed,
3655
+ deceleration,
3656
+ duration: beltSpeed / deceleration,
3657
+ elapsed: 0,
3658
+ covered: 0,
3659
+ stride
3660
+ } : null;
3661
+ return;
3662
+ }
3663
+ active = speed > 0 ? {
3664
+ kind: "brake",
3665
+ x,
3666
+ z,
3667
+ ux: active.ux,
3668
+ uz: active.uz,
3669
+ initialSpeed: speed,
3670
+ duration: speed / DECELERATION,
3671
+ elapsed: 0,
3672
+ covered: 0
3673
+ } : null;
3674
+ }
3675
+ function request(command) {
3676
+ if (active) {
3677
+ pending = command;
3678
+ brake();
3679
+ }
3680
+ if (!active) {
3681
+ pending = null;
3682
+ launch(command);
3683
+ }
3684
+ }
3685
+ return {
3686
+ go(targetX, targetZ, config2 = {}) {
3687
+ options(config2);
3688
+ const { gait: gait3 = "walk" } = config2;
3689
+ point(targetX, targetZ);
3690
+ if (typeof gait3 !== "string" || !Object.hasOwn(CAT_GAITS, gait3))
3691
+ throw new RangeError("Invalid cat gait");
3692
+ request({ kind: "go", x: targetX, z: targetZ, gait: gait3 });
3693
+ },
3694
+ stop() {
3695
+ pending = null;
3696
+ brake();
3697
+ },
3698
+ pounce(targetX, targetZ, config2 = {}) {
3699
+ options(config2);
3700
+ const { height = 0.18, landingY = 0 } = config2;
3701
+ point(targetX, targetZ);
3702
+ if (!Number.isFinite(height) || height <= 0 || height > 1)
3703
+ throw new RangeError("Invalid pounce height");
3704
+ if (!Number.isFinite(landingY) || landingY < 0 || landingY > 1)
3705
+ throw RangeError("Invalid landing support");
3706
+ request({ kind: "pounce", x: targetX, z: targetZ, height, landingY });
3707
+ },
3708
+ wheel(config2 = {}) {
3709
+ options(config2);
3710
+ const { duration = 10, speed: speed2 = 0.65, radius = 0.6, stride = 0.32 } = config2;
3711
+ if (![duration, speed2, radius, stride].every(Number.isFinite) || duration < 2 || duration > 30 || speed2 <= 0 || speed2 > 2 || radius < 0.3 || radius > 1 || stride < 0.05 || stride > 1)
3712
+ throw RangeError("Invalid wheel motion");
3713
+ request({ kind: "wheel", duration, peakSpeed: speed2, radius, stride });
3714
+ },
3715
+ step(dt) {
3716
+ if (!Number.isFinite(dt) || dt < 0 || dt > 0.1)
3717
+ throw new RangeError("Invalid cat step");
3718
+ let remaining = dt;
3719
+ while (active && remaining > 0) {
3720
+ const a = active, duration = a.plan?.duration ?? a.duration;
3721
+ const used = Math.min(remaining, duration - a.elapsed);
3722
+ a.elapsed = Math.min(duration, a.elapsed + used);
3723
+ if (duration - a.elapsed < 0.0000000001)
3724
+ a.elapsed = duration;
3725
+ remaining = Math.max(0, remaining - used);
3726
+ const done = a.elapsed >= duration;
3727
+ let covered;
3728
+ if (["wheel", "wheel-brake"].includes(a.kind)) {
3729
+ if (a.kind === "wheel") {
3730
+ beltSpeed = a.peakSpeed * Math.sin(Math.PI * a.elapsed / duration) ** 2;
3731
+ covered = a.peakSpeed * (a.elapsed / 2 - duration * Math.sin(2 * Math.PI * a.elapsed / duration) / (4 * Math.PI));
3732
+ } else {
3733
+ beltSpeed = Math.max(0, a.initialSpeed - a.deceleration * a.elapsed);
3734
+ covered = a.initialSpeed * a.elapsed - a.deceleration * a.elapsed * a.elapsed / 2;
3735
+ }
3736
+ const ds = covered - a.covered;
3737
+ beltDistance += ds;
3738
+ wheelPhase += ds / a.stride;
3739
+ a.covered = covered;
3740
+ if (done) {
3741
+ beltSpeed = 0;
3742
+ if (Math.abs(wheelPhase - Math.round(wheelPhase)) < 0.000000001)
3743
+ wheelPhase = Math.round(wheelPhase);
3744
+ active = null;
3745
+ if (pending) {
3746
+ const command = pending;
3747
+ pending = null;
3748
+ launch(command);
3749
+ }
3750
+ }
3751
+ continue;
3752
+ } else if (a.kind === "pounce") {
3753
+ covered = a.length * (a.elapsed / duration);
3754
+ speed = a.length / duration;
3755
+ y = a.startY + a.vy * a.elapsed - GRAVITY * a.elapsed * a.elapsed / 2;
3756
+ } else if (a.kind === "brake") {
3757
+ covered = a.initialSpeed * a.elapsed - DECELERATION * a.elapsed * a.elapsed / 2;
3758
+ speed = Math.max(0, a.initialSpeed - DECELERATION * a.elapsed);
3759
+ } else {
3760
+ const state = a.plan.at(a.elapsed);
3761
+ covered = state.distance;
3762
+ speed = state.speed;
3763
+ }
3764
+ x = a.x + a.ux * covered;
3765
+ z = a.z + a.uz * covered;
3766
+ advance(covered - a.covered);
3767
+ a.covered = covered;
3768
+ if (done) {
3769
+ if (a.kind !== "brake") {
3770
+ x = a.targetX;
3771
+ z = a.targetZ;
3772
+ }
3773
+ speed = 0;
3774
+ if (a.kind === "pounce")
3775
+ supportY = a.targetY;
3776
+ y = supportY;
3777
+ active = null;
3778
+ if (pending) {
3779
+ const command = pending;
3780
+ pending = null;
3781
+ launch(command);
3782
+ }
3783
+ }
3784
+ }
3785
+ },
3786
+ sample() {
3787
+ return {
3788
+ x,
3789
+ y,
3790
+ z,
3791
+ speed,
3792
+ distance,
3793
+ phase,
3794
+ gait: currentGait,
3795
+ moving: active !== null,
3796
+ heading,
3797
+ supportY,
3798
+ airborne: active?.kind === "pounce",
3799
+ flightProgress: active?.kind === "pounce" ? active.elapsed / active.duration : 0,
3800
+ exercising: ["wheel", "wheel-brake"].includes(active?.kind),
3801
+ beltSpeed,
3802
+ beltDistance,
3803
+ wheelPhase,
3804
+ wheelAngle: beltDistance / wheelRadius
3805
+ };
3806
+ }
3807
+ };
3808
+ }
3809
+ var diagonal2 = 0.0254 * 32;
3810
+ var screenWidth = diagonal2 * 16 / Math.hypot(16, 9);
3811
+ var screenHeight = diagonal2 * 9 / Math.hypot(16, 9);
3812
+ var pitch2 = 9 * Math.PI / 180;
3813
+ var c2 = Math.cos(pitch2);
3814
+ var s2 = Math.sin(pitch2);
3815
+ function projectCat(x, y, z, config = {}) {
3816
+ options(config);
3817
+ const { width, height, canvasMeters = 0.6 } = config;
3818
+ if (![x, y, z, width, height, canvasMeters].every(Number.isFinite) || width <= 0 || height <= 0 || canvasMeters <= 0)
3819
+ throw new RangeError("Invalid cat projection");
3820
+ const cameraY = c2 * (y - 0.45) - s2 * (z - 1.65);
3821
+ const depth = -s2 * (y - 0.45) - c2 * (z - 1.65);
3822
+ if (!Number.isFinite(depth) || depth <= 0)
3823
+ throw new RangeError("Cat point behind camera");
3824
+ const focal = 0.7 * Math.min(width / screenWidth, height / screenHeight);
3825
+ const result = {
3826
+ x: width / 2 + focal * x / depth,
3827
+ y: height / 2 - focal * cameraY / depth,
3828
+ size: focal * canvasMeters / depth,
3829
+ depth
3830
+ };
3831
+ if (!Object.values(result).every(Number.isFinite))
3832
+ throw new RangeError("Invalid cat projection range");
3833
+ return result;
3834
+ }
3835
+ function unprojectCatGround(u, v, { width, height } = {}) {
3836
+ if (![u, v, width, height].every(Number.isFinite) || width <= 0 || height <= 0)
3837
+ throw RangeError("Invalid cat ground pixel");
3838
+ const focal = 0.7 * Math.min(width / screenWidth, height / screenHeight), q = (height / 2 - v) / focal;
3839
+ const denominator = s2 - q * c2;
3840
+ if (Math.abs(denominator) < 0.0000000001)
3841
+ throw RangeError("Cat ground ray at horizon");
3842
+ const dz2 = -0.45 * (c2 + q * s2) / denominator, depth = s2 * 0.45 - c2 * dz2;
3843
+ if (depth <= 0 || !Number.isFinite(depth))
3844
+ throw RangeError("Cat ground behind camera");
3845
+ return { x: (u - width / 2) * depth / focal, z: dz2 + 1.65 };
3846
+ }
3847
+
3848
+ // integrations/purrviewer/src/habitat.mjs
3849
+ var CAT_HABITAT = Object.freeze({
3850
+ tower: Object.freeze({ x: -0.61, z: -1.9, lowX: -0.43, lowY: 0.21, highX: -0.65, highY: 0.42, approachX: -0.12 }),
3851
+ wheel: Object.freeze({ x: 0.57, z: -1.9, radius: 0.42, catX: 0.53, stride: 0.32 })
3852
+ });
3853
+ function paintHabitat(pixels, width, height, angle = 0) {
3854
+ const at = (x, y, value) => {
3855
+ x = Math.round(x);
3856
+ y = Math.round(y);
3857
+ if (x < 0 || y < 0 || x >= width || y >= height)
3858
+ return;
3859
+ const i = (y * width + x) * 4;
3860
+ if (pixels[i])
3861
+ return;
3862
+ pixels[i] = pixels[i + 1] = pixels[i + 2] = value;
3863
+ };
3864
+ const point2 = (x, y, z) => projectCat(x, y, z, { width, height });
3865
+ const line = (a, b, value = 105) => {
3866
+ const n = Math.max(1, Math.ceil(Math.hypot(b.x - a.x, b.y - a.y)));
3867
+ for (let i = 0;i <= n; i++)
3868
+ at(a.x + (b.x - a.x) * i / n, a.y + (b.y - a.y) * i / n, value);
3869
+ };
3870
+ const edge = (x, y, z, xx, yy, zz, value) => line(point2(x, y, z), point2(xx, yy, zz), value);
3871
+ const { tower: t, wheel: w } = CAT_HABITAT;
3872
+ const platform = (x, y, half) => {
3873
+ edge(x - half, y, t.z - 0.12, x + half, y, t.z - 0.12, 150);
3874
+ edge(x - half, y, t.z + 0.12, x + half, y, t.z + 0.12, 205);
3875
+ edge(x - half, y, t.z - 0.12, x - half, y, t.z + 0.12, 150);
3876
+ edge(x + half, y, t.z - 0.12, x + half, y, t.z + 0.12, 150);
3877
+ edge(x - half, y - 0.025, t.z + 0.12, x + half, y - 0.025, t.z + 0.12, 100);
3878
+ };
3879
+ platform(t.x, 0, 0.34);
3880
+ platform(t.lowX, t.lowY, 0.21);
3881
+ platform(t.highX, t.highY, 0.21);
3882
+ for (const [x, top] of [[t.lowX, t.lowY], [t.highX, t.highY]]) {
3883
+ edge(x - 0.025, 0, t.z, x - 0.025, top - 0.025, t.z, 90);
3884
+ edge(x + 0.025, 0, t.z, x + 0.025, top - 0.025, t.z, 125);
3885
+ for (let y = 0.025;y < top - 0.025; y += 0.025)
3886
+ edge(x - 0.024, y, t.z, x + 0.024, y + 0.008, t.z, 65);
3887
+ }
3888
+ for (const [z, value] of [[w.z - 0.08, 65], [w.z, 155]]) {
3889
+ let prev = point2(w.x + w.radius, w.radius, z);
3890
+ for (let i = 1;i <= 112; i++) {
3891
+ const a = i * Math.PI * 2 / 112, next = point2(w.x + w.radius * Math.cos(a), w.radius + w.radius * Math.sin(a), z);
3892
+ line(prev, next, value);
3893
+ prev = next;
3894
+ }
3895
+ }
3896
+ for (let i = 0;i < 24; i++) {
3897
+ const a = i * Math.PI * 2 / 24 - angle;
3898
+ edge(w.x + (w.radius - 0.018) * Math.cos(a), w.radius + (w.radius - 0.018) * Math.sin(a), w.z, w.x + (w.radius - 0.05) * Math.cos(a), w.radius + (w.radius - 0.05) * Math.sin(a), w.z, 110);
3899
+ }
3900
+ edge(w.x - 0.25, -0.025, w.z, w.x + 0.25, -0.025, w.z, 130);
3901
+ edge(w.x - 0.25, -0.025, w.z, w.x - 0.18, 0.045, w.z, 95);
3902
+ edge(w.x + 0.25, -0.025, w.z, w.x + 0.18, 0.045, w.z, 95);
3903
+ }
3904
+
3905
+ // integrations/purrviewer/src/life.mjs
3906
+ var GROUND = 0.000001;
3907
+ var hold = (clip, duration) => ({ clip, duration, elapsed: 0, progress: 0 });
3908
+ var matHold = (clip, duration) => ({ ...hold(clip, duration), mat: true });
3909
+ var towerLoad = (duration, facing) => ({ ...hold("tower-load", duration), facing });
3910
+ var travel2 = (clip, command) => ({ clip, command, issued: false, acknowledged: false, progress: 0 });
3911
+ var stretchPhases = () => [
3912
+ hold("stretch-enter", 1.15),
3913
+ hold("stretch-reach", 0.65),
3914
+ hold("stretch-hold", 1.25),
3915
+ hold("stretch-release", 0.7),
3916
+ hold("stretch-leave", 1.15)
3917
+ ];
3918
+ function createCatLife({ seed = 731 } = {}) {
3919
+ if (!Number.isInteger(seed) || seed < 0 || seed > 4294967295) {
3920
+ throw new RangeError("seed must be an unsigned 32-bit integer");
3921
+ }
3922
+ let randomState = seed;
3923
+ const random = () => {
3924
+ randomState = randomState + 1831565813 >>> 0;
3925
+ let n = Math.imul(randomState ^ randomState >>> 15, randomState | 1);
3926
+ n ^= n + Math.imul(n ^ n >>> 7, n | 61);
3927
+ return ((n ^ n >>> 14) >>> 0) / 4294967296;
3928
+ };
3929
+ const range = (min, max) => min + random() * (max - min);
3930
+ let clock = 0;
3931
+ let lastChoice = "observe";
3932
+ let roamAfter = 20;
3933
+ let playAfter = 90;
3934
+ let companyAfter = 60, attentionUntil = 0, eyeAfter = 0;
3935
+ let habitatAfter = 100;
3936
+ let pending = null;
3937
+ let stopSent = false;
3938
+ let episode = {
3939
+ intent: "observe",
3940
+ label: "Watching quietly",
3941
+ direct: false,
3942
+ phases: [
3943
+ hold("sit", 1.2),
3944
+ hold("eye-greeting", 3.4),
3945
+ hold("tail-swish", 3.6),
3946
+ hold("loaf-enter", 0.9),
3947
+ hold("loaf-rest", range(12, 24)),
3948
+ hold("loaf-leave", 0.9),
3949
+ hold("sit", 2)
3950
+ ]
3951
+ };
3952
+ let index = 0;
3953
+ const current = () => episode.phases[index];
3954
+ const grounded = (sample) => !sample.airborne && sample.y <= (sample.supportY ?? 0) + GROUND;
3955
+ const settled = (sample) => grounded(sample) && !sample.moving;
3956
+ function toyPhases(sample, target) {
3957
+ const dx = target.x - sample.x;
3958
+ const dz2 = target.z - sample.z;
3959
+ const distance = Math.hypot(dx, dz2);
3960
+ const phases = [];
3961
+ if (distance > 0.35) {
3962
+ phases.push(travel2("creep", {
3963
+ kind: "go",
3964
+ x: target.x - dx / distance * 0.3,
3965
+ z: target.z - dz2 / distance * 0.3,
3966
+ gait: "creep"
3967
+ }));
3968
+ }
3969
+ phases.push(hold("crouch", 0.65), travel2("pounce", { kind: "pounce", x: target.x, z: target.z, height: 0.18 }), hold("land", 0.3), hold("bat", 0.9), hold("sit", 2.5));
3970
+ return phases;
3971
+ }
3972
+ function localToy(sample) {
3973
+ const direction = Math.abs(sample.x) > 0.3 ? sample.x > 0 ? -1 : 1 : random() < 0.5 ? -1 : 1;
3974
+ return { x: sample.x + direction * 0.6, z: sample.z };
3975
+ }
3976
+ function companyPhases(sample) {
3977
+ const target = { x: sample.x < 0 ? -0.18 : 0.18, z: Math.min(0, Math.max(-0.35, sample.z + 0.65)) };
3978
+ const phases = [];
3979
+ const dx = target.x - sample.x, dz2 = target.z - sample.z, distance = Math.hypot(dx, dz2);
3980
+ if (distance > 0.18) {
3981
+ const scale = Math.min(1, 0.8 / distance);
3982
+ phases.push(travel2("walk", { kind: "go", x: sample.x + dx * scale, z: sample.z + dz2 * scale, gait: "walk" }));
3983
+ }
3984
+ phases.push(hold("sit", 1.2), ...clock >= eyeAfter ? [hold("eye-greeting", 3.4)] : [], matHold("knead-enter", 0.8), matHold("knead", 4.8), matHold("knead-leave", 0.8), matHold("loaf-rest", range(25, 45)), matHold("loaf-leave", 0.9), matHold("sit", 2));
3985
+ companyAfter = clock + range(180, 300);
3986
+ return phases;
3987
+ }
3988
+ function descent(sample) {
3989
+ if (!(sample.supportY > 0))
3990
+ return [];
3991
+ const t = CAT_HABITAT.tower;
3992
+ return [
3993
+ towerLoad(0.4, 1),
3994
+ travel2("tower-down", { kind: "pounce", x: t.approachX, z: t.z, height: 0.055, landingY: 0 }),
3995
+ hold("tower-land", 0.4),
3996
+ hold("sit", 0.7)
3997
+ ];
3998
+ }
3999
+ function habitatPhases(kind) {
4000
+ habitatAfter = clock + range(160, 260);
4001
+ const t = CAT_HABITAT.tower, w = CAT_HABITAT.wheel;
4002
+ if (kind === "wheel")
4003
+ return [
4004
+ travel2("walk", { kind: "go", x: w.catX, z: w.z, gait: "walk" }),
4005
+ hold("wheel-enter", 0.65),
4006
+ travel2("wheel-run", { kind: "wheel", duration: 10, speed: 2 * 10 * w.stride / 10, radius: w.radius, stride: w.stride }),
4007
+ hold("wheel-leave", 0.7),
4008
+ travel2("walk", { kind: "go", x: w.catX - 0.3, z: w.z + 0.14, gait: "walk" }),
4009
+ hold("sit", 3)
4010
+ ];
4011
+ return [
4012
+ travel2("walk", { kind: "go", x: t.approachX, z: t.z, gait: "walk" }),
4013
+ towerLoad(0.55, -1),
4014
+ travel2("tower-up", { kind: "pounce", x: t.lowX, z: t.z, height: 0.07, landingY: t.lowY }),
4015
+ hold("tower-land", 0.35),
4016
+ towerLoad(0.4, -1),
4017
+ travel2("tower-up", { kind: "pounce", x: t.highX, z: t.z, height: 0.065, landingY: t.highY }),
4018
+ hold("tower-land", 0.35),
4019
+ hold("sit", 2),
4020
+ hold("eye-greeting", 3.4),
4021
+ hold("loaf-enter", 0.9),
4022
+ hold("loaf-rest", range(15, 25)),
4023
+ hold("loaf-leave", 0.9),
4024
+ towerLoad(0.4, 1),
4025
+ travel2("tower-down", { kind: "pounce", x: t.lowX, z: t.z, height: 0.04, landingY: t.lowY }),
4026
+ hold("tower-land", 0.35),
4027
+ ...descent({ supportY: t.lowY })
4028
+ ];
4029
+ }
4030
+ function beginInteraction(sample) {
4031
+ const { kind, target } = pending;
4032
+ const previousPhase = current(), previous = previousPhase.clip;
4033
+ const fromSleep = ["sleep", "loaf"].includes(previous);
4034
+ const recovery = previous === "wheel-run" ? [hold("wheel-leave", 0.7)] : ["loaf-enter", "loaf-rest"].includes(previous) ? [previousPhase.mat ? matHold("loaf-leave", 0.9) : hold("loaf-leave", 0.9)] : ["knead-enter", "knead"].includes(previous) ? [matHold("knead-leave", 0.8), matHold("loaf-leave", 0.9)] : previous === "knead-leave" ? [matHold("loaf-leave", 0.9)] : fromSleep ? [hold("wake", 1.8), ...kind === "stretch" ? [] : [hold("stretch-leave", 1.15)]] : ["stretch-reach", "stretch-hold"].includes(previous) ? [hold("stretch-release", 0.7), hold("stretch-leave", 1.15)] : ["stretch-enter", "stretch-release", "wake"].includes(previous) ? [hold("stretch-leave", 1.15)] : previous.startsWith("feed-") && previous !== "feed-leave" ? [hold("feed-leave", 1.35)] : ["treat", "stretch", "greet", "company"].includes(kind) && ["walk", "creep", "land"].includes(previous) ? [hold("sit", 0.55)] : [];
4035
+ pending = null;
4036
+ stopSent = false;
4037
+ index = 0;
4038
+ episode = {
4039
+ intent: kind,
4040
+ direct: true,
4041
+ label: kind === "toy" ? "Stalking a toy" : kind === "treat" ? "Enjoying a nearby treat" : kind === "stretch" ? "A long supported stretch" : kind === "tail" ? "A little tail-tip attention" : kind === "company" ? "Choosing a quiet place nearby" : "A soft eye greeting",
4042
+ phases: [...recovery, ...descent(sample), ...kind === "toy" ? toyPhases(sample, target ?? localToy(sample)) : kind === "treat" ? [
4043
+ hold("feed-enter", 1.35),
4044
+ hold("feed-sniff", 0.45),
4045
+ hold("feed-lick", 3.6),
4046
+ hold("feed-finish", 0.45),
4047
+ hold("feed-leave", 1.35),
4048
+ hold("sit", 1)
4049
+ ] : kind === "stretch" ? [...stretchPhases().slice(fromSleep ? 1 : 0), hold("sit", 2)] : kind === "tail" ? [hold("tail-swish", 3.6), hold("sit", 1)] : kind === "company" ? companyPhases(sample) : ["tower", "wheel"].includes(kind) ? habitatPhases(kind) : clock >= eyeAfter ? [hold("sit", 0.45), hold("eye-greeting", 3.4), hold("sit", 1)] : [hold("sit", 1.2)]]
4050
+ };
4051
+ roamAfter = Math.max(roamAfter, clock + 20);
4052
+ playAfter = Math.max(playAfter, clock + 90);
4053
+ }
4054
+ function choose(sample) {
4055
+ const choices = [
4056
+ ["observe", 35],
4057
+ ["groom", 20],
4058
+ ["sleep", 20],
4059
+ ["stretch", 8],
4060
+ ["roam", clock >= roamAfter ? 12 : 0],
4061
+ ["play", clock >= playAfter ? 5 : 0],
4062
+ ["company", clock >= companyAfter ? clock < attentionUntil ? 12 : 6 : 0],
4063
+ ["tower", clock >= habitatAfter ? 5 : 0],
4064
+ ["wheel", clock >= habitatAfter ? 3 : 0]
4065
+ ].filter(([kind2, weight]) => kind2 !== lastChoice && weight > 0);
4066
+ let ticket = random() * choices.reduce((sum, [, weight]) => sum + weight, 0);
4067
+ const kind = choices.find(([, weight]) => (ticket -= weight) < 0)[0];
4068
+ lastChoice = kind;
4069
+ let phases;
4070
+ let label;
4071
+ switch (kind) {
4072
+ case "observe":
4073
+ label = "Watching quietly";
4074
+ phases = random() < 0.7 ? [
4075
+ hold("sit", 2),
4076
+ hold("tail-swish", 3.6),
4077
+ hold("loaf-enter", 0.9),
4078
+ hold("loaf-rest", range(12, 24)),
4079
+ hold("loaf-leave", 0.9),
4080
+ hold("sit", 2)
4081
+ ] : [hold("sit", range(8, 16)), random() < 0.5 ? hold("eye-greeting", 3.4) : hold("blink", 0.8), hold("tail-tip", 2.8), hold("idle", range(4, 8))];
4082
+ break;
4083
+ case "groom":
4084
+ label = "A little grooming";
4085
+ phases = [hold("sit", 0.6), hold("groom", range(5, 9)), hold("sit", 2)];
4086
+ break;
4087
+ case "sleep":
4088
+ label = "A long quiet nap";
4089
+ phases = [
4090
+ hold("loaf", 2),
4091
+ hold("sleep", range(60, 120)),
4092
+ hold("wake", 1.8),
4093
+ ...stretchPhases().slice(1),
4094
+ hold("sit", 2)
4095
+ ];
4096
+ break;
4097
+ case "stretch":
4098
+ label = "Stretching out";
4099
+ phases = [...stretchPhases(), hold("sit", range(5, 9))];
4100
+ break;
4101
+ case "roam": {
4102
+ label = "A short wander";
4103
+ const angle = range(0, Math.PI * 2);
4104
+ const distance = range(0.35, 0.8);
4105
+ let x = Math.max(-0.55, Math.min(0.55, sample.x + Math.cos(angle) * distance));
4106
+ let z = Math.max(-3.5, Math.min(0, sample.z + Math.sin(angle) * distance));
4107
+ if (Math.hypot(x - sample.x, z - sample.z) < 0.15) {
4108
+ x = sample.x > 0 ? -0.35 : 0.35;
4109
+ z = Math.max(-3, Math.min(0, sample.z - 0.3));
4110
+ }
4111
+ phases = [hold("idle", 0.8), travel2("walk", {
4112
+ kind: "go",
4113
+ x,
4114
+ z,
4115
+ gait: "walk"
4116
+ }), hold("sit", range(6, 10))];
4117
+ roamAfter = clock + 35;
4118
+ break;
4119
+ }
4120
+ case "play":
4121
+ label = "An imagined little hunt";
4122
+ phases = toyPhases(sample, localToy(sample));
4123
+ playAfter = clock + 120;
4124
+ break;
4125
+ case "company":
4126
+ label = "Choosing a quiet place nearby";
4127
+ phases = companyPhases(sample);
4128
+ break;
4129
+ case "tower":
4130
+ case "wheel":
4131
+ label = kind === "tower" ? "A high quiet perch" : "A short wheel run";
4132
+ phases = habitatPhases(kind);
4133
+ break;
4134
+ }
4135
+ episode = { intent: kind, label, direct: false, phases };
4136
+ index = 0;
4137
+ }
4138
+ function advance(sample) {
4139
+ const phase = current();
4140
+ if (pending && phase.command?.kind !== "pounce") {
4141
+ beginInteraction(sample);
4142
+ } else if (++index === episode.phases.length) {
4143
+ if (episode.direct && ["call", "greet", "treat"].includes(episode.intent))
4144
+ attentionUntil = clock + 90;
4145
+ choose(sample);
4146
+ }
4147
+ if (current().clip === "eye-greeting")
4148
+ eyeAfter = clock + 20;
4149
+ }
4150
+ function step(dt, sample) {
4151
+ if (!Number.isFinite(dt) || dt < 0 || dt > 0.1) {
4152
+ throw new RangeError("dt must be finite and between 0 and .1 seconds");
4153
+ }
4154
+ if (!sample || !["x", "y", "z"].every((key) => Number.isFinite(sample[key])) || sample.y < 0 || typeof sample.moving !== "boolean") {
4155
+ throw new TypeError("motionSample requires finite x/y/z, nonnegative y and boolean moving");
4156
+ }
4157
+ if (dt === 0)
4158
+ return null;
4159
+ clock += dt;
4160
+ let phase = current();
4161
+ const committedPounce = phase.command?.kind === "pounce" && phase.issued;
4162
+ if (pending && !committedPounce) {
4163
+ if (!grounded(sample))
4164
+ return null;
4165
+ if (sample.moving || phase.command && phase.issued && !stopSent) {
4166
+ if (!stopSent) {
4167
+ stopSent = true;
4168
+ return { kind: "stop" };
4169
+ }
4170
+ return null;
4171
+ }
4172
+ if (stopSent || phase.command && !phase.issued) {
4173
+ beginInteraction(sample);
4174
+ return null;
4175
+ }
4176
+ }
4177
+ if (phase.done && settled(sample)) {
4178
+ advance(sample);
4179
+ return null;
4180
+ }
4181
+ if (phase.command) {
4182
+ if (!phase.issued) {
4183
+ if (!settled(sample))
4184
+ return null;
4185
+ phase.issued = true;
4186
+ return { ...phase.command };
4187
+ }
4188
+ if (sample.moving)
4189
+ phase.acknowledged = true;
4190
+ if (phase.acknowledged && settled(sample)) {
4191
+ phase.progress = 1;
4192
+ phase.done = true;
4193
+ }
4194
+ return null;
4195
+ }
4196
+ if (!settled(sample))
4197
+ return null;
4198
+ if (pending && !["land", "tower-land"].includes(phase.clip)) {
4199
+ phase.exitStart ??= phase.progress;
4200
+ phase.exitElapsed = (phase.exitElapsed ?? 0) + dt;
4201
+ phase.progress = phase.exitStart + (1 - phase.exitStart) * Math.min(1, phase.exitElapsed / 0.2);
4202
+ } else {
4203
+ phase.elapsed = Math.min(phase.duration, phase.elapsed + dt);
4204
+ phase.progress = Math.min(1, phase.elapsed / phase.duration);
4205
+ }
4206
+ phase.done = phase.progress >= 1;
4207
+ return null;
4208
+ }
4209
+ return {
4210
+ step,
4211
+ interact(kind, { target } = {}) {
4212
+ if (!["treat", "toy", "call", "stretch", "tail", "greet", "company", "tower", "wheel"].includes(kind))
4213
+ return false;
4214
+ if (target !== undefined && (!target || !Number.isFinite(target.x) || !Number.isFinite(target.z)))
4215
+ return false;
4216
+ if (["call", "greet"].includes(kind) && (current().clip === "eye-greeting" || episode.direct && ["call", "greet"].includes(episode.intent) && !pending))
4217
+ return true;
4218
+ if (kind === "company" && episode.intent === "company" && !pending)
4219
+ return true;
4220
+ if (["tower", "wheel"].includes(kind) && episode.intent === kind && !pending)
4221
+ return true;
4222
+ pending = { kind, target: target === undefined ? undefined : { x: target.x, z: target.z } };
4223
+ return true;
4224
+ },
4225
+ get report() {
4226
+ const phase = current();
4227
+ return {
4228
+ intent: episode.intent,
4229
+ label: episode.label,
4230
+ clip: phase.clip,
4231
+ progress: phase.progress,
4232
+ busy: Boolean(pending || episode.direct || ["roam", "play"].includes(episode.intent)),
4233
+ food: phase.clip.startsWith("feed-") ? { remaining: phase.clip === "feed-lick" ? 1 - phase.progress : ["feed-finish", "feed-leave"].includes(phase.clip) ? 0 : 1 } : null,
4234
+ mat: Boolean(phase.mat),
4235
+ facing: phase.facing ?? null,
4236
+ resting: ["sleep", "loaf-rest"].includes(phase.clip) && !pending,
4237
+ sleeping: phase.clip === "sleep"
4238
+ };
4239
+ }
4240
+ };
4241
+ }
4242
+
4243
+ // integrations/purrviewer/src/contact.mjs
4244
+ var viewport = { width: 320, height: 180 };
4245
+ function createCatContacts(track) {
4246
+ const radii = track.feet.map((row) => row.map((foot) => Math.min(28, ...row.filter((other) => other.id !== foot.id).map((other) => 0.44 * Math.hypot(other.x - foot.x, other.y - foot.y)))));
4247
+ const anchors = new Map;
4248
+ let feet = [], mirror = false, pose = 0;
4249
+ return {
4250
+ reset() {
4251
+ anchors.clear();
4252
+ feet = [];
4253
+ },
4254
+ update(sample, flipped) {
4255
+ mirror = flipped;
4256
+ const n = track.feet.length, phase = (sample.phase % 1 + 1) % 1, index = Math.min(n - 1, Math.floor(phase * n)), cycle = Math.floor(sample.phase);
4257
+ pose = index;
4258
+ feet = track.feet[index];
4259
+ const p = projectCat(sample.x, 0, sample.z, viewport), scale = p.size / 320, sign = mirror ? -1 : 1;
4260
+ const active = new Set;
4261
+ for (const [j, foot] of feet.entries()) {
4262
+ if (!foot.stance)
4263
+ continue;
4264
+ active.add(foot.id);
4265
+ const start = track.starts[foot.id][index], episode = cycle - (start > index ? 1 : 0) + "/" + start;
4266
+ let anchor = anchors.get(foot.id);
4267
+ if (!anchor || anchor.episode !== episode) {
4268
+ anchor = { episode, world: unprojectCatGround(p.x + sign * (foot.x - 160) * scale, p.y + (foot.y - 290) * scale, viewport), released: false };
4269
+ anchors.set(foot.id, anchor);
4270
+ }
4271
+ const target = projectCat(anchor.world.x, 0, anchor.world.z, viewport);
4272
+ const dx = 160 + sign * (target.x - p.x) / scale - foot.x, dy2 = 290 + (target.y - p.y) / scale - foot.y;
4273
+ anchor.released = Math.hypot(dx, dy2) > Math.min(6, radii[index][j] * 0.22) || Math.abs(dy2) > 3;
4274
+ }
4275
+ for (const id of anchors.keys())
4276
+ if (!active.has(id))
4277
+ anchors.delete(id);
4278
+ },
4279
+ pins(sample) {
4280
+ const p = projectCat(sample.x, 0, sample.z, viewport), scale = p.size / 320, sign = mirror ? -1 : 1, pins = [];
4281
+ for (const [j, foot] of feet.entries()) {
4282
+ const anchor = anchors.get(foot.id);
4283
+ if (!foot.stance || !anchor || anchor.released)
4284
+ continue;
4285
+ const target = projectCat(anchor.world.x, 0, anchor.world.z, viewport);
4286
+ pins.push({ id: foot.id, radius: radii[pose][j], source: { x: foot.x, y: foot.y }, target: { x: 160 + sign * (target.x - p.x) / scale, y: 290 + (target.y - p.y) / scale }, world: { ...anchor.world }, episode: anchor.episode });
4287
+ }
4288
+ return pins;
4289
+ }
4290
+ };
4291
+ }
4292
+ function footSourcePoint2(x, y, pins, out = {}) {
4293
+ out.x = x;
4294
+ out.y = y;
4295
+ if (y <= 225 || !pins.length)
4296
+ return out;
4297
+ for (const pin of pins) {
4298
+ const dx = pin.target.x - pin.source.x, dy2 = pin.target.y - pin.source.y, radius = pin.radius ?? 28;
4299
+ if (radius <= 0 || Math.hypot(dx, dy2) > Math.min(6, 0.22 * radius) || Math.abs(dy2) > 3)
4300
+ continue;
4301
+ if (Math.hypot(x - pin.source.x, y - pin.source.y) >= radius)
4302
+ continue;
4303
+ let u = x - dx, v = y - dy2;
4304
+ for (let i = 0;i < 12; i++) {
4305
+ const distance = Math.hypot(u - pin.source.x, v - pin.source.y);
4306
+ const t = Math.max(0, Math.min(1, (distance - radius * 0.3) / (radius * 0.7))), weight = 1 - t * t * (3 - 2 * t);
4307
+ u = x - dx * weight;
4308
+ v = y - dy2 * weight;
4309
+ }
4310
+ out.x = u;
4311
+ out.y = v;
4312
+ return out;
4313
+ }
4314
+ return out;
4315
+ }
4316
+
4317
+ // integrations/purrviewer/src/purrviewer-core.mjs
4318
+ function createCatFromAssets({ seed = 731 } = {}, lease, kernel = null) {
4319
+ if (!Number.isInteger(seed) || seed < 0 || seed > 4294967295)
4320
+ throw TypeError("Invalid cat seed");
4321
+ let asset = lease.data;
4322
+ const release = lease.release, motion = createCatMotion(), life = createCatLife({ seed }), contacts = createCatContacts(asset.meta.walk), sourcePoint = { x: 0, y: 0 };
4323
+ let pixels = null, disposed = false, lastClip = "sit", lastFrame = null, prep = null, view = "front", mirror = false, settle = null, lastMoving = false, lastLifeClip = "sit", lastPaintKey = "", lastText = "", rasterizations = 0, heldHits = 0;
4324
+ const alive = () => {
4325
+ if (disposed)
4326
+ throw Error("Cat is disposed");
4327
+ };
4328
+ const direct = (command) => {
4329
+ if (command.kind === "go")
4330
+ motion.go(command.x, command.z, { gait: command.gait });
4331
+ else if (command.kind === "pounce")
4332
+ motion.pounce(command.x, command.z, { height: command.height, landingY: command.landingY ?? 0 });
4333
+ else if (command.kind === "wheel") {
4334
+ view = "side";
4335
+ mirror = false;
4336
+ settle = null;
4337
+ motion.wheel(command);
4338
+ } else if (command.kind === "stop") {
4339
+ prep = null;
4340
+ motion.stop();
4341
+ }
4342
+ };
4343
+ function issue(command) {
4344
+ if (command.kind === "go" && view === "front" && !motion.sample().moving) {
4345
+ const s3 = motion.sample(), dx = command.x - s3.x, dz2 = command.z - s3.z, length2 = Math.hypot(dx, dz2);
4346
+ if (length2 > 0.01) {
4347
+ const targetView = Math.abs(dz2 / length2) > 0.8 ? dz2 > 0 ? "front" : "rear" : "side";
4348
+ prep = { age: 0, seconds: 0.4, command, targetView, clip: targetView === "front" ? "rise-front" : "rise" };
4349
+ mirror = dx < 0;
4350
+ settle = null;
4351
+ return;
4352
+ }
4353
+ }
4354
+ if (command.kind !== "stop")
4355
+ settle = null;
4356
+ direct(command);
4357
+ }
4358
+ return {
4359
+ step(dt) {
4360
+ alive();
4361
+ if (!Number.isFinite(dt) || dt < 0 || dt > 0.25)
4362
+ throw RangeError("Invalid cat dt");
4363
+ let left = dt;
4364
+ while (left > 0.0000000001) {
4365
+ const step = Math.min(1 / 60, left);
4366
+ left -= step;
4367
+ if (prep) {
4368
+ prep.age += step;
4369
+ if (prep.age >= prep.seconds) {
4370
+ const cmd = prep.command;
4371
+ view = prep.targetView;
4372
+ prep = null;
4373
+ direct(cmd);
4374
+ }
4375
+ }
4376
+ motion.step(step);
4377
+ const s3 = motion.sample();
4378
+ const command = life.step(step, s3);
4379
+ if (command)
4380
+ issue(command);
4381
+ const report = life.report, clip = report.clip;
4382
+ if (s3.moving && !prep && !s3.exercising) {
4383
+ const sx = Math.sin(s3.heading), sz = Math.cos(s3.heading);
4384
+ mirror = sx < 0;
4385
+ view = s3.airborne ? "side" : Math.abs(sz) > 0.8 ? sz > 0 ? "front" : "rear" : "side";
4386
+ settle = null;
4387
+ }
4388
+ if (clip !== lastLifeClip) {
4389
+ if (clip === "stretch-enter") {
4390
+ view = "front";
4391
+ mirror = false;
4392
+ settle = null;
4393
+ }
4394
+ if (clip === "knead-enter") {
4395
+ view = "front";
4396
+ mirror = false;
4397
+ settle = null;
4398
+ }
4399
+ if (["tower-load", "tower-land", "wheel-enter", "wheel-leave"].includes(clip)) {
4400
+ view = "side";
4401
+ settle = null;
4402
+ if (clip.startsWith("wheel"))
4403
+ mirror = false;
4404
+ }
4405
+ if (clip === "tower-load")
4406
+ mirror = report.facing < 0;
4407
+ if (clip === "stretch-reach") {
4408
+ view = "side";
4409
+ mirror = false;
4410
+ settle = null;
4411
+ }
4412
+ if (clip === "feed-enter") {
4413
+ view = "side";
4414
+ mirror = false;
4415
+ settle = null;
4416
+ }
4417
+ if (clip === "sit" && ["bat", "stretch", "stretch-leave", "feed-leave", "loaf-leave"].includes(lastLifeClip)) {
4418
+ view = "front";
4419
+ mirror = false;
4420
+ settle = null;
4421
+ }
4422
+ lastLifeClip = clip;
4423
+ }
4424
+ const quiet = ["idle", "sit", "blink", "eye-greeting", "groom", "loaf", "sleep", "tail-swish", "tail-tip"].includes(clip);
4425
+ if (!s3.moving && !prep && quiet) {
4426
+ if (!settle && (lastMoving || view !== "front"))
4427
+ settle = { age: 0, seconds: 0.45, clip: view === "front" ? "sit-front" : "sit-down" };
4428
+ if (settle) {
4429
+ settle.age += step;
4430
+ if (settle.age >= settle.seconds) {
4431
+ view = "front";
4432
+ mirror = false;
4433
+ settle = null;
4434
+ }
4435
+ }
4436
+ } else
4437
+ settle = null;
4438
+ lastMoving = s3.moving;
4439
+ if (s3.moving && !s3.airborne && !s3.exercising && s3.supportY === 0 && !prep && view === "side")
4440
+ contacts.update(s3, mirror);
4441
+ else
4442
+ contacts.reset();
4443
+ }
4444
+ },
4445
+ frame({ columns = 160, rows = 72 } = {}) {
4446
+ alive();
4447
+ if (!Number.isInteger(columns) || !Number.isInteger(rows) || columns < 8 || rows < 4 || columns * rows > 125000)
4448
+ throw RangeError("Invalid cat viewport");
4449
+ const width = columns * 2, height = rows * 4, n = width * height * 4;
4450
+ if (!pixels || pixels.length !== n) {
4451
+ pixels = new Uint8ClampedArray(n);
4452
+ for (let i = 3;i < n; i += 4)
4453
+ pixels[i] = 255;
4454
+ }
4455
+ const s3 = motion.sample(), r = life.report;
4456
+ let clip = r.clip ?? "sit", progress = r.progress ?? 0;
4457
+ if (s3.moving && !s3.airborne && !s3.exercising) {
4458
+ clip = view === "side" ? "walk" : view === "front" ? "walk-front" : "walk-rear";
4459
+ progress = (s3.phase % 1 + 1) % 1;
4460
+ }
4461
+ if (s3.airborne) {
4462
+ clip = r.clip === "tower-up" ? "tower-flight" : r.clip === "tower-down" ? "tower-fall" : "pounce";
4463
+ progress = s3.flightProgress;
4464
+ }
4465
+ if (s3.exercising || r.clip === "wheel-run") {
4466
+ clip = "wheel-run";
4467
+ progress = (s3.wheelPhase % 1 + 1) % 1;
4468
+ }
4469
+ if (prep) {
4470
+ clip = prep.clip;
4471
+ progress = Math.min(1, prep.age / prep.seconds);
4472
+ }
4473
+ if (settle) {
4474
+ clip = settle.clip;
4475
+ progress = Math.min(1, settle.age / settle.seconds);
4476
+ }
4477
+ if (clip === "feed-lick")
4478
+ progress = progress >= 1 ? 0 : progress * 9 % 1;
4479
+ if (clip === "knead")
4480
+ progress = progress >= 1 ? 0 : progress * 4 % 1;
4481
+ const keys = asset.meta.clips[clip] ?? asset.meta.clips.sit;
4482
+ const pins = clip === "walk" ? contacts.pins(s3) : [];
4483
+ const key = keys[Math.min(keys.length - 1, Math.floor(Math.max(0, Math.min(1, progress)) * keys.length))];
4484
+ const food = r.food ? Math.ceil(r.food.remaining * 16) / 16 : null;
4485
+ const wheelAngle = Math.round(s3.wheelAngle * 120) / 120;
4486
+ const p = projectCat(s3.x, s3.y, s3.z, { width, height, canvasMeters: 0.6 }), paintKey = [width, height, key, mirror, p.x, p.y, p.size, food, r.mat, wheelAngle].join("/");
4487
+ const dish = p && food !== null ? {
4488
+ x: p.x + p.size * (269 / 320 - 0.5),
4489
+ y: p.y + p.size * (288 - 290) / 320,
4490
+ radius: p.size * 18 / 320,
4491
+ remaining: food
4492
+ } : null;
4493
+ const mat = r.mat ? { x: p.x, y: p.y + p.size * 3 / 320, radiusX: p.size * 72 / 320, radiusY: p.size * 12 / 320 } : null;
4494
+ const result = (text) => ({ text, pixels, columns, rows, color: asset.meta.color, state: { ...s3, clip, frame: key }, contacts: pins, report: r, placement: p, dish, mat });
4495
+ lastClip = clip;
4496
+ lastFrame = key;
4497
+ if (paintKey === lastPaintKey) {
4498
+ heldHits++;
4499
+ return result(lastText);
4500
+ }
4501
+ if (kernel?.clear)
4502
+ kernel.clear(pixels);
4503
+ else
4504
+ for (let i = 0;i < n; i += 4)
4505
+ pixels[i] = pixels[i + 1] = pixels[i + 2] = 0;
4506
+ const gray = asset.store.getFrame(key);
4507
+ if (p) {
4508
+ const size = p.size, left = p.x - size * 0.5, top = p.y - size * (290 / 320), x0 = Math.max(0, Math.floor(left)), y0 = Math.max(0, Math.floor(top)), x1 = Math.min(width, Math.ceil(left + size)), y1 = Math.min(height, Math.ceil(top + size));
4509
+ if (kernel?.catBlit)
4510
+ kernel.catBlit(pixels, gray, { width, height, x: p.x, y: p.y, size: p.size, mirror, pins });
4511
+ else
4512
+ for (let y = y0;y < y1; y++)
4513
+ for (let x = x0;x < x1; x++) {
4514
+ let u = (x + 0.5 - left) * 320 / size - 0.5, v = (y + 0.5 - top) * 320 / size - 0.5;
4515
+ if (mirror)
4516
+ u = 319 - u;
4517
+ if (v > 225 && pins.length) {
4518
+ const source = footSourcePoint2(u, v, pins, sourcePoint);
4519
+ u = source.x;
4520
+ v = source.y;
4521
+ }
4522
+ if (u < 0 || v < 0 || u > 319 || v > 319)
4523
+ continue;
4524
+ const a = Math.floor(u), b = Math.floor(v), fu = u - a, fv = v - b, aa = Math.min(319, a + 1), bb = Math.min(319, b + 1);
4525
+ const value = Math.round((gray[b * 320 + a] * (1 - fu) + gray[b * 320 + aa] * fu) * (1 - fv) + (gray[bb * 320 + a] * (1 - fu) + gray[bb * 320 + aa] * fu) * fv), at = (y * width + x) * 4;
4526
+ pixels[at] = pixels[at + 1] = pixels[at + 2] = value;
4527
+ }
4528
+ if (mat) {
4529
+ for (let y = Math.max(0, Math.floor(mat.y - mat.radiusY));y < Math.min(height, Math.ceil(mat.y + mat.radiusY)); y++)
4530
+ for (let x = Math.max(0, Math.floor(mat.x - mat.radiusX));x < Math.min(width, Math.ceil(mat.x + mat.radiusX)); x++) {
4531
+ const e = ((x + 0.5 - mat.x) / mat.radiusX) ** 4 + ((y + 0.5 - mat.y) / mat.radiusY) ** 4;
4532
+ if (e >= 1)
4533
+ continue;
4534
+ const at = (y * width + x) * 4;
4535
+ if (pixels[at])
4536
+ continue;
4537
+ const value = e > 0.72 ? 180 : 90;
4538
+ pixels[at] = pixels[at + 1] = pixels[at + 2] = value;
4539
+ }
4540
+ }
4541
+ if (dish) {
4542
+ const rx = dish.radius, ry = p.size * 4 / 320;
4543
+ for (let y = Math.max(0, Math.floor(dish.y - ry - 3));y < Math.min(height, Math.ceil(dish.y + ry + 2)); y++)
4544
+ for (let x = Math.max(0, Math.floor(dish.x - rx - 1));x < Math.min(width, Math.ceil(dish.x + rx + 2)); x++) {
4545
+ const dx = (x + 0.5 - dish.x) / rx, dy2 = (y + 0.5 - dish.y) / ry, e = dx * dx + dy2 * dy2;
4546
+ let value = e < 1 && e > 0.48 ? 150 : e <= 0.48 ? 48 : 0;
4547
+ const pasteRadius = Math.sqrt(food) * 0.72;
4548
+ if (food > 0 && pasteRadius > 0 && dx * dx + (dy2 + 0.5) * (dy2 + 0.5) < pasteRadius * pasteRadius)
4549
+ value = 220;
4550
+ if (!value)
4551
+ continue;
4552
+ const at = (y * width + x) * 4;
4553
+ pixels[at] = pixels[at + 1] = pixels[at + 2] = Math.max(pixels[at], value);
4554
+ }
4555
+ }
4556
+ }
4557
+ paintHabitat(pixels, width, height, wheelAngle);
4558
+ rasterizations++;
4559
+ lastPaintKey = paintKey;
4560
+ lastText = ((kernel?.braille) ? kernel.braille(pixels, width, height, 1.22, 1.08) : rgbaToBraille2(pixels, width, height, { method: "stable", brightness: 1.22, contrast: 1.08 })).text;
4561
+ return result(lastText);
4562
+ },
4563
+ interact(kind, options2) {
4564
+ alive();
4565
+ if (!["treat", "toy", "call", "stretch", "tail", "greet", "company", "tower", "wheel"].includes(kind))
4566
+ throw TypeError("Invalid cat interaction");
4567
+ const accepted = life.interact(kind, options2);
4568
+ if (accepted && prep) {
4569
+ prep = null;
4570
+ }
4571
+ return accepted;
4572
+ },
4573
+ get report() {
4574
+ alive();
4575
+ return life.report;
4576
+ },
4577
+ get stats() {
4578
+ alive();
4579
+ return { residentPixelBytes: asset.store.residentPixelBytes ?? asset.store.residentBytes, cachedFrames: asset.store.stats().cachedFrames, rgbaBytes: pixels?.byteLength ?? 0, lastClip, lastFrame, leases: lease.users?.() ?? 1, rasterizations, heldHits };
4580
+ },
4581
+ dispose() {
4582
+ if (disposed)
4583
+ return;
4584
+ disposed = true;
4585
+ pixels = null;
4586
+ prep = null;
4587
+ contacts.reset();
4588
+ asset = null;
4589
+ lastText = lastPaintKey = "";
4590
+ release();
4591
+ }
4592
+ };
4593
+ }
4594
+
4595
+ // integrations/native-windows/runtime.mjs
4596
+ init_puppy_frame_indexed();
4597
+ init_puppy_frame_store();
4598
+ var config = globalThis.__nativeConfig ?? {};
4599
+ var root = scriptArgs[1];
4600
+ var kernel = globalThis.__fast;
4601
+ var pet = null;
4602
+ var kind = config.pet ?? "dog";
4603
+ var disposed = false;
4604
+ var json = (name) => {
4605
+ const source = std.loadFile(root + "/" + name);
4606
+ if (source === null)
4607
+ throw Error("Missing pet metadata: " + name);
4608
+ return JSON.parse(source);
4609
+ };
4610
+ function provider() {
4611
+ const stores2 = [];
4612
+ const release = () => {
4613
+ for (const store of stores2)
4614
+ store.dispose();
4615
+ stores2.length = 0;
4616
+ };
4617
+ const frames = (name, frameIndices) => {
4618
+ const file = std.open(root + "/" + name, "rb");
4619
+ if (!file)
4620
+ throw Error("Missing pet artwork: " + name);
4621
+ let owned = true;
4622
+ try {
4623
+ const read = (at, n) => {
4624
+ if (file.seek(at, std.SEEK_SET) !== 0)
4625
+ throw Error("Invalid art seek");
4626
+ const bytes = new Uint8Array(n);
4627
+ if (file.read(bytes.buffer, 0, n) !== n)
4628
+ throw Error("Truncated pet artwork");
4629
+ return bytes;
4630
+ };
4631
+ file.seek(0, std.SEEK_END);
4632
+ const size = file.tell(), length2 = indexedIndexLength(read(0, 20)), index = readIndexedIndex(read(0, length2), size);
4633
+ const store = registerFrameStore(createIndexedFrameStore(index, { read, expand: (b, n) => kernel.inflate(b, n), close: () => file.close(), frameIndices, kernel }));
4634
+ stores2.push(store);
4635
+ owned = false;
4636
+ return store;
4637
+ } finally {
4638
+ if (owned)
4639
+ file.close();
4640
+ }
4641
+ };
4642
+ return { frames, release };
4643
+ }
4644
+ function make(next) {
4645
+ if (!["dog", "cat"].includes(next))
4646
+ throw Error("Unknown pet");
4647
+ const p = provider();
4648
+ try {
4649
+ if (next === "cat")
4650
+ return createCatFromAssets({ seed: config.seed ?? 731 }, { data: { meta: json("integrations/purrviewer/art/ginger.json"), store: p.frames("integrations/purrviewer/art/ginger.rvz") }, release: p.release }, kernel);
4651
+ const data = { info: json(PET_ASSETS.strideInfo), meta: json(PET_ASSETS.stableInfo), cache: p.frames(PET_ASSETS.stride), assets: {} };
4652
+ for (const key of ["rear", "approach", "seat", "arrivalSeat", "behaviors", "actions", "interactions", "comfort"]) {
4653
+ const base = PET_ASSETS[key], info = json(base + ".json");
4654
+ const indices = key === "interactions" ? [...new Set(Object.values(info.clips).flatMap((list) => list.flatMap((pair) => Array.from({ length: info.samples }, (_, s3) => pair * info.samples + s3))))] : undefined;
4655
+ data.assets[key] = { info, bytes: p.frames(base + "-q5.rvz", indices) };
4656
+ }
4657
+ return createPetFromAssets({ seed: config.seed ?? 731, returnStyle: config.returnStyle ?? "auto" }, { data, release: p.release }, kernel);
4658
+ } catch (error) {
4659
+ p.release();
4660
+ throw error;
4661
+ }
4662
+ }
4663
+ pet = make(kind);
4664
+ globalThis.__desktopReady = true;
4665
+ globalThis.__desktopFrame = (dt, columns, rows, force = false) => {
4666
+ if (disposed)
4667
+ throw Error("Native pet is disposed");
4668
+ pet.step(dt);
4669
+ const options2 = { columns, rows, cellAspect: 0.5 };
4670
+ return !force && pet.needsFrame?.(options2) === false ? null : pet.frame(options2).text;
4671
+ };
4672
+ globalThis.__desktopInteract = (action) => {
4673
+ if (kind === "cat")
4674
+ return pet.interact({ ball: "toy", tug: "company", paw: "greet", wall: "greet" }[action] ?? action);
4675
+ return action === "paw" || action === "wall" ? pet.notify({ kind: "approval", gesture: action }) : pet.interact(action);
4676
+ };
4677
+ globalThis.__desktopSwitchPet = (next) => {
4678
+ if (!["dog", "cat"].includes(next))
4679
+ throw Error("Unknown pet");
4680
+ if (next === kind)
4681
+ return true;
4682
+ pet.dispose();
4683
+ pet = null;
4684
+ pet = make(next);
4685
+ kind = next;
4686
+ return true;
4687
+ };
4688
+ globalThis.__desktopDispose = () => {
4689
+ if (disposed)
4690
+ return;
4691
+ disposed = true;
4692
+ pet?.dispose();
4693
+ pet = null;
4694
+ };