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:
dejvino
2026-08-07 21:31:49 +02:00
co-authored by Claude Opus 5
parent f4ed3cdc48
commit 62c67ea649
10 changed files with 738 additions and 6 deletions
+8 -1
View File
@@ -42,6 +42,9 @@ export interface HudModel {
completed: number;
parts: number;
notice: string;
/** 0..1 toward wiping the campaign, while R is held. */
resetProgress: number;
muted: boolean;
}
/** Eight-point compass, starting at "straight ahead" and turning left. */
@@ -112,8 +115,12 @@ export function createHud(seed: number) {
`[debug] heat ${bar(model.heat.value)} ${model.heat.level ?? '—'}`,
'',
`seed ${seed}`,
'WASD drive · space handbrake · R respawn',
`WASD drive · space handbrake · R respawn · M sound ${model.muted ? 'off' : 'on'}`,
'hold R to reset the campaign',
];
if (model.resetProgress > 0.06) {
lines.push('', `RESETTING ${bar(Math.min(1, model.resetProgress))} keep holding R`);
}
if (model.notice) lines.push('', `» ${model.notice}`);
el.textContent = lines.join('\n');
},