@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-paper.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import {createCachedTween,retimeRun,cleanGroundOffset} from './puppy-clean.mjs';
|
|
2
2
|
import {createPaperFootLock,footSourcePoint,createRearSampler,createPaperSupportEpisodes} from './puppy-paper-contact.mjs';
|
|
3
3
|
import {createPaperSeatSampler} from './puppy-paper-seat.mjs';
|
|
4
|
-
import {planPaperRun,planPaperWalk,paperWalkTarget,planPaperApproach,planPaperApproachTravel,PAPER_GAITS,PAPER_GROUND,paperGroundTarget} from './puppy-paper-world.mjs';
|
|
4
|
+
import {planPaperRun,planPaperWalk,planPaperCarry,planPaperStumble,planPaperTravel,paperWalkTarget,planPaperApproach,planPaperApproachTravel,PAPER_DYNAMICS,PAPER_GAITS,PAPER_GROUND,paperGroundTarget} from './puppy-paper-world.mjs';
|
|
5
5
|
import {createPaperDirector} from './puppy-paper-director.mjs';
|
|
6
6
|
import {createPaperBehaviorSampler,paperBehaviorPose} from './puppy-paper-behaviors.mjs';
|
|
7
7
|
import {createPaperAreaSampler} from './puppy-paper-area.mjs';
|
|
8
8
|
import {PAPER_ACTION_DURATIONS,createPaperActionSampler,paperActionPose} from './puppy-paper-actions.mjs';
|
|
9
9
|
import {createPawTransport} from './puppy-paper-paw-transport.mjs';
|
|
10
10
|
import {createCompanionSpriteSampler} from './puppy-companion-sprites.mjs';
|
|
11
|
-
import {createComfortSpriteSampler} from './puppy-comfort-sprites.mjs';
|
|
11
|
+
import {createComfortSpriteSampler,comfortMotion,comfortMotionKey} from './puppy-comfort-sprites.mjs';
|
|
12
12
|
import {mouthSourceFrame,mouthAnchor,projectMouth} from './puppy-mouth-anchor.mjs';
|
|
13
13
|
import {trickClipEdges} from './puppy-paper-trick-timing.mjs';
|
|
14
14
|
|
|
@@ -37,12 +37,21 @@ export function paperWatchProgress(age){
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// Original artwork only. Physics supplies ground travel; no procedural dog is drawn.
|
|
40
|
-
export function createPaperActor(animal,info,{manualTricks=false,trickTimings=null,walkTransitions=false,comfortTimings=PAPER_COMFORT_TIMINGS}={}){
|
|
40
|
+
export function createPaperActor(animal,info,{manualTricks=false,trickTimings=null,walkTransitions=false,comfortTimings=PAPER_COMFORT_TIMINGS,scrambleSeconds=0,fetchMotions=null}={}){
|
|
41
|
+
if(!Number.isFinite(scrambleSeconds)||scrambleSeconds<0||scrambleSeconds>3)throw Error('Invalid scramble duration');
|
|
42
|
+
if(fetchMotions&&(!Number.isFinite(fetchMotions.scramble?.entryPhase)||fetchMotions.scramble.entryPhase<=0||fetchMotions.scramble.entryPhase>=1||!Number.isFinite(fetchMotions.scramble.brakeSeconds)||fetchMotions.scramble.brakeSeconds<=0||fetchMotions.scramble.brakeSeconds>scrambleSeconds||!['front','rear'].every(v=>Number.isFinite(fetchMotions.pickups?.[v]?.duration)&&fetchMotions.pickups[v].duration>0&&fetchMotions.pickups[v].duration<=3)))throw Error('Invalid fetch actor metadata');
|
|
43
|
+
if(fetchMotions?.scramble.trajectory==='single-pass-target'){
|
|
44
|
+
if(fetchMotions.scramble.entryView!=='rear'||fetchMotions.scramble.duration!==scrambleSeconds)throw Error('Invalid single-pass view/duration');
|
|
45
|
+
planPaperStumble(1,fetchMotions.scramble); // Eagerly validate the complete owned motion profile.
|
|
46
|
+
}
|
|
47
|
+
const recovery=fetchMotions?.scramble?.recovery;
|
|
48
|
+
if(recovery&&(![recovery.start,recovery.end,recovery.distance,recovery.height].every(Number.isFinite)||recovery.start<fetchMotions.scramble.brakeSeconds||recovery.end<=recovery.start||recovery.end>scrambleSeconds||recovery.distance<0||recovery.distance>.2||recovery.height<0||recovery.height>.1))throw Error('Invalid fetch recovery metadata');
|
|
41
49
|
const bellyTiming=comfortTimings?.belly;
|
|
42
50
|
if(!bellyTiming||![bellyTiming.hold,bellyTiming.enter,bellyTiming.exit,comfortTimings.tugExit].every(Number.isFinite)||bellyTiming.hold<=0||bellyTiming.hold>=1||bellyTiming.enter<=0||bellyTiming.exit<=0||comfortTimings.tugExit<=0)throw Error('Invalid comfort timing');
|
|
43
51
|
let state='idle',age=0,phase=0,flip=false,nextFlip=false,target=null,route=false,routeIndex=0,view='front',nextView='front',turnView='front',turnFlip=false;
|
|
44
52
|
let held=null,waypoints=[];
|
|
45
53
|
let preparationRate=1;
|
|
54
|
+
let scrambleArmed=false,movingScramble=null,carryMode=null;
|
|
46
55
|
let arrived=false;
|
|
47
56
|
let straight=false,seatProgress=0,seatVariant='default',travel=null,approachPlan=null,travelAge=0,travelDistance=0,motionSpeed=0,motionStage='rest',completed=false;
|
|
48
57
|
let watchSide='left',restProgress=0,idleHold=2.5;
|
|
@@ -50,7 +59,7 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
|
|
|
50
59
|
let comfort=null,comfortSeconds=0,sleepLook=null,nextSleepLook={variant:'side',right:false};
|
|
51
60
|
const director=createPaperDirector();
|
|
52
61
|
const points=[[.12,PAPER_GROUND.away]];
|
|
53
|
-
const enter=s=>{state=s;age=0;};
|
|
62
|
+
const enter=s=>{state=s;age=0;if(s==='idle')preparationRate=1;};
|
|
54
63
|
const nextRoute=()=>{const choice=director.choose({x:animal.position.x,z:animal.position.z,seated:view==='approach'&&seatProgress===1});idleHold=choice.holdSeconds;if(choice.kind==='watch'){watchSide=choice.side;enter('watch');}else if(choice.kind==='rest')enter('recline');else if(choice.kind==='come')go(animal.position.x,.2,{direct:true});else go(...choice.target);};
|
|
55
64
|
const settle=()=>{if(view==='approach'&&completed)seatVariant='arrival';enter(view==='approach'?'sit-down':'settle');};
|
|
56
65
|
const stop=()=>{animal.follow({x:animal.position.x,y:0,z:animal.position.z},{x:0,y:0,z:0},{heading:0});motionSpeed=0;};
|
|
@@ -58,8 +67,9 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
|
|
|
58
67
|
const viewTurnSeconds=()=>snappyPreparation()?.24:walkTransitions&&movementMode==='walk'?.4:1;
|
|
59
68
|
const turnSeconds=()=>snappyPreparation()?.22:.85;
|
|
60
69
|
const launchSeconds=()=>snappyPreparation()?.10:.25;
|
|
61
|
-
const dispatch=(fn,{keepDown=false}={})=>{
|
|
62
|
-
if(
|
|
70
|
+
const dispatch=(fn,{keepDown=false,urgent=false}={})=>{
|
|
71
|
+
if(urgent)preparationRate=3;
|
|
72
|
+
if(activeAction||state==='tug'||state==='scramble'||state==='pickup'||(movementMode==='walk'&&state==='run')){route=false;deferred=()=>dispatch(fn,{keepDown,urgent});hurryComfort();return true;}
|
|
63
73
|
// The calibrated trick down pose has its own byte-shared recovery; never
|
|
64
74
|
// jump back into the older/larger rest illustration on another command.
|
|
65
75
|
if(manualDown&&!keepDown){route=false;deferred=fn;playAction('lie-up',()=>{manualDown=false;view='approach';seatProgress=1;seatVariant='default';enter('idle');drain();},.55);return true;}
|
|
@@ -87,10 +97,28 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
|
|
|
87
97
|
const playAction=(name,done,duration=PAPER_ACTION_DURATIONS[name])=>{const timing=trickTimings?.[name];duration=timing?.duration??duration;if(!Number.isFinite(duration)||duration<=0)throw Error('Invalid paper action duration');activeAction={name,duration,timing};actionDone=done;stop();enter('action');};
|
|
88
98
|
const finishTrick=name=>{if(name!=='spin'){view='approach';flip=false;seatProgress=1;seatVariant='default';enter('idle');}else{view='front';flip=false;seatProgress=0;enter('settle');}drain();};
|
|
89
99
|
const issue=(skipEntry=false)=>{
|
|
100
|
+
// The walk-enter drawing finishes at a shared standing endpoint. A direct
|
|
101
|
+
// interaction queued during it must not start the obsolete walk afterwards.
|
|
102
|
+
if(skipEntry&&deferred){drain();return;}
|
|
90
103
|
if(movementMode==='walk'&&!skipEntry){playAction('walk-enter',()=>issue(true));return;}
|
|
91
|
-
|
|
104
|
+
arrived=false;completed=false;travelAge=0;travelDistance=0;const x=animal.position.x,z=animal.position.z;if(straight&&view==='approach')target[0]=x;
|
|
92
105
|
const dx=target[0]-x,dz=target[1]-z,distance=Math.hypot(dx,dz),run=movementMode==='walk'?planPaperWalk(distance):planPaperRun(distance);phase=Math.ceil(phase);
|
|
93
106
|
travel={x,z,dx,dz,heading:Math.atan2(dx,dz),stride:view==='approach'?PAPER_GAITS.approach.stride:run.stride,plan:view==='approach'?planPaperApproachTravel(distance):run.plan};approachPlan=view==='approach'?planPaperApproach(distance):null;stop();enter('run');
|
|
107
|
+
if(['trot','run-trot'].includes(carryMode)&&fetchMotions?.carryTrot&&view==='approach'&&distance>0){
|
|
108
|
+
const carry=planPaperCarry(distance,{mixed:carryMode==='run-trot',settleSeconds:fetchMotions.carryTrot.settle.duration});
|
|
109
|
+
Object.assign(travel,{plan:carry,carry,stride:carry.stride});approachPlan=null;
|
|
110
|
+
}
|
|
111
|
+
if(scrambleArmed&&fetchMotions?.scramble.trajectory==='single-pass-target'&&view===fetchMotions.scramble.entryView&&!waypoints.length){
|
|
112
|
+
const stumble=planPaperStumble(distance,fetchMotions.scramble);
|
|
113
|
+
Object.assign(travel,{plan:stumble.plan,stride:stumble.stride,stumble,scrambleAt:stumble.entryTime});
|
|
114
|
+
}else if(scrambleArmed&&fetchMotions&&view==='front'&&!waypoints.length&&fetchMotions.scramble.trajectory!=='single-pass-target'){
|
|
115
|
+
// Last matching forepaw contact before the endpoint. Invert the existing
|
|
116
|
+
// distance profile once, not on every frame. Source frame pair5/blend0 is
|
|
117
|
+
// shared EXACTLY with the new motion's first key.
|
|
118
|
+
const contact=fetchMotions.scramble.entryPhase,cycle=Math.max(0,Math.floor(distance/travel.stride-contact-.15)),d=(cycle+contact)*travel.stride;
|
|
119
|
+
let lo=0,hi=travel.plan.duration;for(let i=0;i<40;i++){const mid=(lo+hi)/2;if(travel.plan.at(mid).distance<d)lo=mid;else hi=mid;}
|
|
120
|
+
travel.scrambleAt=(lo+hi)/2;
|
|
121
|
+
}
|
|
94
122
|
};
|
|
95
123
|
const prepareAction=()=>{
|
|
96
124
|
const name=pendingAction,desired=name==='spin'?'front':'approach';
|
|
@@ -116,8 +144,8 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
|
|
|
116
144
|
playAction(name,()=>finishTrick(name));
|
|
117
145
|
};
|
|
118
146
|
const prepareDirection=()=>{if(pendingAction){prepareAction();return;}if(!target){settle();return;}if(nextView!==view){turnView=view==='front'?nextView:'front';enter('view-turn');}else if(view==='front'&&nextFlip!==flip){turnFlip=nextFlip;enter('turn');}else issue();};
|
|
119
|
-
const requestAction=name=>{
|
|
120
|
-
preparationRate=1;
|
|
147
|
+
const requestAction=(name,{urgent=false}={})=>{
|
|
148
|
+
preparationRate=urgent?3:1;
|
|
121
149
|
if(name==='roll'&&manualDown&&state==='down'){route=false;target=null;waypoints=[];pendingAction=null;stayDown=false;manualDown=false;restProgress=0;playAction('roll-down',()=>finishTrick('roll'));return true;}
|
|
122
150
|
if(name==='roll'&&state==='down'&&restProgress===.25){route=false;target=null;waypoints=[];pendingAction=null;stayDown=false;restProgress=0;playAction('roll-down',()=>finishTrick('roll'));return true;}
|
|
123
151
|
if(name==='belly'&&state==='down'&&(manualDown||restProgress===.25)){route=false;target=null;waypoints=[];pendingAction=null;startBelly(comfortSeconds);return true;}
|
|
@@ -132,9 +160,11 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
|
|
|
132
160
|
return true;
|
|
133
161
|
};
|
|
134
162
|
const faceTarget=direct=>{nextFlip=target[0]<animal.position.x;const dz=target[1]-animal.position.z,dx=target[0]-animal.position.x;straight=direct;nextView=direct?'approach':Math.abs(dz)>.16&&Math.abs(dz)>Math.abs(dx)*.7?(dz<0?'rear':'approach'):'front';};
|
|
135
|
-
function go(x,z,{direct=false,walking=false,urgent=false}={}){
|
|
163
|
+
function go(x,z,{direct=false,walking=false,urgent=false,scramble=false,returnMode=null}={}){
|
|
136
164
|
if(![x,z].every(Number.isFinite))throw Error('Invalid paper target');
|
|
137
165
|
preparationRate=urgent?3:1;
|
|
166
|
+
carryMode=returnMode;
|
|
167
|
+
scrambleArmed=!!(scramble&&fetchMotions);movingScramble=null;
|
|
138
168
|
target=paperGroundTarget(x,z);
|
|
139
169
|
pendingAction=null;stayDown=false;comfort=null;movementMode=walking?'walk':'run';
|
|
140
170
|
const dz=target[1]-animal.position.z,dx=target[0]-animal.position.x;
|
|
@@ -155,13 +185,29 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
|
|
|
155
185
|
}
|
|
156
186
|
}
|
|
157
187
|
return{
|
|
188
|
+
// Attention interrupts a journey, not its momentum. Continue the current
|
|
189
|
+
// heading/gait while braking from the actual velocity, then recover to sit.
|
|
190
|
+
// Rolls/tugs still finish their connected authored recovery via dispatch.
|
|
191
|
+
attend(){
|
|
192
|
+
if(state!=='run'||!travel||!target||motionSpeed<1e-8)return dispatch(()=>requestAction('sit',{urgent:true}),{urgent:true});
|
|
193
|
+
const n=Math.hypot(travel.dx,travel.dz),remaining=Math.max(0,travel.plan.distance-(travelDistance-(travel.distanceBase??0)));
|
|
194
|
+
const a=movementMode==='walk'?PAPER_DYNAMICS.walkDeceleration:PAPER_DYNAMICS.deceleration;
|
|
195
|
+
const distance=Math.min(remaining,motionSpeed**2/(2*a));
|
|
196
|
+
if(!n||distance<1e-10)return dispatch(()=>requestAction('sit',{urgent:true}),{urgent:true});
|
|
197
|
+
const x=animal.position.x,z=animal.position.z,dx=travel.dx/n*distance,dz=travel.dz/n*distance;
|
|
198
|
+
const plan=planPaperTravel(distance,{speed:motionSpeed,startSpeed:motionSpeed,deceleration:motionSpeed**2/(2*distance)});
|
|
199
|
+
travel={...travel,x,z,dx,dz,plan,distanceBase:travelDistance,attending:true};
|
|
200
|
+
travelAge=0;target=[x+dx,z+dz];waypoints=[];route=false;deferred=null;
|
|
201
|
+
pendingAction='sit';preparationRate=3;scrambleArmed=false;completed=false;
|
|
202
|
+
return true;
|
|
203
|
+
},
|
|
158
204
|
go(x,z){if(![x,z].every(Number.isFinite))throw Error('Invalid paper target');return dispatch(()=>{route=false;go(x,z);});},
|
|
159
|
-
// Eager attention changes preparation cadence only. Existing dispatch
|
|
160
|
-
// actions/walks at safe endpoints;
|
|
161
|
-
chase(x,z){if(![x,z].every(Number.isFinite))throw Error('Invalid paper target');return dispatch(()=>{route=false;go(x,z,{urgent:true});});},
|
|
205
|
+
// Eager attention changes preparation/recovery cadence only. Existing dispatch
|
|
206
|
+
// keeps actions/walks at safe endpoints; root physics and gait are unchanged.
|
|
207
|
+
chase(x,z,{scramble=false,returnMode=null}={}){if(![x,z].every(Number.isFinite)||![null,'brake','trot','run-trot'].includes(returnMode))throw Error('Invalid paper target/return mode');return dispatch(()=>{route=false;go(x,z,{urgent:true,scramble,returnMode});},{urgent:true});},
|
|
162
208
|
come(){return dispatch(()=>{route=false;if(state==='idle'&&seatProgress===1&&Math.abs(animal.position.z-.2)<.002)return;go(animal.position.x,.2,{direct:true});});},
|
|
163
209
|
roam(){return dispatch(()=>{route=true;routeIndex=0;go(...points[routeIndex]);});},
|
|
164
|
-
sit(){return dispatch(()=>requestAction('sit'));},spin(){return dispatch(()=>requestAction('spin'));},roll(){return dispatch(()=>requestAction('roll'),{keepDown:true});},
|
|
210
|
+
sit({urgent=false}={}){return dispatch(()=>requestAction('sit',{urgent}),{urgent});},spin(){return dispatch(()=>requestAction('spin'));},roll(){return dispatch(()=>requestAction('roll'),{keepDown:true});},
|
|
165
211
|
walk(side,distance=1.05){if(!['left','right'].includes(side)||!Number.isFinite(distance)||distance<=0)throw Error('Invalid walk command');return dispatch(()=>{const [x,z]=paperWalkTarget(animal.position.x,animal.position.z,side==='left'?-1:1,distance);if(Math.abs(x-animal.position.x)<1e-6)return false;route=false;go(x,z,{walking:true});return true;});},
|
|
166
212
|
look(side='left'){if(state!=='idle'||seatProgress!==1)return false;route=false;watchSide=side==='right'?'right':'left';enter('watch');return true;},
|
|
167
213
|
lie(){const name=manualDown||activeAction?.name==='lie-down'||restProgress>0?'sit':'lie';return dispatch(()=>requestAction(name));},
|
|
@@ -169,25 +215,51 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
|
|
|
169
215
|
belly(seconds=6){comfortDuration('belly',seconds);return dispatch(()=>{comfortSeconds=seconds;return requestAction('belly');},{keepDown:true});},
|
|
170
216
|
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');});},
|
|
171
217
|
tug(seconds=6){comfortDuration('tug',seconds);return dispatch(()=>{comfortSeconds=seconds;return requestAction('tug');});},
|
|
218
|
+
get canScramble(){return scrambleSeconds>0;},
|
|
219
|
+
get canMovingScramble(){return !!fetchMotions;},
|
|
220
|
+
get canSinglePassScramble(){return fetchMotions?.scramble.trajectory==='single-pass-target';},
|
|
221
|
+
get canPickup(){return !!fetchMotions;},
|
|
222
|
+
get canCarryTrot(){return !!fetchMotions?.carryTrot;},
|
|
223
|
+
pickup(){if(!fetchMotions||state!=='idle'||seatProgress>0||!['rear','front'].includes(view))return false;route=false;target=null;waypoints=[];pendingAction=null;stop();comfort={kind:'pickup',seconds:fetchMotions.pickups[view].duration};enter('pickup');return true;},
|
|
224
|
+
scramble(){if(!scrambleSeconds||state!=='idle'||view!=='front'||seatProgress>0)return false;route=false;target=null;waypoints=[];pendingAction=null;stop();comfort={kind:'scramble',seconds:scrambleSeconds};enter('scramble');return true;},
|
|
172
225
|
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');});},
|
|
173
226
|
update(dt){
|
|
174
227
|
if(!Number.isFinite(dt)||dt<0||dt>.1)throw Error('Invalid paper step');
|
|
175
228
|
const beforeX=animal.position.x,beforeZ=animal.position.z;
|
|
229
|
+
let enteredScramble=false;
|
|
176
230
|
if(state==='run'&&travel&&target){
|
|
177
231
|
travelAge+=dt;const p=travel.plan.at(travelAge),ratio=travel.plan.distance?p.distance/travel.plan.distance:1;
|
|
178
|
-
travelDistance=p.distance;motionSpeed=p.speed;motionStage=p.stage;
|
|
232
|
+
travelDistance=(travel.distanceBase??0)+p.distance;motionSpeed=p.speed;motionStage=p.stage;
|
|
179
233
|
const gain=travel.plan.distance?p.speed/travel.plan.distance:0;
|
|
180
234
|
animal.follow({x:travel.x+travel.dx*ratio,y:0,z:travel.z+travel.dz*ratio},{x:travel.dx*gain,y:0,z:travel.dz*gain},{heading:travel.heading});
|
|
181
235
|
arrived=p.done;
|
|
236
|
+
if(scrambleArmed&&travel.scrambleAt!==undefined&&travelAge>=travel.scrambleAt){
|
|
237
|
+
const entry=travel.plan.at(travel.scrambleAt),ratio=entry.distance/travel.plan.distance,n=travel.plan.distance;
|
|
238
|
+
const x=travel.x+travel.dx*ratio,z=travel.z+travel.dz*ratio,ux=travel.dx/n,uz=travel.dz/n;
|
|
239
|
+
const end=paperGroundTarget(x+ux*entry.speed*fetchMotions.scramble.brakeSeconds/2,z+uz*entry.speed*fetchMotions.scramble.brakeSeconds/2);
|
|
240
|
+
const distance=Math.hypot(end[0]-x,end[1]-z),brake=entry.speed?2*distance/entry.speed:0;
|
|
241
|
+
const fullEnd=paperGroundTarget(end[0]+ux*(recovery?.distance??0),end[1]+uz*(recovery?.distance??0));
|
|
242
|
+
movingScramble={x,z,ux,uz,speed:entry.speed,brake,distance,travelBase:entry.distance,hop:Math.hypot(fullEnd[0]-end[0],fullEnd[1]-end[1]),profile:travel.stumble??null};
|
|
243
|
+
scrambleArmed=false;target=null;waypoints=[];comfort={kind:'scramble',seconds:scrambleSeconds};enter('scramble');age=travelAge-travel.scrambleAt;enteredScramble=true;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if(state==='scramble'&&movingScramble){
|
|
247
|
+
const m=movingScramble,time=age+(enteredScramble?0:dt),t=Math.min(m.brake,time),p=recovery?Math.max(0,Math.min(1,(time-recovery.start)/(recovery.end-recovery.start))):0;
|
|
248
|
+
const hop=m.hop*(1-Math.cos(Math.PI*p))/2,hopSpeed=recovery&&p>0&&p<1?m.hop*Math.PI/(2*(recovery.end-recovery.start))*Math.sin(Math.PI*p):0;
|
|
249
|
+
const kinematic=m.profile?.impactAt(time),speed=kinematic?.speed??((m.brake?m.speed*(1-t/m.brake):0)+hopSpeed),d=kinematic?.distance??((m.brake?m.speed*t-.5*m.speed/m.brake*t*t:0)+hop);
|
|
250
|
+
animal.follow({x:m.x+m.ux*d,y:0,z:m.z+m.uz*d},{x:m.ux*speed,y:0,z:m.uz*speed},{heading:travel.heading});travelDistance=m.travelBase+d;motionSpeed=speed;motionStage=kinematic?.stage??(hopSpeed>1e-8?'recovery':speed?'braking':'rest');
|
|
182
251
|
}
|
|
183
252
|
animal.update(dt);
|
|
184
253
|
const prepareDt=dt*preparationRate;
|
|
185
|
-
|
|
254
|
+
// Urgent fetches accelerate only authored preparation/recovery, never root
|
|
255
|
+
// travel or its distance-driven stride. Deferred tricks reach their shared
|
|
256
|
+
// endpoint promptly instead of ignoring a thrown ball for several seconds.
|
|
257
|
+
if(!enteredScramble)age+=['view-turn','turn','rise','watch','arrival-exit','launch','wake','finish','settle'].includes(state)||(state==='action'&&deferred)?prepareDt:dt;
|
|
186
258
|
const distance=Math.hypot(animal.position.x-beforeX,animal.position.z-beforeZ);
|
|
187
259
|
director.update(dt,state==='run',distance);
|
|
188
260
|
if(state==='run'){
|
|
189
|
-
phase+=distance/travel.stride;
|
|
190
|
-
if(target&&arrived){completed=true;stop();target=null;if(movementMode==='walk')playAction('walk-exit',()=>{movementMode='run';enter(walkTransitions?'idle':'settle');drain();});else enter('finish');}
|
|
261
|
+
phase+=distance/(travel.carry&&travelDistance<travel.carry.runDistance?PAPER_GAITS.approach.stride:travel.stride);
|
|
262
|
+
if(target&&arrived){completed=true;stop();target=null;if(movementMode==='walk')playAction('walk-exit',()=>{movementMode='run';enter(walkTransitions?'idle':'settle');if(pendingAction)prepareDirection();else drain();});else enter('finish');}
|
|
191
263
|
}else if(state==='action'&&age>=activeAction.duration){
|
|
192
264
|
const done=actionDone;activeAction=null;actionDone=null;done();
|
|
193
265
|
}else if(state==='launch'&&age>=launchSeconds()){
|
|
@@ -200,13 +272,14 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
|
|
|
200
272
|
}else if(pendingAction)prepareDirection();else settle();
|
|
201
273
|
}else if(state==='view-turn'&&age>=viewTurnSeconds()){view=turnView;prepareDirection();}
|
|
202
274
|
else if(state==='turn'&&age>=turnSeconds()){flip=turnFlip;prepareDirection();}
|
|
203
|
-
else if(state==='sit-down'){seatProgress=Math.min(1,seatProgress+
|
|
275
|
+
else if(state==='sit-down'){seatProgress=Math.min(1,seatProgress+prepareDt/(snappyPreparation()?.35:.95));if(seatProgress===1){seatVariant='default';enter('idle');if(pendingAction)prepareDirection();}}
|
|
204
276
|
else if(state==='rise'){seatProgress=Math.max(0,seatProgress-prepareDt/(snappyPreparation()?.28:.65));if(seatProgress===0){if(seatVariant==='arrival')enter('arrival-exit');else prepareDirection();}}
|
|
205
277
|
else if(state==='arrival-exit'&&age>=.22){seatVariant='default';prepareDirection();}
|
|
206
278
|
else if(state==='watch'&&age>=PAPER_WATCH_TIMING.total){if(pendingAction)prepareDirection();else if(target)enter('rise');else enter('idle');}
|
|
207
279
|
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');}
|
|
208
280
|
else if(state==='sleep'&&age>=comfort.seconds){comfort=null;enter('wake');}
|
|
209
281
|
else if(state==='tug'&&tugProgress()>=1){comfort=null;enter('idle');drain();}
|
|
282
|
+
else if((state==='scramble'||state==='pickup')&&age>=comfort.seconds){comfort=null;movingScramble=null;stop();enter('idle');drain();}
|
|
210
283
|
else if(state==='nap'&&age>=5&&!stayDown){enter('wake');}
|
|
211
284
|
else if(state==='wake'){restProgress=Math.max(0,restProgress-prepareDt/1.2);if(restProgress===0){if(pendingAction)prepareDirection();else if(target)enter('rise');else enter('idle');}}
|
|
212
285
|
else if(state==='idle'&&route&&age>=idleHold)nextRoute();
|
|
@@ -217,7 +290,7 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
|
|
|
217
290
|
let pair=20,blend=0,kind=state,rearPair=null,approachPair=null;
|
|
218
291
|
let seat=state==='sit-down'||state==='rise'||seatProgress>0?Math.round(seatProgress*48)/48:null;
|
|
219
292
|
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;
|
|
293
|
+
let behavior=null,behaviorProgress=0,comfortPose=state==='scramble'?{clip:'scramble',view,progress:Math.min(1,age/comfort.seconds)}:state==='pickup'?{clip:'pickup',view,progress:Math.min(1,age/comfort.seconds)}:state==='tug'?{clip:'tug',progress:tugProgress()}:sleepLook&&restProgress>.72?{clip:'sleep',...sleepLook,blend:comfortEase((restProgress-.72)/.28)}:null;
|
|
221
294
|
if(activeAction?.comfort)kind=activeAction.comfort;
|
|
222
295
|
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;
|
|
223
296
|
if(manualDown&&state==='down'){actionName='lie-down';actionProgress=1;}
|
|
@@ -227,7 +300,11 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
|
|
|
227
300
|
// This clip is a calibrated standing half-turn, not walk-specific motion.
|
|
228
301
|
// The old turn's intro/outro added an unrelated squat to every reversal.
|
|
229
302
|
if(walkTransitions&&state==='turn'){actionName='walk-turn';actionProgress=Math.min(1,age/turnSeconds());}
|
|
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;}}
|
|
303
|
+
if(view==='approach'&&approachPlan&&(state==='run'||(state==='finish'&&completed))){const p=approachPlan.at(travelDistance);if(state==='finish'&&!travel?.attending){behavior='brake';behaviorProgress=1;}else if(p.clip!=='run'){behavior=p.clip;behaviorProgress=p.progress;}}
|
|
304
|
+
if(view==='approach'&&travel?.carry&&(state==='run'||state==='finish')){
|
|
305
|
+
const p=state==='finish'?{clip:'carry-settle',progress:1}:travel.carry.poseAt(travelDistance);
|
|
306
|
+
if(p.clip!=='run'){comfortPose=p;behavior=null;actionName=null;}
|
|
307
|
+
}
|
|
231
308
|
if(state==='arrival-exit'){behavior='finish';behaviorProgress=Math.min(1,age/.22);}
|
|
232
309
|
if(state==='watch'){behavior=`watch-${watchSide}`;behaviorProgress=paperWatchProgress(age);}
|
|
233
310
|
if(restProgress>0||state==='recline'||state==='wake'||state==='nap'){behavior='rest';behaviorProgress=restProgress;}
|
|
@@ -250,7 +327,8 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
|
|
|
250
327
|
blend=Math.round(blend*16)/16;
|
|
251
328
|
const key=`${state}/${actionName}/${view}/${turnView}/${poseFps}/${Math.floor(clock*poseFps)}`;
|
|
252
329
|
if(held?.key===key){pair=held.pair;blend=held.blend;rearPair=held.rearPair;approachPair=held.approachPair;seat=held.seat;behavior=held.behavior;behaviorProgress=held.behaviorProgress;actionName=held.actionName;actionProgress=held.actionProgress;comfortPose=held.comfortPose;}else held={key,pair,blend,rearPair,approachPair,seat,behavior,behaviorProgress,actionName,actionProgress,comfortPose,posePhase:phase};
|
|
253
|
-
|
|
330
|
+
const hopProgress=recovery&&movingScramble?Math.max(0,Math.min(1,(age-recovery.start)/(recovery.end-recovery.start))):0;
|
|
331
|
+
return{state:state==='run'||state==='finish'?'run':kind,pair,blend,rearPair,approachPair,seatProgress:seat,seatVariant,behavior,behaviorProgress,actionName,actionProgress,queued:!!deferred,view,straight,flip:state==='turn'&&!walkTransitions?false:flip,phase,posePhase:held.posePhase,stride:travel?.carry&&travelDistance<travel.carry.runDistance?PAPER_GAITS.approach.stride:travel?.stride??PAPER_GAITS.front.stride,heading:travel?.heading??0,speed:motionSpeed,motionStage,travelDistance,energy:director.report.energy,x:animal.position.x,z:animal.position.z,...(movingScramble?{bodyLift:recovery&&hopProgress>0&&hopProgress<1?recovery.height*Math.sin(Math.PI*hopProgress):0}:{}),...(comfortPose?{comfortPose:{...comfortPose}}:{})};
|
|
254
332
|
},get state(){return state;}
|
|
255
333
|
};
|
|
256
334
|
}
|
|
@@ -293,18 +371,23 @@ export function createPaperRenderer(cache,info,meta,width,height,{rear,approach,
|
|
|
293
371
|
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;
|
|
294
372
|
// Quantize source lookup only, never the projected rectangle. Rounding side,
|
|
295
373
|
// left and top first introduced whole-body scale/position jumps at thresholds.
|
|
296
|
-
const
|
|
374
|
+
const motion=isComfort?comfortMotion(state.comfortPose,comfort.info):null;
|
|
375
|
+
const left=width*x-sideX/2,floor=motion?(motion.floorOffsets?.[comfortMotionKey(state.comfortPose.progress,motion)]??motion.floorOffset??0):isInteraction||isAction||isBehavior||isSeat||isRear||isApproach?0:cleanGroundOffset(state,renderMeta),top=height*y-(300-floor)/320*side;
|
|
297
376
|
let feet=[];
|
|
298
|
-
if(contactEnabled&&
|
|
377
|
+
if(contactEnabled&&isComfort&&state.comfortPose.clip==='carry-trot'){
|
|
378
|
+
const k=comfortMotionKey(state.comfortPose.progress,motion);
|
|
379
|
+
feet=motion.feet[k].map(f=>({...f,episode:`carry/${state.comfortPose.cycle}/${f.id}`}));
|
|
380
|
+
if(state.flip)feet=feet.map(p=>({...p,x:319-p.x}));
|
|
381
|
+
}else if(contactEnabled&&isAction&&state.actionName==='walk'){
|
|
299
382
|
const p=paperActionPose('walk',state.actionProgress,actions.info);feet=walkEpisodes(actions.info.feet[p.pair][Math.round(p.blend*8)],p.pair,p.blend,state.actionProgress);
|
|
300
383
|
if(state.flip)feet=feet.map(p=>({...p,x:319-p.x}));
|
|
301
|
-
}else if(contactEnabled&&rear&&state.state==='run'&&!isAction){
|
|
384
|
+
}else if(contactEnabled&&rear&&state.state==='run'&&!isAction&&!isComfort){
|
|
302
385
|
const b=isBehavior?paperBehaviorPose(state,behaviors.info):null;
|
|
303
386
|
feet=isBehavior?behaviors.info.feet[b.pair][Math.round(b.blend*8)]:isApproach?approach.info.feet[state.approachPair][Math.round(state.blend*8)]:isRear?rear.info.rearFeet[state.rearPair][Math.round(state.blend*8)]:rear.info.frontFeet[state.pair]?.[Math.round(state.blend*16)]??[];
|
|
304
387
|
feet=isBehavior?behaviorEpisodes[state.behavior](feet,b.pair,b.blend,state.behaviorProgress):isApproach?sourceEpisodes.approach(feet,state.approachPair,state.blend,state.posePhase??state.phase??0):isRear?sourceEpisodes.rear(feet,state.rearPair,state.blend,state.posePhase??state.phase??0):sourceEpisodes.front(feet,state.pair,state.blend,state.posePhase??state.phase??0);
|
|
305
388
|
if(state.flip)feet=feet.map(p=>({...p,x:319-p.x}));
|
|
306
389
|
}
|
|
307
|
-
const pins=lock.update(feet,{left,top,side,sideX,groundFromPixel,pixelFromGround,root:{x:state.x,z:state.z,heading:state.heading??0}},`${state.view??'front'}/${state.flip}/${state.state==='run'}/${state.actionName??state.behavior??'gallop'}`);render.lastPins=pins;
|
|
390
|
+
const pins=lock.update(feet,{left,top,side,sideX,groundFromPixel,pixelFromGround,root:{x:state.x,z:state.z,heading:state.heading??0}},`${state.view??'front'}/${state.flip}/${state.state==='run'}/${state.comfortPose?.clip??state.actionName??state.behavior??'gallop'}`);render.lastPins=pins;
|
|
308
391
|
const transport=pawTransport?createPawTransport(feet,pins,{opaque:(x,y)=>{
|
|
309
392
|
const rawX=x+originX,rawY=y+originY;if(rawX<0||rawX>=sourceN||rawY<0||rawY>=sourceN)return false;
|
|
310
393
|
let sx=Math.floor(rawX);if(state.flip)sx=sourceN-1-sx;
|
|
@@ -319,6 +402,10 @@ export function createPaperRenderer(cache,info,meta,width,height,{rear,approach,
|
|
|
319
402
|
const mouthInfo={behaviors:behaviors?.info,seat:seat?.info,arrivalSeat:arrivalSeat?.info,approach:approach?.info}[mouthFamily];
|
|
320
403
|
const mouthFrame=mouthFamily?mouthSourceFrame(mouthFamily,state,mouthInfo):null;
|
|
321
404
|
render.lastMouth=projectMouth(mouthFamily?mouthAnchor(mouthFamily,mouthFrame,mouthInfo?.sha256):null,{left,top,side,sideX},state.flip,{family:mouthFamily,frame:mouthFrame});
|
|
405
|
+
if(motion?.mouth){
|
|
406
|
+
const k=comfortMotionKey(state.comfortPose.progress,motion),p=motion.mouth[k];
|
|
407
|
+
render.lastMouth=projectMouth({x:p[0],y:p[1],scale:1,score:null},{left,top,side,sideX},state.flip,{family:'comfort',frame:motion.frames[k]});
|
|
408
|
+
}
|
|
322
409
|
const drawTop=top-originY/320*side,bottom=top+Math.max(sourceN-originY,...pins.map(p=>p.target.y+20))/320*side;
|
|
323
410
|
const drawLeft=left+Math.min(-originX,...pins.map(p=>p.target.x-20))/320*sideX,drawRight=left+Math.max(sourceN-originX,...pins.map(p=>p.target.x+20))/320*sideX;
|
|
324
411
|
for(let dy=Math.max(0,minify?Math.floor(drawTop):Math.ceil(drawTop-.5));dy<Math.min(height,minify?Math.ceil(bottom):Math.ceil(bottom-.5));dy++)for(let dx=Math.max(0,minify?Math.floor(drawLeft):Math.ceil(drawLeft-.5));dx<Math.min(width,minify?Math.ceil(drawRight):Math.ceil(drawRight-.5));dx++){
|
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 [--ink cream|gold|dark]
|
|
16
|
+
retrieviewer desktop [--ink cream|gold|dark] [--return-style auto|brake|trot|run-trot]
|
|
17
17
|
retrieviewer desktop-status [--json]
|
|
18
18
|
retrieviewer desktop-stop
|
|
19
19
|
retrieviewer desktop-clean --yes
|
|
@@ -27,12 +27,13 @@ Desktop is a separate native overlay, NOT a Codex plugin. Choose a window once;
|
|
|
27
27
|
use Codex normally. Exit from its tray icon or desktop-stop. No auto-start is added.
|
|
28
28
|
Windows is verified; macOS and Linux X11 adapters are experimental. Native Wayland
|
|
29
29
|
app tracking is not supported. Linux targets AlmaLinux8/GLIBC2.28, system Python3.6+.
|
|
30
|
+
Return style defaults to auto (mix all three). Restart the companion to change it.
|
|
30
31
|
`;
|
|
31
32
|
export function parseArguments(args){
|
|
32
33
|
const [command='help',...rest]=args,options={};
|
|
33
34
|
for(let i=0;i<rest.length;i++){
|
|
34
35
|
const token=rest[i];
|
|
35
|
-
if(token==='--agent'||token==='--ink'){
|
|
36
|
+
if(token==='--agent'||token==='--ink'||token==='--return-style'){
|
|
36
37
|
const key=token.slice(2);
|
|
37
38
|
if(options[key]||!rest[i+1]||rest[i+1].startsWith('-'))throw Error(token+' requires one value');
|
|
38
39
|
options[key]=rest[++i];
|
|
@@ -41,8 +42,9 @@ export function parseArguments(args){
|
|
|
41
42
|
}
|
|
42
43
|
if(options.agent&&!AGENTS.some(a=>a.id===options.agent))throw Error('Unknown agent: '+options.agent);
|
|
43
44
|
if(options.ink&&!['cream','gold','dark'].includes(options.ink))throw Error('--ink must be cream, gold or dark');
|
|
45
|
+
if(options['return-style']&&!['auto','brake','trot','run-trot'].includes(options['return-style']))throw Error('--return-style must be auto, brake, trot or run-trot');
|
|
44
46
|
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);
|
|
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']};
|
|
47
|
+
const allowed={desktop:['ink','return-style'],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']};
|
|
46
48
|
for(const key of Object.keys(options))if(!(allowed[command]??[]).includes(key))throw Error(`--${key} is not supported by ${command}`);
|
|
47
49
|
return {command,options};
|
|
48
50
|
}
|
|
@@ -53,7 +55,7 @@ export async function runCli(args,{input=process.stdin,output=process.stdout,err
|
|
|
53
55
|
if(['desktop-clean','desktop-remove'].includes(command)&&!options.yes)throw Error('Use '+command+' --yes to remove only the companion cache.');
|
|
54
56
|
const api=desktop??await import('./integrations/windows-overlay/desktop.mjs');
|
|
55
57
|
const action={'desktop':'startDesktop','desktop-status':'desktopStatus','desktop-stop':'stopDesktop','desktop-clean':'cleanDesktop','desktop-remove':'removeDesktop'}[command];
|
|
56
|
-
emit(await api[action](options.ink?{ink:options.ink}:{}));return 0;
|
|
58
|
+
emit(await api[action]({...options.ink?{ink:options.ink}:{},...options['return-style']?{returnStyle:options['return-style']}:{} }));return 0;
|
|
57
59
|
}
|
|
58
60
|
if(command==='preview'){await import('./examples/live.mjs');return 0;}
|
|
59
61
|
if(['status','doctor','agents'].includes(command)){
|
package/retrieviewer.d.mts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/** A headless pet: the host owns input, time and terminal rendering. */
|
|
2
|
+
export type FetchReturnStyle = 'auto' | 'brake' | 'trot' | 'run-trot';
|
|
2
3
|
export interface PetOptions {
|
|
3
4
|
/** Asset names are relative; return original file bytes (keep .gz files compressed). */
|
|
4
5
|
loadAsset?: (name: string) => Promise<ArrayBuffer | ArrayBufferView>;
|
|
5
6
|
seed?: number;
|
|
6
7
|
energy?: number;
|
|
8
|
+
/** Default auto mixes all three existing return styles without adjacent repeats. */
|
|
9
|
+
returnStyle?: FetchReturnStyle;
|
|
7
10
|
/** Keep true for the current paw correction; false selects the legacy comparison. */
|
|
8
11
|
pawTransport?: boolean;
|
|
9
12
|
/** Default q5 (32 levels). Original requires separately supplied original assets. */
|
|
@@ -32,6 +35,8 @@ export interface PetState {
|
|
|
32
35
|
seatProgress: number | null;
|
|
33
36
|
behavior: string | null;
|
|
34
37
|
actionName: string | null;
|
|
38
|
+
/** Physical vertical lift during an optional authored recovery bound. */
|
|
39
|
+
bodyLift?: number;
|
|
35
40
|
[key: string]: unknown;
|
|
36
41
|
}
|
|
37
42
|
export interface LifeReport {
|
|
@@ -47,7 +52,12 @@ export interface LifeReport {
|
|
|
47
52
|
decisions: number;
|
|
48
53
|
cooldowns: {ball: number; treat: number; tug: number};
|
|
49
54
|
gesture: {clip: string; progress: number} | null;
|
|
50
|
-
ball: {x: number; z: number; height: number; carried: boolean} | null;
|
|
55
|
+
ball: {x: number; z: number; height: number; carried: boolean; spin?: number; speed?: number; phase?: 'flight' | 'roll' | 'rest'; settled?: boolean} | null;
|
|
56
|
+
/** Local deterministic fetch variation; null outside a ball interaction. */
|
|
57
|
+
fetchPattern: 'clean' | 'rolling' | 'miss' | 'tumble' | null;
|
|
58
|
+
/** Ball-carry gait, selected independently of the fetch/roaming random stream. */
|
|
59
|
+
returnStyle: FetchReturnStyle;
|
|
60
|
+
returnMode: Exclude<FetchReturnStyle, 'auto'> | null;
|
|
51
61
|
/** Rare autonomous visitor; absent during direct user interactions. */
|
|
52
62
|
butterfly: {x: number; z: number; height: number; phase: number; opacity: number} | null;
|
|
53
63
|
treat: {x: number; z: number; height: number} | null;
|
|
@@ -79,8 +89,12 @@ export interface Pet {
|
|
|
79
89
|
/** Seconds. Fixed 60 Hz simulation, at most .1 s catch-up per call. */
|
|
80
90
|
step(dt: number): number;
|
|
81
91
|
frame(options: FrameOptions): PetFrame;
|
|
92
|
+
/** Conservative pre-render check for held poses. Simulation still needs step(). */
|
|
93
|
+
needsFrame(options: FrameOptions): boolean;
|
|
82
94
|
/** False when busy or cooling down. */
|
|
83
95
|
interact(kind: 'ball' | 'treat' | 'tug'): boolean;
|
|
96
|
+
/** Sets the next throw's return style. Does not interrupt a fetch in progress. */
|
|
97
|
+
setReturnStyle(style: FetchReturnStyle): FetchReturnStyle;
|
|
84
98
|
readonly disposed: boolean;
|
|
85
99
|
dispose(): void;
|
|
86
100
|
}
|
package/retrieviewer.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import {createPaperActor,createPaperRenderer,createPaperComfortTimings} from './
|
|
|
2
2
|
import {createPaperProjection,createPaperBody} from './puppy-paper-world.mjs';
|
|
3
3
|
import {rgbaToBraille} from './braille.mjs';
|
|
4
4
|
import {createSnappyTrickTimings} from './puppy-paper-trick-timing.mjs';
|
|
5
|
-
import {createCompanionLife} from './puppy-companion-life.mjs';
|
|
5
|
+
import {createCompanionLife,FETCH_RETURN_STYLES} from './puppy-companion-life.mjs';
|
|
6
6
|
import {paintCompanionObjects} from './puppy-companion-objects.mjs';
|
|
7
7
|
import {readPackedFrames} from './puppy-frame-store.mjs';
|
|
8
8
|
|
|
@@ -59,13 +59,29 @@ async function gunzip(bytes){
|
|
|
59
59
|
// Only immutable art/metadata is shared. Each pet still owns its animal, actor,
|
|
60
60
|
// life, renderer and output. Last dispose drops the shared lease, not other pets.
|
|
61
61
|
const sharedAssets=new Map();
|
|
62
|
+
// Conservative pre-render invalidation: only known held source poses qualify.
|
|
63
|
+
// Moving poses, gestures, sleep cues and visitors always take the normal path.
|
|
64
|
+
// One small key, no second pixel buffer or animation cache. Needs/cooldowns are
|
|
65
|
+
// intentionally absent: they still advance, but do not draw anything themselves.
|
|
66
|
+
function heldFrameKey(state,report,{columns,rows,cellAspect=.5,camera,poseFps=24,contactEnabled=true}={}){
|
|
67
|
+
const seated=state.state==='idle'&&state.seatProgress===1&&!state.behavior&&!state.actionName&&!state.comfortPose;
|
|
68
|
+
const down=state.state==='down'&&(state.actionName==='lie-down'&&state.actionProgress===1||state.behavior==='rest');
|
|
69
|
+
if((!seated&&!down)||report.gesture||report.butterfly||report.snore!==null&&report.snore!==undefined)return null;
|
|
70
|
+
return JSON.stringify([columns,rows,cellAspect,poseFps,!!contactEnabled,camera?.viewportWidth,camera?.viewportHeight,
|
|
71
|
+
camera?.rect?.left,camera?.rect?.top,camera?.rect?.width,camera?.rect?.height,
|
|
72
|
+
state.state,state.x,state.z,state.bodyLift??0,state.view,state.flip,state.seatProgress,state.seatVariant,
|
|
73
|
+
state.actionName,down?state.actionProgress:0,state.behavior,state.behaviorProgress,
|
|
74
|
+
report.ball,report.treat]);
|
|
75
|
+
}
|
|
62
76
|
async function loadPetAssets(loadAsset,artProfile){
|
|
63
77
|
// Node keeps filtered crops compressed; only the existing two-frame LRU is
|
|
64
78
|
// expanded. Browser bundles retain the portable bit-packed implementation.
|
|
65
79
|
let codec;
|
|
66
80
|
if(typeof process!=='undefined'&&process.versions?.node){
|
|
67
81
|
const {deflateRawSync,inflateRawSync}=await import('node:zlib');
|
|
68
|
-
|
|
82
|
+
// Level 3 trims retained crop payload without the multi-second startup cost
|
|
83
|
+
// of levels 6..9. Pixels, synchronous frame access and the LRU stay identical.
|
|
84
|
+
codec={compress:bytes=>deflateRawSync(bytes,{level:3}),expand:(bytes,size)=>inflateRawSync(bytes,{maxOutputLength:size})};
|
|
69
85
|
}
|
|
70
86
|
const load=async name=>bytesOf(name,await loadAsset(name));
|
|
71
87
|
const json=async name=>JSON.parse(new TextDecoder().decode(await load(name)));
|
|
@@ -94,13 +110,14 @@ async function acquireAssets(loadAsset,artProfile){
|
|
|
94
110
|
try{return{data:await entry.promise,release};}catch(error){release();throw error;}
|
|
95
111
|
}
|
|
96
112
|
|
|
97
|
-
export async function createPet({loadAsset=defaultLoadAsset,seed,energy,pawTransport=true,artProfile=DEFAULT_ART_PROFILE}={}){
|
|
113
|
+
export async function createPet({loadAsset=defaultLoadAsset,seed,energy,pawTransport=true,artProfile=DEFAULT_ART_PROFILE,returnStyle='auto'}={}){
|
|
98
114
|
if(typeof loadAsset!=='function')throw TypeError('loadAsset must be a function');
|
|
99
115
|
if(seed===undefined)seed=globalThis.crypto?.getRandomValues?globalThis.crypto.getRandomValues(new Uint32Array(1))[0]:731;
|
|
100
116
|
if(!Number.isInteger(seed)||seed<0||seed>0xffffffff)throw RangeError('seed must be a uint32');
|
|
101
117
|
if(energy!==undefined&&(!Number.isFinite(energy)||energy<0||energy>1))throw RangeError('energy must be within 0..1');
|
|
102
118
|
if(typeof pawTransport!=='boolean')throw TypeError('pawTransport must be boolean');
|
|
103
119
|
if(!ART_PROFILES.includes(artProfile))throw RangeError('artProfile must be original or q5');
|
|
120
|
+
if(!FETCH_RETURN_STYLES.includes(returnStyle))throw RangeError('returnStyle must be auto, brake, trot or run-trot');
|
|
104
121
|
let lease=await acquireAssets(loadAsset,artProfile),data=lease.data;
|
|
105
122
|
const release=lease.release;lease=null;
|
|
106
123
|
let {cache,info,meta}=data;const assets={...data.assets};data=null;
|
|
@@ -108,10 +125,10 @@ export async function createPet({loadAsset=defaultLoadAsset,seed,energy,pawTrans
|
|
|
108
125
|
try{
|
|
109
126
|
if(!assets.actions.info.clips?.['lie-down']||!assets.actions.info.clips?.['lie-up'])throw Error('v9 action cache lacks lie-down/lie-up clips');
|
|
110
127
|
animal=createPaperBody();
|
|
111
|
-
actor=createPaperActor(animal,info,{manualTricks:true,trickTimings:createSnappyTrickTimings(assets.actions.info),comfortTimings:createPaperComfortTimings(assets.actions.info),walkTransitions:true});
|
|
112
|
-
life=createCompanionLife(actor,{seed,...energy===undefined?{}:{energy}});
|
|
128
|
+
actor=createPaperActor(animal,info,{manualTricks:true,trickTimings:createSnappyTrickTimings(assets.actions.info),comfortTimings:createPaperComfortTimings(assets.actions.info),walkTransitions:true,scrambleSeconds:assets.comfort.info.scramble?.duration??0,fetchMotions:assets.comfort.info.version>=4?assets.comfort.info:null});
|
|
129
|
+
life=createCompanionLife(actor,{seed,returnStyle,...energy===undefined?{}:{energy}});
|
|
113
130
|
}catch(e){animal?.dispose?.();release();throw e;}
|
|
114
|
-
let paint=null,paintKey='',time=0,acc=0,disposed=false;
|
|
131
|
+
let paint=null,paintKey='',time=0,acc=0,disposed=false,lastHeldFrameKey=null;
|
|
115
132
|
const alive=()=>{if(disposed)throw Error('Pet is disposed');};
|
|
116
133
|
return {
|
|
117
134
|
// Fixed 1/60 s simulation; host deltas are clamped to 0.1 s so a stalled host
|
|
@@ -122,6 +139,9 @@ export async function createPet({loadAsset=defaultLoadAsset,seed,energy,pawTrans
|
|
|
122
139
|
while(acc>=STEP_SECONDS-1e-9){actor.update(STEP_SECONDS);life.update(STEP_SECONDS);time+=STEP_SECONDS;acc=Math.max(0,acc-STEP_SECONDS);n++;}
|
|
123
140
|
return n;
|
|
124
141
|
},
|
|
142
|
+
// Optional host optimization. False only when a known held pose and all
|
|
143
|
+
// visible objects/settings match the last frame; frame() itself never skips.
|
|
144
|
+
needsFrame(options){alive();const key=heldFrameKey(actor.sample(time,{poseFps:options?.poseFps??24}),life.report,options);return key===null||key!==lastHeldFrameKey;},
|
|
125
145
|
// Plain Braille text sized exactly columns x rows (2x4 dots per cell). `pixels`
|
|
126
146
|
// is the renderer's buffer, valid only until the next frame()/dispose().
|
|
127
147
|
frame({columns,rows,cellAspect=.5,camera,poseFps=24,contactEnabled=true}={}){
|
|
@@ -135,18 +155,21 @@ export async function createPet({loadAsset=defaultLoadAsset,seed,energy,pawTrans
|
|
|
135
155
|
const projection=createPaperProjection({viewportWidth,viewportHeight,rect:camera?.rect??{left:0,top:0,width:viewportWidth,height:viewportHeight},width,height});
|
|
136
156
|
const state=actor.sample(time,{poseFps}),report=life.report;
|
|
137
157
|
if(report.gesture)state.interactionPose=report.gesture;
|
|
138
|
-
const placement=projection.placement(state.x,state.z),pixels=paint(state,{...placement,contactEnabled:!!contactEnabled});
|
|
158
|
+
const placement=projection.placement(state.x,state.z,state.bodyLift??0),pixels=paint(state,{...placement,contactEnabled:!!contactEnabled});
|
|
139
159
|
paintCompanionObjects(pixels,width,height,projection,report,state,paint.lastMouth);
|
|
140
160
|
const {text}=rgbaToBraille(pixels,width,height,{method:'stable',brightness:1.18,contrast:1.1});
|
|
161
|
+
lastHeldFrameKey=heldFrameKey(state,report,{columns,rows,cellAspect,camera,poseFps,contactEnabled});
|
|
141
162
|
return {text,columns,rows,pixels,state,report,placement,projection,artProfile,mouth:paint.lastMouth,contacts:paint.report()};
|
|
142
163
|
},
|
|
143
164
|
// 'ball' | 'treat' | 'tug'. False when busy or cooling down; unknown kinds throw.
|
|
144
165
|
interact(kind){alive();if(!INTERACTIONS.includes(kind))throw TypeError(`Unknown interaction: ${kind}`);return life.interact(kind);},
|
|
166
|
+
// Applies to the next ball throw; never rewrites a fetch already in motion.
|
|
167
|
+
setReturnStyle(style){alive();return life.setReturnStyle(style);},
|
|
145
168
|
get disposed(){return disposed;},
|
|
146
169
|
dispose(){
|
|
147
170
|
if(disposed)return;disposed=true;
|
|
148
171
|
try{animal.dispose?.();}finally{
|
|
149
|
-
animal=actor=life=paint=cache=info=meta=null;paintKey='';
|
|
172
|
+
animal=actor=life=paint=cache=info=meta=null;paintKey='';lastHeldFrameKey=null;
|
|
150
173
|
for(const k of BUNDLES)assets[k]=null;
|
|
151
174
|
release();
|
|
152
175
|
}
|