motion / src /game-driver.js
harshthedev's picture
SuperHQ Motion experiment: locomotion and action checkpoints, runtime, clips, evaluation
6adbbb1 verified
Raw History Blame Contribute Delete
3.51 kB
// Game-side intent, transition routing and heading. The network still generates every pose.
export function transitionPath(config, from, to) {
if (!config.profiles[from] || !config.profiles[to]) throw Error('Unknown motion profile.');
const queue = [[from]], seen = new Set([from]);
while (queue.length) {
const path = queue.shift(), last = path.at(-1);
if (last === to) return path.slice(1);
for (const [a, b] of config.transitions) {
const next = a === last ? b : b === last ? a : null;
if (next && !seen.has(next)) { seen.add(next); queue.push([...path, next]); }
}
}
throw Error('No validated transition path.');
}
export class PlaygroundDriver {
constructor(controller) { this.controller = controller; this.reset(); }
reset() {
this.controller.reset(); this.position = [0, 0, 0]; this.heading = 0;
this.lastSimulation = [0, 0, 0]; this.desired = 'Idle_Loop'; this.path = [];
this.events = []; this.frames = 0;
}
step({moving = false, crouch = false, jog = false, sprint = false, style = 'natural', heading = this.heading} = {}) {
if (!Number.isFinite(heading) || !['natural', 'carry', 'zombie'].includes(style)) throw Error('Invalid playground intent.');
const c = this.controller;
this.desired = crouch ? (moving ? 'Crouch_Fwd_Loop' : 'Crouch_Idle_Loop') :
style === 'zombie' ? (moving ? 'Zombie_Walk_Fwd_Loop' : 'Zombie_Idle_Loop') :
!moving ? 'Idle_Loop' : style === 'carry' ? 'Walk_Carry_Loop' :
sprint ? 'Sprint_Loop' : jog ? 'Jog_Fwd_Loop' : 'Walk_Loop';
// Recompute from current target so rapid input changes never reset phase or interrupt a blend.
this.path = transitionPath(c.config, c.profile, this.desired);
// Never start walking solely to satisfy a stationary stance/style request.
// Missing idle-to-idle edges remain pending until the player actually moves.
const waitingForMovement = !moving && this.path.length > 0 &&
c.config.profiles[c.profile].gait === 'idle' &&
this.path.some(name => c.config.profiles[name].gait !== 'idle') &&
c.clock.time - c.clock.start >= c.clock.transitionSeconds - 1e-8;
if (this.path.length && !waitingForMovement && c.clock.time - c.clock.start >= c.clock.transitionSeconds - 1e-8) {
const from = c.profile, to = this.path[0]; c.setProfile(to);
this.events.push({frame: this.frames, from, to}); if (this.events.length > 100) this.events.shift();
}
if (moving) {
const delta = Math.atan2(Math.sin(heading - this.heading), Math.cos(heading - this.heading));
this.heading += Math.max(-.075, Math.min(.075, delta));
}
const pose = c.step(), dx = pose.simulationPosition[0] - this.lastSimulation[0], dz = pose.simulationPosition[2] - this.lastSimulation[2];
const sin = Math.sin(this.heading), cos = Math.cos(this.heading);
this.position[0] += dx * cos + dz * sin; this.position[2] += dz * cos - dx * sin;
this.lastSimulation = pose.simulationPosition.slice(); this.frames++;
// Keep inference in the canonical forward frame; the host transforms the resulting pose.
const root = pose.rootPosition.map((v, i) => v - pose.simulationPosition[i]);
return {...pose, rootPosition: root, worldPosition: this.position.slice(), heading: this.heading,
desired: this.desired, path: transitionPath(c.config, c.profile, this.desired),
waitingForMovement: !!waitingForMovement,
blending: c.clock.time - c.clock.start < c.clock.transitionSeconds - 1e-8};
}
}