SOURCE / PINNED RELEASE
Made of little things.
Supersonic RC Revive
- Release
- 1ba42f1ca1d6…
- Author-recorded commit
- baecad10b1cd…
- License
- LICENSE
- Author’s source reference
- nostr://npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr/wss%3A%2F%2Fgit.napplet.soy%2F/n-143146b0d6f
Archive hash verified: 90d22b206672eba4…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
// Headless driving harness used by tools/sim/drive.mjs (not bundled).
import { Quaternion, Vector3 } from 'three';
import { Input, type DirectorKey } from '../input.js';
import { type CollisionFile, type LevelData, type Markers, parseRayMesh } from '../level/data.js';
import { deriveLevel } from '../level/editorLevel.js';
import { DrivingManager, ORIGINAL_FRAME_MS } from '../lingo/drivingManager.js';
import { initPhysics, type HavokOptions } from '../physics/havok.js';
/** A level as tools/sim reads it: legacy JSON + ray mesh files, or a Blender level GLB. */
export type RawLevel = { markers: Markers; collision: CollisionFile; rayMeshBuffer: ArrayBuffer } | { levelGlb: ArrayBuffer };
function levelData(raw: RawLevel): LevelData {
if ('levelGlb' in raw) {
const { level, warnings } = deriveLevel(raw.levelGlb);
for (const w of warnings) console.warn(`level.glb: ${w}`);
return level;
}
return { markers: raw.markers, collision: raw.collision, rayMesh: parseRayMesh(raw.rayMeshBuffer) };
}
const KEYS: Record<string, DirectorKey> = { u: 126, d: 125, l: 123, r: 124, s: ' ' };
export async function runDrive(
raw: RawLevel,
script: string,
overrides: Partial<HavokOptions>,
printEvery = 0.25,
): Promise<void> {
await initPhysics();
const input = new Input();
const dm = new DrivingManager(
levelData(raw),
input,
overrides,
);
const steps = script.split(',').map((s) => {
const [keys, secs] = s.split(':');
return { keys: keys === '-' ? [] : [...keys].map((k) => KEYS[k]), secs: Number(secs) };
});
let t = 0;
let nextPrint = 0;
const rb = dm.car.rb;
const start = rb.position;
const fmt = (v: Vector3) => `${v.x.toFixed(0)},${v.y.toFixed(0)},${v.z.toFixed(1)}`;
for (const step of steps) {
input.clear();
for (const k of step.keys) input.setVirtual(k, true);
const end = t + step.secs;
while (t < end - 1e-9) {
dm.stepFrame(ORIGINAL_FRAME_MS);
t += ORIGINAL_FRAME_MS / 1000;
if (t >= nextPrint) {
nextPrint += printEvery;
const v = rb.linearVelocity;
const up = new Vector3().setFromMatrixColumn(rb.transform, 2);
const fwd = new Vector3().setFromMatrixColumn(rb.transform, 1);
const heading = (Math.atan2(-fwd.x, fwd.y) * 180) / Math.PI;
const ground = dm.car.wheels.map((w) => w.pOnGround).join('');
const springs = dm.car.wheels.map((w) => w.pSpringLength.toFixed(1)).join('/');
console.log(
`t=${t.toFixed(2)} keys=${step.keys.join('+') || '-'} pos=${fmt(rb.position)} dist=${rb.position.distanceTo(start).toFixed(0)} ` +
`speed=${v.length().toFixed(0)} hdg=${heading.toFixed(0)} upZ=${up.z.toFixed(2)} ground=${ground} springs=${springs} ` +
`skid=${dm.car.pSkidding} wz=${rb.angularVelocity.z.toFixed(1)} rays=${dm.car.wheels.map((w) => w.pGroundNormal.toArray().map((c) => c.toFixed(2)).join(" ")).join("|")}`,
);
}
}
}
dm.destroy();
}
/**
* Replay per-frame key states (126, 125, 123, 124, space as 0/1) logged from
* the original and return the same columns its logger writes, one row per frame.
*/
export async function runReplay(
raw: RawLevel,
frames: number[][],
overrides: Partial<HavokOptions>,
/** Optional per-frame chunk lists (ms) decoded from the original's simTime deltas. */
chunks?: number[][],
/** Logger experiments: after frame k, set [position, axis, angleDeg, velocity] and drop queued forces. */
experiments: Record<number, [number[], number[], number, number[]]> = {},
): Promise<number[][]> {
await initPhysics();
const input = new Input();
const dm = new DrivingManager(
levelData(raw),
input,
overrides,
);
const rb = dm.car.rb;
const order: DirectorKey[] = [126, 125, 123, 124, ' '];
const rows: number[][] = [];
for (const [f, keys] of frames.entries()) {
order.forEach((k, i) => input.setVirtual(k, keys[i] === 1));
if (chunks) for (const c of chunks[f]) dm.update(c);
else dm.stepFrame(ORIGINAL_FRAME_MS);
const exp = experiments[f];
if (exp) {
dm.havokManager.clearQueuedForces();
rb.position = new Vector3(...exp[0]);
rb.quaternion = new Quaternion().setFromAxisAngle(new Vector3(...exp[1]).normalize(), (exp[2] * Math.PI) / 180);
rb.linearVelocity = new Vector3(...exp[3]);
rb.angularVelocity = new Vector3();
}
const p = rb.position;
const q = rb.quaternion;
const angle = 2 * Math.acos(Math.min(1, Math.abs(q.w)));
const s = Math.sqrt(Math.max(1e-12, 1 - q.w * q.w)) * Math.sign(q.w || 1);
const v = rb.linearVelocity;
const w = rb.angularVelocity;
const row = [0, dm.havokManager.getSimTime(), p.x, p.y, p.z, q.x / s, q.y / s, q.z / s, (angle * 180) / Math.PI, v.x, v.y, v.z, w.x, w.y, w.z];
for (const wh of dm.car.wheels) row.push(wh.pOnGround, wh.pSpringLength, wh.pRotation);
row.push(...keys, dm.car.pSkidding);
rows.push(row);
}
dm.destroy();
return rows;
}
/**
* Start the port from a logged original state (row `start`: position, axis-angle
* rotation, velocities, steering), queue that frame's wheel forces, then replay
* the following frames' chunks and keys. Isolates contact responses (landings)
* from accumulated drift.
*/
export async function runInject(
raw: RawLevel,
start: number[],
frames: number[][],
chunks: number[][],
overrides: Partial<HavokOptions>,
): Promise<number[][]> {
await initPhysics();
const input = new Input();
const dm = new DrivingManager(
levelData(raw),
input,
overrides,
);
const rb = dm.car.rb;
const order: DirectorKey[] = [126, 125, 123, 124, ' '];
rb.position = new Vector3(start[2], start[3], start[4]);
const axis = new Vector3(start[5], start[6], start[7]);
rb.quaternion = axis.lengthSq() > 1e-12 ? new Quaternion().setFromAxisAngle(axis.normalize(), (start[8] * Math.PI) / 180) : new Quaternion();
rb.linearVelocity = new Vector3(start[9], start[10], start[11]);
rb.angularVelocity = new Vector3(start[12], start[13], start[14]);
dm.car.wheels.forEach((w, i) => (w.pRotation = start[17 + 3 * i]));
order.forEach((k, i) => input.setVirtual(k, start[27 + i] === 1));
dm.car.update();
const rows: number[][] = [];
for (const [f, keys] of frames.entries()) {
order.forEach((k, i) => input.setVirtual(k, keys[i] === 1));
for (const c of chunks[f]) dm.update(c);
const p = rb.position;
const q = rb.quaternion;
const angle = 2 * Math.acos(Math.min(1, Math.abs(q.w)));
const s = Math.sqrt(Math.max(1e-12, 1 - q.w * q.w)) * Math.sign(q.w || 1);
const v = rb.linearVelocity;
const w = rb.angularVelocity;
const row = [0, start[1] + dm.havokManager.getSimTime(), p.x, p.y, p.z, q.x / s, q.y / s, q.z / s, (angle * 180) / Math.PI, v.x, v.y, v.z, w.x, w.y, w.z];
for (const wh of dm.car.wheels) row.push(wh.pOnGround, wh.pSpringLength, wh.pRotation);
row.push(...keys, dm.car.pSkidding);
const c = rb.centerOfMass;
row.push(c.x, c.y, c.z, rb.mass);
for (const wh of dm.car.wheels) row.push(wh.pLoad, wh.pSpeed.x, wh.pSpeed.y, wh.pSpeed.z);
rows.push(row);
}
dm.destroy();
return rows;
}
