const DT = 1 / 60; const smooth = t => {t=Math.max(0,Math.min(1,t));return t*t*(3-2*t);}; const vector = (a,n) => (Array.isArray(a)||ArrayBuffer.isView(a))&&a.length===n&&Array.from(a).every(Number.isFinite); function profile(value,rate) { if(!Number.isFinite(rate)||rate<.75||rate>1.25||!value|| !vector(value.flags,9)||value.flags.some(v=>v!==0&&v!==1)||value.flags[0]!==1|| value.flags.slice(1,5).reduce((a,b)=>a+b,0)!==1|| !Number.isFinite(value.frequency_hz)||value.frequency_hz<=0||value.frequency_hz>2|| !Number.isFinite(value.speed_mps)||value.speed_mps<0||value.speed_mps>12)throw Error('Invalid Motion profile or playback rate.'); const velocity=value.velocity??[0,0,value.speed_mps]; if(!vector(velocity,3)||velocity[1]!==0||Math.abs(Math.hypot(...velocity)-value.speed_mps)>1e-4|| (value.flags[1]===1&&value.speed_mps!==0))throw Error('Invalid flat-ground profile velocity.'); return {flags:Array.from(value.flags),velocity:velocity.map(v=>v*rate),frequency:value.frequency_hz*rate}; } /** Causal 34-feature clock for the game repair checkpoint. Phase is in cycles. * Call step once per 1/60 second. The host owns movement and collision handling. * Supported profile pairs are listed in the checkpoint study protocol. */ export class MotionProfileClock { constructor(initial,{rate=1,transitionSeconds=.75}={}) { if(!Number.isFinite(transitionSeconds)||transitionSeconds<.5||transitionSeconds>1)throw Error('Transition duration must be .5 to 1 second.'); this.transitionSeconds=transitionSeconds;this.reset(initial,{rate}); } reset(initial,{rate=1,phase=0,position=[0,0,0]}={}) { const p=profile(initial,rate); if(!Number.isFinite(phase)||!vector(position,3)||position[1]!==0)throw Error('Invalid flat-ground reset.'); this.from=p;this.to=p;this.start=-this.transitionSeconds;this.time=0; this.phase=((phase%1)+1)%1;this.position=Array.from(position);this.frame=0; } at(time) { const u=smooth((time-this.start)/this.transitionSeconds),mix=(a,b)=>a*(1-u)+b*u; return {flags:this.from.flags.map((v,i)=>mix(v,this.to.flags[i])), velocity:this.from.velocity.map((v,i)=>mix(v,this.to.velocity[i])),frequency:mix(this.from.frequency,this.to.frequency)}; } setProfile(value,{rate=1}={}) { const target=profile(value,rate),current=this.at(this.time); this.from=current;this.to=target;this.start=this.time; } step() { const p=this.at(this.time),input=new Float32Array(34),delta=[0,0,0]; // Same fixed-step trajectory integration used in the training targets. for(let n=1;n<=36;n++) { const previous=this.at(this.time+(n-1)*DT); for(let j=0;j<3;j++)delta[j]+=previous.velocity[j]*DT; if(n%12===0){const future=this.at(this.time+n*DT);input.set([delta[0],delta[2],future.velocity[0],future.velocity[2],0,1],4+6*(n/12-1));} } input.set(input.subarray(6,10),0); input.set([Math.sin(this.phase*2*Math.PI),Math.cos(this.phase*2*Math.PI),p.frequency],22);input.set(p.flags,25); this.phase=(this.phase+p.frequency*DT)%1; for(let j=0;j<3;j++)this.position[j]+=p.velocity[j]*DT; this.time+=DT;this.frame++; return {input,phase:this.phase,simulationPosition:this.position.slice(),frequency:p.frequency, speed:Math.hypot(...p.velocity),flags:p.flags,frame:this.frame}; } }