Phase 7: sound, synthesised from the simulation
No audio files. The build stays self-contained, so the engine is detuned oscillators, tyres and gunfire are shaped noise, ambience is a filtered drone. The constraint pays: sound generated from the sim can carry the sim's state instead of decorating it. The engine has gears, so the note climbs and drops rather than tracking the speedometer forever. A worn engine sounds wrong rather than merely slow — roughness rises and the top of the rev range falls away as condition drops, so decline is audible as well as felt. Bald tyres squeal sooner than good ones, off the lateral slip the physics actually reports. Gunfire falls off with the square of distance and pans as the car turns, so a firefight is a direction. Territory has a tone: the drone drops and thickens past the line, matching the palette. Split so it can be tested: audio/mix.ts holds every decision as plain numbers and is covered; audio/audio.ts is only wiring, which cannot run outside a browser. A test caught panning being mirrored — +X is the car's left in this world, so the sine needed negating. Also adds hold-R-for-five-seconds to wipe the campaign, with a filling meter so it cannot happen by accident, and M to mute. Tapping R still just respawns. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ const IDLE: DriverInput = {
|
||||
handbrake: false,
|
||||
respawn: false,
|
||||
select: null,
|
||||
toggleMute: false,
|
||||
};
|
||||
|
||||
/** Rapier runs headless, so vehicle tuning is checkable without a browser. */
|
||||
|
||||
@@ -16,6 +16,14 @@ export interface PhysicsWorld {
|
||||
/** Static box, added and removed at runtime as road heat rises and falls. */
|
||||
addStaticBox(box: StaticBox): RAPIER.RigidBody;
|
||||
removeBody(body: RAPIER.RigidBody): void;
|
||||
/** What the wheels are doing, for anything that needs to react to grip. */
|
||||
telemetry(): Telemetry;
|
||||
}
|
||||
|
||||
export interface Telemetry {
|
||||
/** Sum of lateral impulses across the wheels, scaled to roughly 0..1+. */
|
||||
sideSlip: number;
|
||||
wheelsOnGround: number;
|
||||
}
|
||||
|
||||
export interface StaticBox {
|
||||
@@ -139,6 +147,19 @@ export async function createPhysics(model: WorldModel): Promise<PhysicsWorld> {
|
||||
return v;
|
||||
},
|
||||
|
||||
telemetry() {
|
||||
let sideSlip = 0;
|
||||
let wheelsOnGround = 0;
|
||||
for (let i = 0; i < WHEELS.length; i++) {
|
||||
if (!vehicle.wheelIsInContact(i)) continue;
|
||||
wheelsOnGround++;
|
||||
// The lateral impulse the tyre had to generate to hold the line. Scaled
|
||||
// against the car's weight, so it reads as "how hard is it working".
|
||||
sideSlip += Math.abs(vehicle.wheelSideImpulse(i) ?? 0);
|
||||
}
|
||||
return { sideSlip: sideSlip / (CAR.mass * 0.09), wheelsOnGround };
|
||||
},
|
||||
|
||||
addStaticBox(box) {
|
||||
const body = world.createRigidBody(
|
||||
RAPIER.RigidBodyDesc.fixed()
|
||||
|
||||
Reference in New Issue
Block a user