@bitefoundry/retrieviewer 0.0.4 → 0.0.6
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 +12 -0
- package/BUTTERFLY.md +30 -0
- package/COMFORT-ART.md +30 -0
- package/INSTALLING.md +4 -0
- package/PUPPY-MEMORY.md +32 -0
- package/README.ko.md +8 -1
- package/README.md +6 -1
- package/RELEASING.md +10 -1
- package/integrations/linux-overlay/overlay.py +6 -0
- package/integrations/macos-overlay/overlay.jxa +1 -1
- package/integrations/windows-overlay/Overlay.cs +3 -2
- package/integrations/windows-overlay/desktop.mjs +2 -1
- package/integrations/windows-overlay/protocol.mjs +7 -5
- package/integrations/windows-overlay/worker.mjs +1 -1
- package/package.json +2 -1
- package/pet-q5-manifest.json +14 -5
- package/puppy-comfort-sprites.mjs +14 -6
- package/puppy-companion-comfort-q5.bin.gz +0 -0
- package/puppy-companion-comfort.json +69 -6
- package/puppy-companion-life.mjs +57 -5
- package/puppy-companion-objects.mjs +16 -0
- package/puppy-companion-sprites.mjs +4 -2
- package/puppy-frame-predictive.mjs +27 -10
- package/puppy-paper-actions.mjs +4 -2
- package/puppy-paper-behaviors-q5.bin.gz +0 -0
- package/puppy-paper-behaviors.mjs +1 -1
- package/puppy-paper-contact.mjs +4 -2
- package/puppy-paper-seat.mjs +4 -2
- package/puppy-paper.mjs +29 -15
- package/retrieviewer-cli.mjs +8 -6
- package/retrieviewer.d.mts +4 -0
- package/retrieviewer.mjs +8 -1
package/ASSET-PROVENANCE.md
CHANGED
|
@@ -35,6 +35,18 @@ must be checked again for the final release bytes.
|
|
|
35
35
|
|
|
36
36
|
## Remaining limits
|
|
37
37
|
|
|
38
|
+
0.0.6 re-registers only the36 watch samples of the existing behavior archive.
|
|
39
|
+
The existing seated body/paws stay fixed while the authored head turns; no new
|
|
40
|
+
image/photo/video reference is introduced. The offline preparation and source
|
|
41
|
+
digest are in tools/prepare-watch-art.* and pet-q5-manifest.json. Butterfly wings
|
|
42
|
+
are small geometric marks, using the same existing output buffer; dog poses
|
|
43
|
+
reuse licensed gallop art. Existing attribution and CC BY-SA4.0 terms remain.
|
|
44
|
+
|
|
45
|
+
0.0.5 extends that ninth licensed archive with four sleeping studies generated
|
|
46
|
+
from the same existing puppy identity/pose references, approved by the owner in
|
|
47
|
+
this conversation. COMFORT-ART.md and the comfort metadata record preparation,
|
|
48
|
+
source hashes and size changes. No new downloaded photo/video input is introduced.
|
|
49
|
+
|
|
38
50
|
Later source candidate: belly/sleep reuse existing licensed roll/rest frames.
|
|
39
51
|
Tug adds eight built-in imagegen keys referenced only from the existing generated
|
|
40
52
|
puppy sheet, not new photographs. COMFORT-ART.md records the prompt, source hash
|
package/BUTTERFLY.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Rare butterfly encounter (0.0.6)
|
|
2
|
+
|
|
3
|
+
No additional dog artwork, sprite archive, dependency, background process or
|
|
4
|
+
timer. Existing short grounded gallops supply the bounding legs and ear motion;
|
|
5
|
+
the visitor is drawn directly into the existing grayscale buffer before Braille
|
|
6
|
+
conversion. Camera, stride-distance integration, contact correction and palette
|
|
7
|
+
are unchanged. This is not a new up-looking/pouncing source illustration.
|
|
8
|
+
|
|
9
|
+
The initial opportunity is scheduled after240–480 active simulation seconds.
|
|
10
|
+
Only an awake, energetic puppy near the viewer may participate; sleep pressure
|
|
11
|
+
takes priority. Subsequent opportunities are at least720 seconds apart, with
|
|
12
|
+
up to480 additional random seconds. Hidden/paused time does not accumulate.
|
|
13
|
+
An opportunity can be delayed by sleep or another coherent episode, rather than
|
|
14
|
+
forcing a timer callback that wakes or interrupts the puppy.
|
|
15
|
+
|
|
16
|
+
The puppy notices, makes three short alternating chases, watches the visitor fly
|
|
17
|
+
away, returns and rests. Each target keeps the butterfly ahead of the muzzle.
|
|
18
|
+
The visitor has an18-second visibility guard. Accepted ball/treat/tug clears the
|
|
19
|
+
encounter and its obsolete route immediately, using normal actor recovery.
|
|
20
|
+
A separate random stream leaves the existing ordinary episode/gait choices
|
|
21
|
+
unchanged before the encounter.
|
|
22
|
+
|
|
23
|
+
Checks: `node --test puppy-butterfly.test.mjs`, `npm test`.
|
|
24
|
+
Generate a lightweight real-engine replay: `node tools/check-butterfly.mjs`.
|
|
25
|
+
Its local `site-dist/butterfly-preview.html` loads prerecorded cropped Braille
|
|
26
|
+
frames, not another full pet asset set. The replay is a QA artifact, not an npm
|
|
27
|
+
asset or an alternate implementation. Serve only `site-dist` on a local address.
|
|
28
|
+
|
|
29
|
+
Version0.0.6 includes this behavior. Existing0.0.5 registry artifacts remain
|
|
30
|
+
unchanged. An already-running companion must be restarted after updating.
|
package/COMFORT-ART.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Comfort extension art record
|
|
2
2
|
|
|
3
|
+
## 0.0.5 sleeping studies
|
|
4
|
+
|
|
5
|
+
Four owner-approved built-in imagegen originals (side, belly, curl, chin), using
|
|
6
|
+
the existing `puppy-behaviors16-sheet.png` identity and `puppy-roll16-v4-sheet.png`
|
|
7
|
+
pose references, extend the same CC BY-SA4.0 comfort archive. No new external
|
|
8
|
+
photo or video input. Source PNGs remain local; metadata records SHA-256 hashes.
|
|
9
|
+
Preparation: `node tools/prepare-sleep-art.mjs` with the preserved local studies.
|
|
10
|
+
It keeps the nine existing tug keys pixel-identical and registers four new static
|
|
11
|
+
keys to ground y=300. There are no duplicated right-facing images: source sampling
|
|
12
|
+
reverses x. The pose is selected once per nap, not switched during a sleeping hold.
|
|
13
|
+
A short rest-to-sleep/rest recovery blend uses existing scratch; the sleeping body
|
|
14
|
+
itself is held, without scale pulsing. The z cue is silent.
|
|
15
|
+
|
|
16
|
+
The archive grows from103549 to129148 bytes (+25599). Packed resident pixels grow
|
|
17
|
+
from291337 to341139 (+49802 bytes). The renderer's mutually exclusive authored
|
|
18
|
+
samplers now share one action-sized RGBA scratch, with320px subviews for smaller
|
|
19
|
+
poses, replacing their separate buffers (2457600 bytes less before tug playback).
|
|
20
|
+
Sleep display is capped at5fps in the desktop pump, while simulation advances
|
|
21
|
+
normally and accepted interactions invalidate the next paint immediately.
|
|
22
|
+
Ink presets only change the native painter color; no RGB sprite families or
|
|
23
|
+
background capture are introduced. Actual RSS depends on host and allocator.
|
|
24
|
+
|
|
25
|
+
The four prompts share: one complete naturally proportioned pale fluffy retriever,
|
|
26
|
+
same bandana, closed eyes, grayscale fine fur on pure black, fixed slightly elevated
|
|
27
|
+
camera, no text/bed/toy/background. Pose clauses: relaxed side sleep; belly-up with
|
|
28
|
+
loosely bent paws and tiny tongue; loose curl with tail near muzzle; chin resting
|
|
29
|
+
on forepaws. Source prompts and PNGs are preserved locally with the approved studies.
|
|
30
|
+
|
|
31
|
+
The following section records the earlier0.0.4 preparation, not the new sleep keys.
|
|
32
|
+
|
|
3
33
|
Source code MIT; generated puppy art and frame data CC BY-SA4.0, retaining the
|
|
4
34
|
credits in ATTRIBUTION.md. This is a development-source record, not new legal
|
|
5
35
|
clearance. No external photograph, YouTube frame or downloaded rig was introduced.
|
package/INSTALLING.md
CHANGED
|
@@ -6,6 +6,10 @@ Desktop setup (Node20+; [OS requirements and verification](DESKTOP-PLATFORMS.md)
|
|
|
6
6
|
2. `retrieviewer desktop` — select the app window in the picker.
|
|
7
7
|
|
|
8
8
|
No agent configuration is needed for this separate transparent companion.
|
|
9
|
+
Choose the dot ink at launch: `retrieviewer desktop --ink gold`, or
|
|
10
|
+
`retrieviewer desktop --ink dark` on light backgrounds. Default: `cream`.
|
|
11
|
+
Exit an existing companion before changing ink. Presets reuse the same Braille
|
|
12
|
+
mask and native painter, without new color sprites or screen capture.
|
|
9
13
|
Windows and AlmaLinux8/GLIBC2.28 X11 have live test evidence. macOS support is
|
|
10
14
|
experimental and not runtime verified; native Wayland is unsupported. Linux needs
|
|
11
15
|
system Python3.6+, X11/Xrender/Xext and a compositor; Windows needs the .NET
|
package/PUPPY-MEMORY.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Runtime weight and performance
|
|
2
2
|
|
|
3
|
+
## Node compressed crops (0.0.6)
|
|
4
|
+
|
|
5
|
+
Node now stores each validated Q5 crop as a small DEFLATE block, using built-in
|
|
6
|
+
`node:zlib`; only the existing two-frame LRU is expanded. The original archive
|
|
7
|
+
buffer is released. Browser hosts keep the portable bit-packed implementation.
|
|
8
|
+
No added runtime dependency, process, timer, lower resolution or lost pose.
|
|
9
|
+
|
|
10
|
+
Same Windows host, fresh processes, same450-frame ball sequence:
|
|
11
|
+
|
|
12
|
+
| Metric | 0.0.5 | 0.0.6 |
|
|
13
|
+
| --- | ---: | ---: |
|
|
14
|
+
| Loaded ArrayBuffers, after native close callbacks + diagnostic GC | 59.7 MiB | 21.4 MiB |
|
|
15
|
+
| Rendered ArrayBuffers | 67.3 MiB | 29.0 MiB |
|
|
16
|
+
| Mean frame time | 3.39 ms | 3.39 ms |
|
|
17
|
+
| P95 frame time | 6.11 ms | 6.27 ms |
|
|
18
|
+
| Initial load | 653 ms | 910 ms |
|
|
19
|
+
| Actual Windows overlay worker RSS, short owned-window checks | 130 MiB | 96–106 MiB |
|
|
20
|
+
|
|
21
|
+
The450-frame output hash is unchanged. `puppy-node-cache.test.mjs` compares every
|
|
22
|
+
packaged source frame in both stores, including the separately repaired watch
|
|
23
|
+
art. These samples are not a RAM ceiling: V8, allocator retention, viewport and
|
|
24
|
+
activity change RSS. The helper is separate. The large off-screen benchmark
|
|
25
|
+
peaked at267/238 MiB; do not substitute the short96–106 MiB samples for that peak.
|
|
26
|
+
Evidence: tools/benchmark-pet.mjs, local dist/006-paired-{before,after}.json and
|
|
27
|
+
dist/006-native-{before,source}.log. Diagnostic GC is never enabled in the pet.
|
|
28
|
+
Shrinking V8's young generation was tested but increased rendering cost, so no
|
|
29
|
+
global or embedded-host V8 flags are imposed.
|
|
30
|
+
|
|
31
|
+
Watch repair adds no frame count; its archive is15,422 bytes smaller. Butterfly
|
|
32
|
+
chasing adds no dog images or persistent image buffer; see BUTTERFLY.md. Existing
|
|
33
|
+
sleep, colors, wake-on-interaction and5fps sleeping repaint remain unchanged.
|
|
34
|
+
|
|
3
35
|
## Comfort behaviors (0.0.4)
|
|
4
36
|
|
|
5
37
|
Belly-up flopping and eyes-closed sleeping reuse the existing action/rest art.
|
package/README.ko.md
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
<p align="center"><a href="https://bitefoundry.github.io/retrieviewer/?lang=ko">직접 만나 보기</a> · <a href="README.md">English</a> · <a href="https://www.npmjs.com/package/@bitefoundry/retrieviewer">npm</a> · <a href="https://github.com/BiteFoundry/retrieviewer/issues">피드백</a></p>
|
|
5
5
|
|
|
6
6
|
앱 창 위에서 혼자 걷고, 뛰고, 앉아서 쉬는 보송한 점자 도트 리트리버예요.
|
|
7
|
-
|
|
7
|
+
옆잠, 발라당, 몸 말기, 앞발에 턱 괴기. 조용하면 양쪽으로 누워 잠들어요.
|
|
8
|
+
가끔 공·간식·터그로 놀아주세요.
|
|
9
|
+
가끔 찾아오는 나비를 쫓기도 해요.
|
|
8
10
|
나머지 시간에는 각자의 일을 하면 돼요.
|
|
9
11
|
[BiteFoundry](https://github.com/BiteFoundry)가 만듭니다.
|
|
10
12
|
런타임에는 계정, 모델 API, 텔레메트리가 필요하지 않아요.
|
|
@@ -31,6 +33,11 @@ Node.js/npm이 없다면 [먼저 설치](PREREQUISITES.md)해 주세요.
|
|
|
31
33
|
통과시킵니다. 앱 창 단위로 동작하고, 대상 창이 비활성화·최소화되면 숨습니다.
|
|
32
34
|
자동 시작이나 셸 래퍼는 등록하지 않아요.
|
|
33
35
|
|
|
36
|
+
도트 색은 실행할 때 고를 수 있어요. 기본은 크림색이고, 골드색은
|
|
37
|
+
`retrieviewer desktop --ink gold`, 밝은 배경에는
|
|
38
|
+
`retrieviewer desktop --ink dark`를 쓰세요. 실행 중이라면 먼저 종료하고 다시
|
|
39
|
+
실행하세요. 배경을 캡처하지 않고, 같은 도트에 색만 바꿉니다.
|
|
40
|
+
|
|
34
41
|
> **Cursor / VS Code Remote SSH 사용자:** 원격 서버의 터미널이 아닌,
|
|
35
42
|
> Cursor 화면이 떠 있는 **로컬 PC의 PowerShell/터미널**에 설치하세요.
|
|
36
43
|
> 서버의 `DISPLAY=:1`은 서버 쪽 X11 화면을 뜻합니다. VNC로 그 화면을 보고
|
package/README.md
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
14
|
A fluffy, autonomous Braille-art retriever that hangs out above an app window.
|
|
15
|
-
It wanders, runs, flops belly-up and naps.
|
|
15
|
+
It wanders, runs, flops belly-up and naps. Occasionally it chases a butterfly.
|
|
16
|
+
Toss a ball, offer a treat or play tug.
|
|
16
17
|
Get back to your thing.
|
|
17
18
|
|
|
18
19
|
Made by [BiteFoundry](https://github.com/BiteFoundry). No model API, account or
|
|
@@ -40,6 +41,10 @@ click-through native overlay—not a patched app, shell wrapper or injected MCP
|
|
|
40
41
|
widget. It follows a whole app window, not a tab, and hides when that window is
|
|
41
42
|
inactive or minimized. No automatic startup entry is installed.
|
|
42
43
|
|
|
44
|
+
Choose the dot color at launch: `retrieviewer desktop --ink gold`, or
|
|
45
|
+
`retrieviewer desktop --ink dark` for light backgrounds. Default: `cream`.
|
|
46
|
+
Quit a running companion first. Colors reuse the same artwork; no screen capture.
|
|
47
|
+
|
|
43
48
|
> **Cursor / VS Code Remote SSH:** run those commands in your **local** computer's
|
|
44
49
|
> terminal, not the remote server's terminal. The puppy belongs where the window
|
|
45
50
|
> is displayed. A server's `DISPLAY=:1` refers to that server's X11 display, which
|
package/RELEASING.md
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
# Retrieviewer 0.0.
|
|
1
|
+
# Retrieviewer 0.0.6 release process
|
|
2
|
+
|
|
3
|
+
Current target: `@bitefoundry/retrieviewer@0.0.6`, public `latest`. The owner
|
|
4
|
+
explicitly requested Node memory optimization, repaired watch motion, completed
|
|
5
|
+
butterfly encounters and npm publication on2026-10-23. Built-in Node zlib keeps
|
|
6
|
+
each Q5 crop compressed; portable browser storage stays unchanged. Watch art
|
|
7
|
+
reuses existing drawings with canonical seated paws/body. Butterfly encounters
|
|
8
|
+
reuse gallop poses with no extra dog archive. MIT/CC BY-SA4.0 unchanged.
|
|
9
|
+
Preserve0.0.5 and all earlier registry bytes; publish only the exact tested0.0.6
|
|
10
|
+
tarball. The historical scope below is not current verification evidence.
|
|
2
11
|
|
|
3
12
|
Target: `@bitefoundry/retrieviewer@0.0.4`, public npm registry, `latest` tag.
|
|
4
13
|
The npm organization `bitefoundry` uses the free public-packages plan; account
|
|
@@ -180,6 +180,12 @@ class Companion(object):
|
|
|
180
180
|
if kind == 'quit':
|
|
181
181
|
self.quit()
|
|
182
182
|
elif kind == 'loaded':
|
|
183
|
+
palette = {'cream': (248, 242, 232), 'gold': (231, 184, 103), 'dark': (67, 48, 24)}
|
|
184
|
+
ink = message.get('ink', 'cream')
|
|
185
|
+
if ink not in palette:
|
|
186
|
+
raise OverlayError('Invalid ink')
|
|
187
|
+
self.tiles.ink = palette[ink]
|
|
188
|
+
self.tiles.cache.clear()
|
|
183
189
|
self.loaded = True
|
|
184
190
|
self.set_status('playing')
|
|
185
191
|
if self.test_mode:
|
|
@@ -238,7 +238,7 @@ function createCore(io, options) {
|
|
|
238
238
|
if (s.quitting) return;
|
|
239
239
|
if (!m || typeof m !== 'object') throw new Error('Invalid overlay message');
|
|
240
240
|
if (m.type === 'quit') { quit(); return; }
|
|
241
|
-
if (m.type === 'loaded') { s.loaded = true; io.status(TEXT.playing); io.loaded(); return; }
|
|
241
|
+
if (m.type === 'loaded') { var palette = {cream:[248,242,232,245],gold:[231,184,103,245],dark:[67,48,24,245]}; var ink = m.ink || 'cream'; if (!Object.prototype.hasOwnProperty.call(palette,ink)) throw new Error('Invalid ink'); INK=palette[ink]; s.loaded = true; io.status(TEXT.playing); io.loaded(); return; }
|
|
242
242
|
if (m.type === 'interaction') { io.status(m.accepted === true ? TEXT.accepted : TEXT.busy); return; }
|
|
243
243
|
if (m.type === 'error') { io.status(TEXT.engineError); quit(); return; }
|
|
244
244
|
if (m.type === 'frame') {
|
|
@@ -61,6 +61,7 @@ class QuietWindow : Form {
|
|
|
61
61
|
sealed class PetWindow : QuietWindow {
|
|
62
62
|
protected override CreateParams CreateParams {get{CreateParams p=base.CreateParams;p.ExStyle|=0x80000|0x20;return p;}} // LAYERED + TRANSPARENT
|
|
63
63
|
internal int PaintedDots;
|
|
64
|
+
internal Color Ink=Color.FromArgb(245,248,242,232);
|
|
64
65
|
internal Bitmap PaintFrame(Dictionary<string,object> m,int cell,Rectangle canvas){
|
|
65
66
|
int cols=Program.Int(m,"columns"),rows=Program.Int(m,"rows"),cx=Program.Int(m,"x"),cy=Program.Int(m,"y");
|
|
66
67
|
string text=Program.Str(m,"text");
|
|
@@ -69,7 +70,7 @@ sealed class PetWindow : QuietWindow {
|
|
|
69
70
|
string[] lines=text.Split('\n');if(lines.Length!=rows)throw new Exception("Invalid overlay row count");
|
|
70
71
|
Bitmap image=new Bitmap(cols*cell,rows*cell*2,PixelFormat.Format32bppPArgb);
|
|
71
72
|
try{
|
|
72
|
-
using(Graphics g=Graphics.FromImage(image))using(Brush ink=new SolidBrush(
|
|
73
|
+
using(Graphics g=Graphics.FromImage(image))using(Brush ink=new SolidBrush(Ink)){
|
|
73
74
|
g.Clear(Color.Transparent);g.SmoothingMode=System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
|
|
74
75
|
int[] dx={0,0,0,1,1,1,0,1},dy={0,1,2,0,1,2,3,3};float step=cell/2f,diameter=Math.Max(1.1f,step*.48f);PaintedDots=0;
|
|
75
76
|
for(int y=0;y<rows;y++){
|
|
@@ -149,7 +150,7 @@ sealed class Companion : ApplicationContext {
|
|
|
149
150
|
for(;;){Dictionary<string,object> m;lock(inputLock){if(messages.Count==0)break;m=messages.Dequeue();}
|
|
150
151
|
string type=Program.Str(m,"type");
|
|
151
152
|
if(type=="quit"){Quit();return;}
|
|
152
|
-
if(type=="loaded"){loaded=true;status.Text="함께 노는 중";if(testMode)((Button)bar.Controls[0]).PerformClick();continue;}
|
|
153
|
+
if(type=="loaded"){string ink=m.ContainsKey("ink")?Program.Str(m,"ink"):"cream";if(ink!="cream"&&ink!="gold"&&ink!="dark")throw new Exception("Invalid ink");pet.Ink=ink=="gold"?Color.FromArgb(245,231,184,103):ink=="dark"?Color.FromArgb(245,67,48,24):Color.FromArgb(245,248,242,232);loaded=true;status.Text="함께 노는 중";if(testMode)((Button)bar.Controls[0]).PerformClick();continue;}
|
|
153
154
|
if(type=="interaction"){status.Text=Program.Bool(m,"accepted")?"좋아!":"잠깐 기다려";continue;}
|
|
154
155
|
if(type=="error"){status.Text="엔진 오류";Quit();return;}
|
|
155
156
|
if(type=="frame"){
|
|
@@ -93,13 +93,14 @@ export async function desktopHelper(options={}){
|
|
|
93
93
|
throw Error('Linux desktop needs system Python3.6+ (AlmaLinux8 platform-python is supported). No packages or system settings were installed.');
|
|
94
94
|
}
|
|
95
95
|
export async function startDesktop(options={}){
|
|
96
|
+
const ink=options.ink??'cream';if(!['cream','gold','dark'].includes(ink))throw Error('Invalid desktop ink');
|
|
96
97
|
// Fail before creating any cache/session on unsupported desktops (e.g. Wayland).
|
|
97
98
|
desktopPlatform(options);
|
|
98
99
|
const status=await desktopStatus(options);if(status.running)return {...status,alreadyRunning:true};
|
|
99
100
|
if(status.stale)await stopDesktop(options);
|
|
100
101
|
const helper=await desktopHelper(options);
|
|
101
102
|
return new Promise((resolve,reject)=>{
|
|
102
|
-
const child=spawn(process.execPath,[fileURLToPath(new URL('./worker.mjs',import.meta.url)),'--helper-spec',JSON.stringify(helper)],{detached:true,windowsHide:true,stdio:['ignore','ignore','ignore','ipc'],env:{...process.env,RETRIEVIEWER_HOME:getConfigPaths(options).root}});
|
|
103
|
+
const child=spawn(process.execPath,[fileURLToPath(new URL('./worker.mjs',import.meta.url)),'--helper-spec',JSON.stringify(helper)],{detached:true,windowsHide:true,stdio:['ignore','ignore','ignore','ipc'],env:{...process.env,RETRIEVIEWER_HOME:getConfigPaths(options).root,RETRIEVIEWER_INK:ink}});
|
|
103
104
|
let settled=false;const finish=(error,message)=>{if(settled)return;settled=true;clearTimeout(timer);if(child.connected)child.disconnect();child.unref();error?reject(error):resolve(message);};
|
|
104
105
|
const timer=setTimeout(()=>{if(child.connected)child.send({command:'abort'});finish(Error('Desktop startup timed out; no agent settings changed'));},20000);
|
|
105
106
|
child.on('error',error=>finish(error));child.on('message',message=>message.error?finish(Error(message.error)):finish(null,message));child.on('exit',code=>finish(Error('Desktop exited during startup ('+code+')')));
|
|
@@ -20,23 +20,25 @@ export function cropFrame(frame,revision){
|
|
|
20
20
|
|
|
21
21
|
// Request/ack rendering: never queue obsolete frames behind a slow native painter.
|
|
22
22
|
export function createFramePump({pet,send,now=()=>performance.now()}){
|
|
23
|
-
let viewport=null,inFlight=false,last=null,lastFrame=null,revision=0,paused=false,disposed=false;
|
|
23
|
+
let viewport=null,inFlight=false,last=null,lastFrame=null,revision=0,paused=false,disposed=false,nextPaint=0;
|
|
24
24
|
const alive=()=>{if(disposed)throw Error('Overlay pump is disposed');};
|
|
25
25
|
return {
|
|
26
|
-
viewport(message){alive();const next=viewportOf(message);revision=message.revision;viewport=next;last=null;lastFrame=null;},
|
|
27
|
-
pause(value){alive();paused=!!value;last=null;lastFrame=null;},
|
|
26
|
+
viewport(message){alive();const next=viewportOf(message);revision=message.revision;viewport=next;last=null;lastFrame=null;nextPaint=0;},
|
|
27
|
+
pause(value){alive();paused=!!value;last=null;lastFrame=null;nextPaint=0;},
|
|
28
28
|
ack(){alive();inFlight=false;},
|
|
29
29
|
tick(){
|
|
30
30
|
alive();if(inFlight||!viewport||paused)return false;
|
|
31
31
|
const t=now(),dt=last===null?0:Math.max(0,(t-last)/1000);last=t;
|
|
32
|
-
pet.step(dt);
|
|
32
|
+
pet.step(dt);if(t<nextPaint)return false;
|
|
33
|
+
const source=pet.frame(viewport),frame=cropFrame(source,revision);
|
|
34
|
+
nextPaint=source.state?.state==='sleep'?t+200:0;
|
|
33
35
|
// A held seat/down pose can be byte-identical for seconds. Advance the life
|
|
34
36
|
// clock normally but avoid JSON, IPC, bitmap allocation and native repaint.
|
|
35
37
|
if(lastFrame&&frame.x===lastFrame.x&&frame.y===lastFrame.y&&frame.columns===lastFrame.columns&&frame.rows===lastFrame.rows&&frame.text===lastFrame.text)return false;
|
|
36
38
|
inFlight=true;
|
|
37
39
|
try{send(frame);lastFrame=frame;}catch(error){inFlight=false;throw error;}return true;
|
|
38
40
|
},
|
|
39
|
-
interact(kind){alive();if(!['ball','treat','tug'].includes(kind))throw Error('Invalid overlay interaction');const accepted=pet.interact(kind);if(accepted)lastFrame=null;return accepted;},
|
|
41
|
+
interact(kind){alive();if(!['ball','treat','tug'].includes(kind))throw Error('Invalid overlay interaction');const accepted=pet.interact(kind);if(accepted){lastFrame=null;nextPaint=0;}return accepted;},
|
|
40
42
|
get hidden(){return !viewport||paused;},
|
|
41
43
|
dispose(){if(!disposed){disposed=true;viewport=lastFrame=null;pet.dispose();}},
|
|
42
44
|
};
|
|
@@ -61,6 +61,6 @@ try{
|
|
|
61
61
|
const {createPet}=await import('../../retrieviewer.mjs');const pet=await createPet();
|
|
62
62
|
if(stopping)pet.dispose();else{
|
|
63
63
|
pump=createFramePump({pet,send:write});if(pendingViewport)pump.viewport(pendingViewport);
|
|
64
|
-
write({type:'loaded'});timer=setInterval(()=>{try{pump.tick();}catch(error){write({type:'error',message:error.message});stop();}},1000/spec.fps);
|
|
64
|
+
write({type:'loaded',ink:process.env.RETRIEVIEWER_INK??'cream'});timer=setInterval(()=>{try{pump.tick();}catch(error){write({type:'error',message:error.message});stop();}},1000/spec.fps);
|
|
65
65
|
}
|
|
66
66
|
}catch(error){sendParent({error:error.message});await stop();}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitefoundry/retrieviewer",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "A fluffy Braille desktop pet for Windows and Linux X11, with experimental macOS support and an embeddable TUI engine.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"homepage": "https://bitefoundry.github.io/retrieviewer/",
|
|
@@ -89,6 +89,7 @@
|
|
|
89
89
|
"PUPPY-MEMORY.md",
|
|
90
90
|
"PUPPY-Q5-COMPARISON.md",
|
|
91
91
|
"COMFORT-ART.md",
|
|
92
|
+
"BUTTERFLY.md",
|
|
92
93
|
"examples/embed.mjs",
|
|
93
94
|
"examples/live.mjs",
|
|
94
95
|
"examples/plugin.mjs",
|
package/pet-q5-manifest.json
CHANGED
|
@@ -80,11 +80,20 @@
|
|
|
80
80
|
"uniqueFrames": 142,
|
|
81
81
|
"sourceSha256": "7918261fb64c57a91739729841fd84c6dd53891196c7fb76ee9c27712ab6ef5d",
|
|
82
82
|
"sourcePackedSha256": "7785ef65b8a2a5bf6f7ce600fe9957a60d9af82066ff49275bba061eac5392d1",
|
|
83
|
-
"encodedBytes":
|
|
84
|
-
"gzipBytes":
|
|
85
|
-
"residentPixelBytes":
|
|
86
|
-
"encodedSha256": "
|
|
87
|
-
"quantizedSha256": "
|
|
83
|
+
"encodedBytes": 6185813,
|
|
84
|
+
"gzipBytes": 1434734,
|
|
85
|
+
"residentPixelBytes": 3845581,
|
|
86
|
+
"encodedSha256": "8e4811c4c72337d094770596b2b73e2f468f92884e6de2e14880ce9b9ad6f351",
|
|
87
|
+
"quantizedSha256": "500dcc9b5edbadf97159fc9256e9d942578e13e36f14c6fecda649a97214318b",
|
|
88
|
+
"watchRegistration": {
|
|
89
|
+
"sourceVersion": "0.0.5",
|
|
90
|
+
"sourceEncodedSha256": "38cfdfcee9c857c56a14f0a9734cb3301b4a04cb8e1ae3fb2e2b2ffe3666240f",
|
|
91
|
+
"canonicalSeat": 50,
|
|
92
|
+
"bodyFromY": 210,
|
|
93
|
+
"neckBlendFromY": 180,
|
|
94
|
+
"headTranslateY": 8,
|
|
95
|
+
"unchangedOtherFrames": true
|
|
96
|
+
}
|
|
88
97
|
},
|
|
89
98
|
"puppy-paper-actions-v9": {
|
|
90
99
|
"file": "puppy-paper-actions-v9-q5.bin.gz",
|
|
@@ -12,14 +12,22 @@ export function comfortFrameIndex(progress,info){
|
|
|
12
12
|
return loop[Math.floor((seconds-.65)/.28)%loop.length];
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export function createComfortSpriteSampler(bytes,info){
|
|
16
|
-
if(!isGrayFrameSource(bytes)
|
|
17
|
-
|
|
15
|
+
export function createComfortSpriteSampler(bytes,info,output){
|
|
16
|
+
if(!isGrayFrameSource(bytes)||![1,2].includes(info?.version)||info.width!==320||info.height!==320||info.frames!==(info.version===2?13:9))throw Error('Invalid comfort sprites');
|
|
17
|
+
if(output!==undefined&&(!(output instanceof Uint8ClampedArray)||output.length!==320*320*4))throw Error('Invalid comfort output');
|
|
18
|
+
const frames=grayFrameReader(bytes,320,320,info.frames);let rgba=output??null;
|
|
19
|
+
if(rgba)for(let i=3;i<rgba.length;i+=4)rgba[i]=255;
|
|
18
20
|
return pose=>{
|
|
19
|
-
if(pose?.clip
|
|
21
|
+
if(!['tug','sleep'].includes(pose?.clip))throw Error('Invalid comfort clip');
|
|
20
22
|
if(!rgba){rgba=new Uint8ClampedArray(320*320*4);for(let i=3;i<rgba.length;i+=4)rgba[i]=255;}
|
|
21
|
-
const index=comfortFrameIndex(pose.progress,info)
|
|
22
|
-
|
|
23
|
+
const index=pose.clip==='sleep'?info.sleep?.[pose.variant]:comfortFrameIndex(pose.progress,info);
|
|
24
|
+
if(!Number.isInteger(index))throw Error('Invalid sleep variant');
|
|
25
|
+
const frame=frames.getFrame(index),base=pose.base,blend=pose.blend??1;
|
|
26
|
+
if(!Number.isFinite(blend)||blend<0||blend>1)throw Error('Invalid sleep blend');
|
|
27
|
+
for(let i=0,j=0;i<frame.length;i++,j+=4){
|
|
28
|
+
const v=frame[pose.right?Math.floor(i/320)*320+319-i%320:i];
|
|
29
|
+
rgba[j]=rgba[j+1]=rgba[j+2]=base&&blend<1?Math.round(base[j]+(v-base[j])*blend):v;
|
|
30
|
+
}
|
|
23
31
|
return rgba;
|
|
24
32
|
};
|
|
25
33
|
}
|
|
Binary file
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version":
|
|
2
|
+
"version": 2,
|
|
3
3
|
"width": 320,
|
|
4
4
|
"height": 320,
|
|
5
|
-
"frames":
|
|
5
|
+
"frames": 13,
|
|
6
6
|
"clips": [
|
|
7
|
-
"tug"
|
|
7
|
+
"tug",
|
|
8
|
+
"sleep"
|
|
8
9
|
],
|
|
9
|
-
"gzipBytes":
|
|
10
|
-
"residentPixelBytes":
|
|
10
|
+
"gzipBytes": 129148,
|
|
11
|
+
"residentPixelBytes": 341139,
|
|
11
12
|
"sourceSheetSha256": "10fde9dacca5cb45acd68f387e7ed3bcf88cd72cf0f9c4220d31d819db91731e",
|
|
12
13
|
"neutralSha256": "19f89a8bcb5a937e528593a8f36db846a0d45a5ee398f1b0873c056e5be2fa0a",
|
|
13
|
-
"encodedSha256": "
|
|
14
|
+
"encodedSha256": "35327cc103b1f7ac2ff1b09ed98658fd8ff1dcef322de9c9b18071a4358b7b87",
|
|
14
15
|
"method": "Built-in imagegen authored tug keys. One fixed scale, ground/center translation only. Exact existing Q5 seat first/last. No expanded tween film or live body deformation.",
|
|
15
16
|
"registration": {
|
|
16
17
|
"sheetSize": [
|
|
@@ -73,5 +74,67 @@
|
|
|
73
74
|
"dy": 74
|
|
74
75
|
}
|
|
75
76
|
]
|
|
77
|
+
},
|
|
78
|
+
"sleep": {
|
|
79
|
+
"side": 9,
|
|
80
|
+
"belly": 10,
|
|
81
|
+
"curl": 11,
|
|
82
|
+
"chin": 12
|
|
83
|
+
},
|
|
84
|
+
"sleepRegistration": [
|
|
85
|
+
{
|
|
86
|
+
"key": "side",
|
|
87
|
+
"sourceBounds": [
|
|
88
|
+
26,
|
|
89
|
+
461,
|
|
90
|
+
1234,
|
|
91
|
+
873
|
|
92
|
+
],
|
|
93
|
+
"width": 254,
|
|
94
|
+
"scale": 0.21026490066225165,
|
|
95
|
+
"ground": 300
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"key": "belly",
|
|
99
|
+
"sourceBounds": [
|
|
100
|
+
28,
|
|
101
|
+
413,
|
|
102
|
+
1239,
|
|
103
|
+
881
|
|
104
|
+
],
|
|
105
|
+
"width": 224,
|
|
106
|
+
"scale": 0.18497109826589594,
|
|
107
|
+
"ground": 300
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"key": "curl",
|
|
111
|
+
"sourceBounds": [
|
|
112
|
+
44,
|
|
113
|
+
373,
|
|
114
|
+
1213,
|
|
115
|
+
992
|
|
116
|
+
],
|
|
117
|
+
"width": 216,
|
|
118
|
+
"scale": 0.1847733105218135,
|
|
119
|
+
"ground": 300
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"key": "chin",
|
|
123
|
+
"sourceBounds": [
|
|
124
|
+
116,
|
|
125
|
+
435,
|
|
126
|
+
1218,
|
|
127
|
+
953
|
|
128
|
+
],
|
|
129
|
+
"width": 198,
|
|
130
|
+
"scale": 0.17967332123411978,
|
|
131
|
+
"ground": 300
|
|
132
|
+
}
|
|
133
|
+
],
|
|
134
|
+
"sleepSourceHashes": {
|
|
135
|
+
"side": "85c567eb0958a213ee5b93d0a8592639489f5f29dded35e0d1d22e920ed1fe9f",
|
|
136
|
+
"belly": "e1967d9156a119d04e25fd6428fe5dcdbd2156c5e4eba0347217847874006109",
|
|
137
|
+
"curl": "10c9a85701af3587c48c1d8158972a07026e79a18a2375d86108538eda05aae7",
|
|
138
|
+
"chin": "e971eaa6861b597f0dba3a487403cee5f120cba0a9f9584d831b5634d096dee7"
|
|
76
139
|
}
|
|
77
140
|
}
|
package/puppy-companion-life.mjs
CHANGED
|
@@ -13,12 +13,15 @@ const OFFER={x:0,z:.3};
|
|
|
13
13
|
const TREAT_OFFSET={z:.08,height:.25};
|
|
14
14
|
const COOLDOWN={ball:6,treat:15,tug:20};
|
|
15
15
|
const FLIGHT=.65,BALL_PEAK=.75,CARRY_HEIGHT=.19,SETTLE_FALLBACK=.6,MAX_DT=.25,ARRIVE=.05;
|
|
16
|
+
// Active simulation seconds, never a wall-clock timer that wakes a hidden pet.
|
|
17
|
+
export const BUTTERFLY_RULES=Object.freeze({firstMin:240,firstSpread:240,cooldownMin:720,cooldownSpread:480,maxVisible:18});
|
|
16
18
|
const LABEL={
|
|
17
19
|
start:'기다리는 중',pause:'잠시 느긋하게 둘러보는 중',lie:'엎드리는 중',rest:'엎드려 쉬는 중',recover:'앉아서 기운 차리는 중',
|
|
18
20
|
sit:'앉는 중',watch:'앉아서 지켜보는 중',look:'고개를 돌려 살피는 중',observe:'가만히 주변 관찰 중',
|
|
19
21
|
stroll:'옆으로 살짝 걸어가는 중',sniff:'잠깐 멈춰 주변을 살피는 중',dash:'저 멀리 달려가는 중',far:'멀리서 둘러보는 중',home:'제자리로 돌아오는 중',
|
|
20
22
|
patrol:'주변을 왕복 산책하는 중',scan:'양쪽을 번갈아 살피는 중',roll:'느긋하게 뒹구는 중',play:'신나서 한 바퀴 도는 중',checkin:'가까이 와서 함께 쉬는 중',
|
|
21
23
|
belly:'배를 보이고 발라당 쉬는 중',sleep:'새근새근 잠자는 중',tug:'장난감을 물고 당기는 중',
|
|
24
|
+
butterflyNotice:'나비를 발견하고 귀를 쫑긋하는 중',butterfly:'나비를 폴짝폴짝 쫓는 중',butterflyBye:'날아가는 나비를 바라보는 중',
|
|
22
25
|
fetch:'공을 쫓아가는 중',pickup:'공을 무는 중',carry:'공을 물고 돌아오는 중',drop:'공을 내려놓는 중',offer:'공을 내려놓고 기다리는 중',
|
|
23
26
|
treatSit:'간식 앞에 앉는 중',chew:'간식 먹는 중'
|
|
24
27
|
};
|
|
@@ -39,10 +42,13 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
|
|
|
39
42
|
if(!Number.isFinite(energy)||energy<0||energy>1)throw RangeError('energy must be within 0..1');
|
|
40
43
|
|
|
41
44
|
const rand=mulberry32(seed),range=(a,b)=>a+(b-a)*rand(),side=()=>rand()<.5?'left':'right';
|
|
42
|
-
|
|
45
|
+
// Separate stream: the rare encounter does not perturb ordinary episodes/gaits.
|
|
46
|
+
const encounterRand=mulberry32(seed^0x4bf17a93),encounterRange=(a,b)=>a+(b-a)*encounterRand();
|
|
47
|
+
let nextButterfly=BUTTERFLY_RULES.firstMin+encounterRange(0,BUTTERFLY_RULES.firstSpread),butterfly=null;
|
|
48
|
+
const needs={energy,social:.5,curiosity:.5,drowsiness:0};
|
|
43
49
|
const cooldowns={ball:0,treat:0,tug:0};
|
|
44
50
|
const episodeCooldowns={roll:0,play:0,belly:0,sleep:0};
|
|
45
|
-
let clock=0,decisions=0,lastEpisode=null;
|
|
51
|
+
let clock=0,decisions=0,lastEpisode=null,quiet=0;
|
|
46
52
|
let queue=[],hold=0,holdIntent=null,waiting=null,gesture=null;
|
|
47
53
|
let intent='start',label=LABEL.start,pending=null,active=null,ball=null,treat=null;
|
|
48
54
|
|
|
@@ -58,6 +64,32 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
|
|
|
58
64
|
const sit=step=>cmd('sit',[],step,()=>!seatedAnyVariant());
|
|
59
65
|
const lie=()=>cmd('lie',[],'lie',()=>actor.state!=='down');
|
|
60
66
|
const walkTo=x=>cmd('walk',()=>{const dx=x-actor.sample(clock).x;return [dx<0?'left':'right',Math.abs(dx)];},'patrol',()=>Math.abs(x-actor.sample(clock).x)>.002);
|
|
67
|
+
const butterflyPoint=()=>{
|
|
68
|
+
if(!butterfly)return null;
|
|
69
|
+
const elapsed=clock-butterfly.since,p=clamp(elapsed/.65,0,1),ease=p*p*(3-2*p);
|
|
70
|
+
const leaving=butterfly.leaving?clamp((clock-butterfly.leaving)/1.1,0,1):0;
|
|
71
|
+
return {x:butterfly.from.x+(butterfly.to.x-butterfly.from.x)*ease+.025*Math.sin(elapsed*5)+leaving*.2*butterfly.side,
|
|
72
|
+
z:butterfly.from.z+(butterfly.to.z-butterfly.from.z)*ease,
|
|
73
|
+
height:.45+.045*Math.sin((clock-butterfly.start)*6)+leaving*.35,
|
|
74
|
+
phase:((clock-butterfly.start)*5)%1,opacity:1-leaving};
|
|
75
|
+
};
|
|
76
|
+
const butterflyEpisode=()=>{
|
|
77
|
+
const s=actor.sample(clock),direction=encounterRand()<.5?-1:1;
|
|
78
|
+
const targets=[clamp(s.x+direction*.36,-.5,.5),clamp(s.x-direction*.38,-.5,.5),clamp(s.x+direction*.24,-.5,.5)];
|
|
79
|
+
const z=s.z;
|
|
80
|
+
const moveFly=x=>fx(()=>{
|
|
81
|
+
const from=butterflyPoint()??{x:s.x-direction*.28,z};
|
|
82
|
+
const heading=x<actor.sample(clock).x?-1:1;
|
|
83
|
+
// The dog root is its body center, NOT its nose. Keep the visitor ahead of
|
|
84
|
+
// the muzzle on each leg, rather than hovering behind the running face.
|
|
85
|
+
butterfly={start:butterfly?.start??clock,since:clock,from:{x:from.x,z:from.z},to:{x:x+heading*.23,z},side:heading,leaving:0};
|
|
86
|
+
});
|
|
87
|
+
// Look names are dog-relative: its left is the viewer's right.
|
|
88
|
+
return [sit('butterflyNotice'),moveFly(targets[0]),cmd('look',[direction<0?'right':'left'],'butterflyNotice'),wait(.8,'butterflyNotice'),
|
|
89
|
+
...targets.flatMap((x,i)=>[...(i?[moveFly(x)]:[]),cmd(typeof actor.chase==='function'?'chase':'go',[x,z],'butterfly'),wait(.3,'butterfly')]),
|
|
90
|
+
fx(()=>{if(butterfly)butterfly.leaving=clock;}),wait(1.1,'butterflyBye'),fx(()=>{butterfly=null;needs.energy-=.12;needs.curiosity-=.25;}),
|
|
91
|
+
home(),sit('recover'),wait(range(3,6),'recover')];
|
|
92
|
+
};
|
|
61
93
|
|
|
62
94
|
// Each episode is a short coherent plan; a pause always leads in so autonomy never stalls.
|
|
63
95
|
const EPISODES={
|
|
@@ -72,7 +104,8 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
|
|
|
72
104
|
checkin:()=>[home(),sit('sit'),cmd('look',[side()],'look'),wait(range(5,10),'checkin'),fx(()=>{needs.social-=.4;})],
|
|
73
105
|
play:()=>[cmd('spin',[],'play'),wait(range(1,2),'pause'),sit('sit'),wait(range(2,4),'watch'),fx(()=>{needs.energy-=.12;needs.curiosity-=.2;})],
|
|
74
106
|
belly:()=>[cmd('belly',[range(5,9)],'belly'),wait(range(2,4),'watch'),fx(()=>{needs.energy+=.08;needs.curiosity-=.15;})],
|
|
75
|
-
sleep:()=>[cmd('sleep',[range(
|
|
107
|
+
sleep:()=>[cmd('sleep',[range(65,110),['side','curl','chin','belly'][Math.floor(rand()*4)],rand()<.5],'sleep'),wait(range(2,4),'recover'),fx(()=>{needs.energy+=.3;})],
|
|
108
|
+
butterfly:butterflyEpisode,
|
|
76
109
|
};
|
|
77
110
|
const weights=()=>{
|
|
78
111
|
const {energy:e,social:s,curiosity:c}=needs;
|
|
@@ -87,6 +120,13 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
|
|
|
87
120
|
const w=weights();if(lastEpisode)w[lastEpisode]=0;
|
|
88
121
|
const kinds=Object.keys(w),total=kinds.reduce((sum,k)=>sum+w[k],0);
|
|
89
122
|
let pick=rand()*total,kind=kinds.find(k=>(pick-=w[k])<0)??'observe';
|
|
123
|
+
// Quiet-time sleep pressure is independent of energy restored by sitting.
|
|
124
|
+
// Wait for the current coherent episode to end; never yank a running puppy.
|
|
125
|
+
if(quiet>=120&&needs.drowsiness>=.35&&episodeCooldowns.sleep===0&&lastEpisode!=='sleep'&&typeof actor.sleep==='function')kind='sleep';
|
|
126
|
+
const position=actor.sample(clock);
|
|
127
|
+
if(kind!=='sleep'&&clock>=nextButterfly&&quiet>=15&&needs.energy>.6&&needs.drowsiness<.35&&position.z> -1.2){
|
|
128
|
+
kind='butterfly';nextButterfly=clock+BUTTERFLY_RULES.cooldownMin+encounterRange(0,BUTTERFLY_RULES.cooldownSpread);
|
|
129
|
+
}
|
|
90
130
|
lastEpisode=kind;decisions++;
|
|
91
131
|
if(kind==='roll')episodeCooldowns.roll=90;
|
|
92
132
|
if(kind==='play')episodeCooldowns.play=120;
|
|
@@ -123,6 +163,9 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
|
|
|
123
163
|
function drift(dt){
|
|
124
164
|
const resting=holdIntent==='rest'||holdIntent==='recover'||actor.state==='sleep'||intent==='belly';
|
|
125
165
|
needs.energy+=resting?.04*dt:-.004*dt;
|
|
166
|
+
const asleep=actor.state==='sleep';
|
|
167
|
+
needs.drowsiness+=asleep?-dt/60:dt/240;
|
|
168
|
+
quiet=active||pending||asleep?0:quiet+dt;
|
|
126
169
|
if(!active){needs.curiosity+=.01*dt;needs.social+=.008*dt;}
|
|
127
170
|
for(const k in needs)needs[k]=clamp(needs[k],0,1);
|
|
128
171
|
}
|
|
@@ -133,6 +176,10 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
|
|
|
133
176
|
for(const k in cooldowns)cooldowns[k]=Math.max(0,cooldowns[k]-dt);
|
|
134
177
|
for(const k in episodeCooldowns)episodeCooldowns[k]=Math.max(0,episodeCooldowns[k]-dt);
|
|
135
178
|
drift(dt);
|
|
179
|
+
// A stalled or externally substituted actor must not leave a permanent insect.
|
|
180
|
+
if(butterfly&&clock-butterfly.start>BUTTERFLY_RULES.maxVisible){
|
|
181
|
+
butterfly=null;queue=[sit('recover'),wait(3,'recover')];hold=0;holdIntent=null;gesture=null;waiting=null;actor.rest();
|
|
182
|
+
}
|
|
136
183
|
const safe=SAFE.has(actor.state);
|
|
137
184
|
if(waiting){
|
|
138
185
|
waiting.age+=dt;if(!safe)waiting.left=true;
|
|
@@ -168,6 +215,11 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
|
|
|
168
215
|
if(!['ball','treat','tug'].includes(kind))return false;
|
|
169
216
|
if(kind==='tug'&&typeof actor.tug!=='function')return false;
|
|
170
217
|
if(pending||active||cooldowns[kind]>0)return false;
|
|
218
|
+
quiet=0;
|
|
219
|
+
if(butterfly||lastEpisode==='butterfly'){
|
|
220
|
+
butterfly=null;queue=[];hold=0;holdIntent=null;gesture=null;waiting=null;
|
|
221
|
+
if(kind!=='ball')actor.sit();
|
|
222
|
+
}
|
|
171
223
|
if(kind==='treat'||kind==='tug'){
|
|
172
224
|
pending=kind;
|
|
173
225
|
// The pet wakes through its authored recovery, not after a long sleep timer.
|
|
@@ -205,8 +257,8 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
|
|
|
205
257
|
const interaction=active??pending;
|
|
206
258
|
return {
|
|
207
259
|
intent,label:pending?`${label} · ${PENDING_LABEL[pending]}`:label,episode:lastEpisode,interaction,busy:interaction!==null,
|
|
208
|
-
ball:ballReport(),treat:treatReport(),tug:actor.state==='tug'?{progress:actor.sample(clock).comfortPose?.progress??0,grip:1}:null,
|
|
209
|
-
snore:actor.state==='sleep'?((clock%3.6)/3.6):null,energy:needs.energy,decisions,cooldowns:{...cooldowns},
|
|
260
|
+
ball:ballReport(),treat:treatReport(),butterfly:butterflyPoint(),tug:actor.state==='tug'?{progress:actor.sample(clock).comfortPose?.progress??0,grip:1}:null,
|
|
261
|
+
snore:actor.state==='sleep'?((clock%3.6)/3.6):null,energy:needs.energy,drowsiness:needs.drowsiness,decisions,cooldowns:{...cooldowns},
|
|
210
262
|
// Local hold clock only: progress freezes between updates; null outside the stationary hold.
|
|
211
263
|
gesture:gesture&&SAFE.has(actor.state)?{clip:gesture.clip,progress:clamp(gesture.elapsed/gesture.duration,0,1)}:null
|
|
212
264
|
};
|
|
@@ -7,6 +7,22 @@ export function paintCompanionObjects(pixels,width,height,projection,report,stat
|
|
|
7
7
|
// Interaction drawings already include the toy/food. Never paint a second
|
|
8
8
|
// floating ball over the baked mouth, or a biscuit through the puppy's chest.
|
|
9
9
|
if(report.gesture)return pixels;
|
|
10
|
+
// Tiny procedural butterfly: two paired wings and a body. World-space size,
|
|
11
|
+
// same fixed camera and palette as the pet; no image/cache or extra render pass.
|
|
12
|
+
if(report.butterfly){
|
|
13
|
+
const b=report.butterfly,p=projection.project(b.x,b.height,b.z);
|
|
14
|
+
const edge=projection.project(b.x+.04,b.height,b.z),up=projection.project(b.x,b.height+.035,b.z);
|
|
15
|
+
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;
|
|
16
|
+
const opening=.22+.78*Math.abs(Math.sin(b.phase*Math.PI)),tone=Math.round(235*b.opacity);
|
|
17
|
+
if([cx,cy,rx,ry,opening,tone].every(Number.isFinite)&&rx>0&&ry>0&&tone>0)
|
|
18
|
+
for(let y=Math.max(0,Math.floor(cy-ry));y<Math.min(height,Math.ceil(cy+ry));y++)
|
|
19
|
+
for(let x=Math.max(0,Math.floor(cx-rx));x<Math.min(width,Math.ceil(cx+rx));x++){
|
|
20
|
+
const u=(x+.5-cx)/rx,v=(y+.5-cy)/ry,a=Math.abs(u)/opening;
|
|
21
|
+
const wing=((a-.5)/.44)**2+((v+.28)/.56)**2<1||((a-.36)/.29)**2+((v-.43)/.37)**2<1;
|
|
22
|
+
const body=Math.abs(u)<.1&&Math.abs(v)<.7;
|
|
23
|
+
if(wing||body){const j=(y*width+x)*4;pixels[j]=pixels[j+1]=pixels[j+2]=Math.max(pixels[j],body?tone:Math.round(tone*(.65+.35*a)));}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
10
26
|
// A quiet, tiny rising "z" suggests snoring. The sleeping dog itself remains
|
|
11
27
|
// the original eyes-closed source pose: no chest/body zoom or squashing.
|
|
12
28
|
if(Number.isFinite(report.snore)&&state.state==='sleep'){
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import {grayFrameReader,isGrayFrameSource} from './puppy-frame-store.mjs';
|
|
2
2
|
// Prebaked interaction samples; the original seat is the exact shared endpoint.
|
|
3
3
|
// Pure sampling, no live image morph, root motion, camera or pose-size fitting.
|
|
4
|
-
export function createCompanionSpriteSampler(bytes,info){
|
|
4
|
+
export function createCompanionSpriteSampler(bytes,info,output){
|
|
5
5
|
const n=320*320;
|
|
6
6
|
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)throw Error('Invalid companion sprite cache');
|
|
7
7
|
for(const key of ['treat-eat','ball-pickup','ball-drop'])if(!Array.isArray(info.clips?.[key])||!info.clips[key].length||!info.clips[key].every(p=>Number.isInteger(p)&&p>=0&&p<info.pairs))throw Error('Invalid companion sprite clip');
|
|
8
|
-
const frames=grayFrameReader(bytes,320,320,info.pairs*9),rgba=new Uint8ClampedArray(n*4);
|
|
8
|
+
const frames=grayFrameReader(bytes,320,320,info.pairs*9),rgba=output??new Uint8ClampedArray(n*4);
|
|
9
|
+
if(!(rgba instanceof Uint8ClampedArray)||rgba.length!==n*4)throw Error('Invalid sprite output');
|
|
10
|
+
for(let i=3;i<rgba.length;i+=4)rgba[i]=255;
|
|
9
11
|
return({clip,progress})=>{
|
|
10
12
|
const list=info.clips[clip];if(!list||!Number.isFinite(progress)||progress<0||progress>1)throw Error('Invalid companion sprite pose');
|
|
11
13
|
const q=progress*list.length,index=Math.min(list.length-1,Math.floor(q)),s=Math.round((q-index)*8),frame=frames.getFrame(list[index]*9+s);
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
// 20 u32[logicalFrameCount] logical -> unique index
|
|
11
11
|
// then uniqueCount records of u32 x,y,w,h,offset,length (length = h*(w+1))
|
|
12
12
|
// then the filtered rows of every unique crop, contiguous in order.
|
|
13
|
-
// At load every crop is
|
|
14
|
-
//
|
|
13
|
+
// At load every crop is validated. The portable store keeps 5-bit packed indices;
|
|
14
|
+
// an optional host codec compresses each filtered crop instead (Node zlib).
|
|
15
|
+
// Neither retains the input archive. Full frames rebuild on demand (LRU of 2).
|
|
15
16
|
const MAGIC=[0x52,0x56,0x51,0x35],RVF1=[0x52,0x56,0x46,0x31],HEADER=20,RECORD=24,MAX_SIDE=16384,BITS=5;
|
|
16
17
|
|
|
17
18
|
export const quantize5=v=>Math.round(v*31/255);
|
|
@@ -80,7 +81,8 @@ function unfilterRow(type,src,at,cur,prev,w){
|
|
|
80
81
|
|
|
81
82
|
// Validate and unfilter. Only the returned typed arrays survive; `bytes` is
|
|
82
83
|
// not referenced by the store built from them.
|
|
83
|
-
function decode(bytes,{frameIndices}={}){
|
|
84
|
+
function decode(bytes,{frameIndices,codec}={}){
|
|
85
|
+
if(codec&&(typeof codec.compress!=='function'||typeof codec.expand!=='function'))fail('codec');
|
|
84
86
|
if(!(bytes instanceof Uint8Array))fail('bytes');
|
|
85
87
|
if(!isPredictiveFrames(bytes)||bytes.length<HEADER)fail('magic');
|
|
86
88
|
const view=new DataView(bytes.buffer,bytes.byteOffset,bytes.byteLength),u32=at=>view.getUint32(at,true);
|
|
@@ -107,27 +109,30 @@ function decode(bytes,{frameIndices}={}){
|
|
|
107
109
|
}
|
|
108
110
|
if(bytes.length!==data+total)fail('pixel length');
|
|
109
111
|
if(pixels*BITS>0xffffffff)fail('pixel count');
|
|
110
|
-
const packed=new Uint8Array(Math.ceil(pixels*BITS/8));
|
|
112
|
+
const packed=codec?null:new Uint8Array(Math.ceil(pixels*BITS/8)),chunks=codec?new Array(unique):null;
|
|
111
113
|
let cur=new Uint8Array(width),prev=new Uint8Array(width),at=data,pos=0;
|
|
112
114
|
for(let u=0;u<unique;u++){
|
|
113
|
-
const w=rects[u*5+2],h=rects[u*5+3];prev.fill(0);
|
|
115
|
+
const w=rects[u*5+2],h=rects[u*5+3],start=at;prev.fill(0);
|
|
114
116
|
for(let y=0;y<h;y++){
|
|
115
117
|
unfilterRow(bytes[at],bytes,at+1,cur,prev,w);at+=w+1;
|
|
116
118
|
for(let x=0;x<w;x++){
|
|
117
|
-
const q=cur[x];if(q>31)fail('quantized value');if(needed&&!needed[u])continue;
|
|
119
|
+
const q=cur[x];if(q>31)fail('quantized value');if(codec||(needed&&!needed[u]))continue;
|
|
118
120
|
const i=pos>>>3,s=pos&7;packed[i]|=(q<<s)&255;if(s>3)packed[i+1]|=q>>(8-s);pos+=BITS;
|
|
119
121
|
}
|
|
120
122
|
const t=prev;prev=cur;cur=t;
|
|
121
123
|
}
|
|
124
|
+
if(codec&&h&&(!needed||needed[u]))chunks[u]=Uint8Array.from(codec.compress(bytes.subarray(start,at)));
|
|
122
125
|
}
|
|
123
|
-
return{width,height,frameCount,unique,map,rects,packed,retained};
|
|
126
|
+
return{width,height,frameCount,unique,map,rects,packed,retained,chunks,codec};
|
|
124
127
|
}
|
|
125
128
|
|
|
126
|
-
function createStore({width,height,frameCount,unique,map,rects,packed,retained}){
|
|
129
|
+
function createStore({width,height,frameCount,unique,map,rects,packed,retained,chunks,codec}){
|
|
127
130
|
const n=width*height,slots=[{unique:-1,frame:null},{unique:-1,frame:null}];let decodes=0,hits=0;
|
|
131
|
+
const pixelBytes=packed?.byteLength??chunks.reduce((sum,c)=>sum+(c?.byteLength??0),0);
|
|
132
|
+
const cur=codec?new Uint8Array(width):null,prev=codec?new Uint8Array(width):null;
|
|
128
133
|
return Object.freeze({
|
|
129
|
-
width,height,frameCount,uniqueCount:unique,byteLength:n*frameCount,residentPixelBytes:
|
|
130
|
-
get residentBytes(){return
|
|
134
|
+
width,height,frameCount,uniqueCount:unique,byteLength:n*frameCount,residentPixelBytes:pixelBytes,
|
|
135
|
+
get residentBytes(){return pixelBytes+map.byteLength+rects.byteLength+(cur?.byteLength??0)+(prev?.byteLength??0)+(retained?.byteLength??0)+slots.reduce((s,e)=>s+(e.frame?.byteLength??0),0);},
|
|
131
136
|
stats(){return{decodes,hits,cachedFrames:slots.filter(e=>e.frame).length};},
|
|
132
137
|
getFrame(index){
|
|
133
138
|
if(!Number.isInteger(index)||index<0||index>=frameCount)fail('frame index');
|
|
@@ -138,6 +143,18 @@ function createStore({width,height,frameCount,unique,map,rects,packed,retained})
|
|
|
138
143
|
const slot=slots.pop();slots.unshift(slot);decodes++;
|
|
139
144
|
const frame=slot.frame??=new Uint8Array(n),k=u*5,x=rects[k],y=rects[k+1],w=rects[k+2],h=rects[k+3];
|
|
140
145
|
frame.fill(0);slot.unique=u;
|
|
146
|
+
if(codec){
|
|
147
|
+
if(!h)return frame;
|
|
148
|
+
const rows=codec.expand(chunks[u],h*(w+1));
|
|
149
|
+
if(rows.length!==h*(w+1))fail('expanded length');
|
|
150
|
+
prev.fill(0);
|
|
151
|
+
for(let r=0,at=0;r<h;r++,at+=w+1){
|
|
152
|
+
unfilterRow(rows[at],rows,at+1,cur,prev,w);
|
|
153
|
+
for(let c=0;c<w;c++)frame[(y+r)*width+x+c]=DISPLAY[cur[c]];
|
|
154
|
+
prev.set(cur.subarray(0,w));
|
|
155
|
+
}
|
|
156
|
+
return frame;
|
|
157
|
+
}
|
|
141
158
|
let pos=rects[k+4]*BITS;
|
|
142
159
|
for(let r=0;r<h;r++){
|
|
143
160
|
const row=(y+r)*width+x;
|
package/puppy-paper-actions.mjs
CHANGED
|
@@ -37,11 +37,13 @@ export function paperActionPose(name,progress,info){
|
|
|
37
37
|
if(!Number.isFinite(blend))throw Error('Invalid paper action: blend');
|
|
38
38
|
return{pair:list[index],blend};
|
|
39
39
|
}
|
|
40
|
-
export function createPaperActionSampler(bytes,info){
|
|
40
|
+
export function createPaperActionSampler(bytes,info,output){
|
|
41
41
|
if(!isGrayFrameSource(bytes))throw Error('Invalid action cache: bytes');
|
|
42
42
|
validatePaperActionInfo(info,bytes.byteLength);
|
|
43
43
|
const stride=info.width*info.height,pairs=info.pairs,frames=grayFrameReader(bytes,info.width,info.height,pairs*9);
|
|
44
|
-
const out=new Uint8ClampedArray(stride*4);
|
|
44
|
+
const out=output??new Uint8ClampedArray(stride*4);
|
|
45
|
+
if(!(out instanceof Uint8ClampedArray)||out.length!==stride*4)throw Error('Invalid action output');
|
|
46
|
+
for(let i=3;i<out.length;i+=4)out[i]=255;
|
|
45
47
|
return state=>{const p=paperActionPose(state.actionName,state.actionProgress,info),sample=Math.round(p.blend*8);
|
|
46
48
|
if(p.pair>=pairs||sample<0||sample>8)throw Error('Invalid paper action: sample out of range');
|
|
47
49
|
const frame=frames.getFrame(p.pair*9+sample);for(let i=0,j=0;i<stride;i++,j+=4)out[j]=out[j+1]=out[j+2]=frame[i];return out;};
|
|
Binary file
|
|
@@ -5,4 +5,4 @@ export function paperBehaviorPose(state,info){
|
|
|
5
5
|
const q=Math.min(list.length-1e-9,progress*list.length),i=Math.floor(q),blend=Math.round((q-i)*8)/8;
|
|
6
6
|
return{pair:list[i],blend};
|
|
7
7
|
}
|
|
8
|
-
export function createPaperBehaviorSampler(bytes,info){const sample=createRearSampler(bytes,info);return state=>{const p=paperBehaviorPose(state,info);return sample(p.pair,p.blend);};}
|
|
8
|
+
export function createPaperBehaviorSampler(bytes,info,output){const sample=createRearSampler(bytes,info,output);return state=>{const p=paperBehaviorPose(state,info);return sample(p.pair,p.blend);};}
|
package/puppy-paper-contact.mjs
CHANGED
|
@@ -91,9 +91,11 @@ export function footSourcePoint(x,y,pins){
|
|
|
91
91
|
return chosen?{x:x-chosen.dx*best,y:y-chosen.dy*best}:{x,y};
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
export function createRearSampler(bytes,info){
|
|
94
|
+
export function createRearSampler(bytes,info,output){
|
|
95
95
|
if(!isGrayFrameSource(bytes)||info.pairs!==18||info.samples!==9||bytes.byteLength!==18*9*320*320)throw Error('Invalid rear sprite cache');
|
|
96
|
-
const frames=grayFrameReader(bytes,320,320,18*9),out=new Uint8ClampedArray(320*320*4);
|
|
96
|
+
const frames=grayFrameReader(bytes,320,320,18*9),out=output??new Uint8ClampedArray(320*320*4);
|
|
97
|
+
if(!(out instanceof Uint8ClampedArray)||out.length!==320*320*4)throw Error('Invalid rear output');
|
|
98
|
+
for(let i=3;i<out.length;i+=4)out[i]=255;
|
|
97
99
|
return(pair,blend)=>{
|
|
98
100
|
if(!Number.isInteger(pair)||pair<0||pair>=18||!Number.isFinite(blend)||blend<0||blend>1)throw Error('Invalid rear pose');
|
|
99
101
|
const frame=frames.getFrame(pair*9+Math.round(blend*8));
|
package/puppy-paper-seat.mjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import {grayFrameReader,isGrayFrameSource} from './puppy-frame-store.mjs';
|
|
2
2
|
// Exact cached samples; no live crossfade. Reverse the same path to stand up.
|
|
3
|
-
export function createPaperSeatSampler(bytes,info){
|
|
3
|
+
export function createPaperSeatSampler(bytes,info,output){
|
|
4
4
|
const stride=320*320;
|
|
5
5
|
if(!isGrayFrameSource(bytes)||info.width!==320||info.height!==320||info.pairs!==3||info.samples!==17||bytes.byteLength!==3*17*stride)throw Error('Invalid paper sitting cache');
|
|
6
|
-
const frames=grayFrameReader(bytes,320,320,3*17),out=new Uint8ClampedArray(stride*4);
|
|
6
|
+
const frames=grayFrameReader(bytes,320,320,3*17),out=output??new Uint8ClampedArray(stride*4);
|
|
7
|
+
if(!(out instanceof Uint8ClampedArray)||out.length!==stride*4)throw Error('Invalid seat output');
|
|
8
|
+
for(let i=3;i<out.length;i+=4)out[i]=255;
|
|
7
9
|
return progress=>{
|
|
8
10
|
if(!Number.isFinite(progress)||progress<0||progress>1)throw Error('Invalid paper sitting progress');
|
|
9
11
|
const q=progress*3,pair=Math.min(2,Math.floor(q)),sample=Math.round((q-pair)*16),frame=frames.getFrame(pair*17+sample);
|
package/puppy-paper.mjs
CHANGED
|
@@ -28,6 +28,13 @@ export function createPaperComfortTimings(actionInfo,{key=PAPER_COMFORT_TIMINGS.
|
|
|
28
28
|
}
|
|
29
29
|
const comfortDuration=(name,seconds)=>{if(typeof seconds!=='number'||!Number.isFinite(seconds)||seconds<=0||seconds>PAPER_COMFORT_LIMITS[name])throw Error(`Invalid ${name} duration`);};
|
|
30
30
|
const comfortEase=u=>u<=0?0:u>=1?1:u*u*(3-2*u);
|
|
31
|
+
// Turn, hold the gaze, then return. Progress addresses the two authored edges;
|
|
32
|
+
// the paws and torso are registered to the same seated source in both edges.
|
|
33
|
+
export const PAPER_WATCH_TIMING=Object.freeze({turn:.6,hold:.65,back:.65,total:1.9});
|
|
34
|
+
export function paperWatchProgress(age){
|
|
35
|
+
const {turn,hold,back}=PAPER_WATCH_TIMING;
|
|
36
|
+
return age<turn?.5*comfortEase(age/turn):age<turn+hold?.5:.5+.5*comfortEase((age-turn-hold)/back);
|
|
37
|
+
}
|
|
31
38
|
|
|
32
39
|
// Original artwork only. Physics supplies ground travel; no procedural dog is drawn.
|
|
33
40
|
export function createPaperActor(animal,info,{manualTricks=false,trickTimings=null,walkTransitions=false,comfortTimings=PAPER_COMFORT_TIMINGS}={}){
|
|
@@ -40,7 +47,7 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
|
|
|
40
47
|
let straight=false,seatProgress=0,seatVariant='default',travel=null,approachPlan=null,travelAge=0,travelDistance=0,motionSpeed=0,motionStage='rest',completed=false;
|
|
41
48
|
let watchSide='left',restProgress=0,idleHold=2.5;
|
|
42
49
|
let pendingAction=null,activeAction=null,actionDone=null,deferred=null,movementMode='run',stayDown=false,manualDown=false,spinReady=false;
|
|
43
|
-
let comfort=null,comfortSeconds=0;
|
|
50
|
+
let comfort=null,comfortSeconds=0,sleepLook=null,nextSleepLook={variant:'side',right:false};
|
|
44
51
|
const director=createPaperDirector();
|
|
45
52
|
const points=[[.12,PAPER_GROUND.away]];
|
|
46
53
|
const enter=s=>{state=s;age=0;};
|
|
@@ -99,7 +106,7 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
|
|
|
99
106
|
pendingAction=null;
|
|
100
107
|
if(name==='sit'){enter('idle');return;}
|
|
101
108
|
if(name==='belly'){const seconds=comfortSeconds;playAction('lie-down',()=>startBelly(seconds),.65);return;}
|
|
102
|
-
if(name==='sleep'){stayDown=false;comfort={kind:'sleep',seconds:comfortSeconds};enter('recline');return;}
|
|
109
|
+
if(name==='sleep'){stayDown=false;sleepLook={...nextSleepLook};comfort={kind:'sleep',seconds:comfortSeconds};enter('recline');return;}
|
|
103
110
|
if(name==='tug'){stop();comfort={kind:'tug',seconds:comfortSeconds};enter('tug');return;}
|
|
104
111
|
if(name==='lie'){
|
|
105
112
|
if(manualTricks){playAction('lie-down',()=>{manualDown=true;enter('down');drain();},.65);}
|
|
@@ -160,7 +167,7 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
|
|
|
160
167
|
lie(){const name=manualDown||activeAction?.name==='lie-down'||restProgress>0?'sit':'lie';return dispatch(()=>requestAction(name));},
|
|
161
168
|
// Comfort poses: bounded finite seconds; same dispatch safety as tricks.
|
|
162
169
|
belly(seconds=6){comfortDuration('belly',seconds);return dispatch(()=>{comfortSeconds=seconds;return requestAction('belly');},{keepDown:true});},
|
|
163
|
-
sleep(seconds=25){comfortDuration('sleep',seconds);return dispatch(()=>{comfortSeconds=seconds;return requestAction('sleep');});},
|
|
170
|
+
sleep(seconds=25,variant='side',right=false){comfortDuration('sleep',seconds);if(!['side','belly','curl','chin'].includes(variant)||typeof right!=='boolean')throw Error('Invalid sleep pose');return dispatch(()=>{comfortSeconds=seconds;nextSleepLook={variant,right};return requestAction('sleep');});},
|
|
164
171
|
tug(seconds=6){comfortDuration('tug',seconds);return dispatch(()=>{comfortSeconds=seconds;return requestAction('tug');});},
|
|
165
172
|
rest(){return dispatch(()=>{route=false;target=null;waypoints=[];pendingAction=null;stayDown=false;comfort=null;stop();if(restProgress>0){enter('wake');}else if(state==='watch'||state==='arrival-exit'){/* finish shared return */}else if(seatProgress>0||state==='rise'||state==='sit-down')enter('sit-down');else if(!['idle','view-turn','turn'].includes(state))enter('finish');});},
|
|
166
173
|
update(dt){
|
|
@@ -196,7 +203,7 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
|
|
|
196
203
|
else if(state==='sit-down'){seatProgress=Math.min(1,seatProgress+dt/(snappyPreparation()?.35:.95));if(seatProgress===1){seatVariant='default';enter('idle');if(pendingAction)prepareDirection();}}
|
|
197
204
|
else if(state==='rise'){seatProgress=Math.max(0,seatProgress-prepareDt/(snappyPreparation()?.28:.65));if(seatProgress===0){if(seatVariant==='arrival')enter('arrival-exit');else prepareDirection();}}
|
|
198
205
|
else if(state==='arrival-exit'&&age>=.22){seatVariant='default';prepareDirection();}
|
|
199
|
-
else if(state==='watch'&&age>=
|
|
206
|
+
else if(state==='watch'&&age>=PAPER_WATCH_TIMING.total){if(pendingAction)prepareDirection();else if(target)enter('rise');else enter('idle');}
|
|
200
207
|
else if(state==='recline'){const end=stayDown?.25:1;restProgress=Math.min(end,restProgress+dt/1.8);if(restProgress===end)enter(stayDown?'down':comfort?.kind==='sleep'?'sleep':'nap');}
|
|
201
208
|
else if(state==='sleep'&&age>=comfort.seconds){comfort=null;enter('wake');}
|
|
202
209
|
else if(state==='tug'&&tugProgress()>=1){comfort=null;enter('idle');drain();}
|
|
@@ -209,7 +216,8 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
|
|
|
209
216
|
if(![12,24,30].includes(poseFps))throw Error('Invalid pose cadence');
|
|
210
217
|
let pair=20,blend=0,kind=state,rearPair=null,approachPair=null;
|
|
211
218
|
let seat=state==='sit-down'||state==='rise'||seatProgress>0?Math.round(seatProgress*48)/48:null;
|
|
212
|
-
|
|
219
|
+
if(restProgress===0&&state!=='recline')sleepLook=null;
|
|
220
|
+
let behavior=null,behaviorProgress=0,comfortPose=state==='tug'?{clip:'tug',progress:tugProgress()}:sleepLook&&restProgress>.72?{clip:'sleep',...sleepLook,blend:comfortEase((restProgress-.72)/.28)}:null;
|
|
213
221
|
if(activeAction?.comfort)kind=activeAction.comfort;
|
|
214
222
|
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;
|
|
215
223
|
if(manualDown&&state==='down'){actionName='lie-down';actionProgress=1;}
|
|
@@ -221,7 +229,7 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
|
|
|
221
229
|
if(walkTransitions&&state==='turn'){actionName='walk-turn';actionProgress=Math.min(1,age/turnSeconds());}
|
|
222
230
|
if(view==='approach'&&approachPlan&&(state==='run'||(state==='finish'&&completed))){const p=approachPlan.at(travelDistance);if(state==='finish'){behavior='brake';behaviorProgress=1;}else if(p.clip!=='run'){behavior=p.clip;behaviorProgress=p.progress;}}
|
|
223
231
|
if(state==='arrival-exit'){behavior='finish';behaviorProgress=Math.min(1,age/.22);}
|
|
224
|
-
if(state==='watch'){behavior=`watch-${watchSide}`;behaviorProgress=
|
|
232
|
+
if(state==='watch'){behavior=`watch-${watchSide}`;behaviorProgress=paperWatchProgress(age);}
|
|
225
233
|
if(restProgress>0||state==='recline'||state==='wake'||state==='nap'){behavior='rest';behaviorProgress=restProgress;}
|
|
226
234
|
if(state==='run'||state==='finish'){
|
|
227
235
|
const p=state==='finish'?phase+(Math.ceil(phase)-phase)*Math.min(1,age/.22):phase;
|
|
@@ -251,14 +259,20 @@ export function createPaperRenderer(cache,info,meta,width,height,{rear,approach,
|
|
|
251
259
|
if(![width,height].every(Number.isInteger)||width<2||height<2||width*height>1_000_000)throw Error('Invalid paper grid');
|
|
252
260
|
const tween=createCachedTween(cache,info),renderMeta={...meta,pairs:info.pair_map,run_sequence:info.run_sequence};
|
|
253
261
|
const out=new Uint8ClampedArray(width*height*4),area=createPaperAreaSampler();
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
const
|
|
262
|
+
// Only one authored source is drawn at a time. Small samplers borrow a view of
|
|
263
|
+
// the action-sized scratch. A sleep blend reads each base pixel before writing
|
|
264
|
+
// that same pixel, so it needs no second image or per-frame temporary buffer.
|
|
265
|
+
const actionScratch=new Uint8ClampedArray(Math.max(320*320,(actions?.info.width??320)*(actions?.info.height??320))*4);
|
|
266
|
+
const spriteScratch=actionScratch.subarray(0,320*320*4);
|
|
267
|
+
for(let i=3;i<actionScratch.length;i+=4)actionScratch[i]=255;
|
|
268
|
+
const rearSample=rear?createRearSampler(rear.bytes,rear.info,spriteScratch):null,lock=createPaperFootLock({maxImageStretch:pawTransport?1.45:Infinity});
|
|
269
|
+
const approachSample=approach?createRearSampler(approach.bytes,approach.info,spriteScratch):null;
|
|
270
|
+
const seatSample=seat?createPaperSeatSampler(seat.bytes,seat.info,spriteScratch):null;
|
|
271
|
+
const arrivalSeatSample=arrivalSeat?createPaperSeatSampler(arrivalSeat.bytes,arrivalSeat.info,spriteScratch):null;
|
|
272
|
+
const behaviorSample=behaviors?createPaperBehaviorSampler(behaviors.bytes,behaviors.info,spriteScratch):null;
|
|
273
|
+
const interactionSample=interactions?createCompanionSpriteSampler(interactions.bytes,interactions.info,spriteScratch):null;
|
|
274
|
+
const comfortSample=comfort?createComfortSpriteSampler(comfort.bytes,comfort.info,spriteScratch):null;
|
|
275
|
+
const actionSample=actions?createPaperActionSampler(actions.bytes,actions.info,actionScratch.subarray(0,actions.info.width*actions.info.height*4)):null,actionArea=actions?createPaperAreaSampler(actions.info.width):null;
|
|
262
276
|
const walkEpisodes=actions?createPaperSupportEpisodes(actions.info.feet,actions.info.clips.walk,9):null;
|
|
263
277
|
const sourceEpisodes=rear?{
|
|
264
278
|
front:createPaperSupportEpisodes(rear.info.frontFeet,[0,1,2,3,4,5,6,7],17),
|
|
@@ -276,7 +290,7 @@ export function createPaperRenderer(cache,info,meta,width,height,{rear,approach,
|
|
|
276
290
|
const isInteraction=isComfort||state.interactionPose&&interactionSample;
|
|
277
291
|
const isBehavior=!isInteraction&&state.behavior&&behaviorSample;
|
|
278
292
|
const isAction=!isInteraction&&state.actionName&&actionSample,sourceN=isAction?actions.info.width:320,originX=(sourceN-320)/2,originY=(sourceN/320-1)*300;
|
|
279
|
-
const image=isComfort?comfortSample(state.comfortPose):isInteraction?interactionSample(state.interactionPose):isAction?actionSample(state):isBehavior?behaviorSample(state):isSeat?(state.seatVariant==='arrival'&&arrivalSeatSample?arrivalSeatSample:seatSample)(state.seatProgress):isApproach?approachSample(state.approachPair,state.blend):isRear?rearSample(state.rearPair,state.blend):tween(state),side=height*size,sideX=side/pixelAspect;
|
|
293
|
+
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;
|
|
280
294
|
// Quantize source lookup only, never the projected rectangle. Rounding side,
|
|
281
295
|
// left and top first introduced whole-body scale/position jumps at thresholds.
|
|
282
296
|
const left=width*x-sideX/2,floor=isInteraction||isAction||isBehavior||isSeat||isRear||isApproach?0:cleanGroundOffset(state,renderMeta),top=height*y-(300-floor)/320*side;
|
package/retrieviewer-cli.mjs
CHANGED
|
@@ -13,7 +13,7 @@ Usage:
|
|
|
13
13
|
retrieviewer doctor [--agent ID] [--json]
|
|
14
14
|
retrieviewer agents [--json]
|
|
15
15
|
retrieviewer preview [--original]
|
|
16
|
-
retrieviewer desktop
|
|
16
|
+
retrieviewer desktop [--ink cream|gold|dark]
|
|
17
17
|
retrieviewer desktop-status [--json]
|
|
18
18
|
retrieviewer desktop-stop
|
|
19
19
|
retrieviewer desktop-clean --yes
|
|
@@ -32,15 +32,17 @@ export function parseArguments(args){
|
|
|
32
32
|
const [command='help',...rest]=args,options={};
|
|
33
33
|
for(let i=0;i<rest.length;i++){
|
|
34
34
|
const token=rest[i];
|
|
35
|
-
if(token==='--agent'){
|
|
36
|
-
|
|
37
|
-
options
|
|
35
|
+
if(token==='--agent'||token==='--ink'){
|
|
36
|
+
const key=token.slice(2);
|
|
37
|
+
if(options[key]||!rest[i+1]||rest[i+1].startsWith('-'))throw Error(token+' requires one value');
|
|
38
|
+
options[key]=rest[++i];
|
|
38
39
|
}else if(['--yes','--json','--dry-run','--original'].includes(token))options[token.slice(2)]=true;
|
|
39
40
|
else throw Error('Unknown argument: '+token);
|
|
40
41
|
}
|
|
41
42
|
if(options.agent&&!AGENTS.some(a=>a.id===options.agent))throw Error('Unknown agent: '+options.agent);
|
|
43
|
+
if(options.ink&&!['cream','gold','dark'].includes(options.ink))throw Error('--ink must be cream, gold or dark');
|
|
42
44
|
if(!['help','--help','-h','enable','disable','disconnect','status','doctor','agents','preview','desktop','desktop-status','desktop-stop','desktop-clean','desktop-remove'].includes(command))throw Error('Unknown command: '+command);
|
|
43
|
-
const allowed={enable:['agent','yes','json','dry-run'],disconnect:['agent','yes','json','dry-run'],disable:['agent','json'],status:['agent','json'],doctor:['agent','json'],agents:['json'],preview:['original'],'desktop-status':['json'],'desktop-clean':['yes'],'desktop-remove':['yes']};
|
|
45
|
+
const allowed={desktop:['ink'],enable:['agent','yes','json','dry-run'],disconnect:['agent','yes','json','dry-run'],disable:['agent','json'],status:['agent','json'],doctor:['agent','json'],agents:['json'],preview:['original'],'desktop-status':['json'],'desktop-clean':['yes'],'desktop-remove':['yes']};
|
|
44
46
|
for(const key of Object.keys(options))if(!(allowed[command]??[]).includes(key))throw Error(`--${key} is not supported by ${command}`);
|
|
45
47
|
return {command,options};
|
|
46
48
|
}
|
|
@@ -51,7 +53,7 @@ export async function runCli(args,{input=process.stdin,output=process.stdout,err
|
|
|
51
53
|
if(['desktop-clean','desktop-remove'].includes(command)&&!options.yes)throw Error('Use '+command+' --yes to remove only the companion cache.');
|
|
52
54
|
const api=desktop??await import('./integrations/windows-overlay/desktop.mjs');
|
|
53
55
|
const action={'desktop':'startDesktop','desktop-status':'desktopStatus','desktop-stop':'stopDesktop','desktop-clean':'cleanDesktop','desktop-remove':'removeDesktop'}[command];
|
|
54
|
-
emit(await api[action]());return 0;
|
|
56
|
+
emit(await api[action](options.ink?{ink:options.ink}:{}));return 0;
|
|
55
57
|
}
|
|
56
58
|
if(command==='preview'){await import('./examples/live.mjs');return 0;}
|
|
57
59
|
if(['status','doctor','agents'].includes(command)){
|
package/retrieviewer.d.mts
CHANGED
|
@@ -42,10 +42,14 @@ export interface LifeReport {
|
|
|
42
42
|
interaction: 'ball' | 'treat' | 'tug' | null;
|
|
43
43
|
busy: boolean;
|
|
44
44
|
energy: number;
|
|
45
|
+
/** Awake-time sleep pressure; ordinary sitting does not erase it. */
|
|
46
|
+
drowsiness: number;
|
|
45
47
|
decisions: number;
|
|
46
48
|
cooldowns: {ball: number; treat: number; tug: number};
|
|
47
49
|
gesture: {clip: string; progress: number} | null;
|
|
48
50
|
ball: {x: number; z: number; height: number; carried: boolean} | null;
|
|
51
|
+
/** Rare autonomous visitor; absent during direct user interactions. */
|
|
52
|
+
butterfly: {x: number; z: number; height: number; phase: number; opacity: number} | null;
|
|
49
53
|
treat: {x: number; z: number; height: number} | null;
|
|
50
54
|
/** Tug-of-war state while a tug interaction is active. */
|
|
51
55
|
tug: {progress: number; grip: number} | null;
|
package/retrieviewer.mjs
CHANGED
|
@@ -60,9 +60,16 @@ async function gunzip(bytes){
|
|
|
60
60
|
// life, renderer and output. Last dispose drops the shared lease, not other pets.
|
|
61
61
|
const sharedAssets=new Map();
|
|
62
62
|
async function loadPetAssets(loadAsset,artProfile){
|
|
63
|
+
// Node keeps filtered crops compressed; only the existing two-frame LRU is
|
|
64
|
+
// expanded. Browser bundles retain the portable bit-packed implementation.
|
|
65
|
+
let codec;
|
|
66
|
+
if(typeof process!=='undefined'&&process.versions?.node){
|
|
67
|
+
const {deflateRawSync,inflateRawSync}=await import('node:zlib');
|
|
68
|
+
codec={compress:bytes=>deflateRawSync(bytes,{level:1}),expand:(bytes,size)=>inflateRawSync(bytes,{maxOutputLength:size})};
|
|
69
|
+
}
|
|
63
70
|
const load=async name=>bytesOf(name,await loadAsset(name));
|
|
64
71
|
const json=async name=>JSON.parse(new TextDecoder().decode(await load(name)));
|
|
65
|
-
const frames=async (name,options)=>readPackedFrames(await gunzip(await load(name)),options);
|
|
72
|
+
const frames=async (name,options)=>readPackedFrames(await gunzip(await load(name)),{...options,codec});
|
|
66
73
|
// One family at a time: never inflate all eight legacy-sized buffers together.
|
|
67
74
|
const data={assets:{}};
|
|
68
75
|
data.info=await json(PET_ASSETS.strideInfo);data.meta=await json(PET_ASSETS.stableInfo);
|