@bitefoundry/retrieviewer 0.0.9 → 0.0.11
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 +37 -0
- package/EMBEDDING.md +6 -0
- package/PUPPY-MEMORY.md +87 -0
- package/README.ko.md +7 -0
- package/README.md +7 -0
- package/RELEASING.md +48 -10
- package/WATCH-REDRAW.md +186 -0
- package/braille.mjs +4 -2
- package/integrations/windows-overlay/desktop.mjs +3 -2
- package/integrations/windows-overlay/protocol.mjs +4 -1
- package/integrations/windows-overlay/worker.mjs +4 -3
- package/package.json +3 -1
- package/pet-q5-manifest.json +60 -24
- package/puppy-comfort-sprites.mjs +26 -3
- package/puppy-companion-ball.mjs +36 -0
- package/puppy-companion-comfort-q5.bin.gz +0 -0
- package/puppy-companion-comfort.json +333 -7
- package/puppy-companion-life.mjs +106 -35
- package/puppy-companion-objects.mjs +3 -1
- package/puppy-paper-area.mjs +6 -3
- package/puppy-paper-behaviors-q5.bin.gz +0 -0
- package/puppy-paper-world.mjs +47 -1
- package/puppy-paper.mjs +113 -26
- package/retrieviewer-cli.mjs +6 -4
- package/retrieviewer.d.mts +15 -1
- package/retrieviewer.mjs +31 -8
package/puppy-companion-life.mjs
CHANGED
|
@@ -1,33 +1,36 @@
|
|
|
1
1
|
// Autonomous companion "life" controller layered over a paper puppy actor.
|
|
2
2
|
// The parent calls actor.update(dt) THEN life.update(dt). Commands are only issued
|
|
3
3
|
// while the actor rests at a shared safe endpoint (idle/down); everything else waits.
|
|
4
|
-
//
|
|
5
|
-
//
|
|
4
|
+
// Accepted interactions ask the actor for safe interruption: chase for a ball,
|
|
5
|
+
// attend for a hand-fed treat/tug. Substitute actors can use safe-endpoint fallback.
|
|
6
6
|
// Parameters are authored for feel, not canine science.
|
|
7
|
+
import {paperCameraPoint,paperGroundTarget,PAPER_SCREEN,PAPER_CAMERA} from './puppy-paper-world.mjs';
|
|
8
|
+
import {createBallPath,createFetchPatterns} from './puppy-companion-ball.mjs';
|
|
7
9
|
|
|
8
10
|
const SAFE=new Set(['idle','down']);
|
|
11
|
+
export const FETCH_RETURN_STYLES=Object.freeze(['auto','brake','trot','run-trot']);
|
|
9
12
|
const HOME={x:0,z:.2};
|
|
10
13
|
const BALL_TARGET={x:0,z:-4};
|
|
11
14
|
const OFFER={x:0,z:.3};
|
|
12
15
|
// Hand-fed treat marker, relative to the puppy's current root (no travel for treats).
|
|
13
16
|
const TREAT_OFFSET={z:.08,height:.25};
|
|
14
17
|
const COOLDOWN={ball:6,treat:15,tug:20};
|
|
15
|
-
const
|
|
18
|
+
const CARRY_HEIGHT=.19,SETTLE_FALLBACK=.6,MAX_DT=.25,ARRIVE=.05;
|
|
16
19
|
// Active simulation seconds, never a wall-clock timer that wakes a hidden pet.
|
|
17
20
|
export const BUTTERFLY_RULES=Object.freeze({firstMin:240,firstSpread:240,cooldownMin:720,cooldownSpread:480,maxVisible:18});
|
|
18
21
|
const LABEL={
|
|
19
22
|
start:'기다리는 중',pause:'잠시 느긋하게 둘러보는 중',lie:'엎드리는 중',rest:'엎드려 쉬는 중',recover:'앉아서 기운 차리는 중',
|
|
20
23
|
sit:'앉는 중',watch:'앉아서 지켜보는 중',look:'고개를 돌려 살피는 중',observe:'가만히 주변 관찰 중',
|
|
21
|
-
stroll:'옆으로 살짝 걸어가는 중',sniff:'잠깐 멈춰 주변을 살피는 중',dash:'저 멀리 달려가는 중',far:'멀리서 둘러보는 중',home:'제자리로 돌아오는 중',
|
|
24
|
+
stroll:'옆으로 살짝 걸어가는 중',sniff:'잠깐 멈춰 주변을 살피는 중',dash:'저 멀리 달려가는 중',far:'멀리서 둘러보는 중',home:'제자리로 돌아오는 중',nest:'쉴 자리를 찾아가는 중',
|
|
22
25
|
patrol:'주변을 왕복 산책하는 중',scan:'양쪽을 번갈아 살피는 중',roll:'느긋하게 뒹구는 중',play:'신나서 한 바퀴 도는 중',checkin:'가까이 와서 함께 쉬는 중',
|
|
23
26
|
belly:'배를 보이고 발라당 쉬는 중',sleep:'새근새근 잠자는 중',tug:'장난감을 물고 당기는 중',
|
|
24
27
|
butterflyNotice:'나비를 발견하고 귀를 쫑긋하는 중',butterfly:'나비를 폴짝폴짝 쫓는 중',butterflyBye:'날아가는 나비를 바라보는 중',
|
|
25
|
-
fetch:'공을 쫓아가는 중',pickup:'공을 무는 중',carry:'공을 물고 돌아오는 중',drop:'공을 내려놓는 중',offer:'공을 내려놓고 기다리는 중',
|
|
28
|
+
fetch:'공을 쫓아가는 중',miss:'굴러가는 공을 한 번 놓친 중',reChase:'공을 다시 쫓아가는 중',tumble:'우당탕 균형을 되찾는 중',pickup:'공을 무는 중',carry:'공을 물고 돌아오는 중',drop:'공을 내려놓는 중',offer:'공을 내려놓고 기다리는 중',
|
|
26
29
|
treatSit:'간식 앞에 앉는 중',chew:'간식 먹는 중'
|
|
27
30
|
};
|
|
28
31
|
const PENDING_LABEL={treat:'곧 간식',tug:'곧 터그놀이'};
|
|
29
32
|
// Baked interaction clips, played only while holding at the shared seated endpoint.
|
|
30
|
-
const GESTURE={'ball-pickup'
|
|
33
|
+
const GESTURE={'ball-pickup':.65,'ball-drop':.65,'treat-eat':2.4};
|
|
31
34
|
const clamp=(v,a,b)=>Math.min(b,Math.max(a,v));
|
|
32
35
|
|
|
33
36
|
function mulberry32(seed){
|
|
@@ -35,13 +38,21 @@ function mulberry32(seed){
|
|
|
35
38
|
return()=>{a=a+0x6d2b79f5>>>0;let t=a;t=Math.imul(t^t>>>15,t|1);t^=t+Math.imul(t^t>>>7,t|61);return((t^t>>>14)>>>0)/4294967296;};
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
export function createCompanionLife(actor,{seed=731,energy=.8}={}){
|
|
41
|
+
export function createCompanionLife(actor,{seed=731,energy=.8,returnStyle='auto'}={}){
|
|
39
42
|
for(const name of ['update','sample','go','come','sit','lie','look','walk','roll','rest'])
|
|
40
43
|
if(!actor||typeof actor[name]!=='function')throw TypeError(`actor.${name} must be a function`);
|
|
41
44
|
if(!Number.isInteger(seed))throw TypeError('seed must be an integer');
|
|
42
45
|
if(!Number.isFinite(energy)||energy<0||energy>1)throw RangeError('energy must be within 0..1');
|
|
46
|
+
const validateReturnStyle=style=>{if(!FETCH_RETURN_STYLES.includes(style))throw RangeError('returnStyle must be auto, brake, trot or run-trot');if(['trot','run-trot'].includes(style)&&!actor.canCarryTrot)throw Error('Carry-trot artwork is not available');};
|
|
47
|
+
validateReturnStyle(returnStyle);
|
|
43
48
|
|
|
44
49
|
const rand=mulberry32(seed),range=(a,b)=>a+(b-a)*rand(),side=()=>rand()<.5?'left':'right';
|
|
50
|
+
// Spatial choices get their own stream, like the rare visitor. Existing gait,
|
|
51
|
+
// energy and interaction timing do not depend on how a resting spot is picked.
|
|
52
|
+
const spaceRand=mulberry32(seed^0x28ae91c7),spaceRange=(a,b)=>a+(b-a)*spaceRand();
|
|
53
|
+
const toyRand=mulberry32(seed^0x6c82b14d),chooseFetch=createFetchPatterns(toyRand,{canTumble:actor.canScramble===true});
|
|
54
|
+
const returnRand=mulberry32(seed^0x73bca165);let lastReturnMode=null;
|
|
55
|
+
let placeSide=spaceRand()<.5?-1:1,outings=0,nearSeconds=0,lastCheckin=-Infinity;
|
|
45
56
|
// Separate stream: the rare encounter does not perturb ordinary episodes/gaits.
|
|
46
57
|
const encounterRand=mulberry32(seed^0x4bf17a93),encounterRange=(a,b)=>a+(b-a)*encounterRand();
|
|
47
58
|
let nextButterfly=BUTTERFLY_RULES.firstMin+encounterRange(0,BUTTERFLY_RULES.firstSpread),butterfly=null;
|
|
@@ -51,6 +62,7 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
|
|
|
51
62
|
let clock=0,decisions=0,lastEpisode=null,quiet=0;
|
|
52
63
|
let queue=[],hold=0,holdIntent=null,waiting=null,gesture=null;
|
|
53
64
|
let intent='start',label=LABEL.start,pending=null,active=null,ball=null,treat=null;
|
|
65
|
+
let fetchPlan=null;
|
|
54
66
|
|
|
55
67
|
const cmd=(name,args,step,when)=>({cmd:name,args,intent:step,label:LABEL[step],when});
|
|
56
68
|
const wait=(seconds,step)=>({hold:seconds,intent:step,label:LABEL[step]});
|
|
@@ -64,6 +76,22 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
|
|
|
64
76
|
const sit=step=>cmd('sit',[],step,()=>!seatedAnyVariant());
|
|
65
77
|
const lie=()=>cmd('lie',[],'lie',()=>actor.state!=='down');
|
|
66
78
|
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);
|
|
79
|
+
const roomRadius=z=>Math.max(.12,paperCameraPoint(0,0,z).depth*PAPER_SCREEN.widthMeters/(2*PAPER_SCREEN.viewDistance)-PAPER_CAMERA.canvasMeters/2);
|
|
80
|
+
const place=nap=>{
|
|
81
|
+
// A nearer side nook keeps a sleeping face readable; outings also use the
|
|
82
|
+
// middle/far ground. X expands with camera depth, not a fixed world corridor.
|
|
83
|
+
const bands=[[1.4,3],[4,8],[.7,1.4],[9,15]],band=bands[outings++%bands.length];
|
|
84
|
+
const z=-(nap?spaceRange(.7,2.6):spaceRange(...band));placeSide*=-1;
|
|
85
|
+
return paperGroundTarget(placeSide*roomRadius(z)*spaceRange(.5,.8),z);
|
|
86
|
+
};
|
|
87
|
+
const travelTo=(target,step)=>cmd('go',target,step,()=>{const s=actor.sample(clock);return Math.hypot(s.x-target[0],s.z-target[1])>.12;});
|
|
88
|
+
const quietPlace=()=>{
|
|
89
|
+
const s=actor.sample(clock);
|
|
90
|
+
// Often stay in the nook just discovered, instead of shuffling before every
|
|
91
|
+
// nap. A front-and-center or very distant puppy first finds a quieter spot.
|
|
92
|
+
if(s.z<=-.5&&s.z>=-3.5&&Math.abs(s.x)>roomRadius(s.z)*.3&&spaceRand()<.65)return [];
|
|
93
|
+
return [travelTo(place(true),'nest')];
|
|
94
|
+
};
|
|
67
95
|
const butterflyPoint=()=>{
|
|
68
96
|
if(!butterfly)return null;
|
|
69
97
|
const elapsed=clock-butterfly.since,p=clamp(elapsed/.65,0,1),ease=p*p*(3-2*p);
|
|
@@ -74,8 +102,9 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
|
|
|
74
102
|
phase:((clock-butterfly.start)*5)%1,opacity:1-leaving};
|
|
75
103
|
};
|
|
76
104
|
const butterflyEpisode=()=>{
|
|
77
|
-
const s=actor.sample(clock),
|
|
78
|
-
const
|
|
105
|
+
const s=actor.sample(clock),radius=roomRadius(s.z);
|
|
106
|
+
const direction=s.x>radius-.3?-1:s.x< -radius+.3?1:encounterRand()<.5?-1:1;
|
|
107
|
+
const targets=[clamp(s.x+direction*.36,-radius,radius),clamp(s.x-direction*.38,-radius,radius),clamp(s.x+direction*.24,-radius,radius)];
|
|
79
108
|
const z=s.z;
|
|
80
109
|
const moveFly=x=>fx(()=>{
|
|
81
110
|
const from=butterflyPoint()??{x:s.x-direction*.28,z};
|
|
@@ -88,29 +117,29 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
|
|
|
88
117
|
return [sit('butterflyNotice'),moveFly(targets[0]),cmd('look',[direction<0?'right':'left'],'butterflyNotice'),wait(.8,'butterflyNotice'),
|
|
89
118
|
...targets.flatMap((x,i)=>[...(i?[moveFly(x)]:[]),cmd(typeof actor.chase==='function'?'chase':'go',[x,z],'butterfly'),wait(.3,'butterfly')]),
|
|
90
119
|
fx(()=>{if(butterfly)butterfly.leaving=clock;}),wait(1.1,'butterflyBye'),fx(()=>{butterfly=null;needs.energy-=.12;needs.curiosity-=.25;}),
|
|
91
|
-
|
|
120
|
+
sit('recover'),wait(range(3,6),'recover')];
|
|
92
121
|
};
|
|
93
122
|
|
|
94
123
|
// Each episode is a short coherent plan; a pause always leads in so autonomy never stalls.
|
|
95
124
|
const EPISODES={
|
|
96
|
-
rest:()=>[lie(),wait(range(10,18),'rest'),sit('recover'),wait(range(3,6),'recover')],
|
|
125
|
+
rest:()=>[...quietPlace(),lie(),wait(range(10,18),'rest'),sit('recover'),wait(range(3,6),'recover')],
|
|
97
126
|
watch:()=>[sit('sit'),cmd('look',[side()],'look'),wait(range(3,9),'watch'),fx(()=>{needs.social-=.3;needs.curiosity+=.1;})],
|
|
98
127
|
observe:()=>[sit('sit'),cmd('look',[side()],'look'),wait(range(3,9),'observe'),fx(()=>{needs.curiosity-=.15;})],
|
|
99
|
-
stroll:()=>[cmd('walk',[side(),range(.
|
|
100
|
-
dash:()=>[
|
|
128
|
+
stroll:()=>[cmd('walk',[side(),range(.22,.5)],'stroll'),wait(range(4,9),'sniff'),fx(()=>{needs.energy-=.08;needs.curiosity-=.2;})],
|
|
129
|
+
dash:()=>[travelTo(place(false),'dash'),sit('far'),wait(range(8,16),'far'),fx(()=>{needs.energy-=.22;needs.curiosity-=.3;})],
|
|
101
130
|
scan:()=>[sit('sit'),cmd('look',['left'],'scan'),wait(range(.5,1.5),'scan'),cmd('look',['right'],'scan'),wait(range(2,4),'observe'),fx(()=>{needs.curiosity-=.25;})],
|
|
102
131
|
patrol:()=>{const x=actor.sample(clock).x,d=range(.16,.3)*(rand()<.5?-1:1);return [walkTo(x+d),wait(range(.6,1.2),'sniff'),walkTo(x-d),wait(range(.6,1.2),'sniff'),walkTo(x),wait(range(1,3),'observe'),fx(()=>{needs.energy-=.1;needs.curiosity-=.25;})];},
|
|
103
132
|
roll:()=>[lie(),wait(range(.7,1.5),'rest'),cmd('roll',[],'roll'),wait(range(2,5),'watch'),fx(()=>{needs.energy-=.08;needs.curiosity-=.15;})],
|
|
104
133
|
checkin:()=>[home(),sit('sit'),cmd('look',[side()],'look'),wait(range(5,10),'checkin'),fx(()=>{needs.social-=.4;})],
|
|
105
134
|
play:()=>[cmd('spin',[],'play'),wait(range(1,2),'pause'),sit('sit'),wait(range(2,4),'watch'),fx(()=>{needs.energy-=.12;needs.curiosity-=.2;})],
|
|
106
135
|
belly:()=>[cmd('belly',[range(5,9)],'belly'),wait(range(2,4),'watch'),fx(()=>{needs.energy+=.08;needs.curiosity-=.15;})],
|
|
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;})],
|
|
136
|
+
sleep:()=>[...quietPlace(),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
137
|
butterfly:butterflyEpisode,
|
|
109
138
|
};
|
|
110
139
|
const weights=()=>{
|
|
111
140
|
const {energy:e,social:s,curiosity:c}=needs;
|
|
112
141
|
return {rest:e<.35?6:(1-e)*1.2,watch:.8+s,observe:.6+c*.6,stroll:1.2*c*e,dash:e>.55?1.2*e*c:0,
|
|
113
|
-
scan:.35+c*.4,patrol:e>.25?.7*c*e:0,checkin
|
|
142
|
+
scan:.35+c*.4,patrol:e>.25?.7*c*e:0,checkin:clock-lastCheckin>=90?.25+s*.7:0,
|
|
114
143
|
roll:episodeCooldowns.roll===0&&e>.45?.35*e:0,
|
|
115
144
|
play:episodeCooldowns.play===0&&e>.6&&typeof actor.spin==='function'?.25*e*c:0,
|
|
116
145
|
belly:episodeCooldowns.belly===0&&typeof actor.belly==='function'?.35:0,
|
|
@@ -120,6 +149,9 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
|
|
|
120
149
|
const w=weights();if(lastEpisode)w[lastEpisode]=0;
|
|
121
150
|
const kinds=Object.keys(w),total=kinds.reduce((sum,k)=>sum+w[k],0);
|
|
122
151
|
let pick=rand()*total,kind=kinds.find(k=>(pick-=w[k])<0)??'observe';
|
|
152
|
+
// A bounded spell near the viewer, not a permanent home magnet. Let an active
|
|
153
|
+
// authored motion finish first; only the next episode changes its destination.
|
|
154
|
+
if(nearSeconds>=40&&needs.energy>.4&&lastEpisode!=='dash')kind='dash';
|
|
123
155
|
// Quiet-time sleep pressure is independent of energy restored by sitting.
|
|
124
156
|
// Wait for the current coherent episode to end; never yank a running puppy.
|
|
125
157
|
if(quiet>=120&&needs.drowsiness>=.35&&episodeCooldowns.sleep===0&&lastEpisode!=='sleep'&&typeof actor.sleep==='function')kind='sleep';
|
|
@@ -128,6 +160,7 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
|
|
|
128
160
|
kind='butterfly';nextButterfly=clock+BUTTERFLY_RULES.cooldownMin+encounterRange(0,BUTTERFLY_RULES.cooldownSpread);
|
|
129
161
|
}
|
|
130
162
|
lastEpisode=kind;decisions++;
|
|
163
|
+
if(kind==='checkin')lastCheckin=clock;
|
|
131
164
|
if(kind==='roll')episodeCooldowns.roll=90;
|
|
132
165
|
if(kind==='play')episodeCooldowns.play=120;
|
|
133
166
|
if(kind==='belly')episodeCooldowns.belly=80;
|
|
@@ -135,15 +168,22 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
|
|
|
135
168
|
queue=[wait(range(3,9),'pause'),...EPISODES[kind]()];
|
|
136
169
|
}
|
|
137
170
|
|
|
138
|
-
const landed=()=>ball?.mode==='flight'&&clock-ball.start
|
|
139
|
-
const arrived=()=>{const s=actor.sample(clock);return SAFE.has(actor.state)&&Math.hypot(s.x-
|
|
171
|
+
const landed=()=>ball?.mode==='flight'&&ball.path.at(clock-ball.start).settled;
|
|
172
|
+
const arrived=()=>{const s=actor.sample(clock),target=fetchPlan.pickupRoot;return SAFE.has(actor.state)&&Math.hypot(s.x-target.x,s.z-target.z)<ARRIVE;};
|
|
173
|
+
const fetchGo=typeof actor.chase==='function'?'chase':'go';
|
|
174
|
+
const directionalPickup=()=>{const s=actor.sample(clock);return actor.canPickup===true&&(s.seatProgress??0)===0&&['rear','front'].includes(s.view);};
|
|
140
175
|
// The leading go is dropped when interact() already dispatched the chase; pickup waits for both arrival and landing.
|
|
141
176
|
const BALL_STEPS=()=>[
|
|
142
|
-
cmd(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
fx(()=>{ball={mode:'ground'
|
|
177
|
+
cmd(fetchGo,[fetchPlan.first.x,fetchPlan.first.z,{scramble:fetchPlan.pattern==='tumble'}],'fetch'),
|
|
178
|
+
...(fetchPlan.pattern==='miss'?[wait(.12,'miss'),cmd(fetchGo,[fetchPlan.pickupRoot.x,fetchPlan.pickupRoot.z],'reChase')]:[]),
|
|
179
|
+
...(fetchPlan.pattern==='tumble'&&!actor.canSinglePassScramble?[...actor.canMovingScramble?[]:[cmd('scramble',[],'tumble')],cmd(fetchGo,[fetchPlan.pickupRoot.x,fetchPlan.pickupRoot.z],'reChase')]:[]),
|
|
180
|
+
{until:()=>landed()&&arrived()},
|
|
181
|
+
fx(()=>{ball={mode:'ground',...fetchPlan.stop};}),
|
|
182
|
+
cmd('pickup',[],'pickup',directionalPickup),
|
|
183
|
+
cmd('sit',[{urgent:true}],'pickup',()=>!directionalPickup()&&unseated()),{...play('ball-pickup','pickup'),when:()=>!directionalPickup()},
|
|
184
|
+
fx(()=>{ball={mode:'carried'};}),cmd(fetchGo,[HOME.x,HOME.z,{returnMode:fetchPlan.returnMode}],'carry'),
|
|
185
|
+
cmd('sit',[{urgent:true}],'drop',unseated),play('ball-drop','drop'),
|
|
186
|
+
fx(()=>{ball={mode:'dropped',start:clock,path:createBallPath({from:OFFER,landing:OFFER,stop:{x:OFFER.x+.035,z:OFFER.z+.06},flight:0,peak:0,deceleration:.2})};needs.social-=.4;}),wait(3,'offer'),fx(()=>{ball=null;fetchPlan=null;})
|
|
147
187
|
];
|
|
148
188
|
// Hand-feed where the puppy already is: sit only if not front-seated, never go/walk/chase/come.
|
|
149
189
|
const TREAT_STEPS=()=>[
|
|
@@ -158,12 +198,14 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
|
|
|
158
198
|
active=pending;pending=null;decisions++;hold=0;
|
|
159
199
|
queue=active==='ball'?BALL_STEPS():active==='tug'?TUG_STEPS():TREAT_STEPS();
|
|
160
200
|
}
|
|
161
|
-
function finishInteraction(){cooldowns[active]=COOLDOWN[active];active=null;}
|
|
201
|
+
function finishInteraction(){cooldowns[active]=COOLDOWN[active];if(active==='ball')lastCheckin=clock;active=null;}
|
|
162
202
|
|
|
163
203
|
function drift(dt){
|
|
164
204
|
const resting=holdIntent==='rest'||holdIntent==='recover'||actor.state==='sleep'||intent==='belly';
|
|
165
205
|
needs.energy+=resting?.04*dt:-.004*dt;
|
|
166
206
|
const asleep=actor.state==='sleep';
|
|
207
|
+
if(!active&&!pending&&!asleep){const position=actor.sample(clock);nearSeconds=position.z>-.5&&Math.abs(position.x)<.32?nearSeconds+dt:0;}
|
|
208
|
+
else nearSeconds=0;
|
|
167
209
|
needs.drowsiness+=asleep?-dt/60:dt/240;
|
|
168
210
|
quiet=active||pending||asleep?0:quiet+dt;
|
|
169
211
|
if(!active){needs.curiosity+=.01*dt;needs.social+=.008*dt;}
|
|
@@ -218,20 +260,47 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
|
|
|
218
260
|
quiet=0;
|
|
219
261
|
if(butterfly||lastEpisode==='butterfly'){
|
|
220
262
|
butterfly=null;queue=[];hold=0;holdIntent=null;gesture=null;waiting=null;
|
|
221
|
-
if(kind!=='ball')actor.sit();
|
|
222
263
|
}
|
|
223
264
|
if(kind==='treat'||kind==='tug'){
|
|
224
265
|
pending=kind;
|
|
225
|
-
|
|
226
|
-
|
|
266
|
+
queue=[];hold=0;holdIntent=null;gesture=null;waiting=null;
|
|
267
|
+
// Show the hand now, not several seconds later after the old route ends.
|
|
268
|
+
if(kind==='treat')treat=true;
|
|
269
|
+
if(typeof actor.attend==='function')actor.attend();
|
|
270
|
+
// Backward-compatible substitute actors may only support sleep recovery.
|
|
271
|
+
else if(['sleep','belly'].includes(actor.sample(clock).state))actor.sit();
|
|
227
272
|
return true;
|
|
228
273
|
}
|
|
229
274
|
// Ball: launch now and drop obsolete controller plans; the actor's own state is left to chase.
|
|
230
|
-
|
|
275
|
+
const throwPosition=actor.sample(clock),pattern=chooseFetch(throwPosition.z>=-1.09);
|
|
276
|
+
const landing={...BALL_TARGET},stop={...BALL_TARGET};
|
|
277
|
+
if(pattern!=='clean')stop.z-=2.5+toyRand()*2;
|
|
278
|
+
let first=pattern==='miss'?{...landing}:{...stop};
|
|
279
|
+
if(pattern==='tumble'){
|
|
280
|
+
if(actor.canSinglePassScramble){
|
|
281
|
+
// One forward pursuit in depth. No shallow side detour, overshoot target,
|
|
282
|
+
// reversal or second chase just to expose an unrelated lateral drawing.
|
|
283
|
+
Object.assign(landing,{x:throwPosition.x,z:throwPosition.z-1.15});Object.assign(stop,{x:throwPosition.x,z:landing.z-1.1});first={...stop};
|
|
284
|
+
}else{
|
|
285
|
+
const sign=throwPosition.x>.1?-1:throwPosition.x<-.1?1:toyRand()<.5?-1:1,z=-1.25,radius=roomRadius(z);
|
|
286
|
+
Object.assign(landing,{x:sign*radius*.1,z});Object.assign(stop,{x:sign*radius*.28,z});first={x:sign*radius*.75,z};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
let pickupRoot={...stop};
|
|
290
|
+
if(actor.canPickup===true){
|
|
291
|
+
const from=Math.hypot(first.x-stop.x,first.z-stop.z)<1e-6?throwPosition:first,dx=stop.x-from.x,dz=stop.z-from.z,d=Math.hypot(dx,dz);
|
|
292
|
+
if(d>.001){const target=paperGroundTarget(stop.x-dx/d*.17,stop.z-dz/d*.17);pickupRoot={x:target[0],z:target[1]};}
|
|
293
|
+
if(pattern==='clean'||pattern==='rolling'||pattern==='tumble'&&actor.canSinglePassScramble)first={...pickupRoot};
|
|
294
|
+
}
|
|
295
|
+
const returns=actor.canCarryTrot?FETCH_RETURN_STYLES.slice(1).filter(mode=>mode!==lastReturnMode):['brake'];
|
|
296
|
+
const returnMode=returnStyle==='auto'?returns[Math.floor(returnRand()*returns.length)]:returnStyle;
|
|
297
|
+
lastReturnMode=returnMode;
|
|
298
|
+
fetchPlan={pattern,stop,first,pickupRoot,returnMode};
|
|
299
|
+
active='ball';decisions++;ball={mode:'flight',start:clock,path:createBallPath({landing,stop})};
|
|
231
300
|
queue=BALL_STEPS();hold=0;holdIntent=null;gesture=null;waiting=null;
|
|
232
301
|
intent='fetch';label=LABEL.fetch;
|
|
233
302
|
const urgent=typeof actor.chase==='function';
|
|
234
|
-
if((urgent||SAFE.has(actor.state))&&actor[urgent?'chase':'go'](
|
|
303
|
+
if((urgent||SAFE.has(actor.state))&&actor[urgent?'chase':'go'](first.x,first.z,{scramble:pattern==='tumble'})!==false)queue.shift();
|
|
235
304
|
return true;
|
|
236
305
|
}
|
|
237
306
|
|
|
@@ -242,22 +311,24 @@ export function createCompanionLife(actor,{seed=731,energy=.8}={}){
|
|
|
242
311
|
}
|
|
243
312
|
function ballReport(){
|
|
244
313
|
if(!ball)return null;
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
314
|
+
const pose=actor.sample(clock);
|
|
315
|
+
// On a rear pickup the muzzle/ball is occluded behind the connected body.
|
|
316
|
+
// Do not draw the ground ball through the hindquarters to make it visible.
|
|
317
|
+
if(pose.state==='pickup'&&pose.view==='rear')return null;
|
|
318
|
+
if(pose.state==='pickup'&&pose.comfortPose?.progress>=.5)return {x:pose.x,z:pose.z,height:CARRY_HEIGHT,carried:true};
|
|
319
|
+
if(ball.mode==='flight'||ball.mode==='dropped')return {...ball.path.at(clock-ball.start),carried:false};
|
|
250
320
|
if(ball.mode==='carried'){const s=actor.sample(clock);return {x:s.x,z:s.z,height:CARRY_HEIGHT,carried:true};}
|
|
251
321
|
return {x:ball.x,z:ball.z,height:0,carried:false};
|
|
252
322
|
}
|
|
253
323
|
|
|
254
324
|
return {
|
|
255
325
|
update,interact,
|
|
326
|
+
setReturnStyle(style){validateReturnStyle(style);returnStyle=style;return style;},
|
|
256
327
|
get report(){
|
|
257
328
|
const interaction=active??pending;
|
|
258
329
|
return {
|
|
259
|
-
intent,label:pending?`${label} · ${PENDING_LABEL[pending]}`:label,episode:lastEpisode,interaction,busy:interaction!==null,
|
|
260
|
-
ball:ballReport(),treat:treatReport(),butterfly:butterflyPoint(),tug:actor.state==='tug'?{progress:actor.sample(clock).comfortPose?.progress??0,grip:1}:null,
|
|
330
|
+
intent:actor.state==='scramble'&&fetchPlan?'tumble':intent,label:pending?`${label} · ${PENDING_LABEL[pending]}`:actor.state==='scramble'&&fetchPlan?LABEL.tumble:label,episode:lastEpisode,interaction,busy:interaction!==null,
|
|
331
|
+
ball:ballReport(),fetchPattern:fetchPlan?.pattern??null,returnStyle,returnMode:fetchPlan?.returnMode??null,treat:treatReport(),butterfly:butterflyPoint(),tug:actor.state==='tug'?{progress:actor.sample(clock).comfortPose?.progress??0,grip:1}:null,
|
|
261
332
|
snore:actor.state==='sleep'?((clock%3.6)/3.6):null,energy:needs.energy,drowsiness:needs.drowsiness,decisions,cooldowns:{...cooldowns},
|
|
262
333
|
// Local hold clock only: progress freezes between updates; null outside the stationary hold.
|
|
263
334
|
gesture:gesture&&SAFE.has(actor.state)?{clip:gesture.clip,progress:clamp(gesture.elapsed/gesture.duration,0,1)}:null
|
|
@@ -47,10 +47,12 @@ export function paintCompanionObjects(pixels,width,height,projection,report,stat
|
|
|
47
47
|
const up=projection.project(o.x,(o.height??0)+o.radius,o.z);ry=Math.abs(up.y-p.y)*height;
|
|
48
48
|
}
|
|
49
49
|
if(![cx,cy,rx,ry].every(Number.isFinite)||rx<=0||ry<=0)continue;
|
|
50
|
+
const spin=o.spin??0,cos=Math.cos(spin),sin=Math.sin(spin);
|
|
50
51
|
for(let y=Math.max(0,Math.floor(cy-ry));y<Math.min(height,Math.ceil(cy+ry));y++)
|
|
51
52
|
for(let x=Math.max(0,Math.floor(cx-rx));x<Math.min(width,Math.ceil(cx+rx));x++){
|
|
52
53
|
const u=(x+.5-cx)/rx,v=(y+.5-cy)/ry;if(u*u+v*v>1)continue;
|
|
53
|
-
const
|
|
54
|
+
const stripe=(v*cos+u*sin)+.25*(u*cos-v*sin);
|
|
55
|
+
const tone=o.kind==='treat'?((x+y)%3?190:85):Math.abs(stripe)<.18?85:225;
|
|
54
56
|
const j=(y*width+x)*4;pixels[j]=pixels[j+1]=pixels[j+2]=tone;
|
|
55
57
|
}
|
|
56
58
|
}
|
package/puppy-paper-area.mjs
CHANGED
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
// fractional edge coverage. No temporal blur, face warp or minimum sprite size.
|
|
3
3
|
export function createPaperAreaSampler(n=320){
|
|
4
4
|
if(!Number.isInteger(n)||n<2||n>640)throw Error('Invalid area source size');
|
|
5
|
-
|
|
5
|
+
// Byte images have an integer sum <=640*640*255, exactly representable in
|
|
6
|
+
// uint32. Allocate only when minification is actually used. Fractional/custom
|
|
7
|
+
// inputs retain the old Float64 arithmetic rather than truncating their art.
|
|
8
|
+
const pitch=n+1;let sums=null;
|
|
6
9
|
const integral=(x,y)=>{
|
|
7
10
|
x=Math.max(0,Math.min(n,x));y=Math.max(0,Math.min(n,y));
|
|
8
11
|
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*pitch+ix;
|
|
9
12
|
return (sums[k]*(1-fx)+sums[k+1]*fx)*(1-fy)+(sums[k+pitch]*(1-fx)+sums[k+pitch+1]*fx)*fy;
|
|
10
13
|
};
|
|
11
14
|
return{
|
|
12
|
-
prepare(image){if(image.length!==n*n*4)throw Error('Invalid area source');sums.fill(0);for(let y=0;y<n;y++){let row=0;for(let x=0;x<n;x++){row+=image[(y*n+x)*4];sums[(y+1)*pitch+x+1]=sums[y*pitch+x+1]+row;}}},
|
|
13
|
-
sample(x,y,dx,dy){if(![x,y,dx,dy].every(Number.isFinite)||Math.min(dx,dy)<=0)throw Error('Invalid area footprint');const l=x-dx/2,r=x+dx/2,t=y-dy/2,b=y+dy/2;return Math.max(0,(integral(r,b)-integral(l,b)-integral(r,t)+integral(l,t))/(dx*dy));}
|
|
15
|
+
prepare(image){if(image.length!==n*n*4)throw Error('Invalid area source');const Type=image instanceof Uint8Array||image instanceof Uint8ClampedArray?Uint32Array:Float64Array;if(!(sums instanceof Type))sums=new Type(pitch*pitch);else sums.fill(0);for(let y=0;y<n;y++){let row=0;for(let x=0;x<n;x++){row+=image[(y*n+x)*4];sums[(y+1)*pitch+x+1]=sums[y*pitch+x+1]+row;}}},
|
|
16
|
+
sample(x,y,dx,dy){if(![x,y,dx,dy].every(Number.isFinite)||Math.min(dx,dy)<=0)throw Error('Invalid area footprint');if(!sums)return 0;const l=x-dx/2,r=x+dx/2,t=y-dy/2,b=y+dy/2;return Math.max(0,(integral(r,b)-integral(l,b)-integral(r,t)+integral(l,t))/(dx*dy));}
|
|
14
17
|
};
|
|
15
18
|
}
|
|
Binary file
|
package/puppy-paper-world.mjs
CHANGED
|
@@ -49,7 +49,7 @@ export function createPaperProjection({viewportWidth,viewportHeight,rect,width,h
|
|
|
49
49
|
const cx=rect.width/2,cy=rect.height/2;
|
|
50
50
|
return{
|
|
51
51
|
project(x,y,z){const p=paperCameraPoint(x,y,z);if(p.depth<=.02)throw Error('Paper point behind camera');return{x:(cx+f*p.x/p.depth)/rect.width,y:(cy-f*p.y/p.depth)/rect.height,depth:p.depth};},
|
|
52
|
-
placement(x,z){const p=this.project(x,
|
|
52
|
+
placement(x,z,lift=0){const p=this.project(x,lift,z),sourceSize=f*PAPER_CAMERA.canvasMeters/p.depth;return{x:p.x,y:p.y,size:sourceSize/rect.height,pixelAspect:(rect.width/width)/(rect.height/height),depth:p.depth,
|
|
53
53
|
groundFromPixel:(px,py)=>this.ground(px/width,py/height),pixelFromGround:point=>{const q=this.project(point.x,0,point.z);return{x:q.x*width,y:q.y*height};}};},
|
|
54
54
|
ground(u,v){
|
|
55
55
|
if(![u,v].every(Number.isFinite))return null;
|
|
@@ -104,12 +104,58 @@ export function planPaperApproachTravel(distance){
|
|
|
104
104
|
return{distance,duration,peak:Math.max(...segments.map(p=>p.peak)),brakingDistance:brakeDistance,at(t){if(!Number.isFinite(t))throw Error('Invalid approach time');if(t>=duration)return{distance,speed:0,stage:'arrived',done:true};let offset=0;for(let i=0;i<segments.length;i++){const p=segments[i];if(t<p.duration||i===segments.length-1){const s=p.at(t);return{...s,distance:offset+s.distance};}t-=p.duration;offset+=p.distance;}}};
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
// Ball-carry return: a true diagonal trot, optionally preceded by whole gallop
|
|
108
|
+
// cycles. The last small gathering steps MOVE through the final centimeters;
|
|
109
|
+
// the standing endpoint is reached at zero velocity, never a skidding still.
|
|
110
|
+
export function planPaperCarry(distance,{mixed=false,settleSeconds=.22}={}){
|
|
111
|
+
if(!Number.isFinite(distance)||distance<=0||typeof mixed!=='boolean'||!Number.isFinite(settleSeconds)||settleSeconds<=0||settleSeconds>1)throw Error('Invalid carry travel');
|
|
112
|
+
const gait=PAPER_GAITS.trot,runDistance=mixed?Math.max(0,Math.floor((distance-1.5)/run.stride))*run.stride:0;
|
|
113
|
+
const remaining=distance-runDistance,gatherDistance=Math.min(.088,remaining*.2),trotDistance=remaining-gatherDistance;
|
|
114
|
+
const cycles=Math.max(1,Math.ceil(trotDistance/gait.stride)),stride=trotDistance/cycles;
|
|
115
|
+
const speed=Math.min(gait.speed,stride*gait.frequency),entrySpeed=Math.min(speed,2*gatherDistance/settleSeconds,Math.sqrt(2*2.5*trotDistance));
|
|
116
|
+
const gatherSeconds=2*gatherDistance/entrySpeed;
|
|
117
|
+
const segments=[];
|
|
118
|
+
if(runDistance)segments.push(planPaperTravel(runDistance,{speed:run.speed,...PAPER_DYNAMICS,endSpeed:speed}));
|
|
119
|
+
segments.push(planPaperTravel(trotDistance,{speed,acceleration:2.5,deceleration:4,startSpeed:runDistance?speed:0,endSpeed:entrySpeed}));
|
|
120
|
+
segments.push(planPaperTravel(gatherDistance,{speed:entrySpeed,startSpeed:entrySpeed,deceleration:entrySpeed/gatherSeconds}));
|
|
121
|
+
const duration=segments.reduce((a,p)=>a+p.duration,0),gatherStart=runDistance+trotDistance;
|
|
122
|
+
return{distance,duration,peak:Math.max(...segments.map(p=>p.peak)),brakingDistance:gatherDistance,stride,runDistance,trotDistance,gatherStart,gatherSeconds,
|
|
123
|
+
poseAt(d){const phase=(d-runDistance)/stride;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))};},
|
|
124
|
+
at(t){if(!Number.isFinite(t))throw Error('Invalid carry time');if(t>=duration)return{distance,speed:0,stage:'arrived',done:true};let offset=0;for(let i=0;i<segments.length;i++){const p=segments[i];if(t<p.duration||i===segments.length-1){const s=p.at(t);return{...s,distance:offset+s.distance};}t-=p.duration;offset+=p.distance;}}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
107
128
|
// Finish at a shared cycle endpoint without animating extra strides in place.
|
|
108
129
|
// Short paths use shorter strides and a correspondingly lower speed ceiling.
|
|
109
130
|
export function planPaperRun(distance){
|
|
110
131
|
const cycles=Math.max(1,Math.ceil(distance/run.stride)),stride=distance/cycles||run.stride;
|
|
111
132
|
return{stride,cycles,plan:planPaperTravel(distance,{speed:Math.min(run.speed,stride*run.frequency),...PAPER_DYNAMICS})};
|
|
112
133
|
}
|
|
134
|
+
|
|
135
|
+
// A single forward chase ending in a braced stumble/catch at the ball. Solve
|
|
136
|
+
// entry distance and stride TOGETHER: positive entry velocity, exact contact
|
|
137
|
+
// phase, no pre-impact braking, no overshoot/second go and no extra run cycle.
|
|
138
|
+
export function planPaperStumble(distance,{entryPhase=.25,brakeSeconds=.27,catchSeconds=.36,catchSpeedRatio=.18,duration=.82}={}){
|
|
139
|
+
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)throw Error('Invalid stumble travel');
|
|
140
|
+
const cycles=Math.max(0,Math.ceil(distance/run.stride-entryPhase)),contacts=cycles+entryPhase,a=PAPER_DYNAMICS.acceleration;
|
|
141
|
+
const coefficient=((1+catchSpeedRatio)*brakeSeconds+catchSpeedRatio*catchSeconds)/2;
|
|
142
|
+
const speedAt=d=>Math.min(run.speed,d/contacts*run.frequency,Math.sqrt(2*a*d));
|
|
143
|
+
let lo=0,hi=distance;
|
|
144
|
+
for(let i=0;i<60;i++){const d=(lo+hi)/2;if(d+coefficient*speedAt(d)<distance)lo=d;else hi=d;}
|
|
145
|
+
const entryDistance=(lo+hi)/2,entrySpeed=speedAt(entryDistance),stride=entryDistance/contacts,catchSpeed=entrySpeed*catchSpeedRatio;
|
|
146
|
+
const entry=planPaperTravel(entryDistance,{speed:entrySpeed,acceleration:a,deceleration:PAPER_DYNAMICS.deceleration,endSpeed:entrySpeed});
|
|
147
|
+
const plan=planPaperTravel(distance,{speed:entrySpeed,acceleration:a,deceleration:PAPER_DYNAMICS.deceleration,endSpeed:entrySpeed});
|
|
148
|
+
const brakeDistance=(entrySpeed+catchSpeed)*brakeSeconds/2,impactDistance=distance-entryDistance;
|
|
149
|
+
return{plan,stride,entryDistance,entrySpeed,entryTime:entry.duration,entryPhase,duration,impactDistance,
|
|
150
|
+
impactAt(age){
|
|
151
|
+
if(!Number.isFinite(age))throw Error('Invalid stumble age');const t=Math.max(0,age);
|
|
152
|
+
if(t>=brakeSeconds+catchSeconds)return{distance:impactDistance,speed:0,stage:'rest'};
|
|
153
|
+
if(t<brakeSeconds){const deceleration=(entrySpeed-catchSpeed)/brakeSeconds;return{distance:entrySpeed*t-deceleration*t*t/2,speed:entrySpeed-deceleration*t,stage:'bracing'};}
|
|
154
|
+
const u=t-brakeSeconds,deceleration=catchSpeed/catchSeconds;
|
|
155
|
+
return{distance:brakeDistance+catchSpeed*u-deceleration*u*u/2,speed:catchSpeed-deceleration*u,stage:'catching'};
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
113
159
|
export function planPaperWalk(distance){
|
|
114
160
|
const gait=PAPER_GAITS.walk,cycles=Math.max(1,Math.ceil(distance/gait.stride)),stride=distance/cycles||gait.stride;
|
|
115
161
|
return{stride,cycles,plan:planPaperTravel(distance,{speed:Math.min(gait.speed,stride*gait.frequency),acceleration:PAPER_DYNAMICS.walkAcceleration,deceleration:PAPER_DYNAMICS.walkDeceleration})};
|