@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.
- package/ASSET-PROVENANCE.md +11 -0
- package/ASSETS-LICENSE.md +6 -0
- package/ATTRIBUTION.md +8 -0
- package/DESKTOP-PLATFORMS.md +1 -1
- package/INSTALLING.md +2 -1
- package/LICENSE +16 -16
- package/NATIVE-RUNTIME.md +95 -0
- package/PREREQUISITES.md +7 -0
- package/PUPPY-MEMORY.md +22 -0
- package/README.ko.md +17 -1
- package/README.md +19 -1
- package/RELEASING.md +35 -0
- package/SPARSE-PAINT.md +66 -0
- package/THIRD-PARTY-NOTICES.md +8 -0
- package/WINDOWS-COMPANION.md +15 -6
- package/braille.mjs +2 -2
- package/integrations/native-windows/CMakeLists.txt +15 -0
- package/integrations/native-windows/THIRD-PARTY-LICENSES.txt +77 -0
- package/integrations/native-windows/application.manifest +8 -0
- package/integrations/native-windows/bin/win32-x64/retrieviewer-native.exe +0 -0
- package/integrations/native-windows/desktop.mjs +48 -0
- package/integrations/native-windows/host.cpp +85 -0
- package/integrations/native-windows/manifest.json +14 -0
- package/integrations/native-windows/native-control.hpp +41 -0
- package/integrations/native-windows/native-paint.hpp +86 -0
- package/integrations/native-windows/native-window.hpp +107 -0
- package/integrations/native-windows/paint-regions.hpp +55 -0
- package/integrations/native-windows/runtime.bundle.mjs +4694 -0
- package/integrations/native-windows/runtime.mjs +55 -0
- package/integrations/native-windows/sources.json +23 -0
- package/integrations/purrviewer/ART.md +289 -0
- package/integrations/purrviewer/ASSETS-LICENSE.md +21 -0
- package/integrations/purrviewer/LICENSE +24 -0
- package/integrations/purrviewer/art/ginger.bin.gz +0 -0
- package/integrations/purrviewer/art/ginger.json +1707 -0
- package/integrations/purrviewer/art/ginger.rvz +0 -0
- package/integrations/purrviewer/src/contact.mjs +63 -0
- package/integrations/purrviewer/src/habitat.mjs +41 -0
- package/integrations/purrviewer/src/life.mjs +280 -0
- package/integrations/purrviewer/src/purrviewer-core.mjs +130 -0
- package/integrations/purrviewer/src/purrviewer.mjs +37 -0
- package/integrations/purrviewer/src/walk-cycle.mjs +3 -0
- package/integrations/purrviewer/src/world.mjs +216 -0
- package/integrations/purrviewer/vendor/retrieviewer/LICENSE-CODE +21 -0
- package/integrations/purrviewer/vendor/retrieviewer/braille.mjs +73 -0
- package/integrations/purrviewer/vendor/retrieviewer/puppy-frame-file.mjs +15 -0
- package/integrations/purrviewer/vendor/retrieviewer/puppy-frame-indexed.mjs +78 -0
- package/integrations/purrviewer/vendor/retrieviewer/puppy-frame-predictive.mjs +170 -0
- package/integrations/purrviewer/vendor/retrieviewer/puppy-frame-store.mjs +114 -0
- package/integrations/windows-overlay/desktop.mjs +11 -3
- package/integrations/windows-overlay/worker.mjs +9 -2
- package/package.json +42 -4
- package/puppy-clean.mjs +2 -2
- package/puppy-companion-sprites.mjs +2 -2
- package/puppy-frame-indexed.mjs +2 -2
- package/puppy-paper-actions.mjs +2 -2
- package/puppy-paper-behaviors.mjs +1 -1
- package/puppy-paper-contact.mjs +2 -2
- package/puppy-paper-seat.mjs +2 -2
- package/puppy-paper.mjs +12 -12
- package/retrieviewer-cli.mjs +10 -6
- 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
|
+
};
|