@bitefoundry/retrieviewer 0.0.8 → 0.0.10
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 +36 -0
- package/PUPPY-MEMORY.md +74 -0
- package/README.ko.md +7 -0
- package/README.md +7 -0
- package/RELEASING.md +32 -11
- package/WATCH-REDRAW.md +175 -0
- package/braille.mjs +4 -2
- package/integrations/windows-overlay/desktop.mjs +3 -2
- package/integrations/windows-overlay/worker.mjs +4 -3
- package/package.json +3 -1
- package/pet-q5-manifest.json +23 -21
- 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 +98 -30
- 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 +91 -23
- package/retrieviewer-cli.mjs +6 -4
- package/retrieviewer.d.mts +13 -1
- package/retrieviewer.mjs +11 -6
|
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})};
|
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,paperWalkTarget,planPaperApproach,planPaperApproachTravel,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;}
|
|
@@ -88,9 +98,24 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
|
|
|
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)=>{
|
|
90
100
|
if(movementMode==='walk'&&!skipEntry){playAction('walk-enter',()=>issue(true));return;}
|
|
91
|
-
|
|
101
|
+
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
102
|
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
103
|
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');
|
|
104
|
+
if(['trot','run-trot'].includes(carryMode)&&fetchMotions?.carryTrot&&view==='approach'&&distance>0){
|
|
105
|
+
const carry=planPaperCarry(distance,{mixed:carryMode==='run-trot',settleSeconds:fetchMotions.carryTrot.settle.duration});
|
|
106
|
+
Object.assign(travel,{plan:carry,carry,stride:carry.stride});approachPlan=null;
|
|
107
|
+
}
|
|
108
|
+
if(scrambleArmed&&fetchMotions?.scramble.trajectory==='single-pass-target'&&view===fetchMotions.scramble.entryView&&!waypoints.length){
|
|
109
|
+
const stumble=planPaperStumble(distance,fetchMotions.scramble);
|
|
110
|
+
Object.assign(travel,{plan:stumble.plan,stride:stumble.stride,stumble,scrambleAt:stumble.entryTime});
|
|
111
|
+
}else if(scrambleArmed&&fetchMotions&&view==='front'&&!waypoints.length&&fetchMotions.scramble.trajectory!=='single-pass-target'){
|
|
112
|
+
// Last matching forepaw contact before the endpoint. Invert the existing
|
|
113
|
+
// distance profile once, not on every frame. Source frame pair5/blend0 is
|
|
114
|
+
// shared EXACTLY with the new motion's first key.
|
|
115
|
+
const contact=fetchMotions.scramble.entryPhase,cycle=Math.max(0,Math.floor(distance/travel.stride-contact-.15)),d=(cycle+contact)*travel.stride;
|
|
116
|
+
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;}
|
|
117
|
+
travel.scrambleAt=(lo+hi)/2;
|
|
118
|
+
}
|
|
94
119
|
};
|
|
95
120
|
const prepareAction=()=>{
|
|
96
121
|
const name=pendingAction,desired=name==='spin'?'front':'approach';
|
|
@@ -116,8 +141,8 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
|
|
|
116
141
|
playAction(name,()=>finishTrick(name));
|
|
117
142
|
};
|
|
118
143
|
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;
|
|
144
|
+
const requestAction=(name,{urgent=false}={})=>{
|
|
145
|
+
preparationRate=urgent?3:1;
|
|
121
146
|
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
147
|
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
148
|
if(name==='belly'&&state==='down'&&(manualDown||restProgress===.25)){route=false;target=null;waypoints=[];pendingAction=null;startBelly(comfortSeconds);return true;}
|
|
@@ -132,9 +157,11 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
|
|
|
132
157
|
return true;
|
|
133
158
|
};
|
|
134
159
|
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}={}){
|
|
160
|
+
function go(x,z,{direct=false,walking=false,urgent=false,scramble=false,returnMode=null}={}){
|
|
136
161
|
if(![x,z].every(Number.isFinite))throw Error('Invalid paper target');
|
|
137
162
|
preparationRate=urgent?3:1;
|
|
163
|
+
carryMode=returnMode;
|
|
164
|
+
scrambleArmed=!!(scramble&&fetchMotions);movingScramble=null;
|
|
138
165
|
target=paperGroundTarget(x,z);
|
|
139
166
|
pendingAction=null;stayDown=false;comfort=null;movementMode=walking?'walk':'run';
|
|
140
167
|
const dz=target[1]-animal.position.z,dx=target[0]-animal.position.x;
|
|
@@ -156,12 +183,12 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
|
|
|
156
183
|
}
|
|
157
184
|
return{
|
|
158
185
|
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});});},
|
|
186
|
+
// Eager attention changes preparation/recovery cadence only. Existing dispatch
|
|
187
|
+
// keeps actions/walks at safe endpoints; root physics and gait are unchanged.
|
|
188
|
+
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
189
|
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
190
|
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});},
|
|
191
|
+
sit({urgent=false}={}){return dispatch(()=>requestAction('sit',{urgent}),{urgent});},spin(){return dispatch(()=>requestAction('spin'));},roll(){return dispatch(()=>requestAction('roll'),{keepDown:true});},
|
|
165
192
|
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
193
|
look(side='left'){if(state!=='idle'||seatProgress!==1)return false;route=false;watchSide=side==='right'?'right':'left';enter('watch');return true;},
|
|
167
194
|
lie(){const name=manualDown||activeAction?.name==='lie-down'||restProgress>0?'sit':'lie';return dispatch(()=>requestAction(name));},
|
|
@@ -169,24 +196,50 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
|
|
|
169
196
|
belly(seconds=6){comfortDuration('belly',seconds);return dispatch(()=>{comfortSeconds=seconds;return requestAction('belly');},{keepDown:true});},
|
|
170
197
|
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
198
|
tug(seconds=6){comfortDuration('tug',seconds);return dispatch(()=>{comfortSeconds=seconds;return requestAction('tug');});},
|
|
199
|
+
get canScramble(){return scrambleSeconds>0;},
|
|
200
|
+
get canMovingScramble(){return !!fetchMotions;},
|
|
201
|
+
get canSinglePassScramble(){return fetchMotions?.scramble.trajectory==='single-pass-target';},
|
|
202
|
+
get canPickup(){return !!fetchMotions;},
|
|
203
|
+
get canCarryTrot(){return !!fetchMotions?.carryTrot;},
|
|
204
|
+
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;},
|
|
205
|
+
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
206
|
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
207
|
update(dt){
|
|
174
208
|
if(!Number.isFinite(dt)||dt<0||dt>.1)throw Error('Invalid paper step');
|
|
175
209
|
const beforeX=animal.position.x,beforeZ=animal.position.z;
|
|
210
|
+
let enteredScramble=false;
|
|
176
211
|
if(state==='run'&&travel&&target){
|
|
177
212
|
travelAge+=dt;const p=travel.plan.at(travelAge),ratio=travel.plan.distance?p.distance/travel.plan.distance:1;
|
|
178
213
|
travelDistance=p.distance;motionSpeed=p.speed;motionStage=p.stage;
|
|
179
214
|
const gain=travel.plan.distance?p.speed/travel.plan.distance:0;
|
|
180
215
|
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
216
|
arrived=p.done;
|
|
217
|
+
if(scrambleArmed&&travel.scrambleAt!==undefined&&travelAge>=travel.scrambleAt){
|
|
218
|
+
const entry=travel.plan.at(travel.scrambleAt),ratio=entry.distance/travel.plan.distance,n=travel.plan.distance;
|
|
219
|
+
const x=travel.x+travel.dx*ratio,z=travel.z+travel.dz*ratio,ux=travel.dx/n,uz=travel.dz/n;
|
|
220
|
+
const end=paperGroundTarget(x+ux*entry.speed*fetchMotions.scramble.brakeSeconds/2,z+uz*entry.speed*fetchMotions.scramble.brakeSeconds/2);
|
|
221
|
+
const distance=Math.hypot(end[0]-x,end[1]-z),brake=entry.speed?2*distance/entry.speed:0;
|
|
222
|
+
const fullEnd=paperGroundTarget(end[0]+ux*(recovery?.distance??0),end[1]+uz*(recovery?.distance??0));
|
|
223
|
+
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};
|
|
224
|
+
scrambleArmed=false;target=null;waypoints=[];comfort={kind:'scramble',seconds:scrambleSeconds};enter('scramble');age=travelAge-travel.scrambleAt;enteredScramble=true;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if(state==='scramble'&&movingScramble){
|
|
228
|
+
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;
|
|
229
|
+
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;
|
|
230
|
+
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);
|
|
231
|
+
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
232
|
}
|
|
183
233
|
animal.update(dt);
|
|
184
234
|
const prepareDt=dt*preparationRate;
|
|
185
|
-
|
|
235
|
+
// Urgent fetches accelerate only authored preparation/recovery, never root
|
|
236
|
+
// travel or its distance-driven stride. Deferred tricks reach their shared
|
|
237
|
+
// endpoint promptly instead of ignoring a thrown ball for several seconds.
|
|
238
|
+
if(!enteredScramble)age+=['view-turn','turn','rise','watch','arrival-exit','launch','wake','finish','settle'].includes(state)||(state==='action'&&deferred)?prepareDt:dt;
|
|
186
239
|
const distance=Math.hypot(animal.position.x-beforeX,animal.position.z-beforeZ);
|
|
187
240
|
director.update(dt,state==='run',distance);
|
|
188
241
|
if(state==='run'){
|
|
189
|
-
phase+=distance/travel.stride;
|
|
242
|
+
phase+=distance/(travel.carry&&travelDistance<travel.carry.runDistance?PAPER_GAITS.approach.stride:travel.stride);
|
|
190
243
|
if(target&&arrived){completed=true;stop();target=null;if(movementMode==='walk')playAction('walk-exit',()=>{movementMode='run';enter(walkTransitions?'idle':'settle');drain();});else enter('finish');}
|
|
191
244
|
}else if(state==='action'&&age>=activeAction.duration){
|
|
192
245
|
const done=actionDone;activeAction=null;actionDone=null;done();
|
|
@@ -200,13 +253,14 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
|
|
|
200
253
|
}else if(pendingAction)prepareDirection();else settle();
|
|
201
254
|
}else if(state==='view-turn'&&age>=viewTurnSeconds()){view=turnView;prepareDirection();}
|
|
202
255
|
else if(state==='turn'&&age>=turnSeconds()){flip=turnFlip;prepareDirection();}
|
|
203
|
-
else if(state==='sit-down'){seatProgress=Math.min(1,seatProgress+
|
|
256
|
+
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
257
|
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
258
|
else if(state==='arrival-exit'&&age>=.22){seatVariant='default';prepareDirection();}
|
|
206
259
|
else if(state==='watch'&&age>=PAPER_WATCH_TIMING.total){if(pendingAction)prepareDirection();else if(target)enter('rise');else enter('idle');}
|
|
207
260
|
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
261
|
else if(state==='sleep'&&age>=comfort.seconds){comfort=null;enter('wake');}
|
|
209
262
|
else if(state==='tug'&&tugProgress()>=1){comfort=null;enter('idle');drain();}
|
|
263
|
+
else if((state==='scramble'||state==='pickup')&&age>=comfort.seconds){comfort=null;movingScramble=null;stop();enter('idle');drain();}
|
|
210
264
|
else if(state==='nap'&&age>=5&&!stayDown){enter('wake');}
|
|
211
265
|
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
266
|
else if(state==='idle'&&route&&age>=idleHold)nextRoute();
|
|
@@ -217,7 +271,7 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
|
|
|
217
271
|
let pair=20,blend=0,kind=state,rearPair=null,approachPair=null;
|
|
218
272
|
let seat=state==='sit-down'||state==='rise'||seatProgress>0?Math.round(seatProgress*48)/48:null;
|
|
219
273
|
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;
|
|
274
|
+
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
275
|
if(activeAction?.comfort)kind=activeAction.comfort;
|
|
222
276
|
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
277
|
if(manualDown&&state==='down'){actionName='lie-down';actionProgress=1;}
|
|
@@ -228,6 +282,10 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
|
|
|
228
282
|
// The old turn's intro/outro added an unrelated squat to every reversal.
|
|
229
283
|
if(walkTransitions&&state==='turn'){actionName='walk-turn';actionProgress=Math.min(1,age/turnSeconds());}
|
|
230
284
|
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;}}
|
|
285
|
+
if(view==='approach'&&travel?.carry&&(state==='run'||state==='finish')){
|
|
286
|
+
const p=state==='finish'?{clip:'carry-settle',progress:1}:travel.carry.poseAt(travelDistance);
|
|
287
|
+
if(p.clip!=='run'){comfortPose=p;behavior=null;actionName=null;}
|
|
288
|
+
}
|
|
231
289
|
if(state==='arrival-exit'){behavior='finish';behaviorProgress=Math.min(1,age/.22);}
|
|
232
290
|
if(state==='watch'){behavior=`watch-${watchSide}`;behaviorProgress=paperWatchProgress(age);}
|
|
233
291
|
if(restProgress>0||state==='recline'||state==='wake'||state==='nap'){behavior='rest';behaviorProgress=restProgress;}
|
|
@@ -250,7 +308,8 @@ export function createPaperActor(animal,info,{manualTricks=false,trickTimings=nu
|
|
|
250
308
|
blend=Math.round(blend*16)/16;
|
|
251
309
|
const key=`${state}/${actionName}/${view}/${turnView}/${poseFps}/${Math.floor(clock*poseFps)}`;
|
|
252
310
|
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
|
-
|
|
311
|
+
const hopProgress=recovery&&movingScramble?Math.max(0,Math.min(1,(age-recovery.start)/(recovery.end-recovery.start))):0;
|
|
312
|
+
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
313
|
},get state(){return state;}
|
|
255
314
|
};
|
|
256
315
|
}
|
|
@@ -293,18 +352,23 @@ export function createPaperRenderer(cache,info,meta,width,height,{rear,approach,
|
|
|
293
352
|
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
353
|
// Quantize source lookup only, never the projected rectangle. Rounding side,
|
|
295
354
|
// left and top first introduced whole-body scale/position jumps at thresholds.
|
|
296
|
-
const
|
|
355
|
+
const motion=isComfort?comfortMotion(state.comfortPose,comfort.info):null;
|
|
356
|
+
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
357
|
let feet=[];
|
|
298
|
-
if(contactEnabled&&
|
|
358
|
+
if(contactEnabled&&isComfort&&state.comfortPose.clip==='carry-trot'){
|
|
359
|
+
const k=comfortMotionKey(state.comfortPose.progress,motion);
|
|
360
|
+
feet=motion.feet[k].map(f=>({...f,episode:`carry/${state.comfortPose.cycle}/${f.id}`}));
|
|
361
|
+
if(state.flip)feet=feet.map(p=>({...p,x:319-p.x}));
|
|
362
|
+
}else if(contactEnabled&&isAction&&state.actionName==='walk'){
|
|
299
363
|
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
364
|
if(state.flip)feet=feet.map(p=>({...p,x:319-p.x}));
|
|
301
|
-
}else if(contactEnabled&&rear&&state.state==='run'&&!isAction){
|
|
365
|
+
}else if(contactEnabled&&rear&&state.state==='run'&&!isAction&&!isComfort){
|
|
302
366
|
const b=isBehavior?paperBehaviorPose(state,behaviors.info):null;
|
|
303
367
|
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
368
|
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
369
|
if(state.flip)feet=feet.map(p=>({...p,x:319-p.x}));
|
|
306
370
|
}
|
|
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;
|
|
371
|
+
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
372
|
const transport=pawTransport?createPawTransport(feet,pins,{opaque:(x,y)=>{
|
|
309
373
|
const rawX=x+originX,rawY=y+originY;if(rawX<0||rawX>=sourceN||rawY<0||rawY>=sourceN)return false;
|
|
310
374
|
let sx=Math.floor(rawX);if(state.flip)sx=sourceN-1-sx;
|
|
@@ -319,6 +383,10 @@ export function createPaperRenderer(cache,info,meta,width,height,{rear,approach,
|
|
|
319
383
|
const mouthInfo={behaviors:behaviors?.info,seat:seat?.info,arrivalSeat:arrivalSeat?.info,approach:approach?.info}[mouthFamily];
|
|
320
384
|
const mouthFrame=mouthFamily?mouthSourceFrame(mouthFamily,state,mouthInfo):null;
|
|
321
385
|
render.lastMouth=projectMouth(mouthFamily?mouthAnchor(mouthFamily,mouthFrame,mouthInfo?.sha256):null,{left,top,side,sideX},state.flip,{family:mouthFamily,frame:mouthFrame});
|
|
386
|
+
if(motion?.mouth){
|
|
387
|
+
const k=comfortMotionKey(state.comfortPose.progress,motion),p=motion.mouth[k];
|
|
388
|
+
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]});
|
|
389
|
+
}
|
|
322
390
|
const drawTop=top-originY/320*side,bottom=top+Math.max(sourceN-originY,...pins.map(p=>p.target.y+20))/320*side;
|
|
323
391
|
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
392
|
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;
|
|
@@ -81,6 +91,8 @@ export interface Pet {
|
|
|
81
91
|
frame(options: FrameOptions): PetFrame;
|
|
82
92
|
/** False when busy or cooling down. */
|
|
83
93
|
interact(kind: 'ball' | 'treat' | 'tug'): boolean;
|
|
94
|
+
/** Sets the next throw's return style. Does not interrupt a fetch in progress. */
|
|
95
|
+
setReturnStyle(style: FetchReturnStyle): FetchReturnStyle;
|
|
84
96
|
readonly disposed: boolean;
|
|
85
97
|
dispose(): void;
|
|
86
98
|
}
|
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
|
|
|
@@ -65,7 +65,9 @@ async function loadPetAssets(loadAsset,artProfile){
|
|
|
65
65
|
let codec;
|
|
66
66
|
if(typeof process!=='undefined'&&process.versions?.node){
|
|
67
67
|
const {deflateRawSync,inflateRawSync}=await import('node:zlib');
|
|
68
|
-
|
|
68
|
+
// Level 3 trims retained crop payload without the multi-second startup cost
|
|
69
|
+
// of levels 6..9. Pixels, synchronous frame access and the LRU stay identical.
|
|
70
|
+
codec={compress:bytes=>deflateRawSync(bytes,{level:3}),expand:(bytes,size)=>inflateRawSync(bytes,{maxOutputLength:size})};
|
|
69
71
|
}
|
|
70
72
|
const load=async name=>bytesOf(name,await loadAsset(name));
|
|
71
73
|
const json=async name=>JSON.parse(new TextDecoder().decode(await load(name)));
|
|
@@ -94,13 +96,14 @@ async function acquireAssets(loadAsset,artProfile){
|
|
|
94
96
|
try{return{data:await entry.promise,release};}catch(error){release();throw error;}
|
|
95
97
|
}
|
|
96
98
|
|
|
97
|
-
export async function createPet({loadAsset=defaultLoadAsset,seed,energy,pawTransport=true,artProfile=DEFAULT_ART_PROFILE}={}){
|
|
99
|
+
export async function createPet({loadAsset=defaultLoadAsset,seed,energy,pawTransport=true,artProfile=DEFAULT_ART_PROFILE,returnStyle='auto'}={}){
|
|
98
100
|
if(typeof loadAsset!=='function')throw TypeError('loadAsset must be a function');
|
|
99
101
|
if(seed===undefined)seed=globalThis.crypto?.getRandomValues?globalThis.crypto.getRandomValues(new Uint32Array(1))[0]:731;
|
|
100
102
|
if(!Number.isInteger(seed)||seed<0||seed>0xffffffff)throw RangeError('seed must be a uint32');
|
|
101
103
|
if(energy!==undefined&&(!Number.isFinite(energy)||energy<0||energy>1))throw RangeError('energy must be within 0..1');
|
|
102
104
|
if(typeof pawTransport!=='boolean')throw TypeError('pawTransport must be boolean');
|
|
103
105
|
if(!ART_PROFILES.includes(artProfile))throw RangeError('artProfile must be original or q5');
|
|
106
|
+
if(!FETCH_RETURN_STYLES.includes(returnStyle))throw RangeError('returnStyle must be auto, brake, trot or run-trot');
|
|
104
107
|
let lease=await acquireAssets(loadAsset,artProfile),data=lease.data;
|
|
105
108
|
const release=lease.release;lease=null;
|
|
106
109
|
let {cache,info,meta}=data;const assets={...data.assets};data=null;
|
|
@@ -108,8 +111,8 @@ export async function createPet({loadAsset=defaultLoadAsset,seed,energy,pawTrans
|
|
|
108
111
|
try{
|
|
109
112
|
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
113
|
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}});
|
|
114
|
+
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});
|
|
115
|
+
life=createCompanionLife(actor,{seed,returnStyle,...energy===undefined?{}:{energy}});
|
|
113
116
|
}catch(e){animal?.dispose?.();release();throw e;}
|
|
114
117
|
let paint=null,paintKey='',time=0,acc=0,disposed=false;
|
|
115
118
|
const alive=()=>{if(disposed)throw Error('Pet is disposed');};
|
|
@@ -135,13 +138,15 @@ export async function createPet({loadAsset=defaultLoadAsset,seed,energy,pawTrans
|
|
|
135
138
|
const projection=createPaperProjection({viewportWidth,viewportHeight,rect:camera?.rect??{left:0,top:0,width:viewportWidth,height:viewportHeight},width,height});
|
|
136
139
|
const state=actor.sample(time,{poseFps}),report=life.report;
|
|
137
140
|
if(report.gesture)state.interactionPose=report.gesture;
|
|
138
|
-
const placement=projection.placement(state.x,state.z),pixels=paint(state,{...placement,contactEnabled:!!contactEnabled});
|
|
141
|
+
const placement=projection.placement(state.x,state.z,state.bodyLift??0),pixels=paint(state,{...placement,contactEnabled:!!contactEnabled});
|
|
139
142
|
paintCompanionObjects(pixels,width,height,projection,report,state,paint.lastMouth);
|
|
140
143
|
const {text}=rgbaToBraille(pixels,width,height,{method:'stable',brightness:1.18,contrast:1.1});
|
|
141
144
|
return {text,columns,rows,pixels,state,report,placement,projection,artProfile,mouth:paint.lastMouth,contacts:paint.report()};
|
|
142
145
|
},
|
|
143
146
|
// 'ball' | 'treat' | 'tug'. False when busy or cooling down; unknown kinds throw.
|
|
144
147
|
interact(kind){alive();if(!INTERACTIONS.includes(kind))throw TypeError(`Unknown interaction: ${kind}`);return life.interact(kind);},
|
|
148
|
+
// Applies to the next ball throw; never rewrites a fetch already in motion.
|
|
149
|
+
setReturnStyle(style){alive();return life.setReturnStyle(style);},
|
|
145
150
|
get disposed(){return disposed;},
|
|
146
151
|
dispose(){
|
|
147
152
|
if(disposed)return;disposed=true;
|