Give the laser its own voice, and let a body sound like its size
The laser was playing boom() — the BOMB — so the showiest weapon in the game sounded like the least showy one. zap() is its own: a highpassed crack, two detuned saws diving through a resonant lowpass, weight underneath so it lands on a phone speaker, and a triangle that rings after the plane has gone. The dive is the whole reading; a rising sweep is a charge-up and this thing never charges. And the NORMAL hit — ball meeting a body without killing it — was borrowing pock(), the racket thock. That is a bright, dry, stationary-band click: exactly right for gut on felt and exactly wrong for felt on a torso, which is why hitting a zombie sounded like hitting a wall. thud() falls instead of sitting still, which is where the wetness comes from, with one quiet bright tick just behind the slap: something gave. The machine keeps its pock — it is furniture and it rings. Both thud() and splat() now take a size, zombieH/1.8, so 1 is a shambler, 1.5 a brute and 2.04 a chapter-4 boss. Everything pitches down and lengthens with it. Measured through a patched AudioParam rather than assumed: the body thump under a hit runs 150 Hz on a shambler, 100 on a brute, 59 on the boss. splat() got the same treatment while I was in there — a resonant band collapsing down the spectrum instead of a plain lowpass, plus a second brighter burst a beat later. One clean hit is a thud; a thud followed by spatter is a body. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0c12157702
commit
5a4c6e4359
4 files changed
+126
-23
No files matched your search
@@ -185,19 +185,72 @@ export function pock(vol = 0.8, freq = 900) { // racket contact / bounce thock
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
export function splat(vol = 0.7) { // wet burst — a zombie meets a first serve
|
||||
// Ball meets BODY, without killing it. This was borrowing pock() — the racket thock — which is a
|
||||
// bright, dry, stationary-band click: exactly right for gut on felt and exactly wrong for felt on a
|
||||
// torso, so every non-fatal hit sounded like the ball had struck a wall. `size` is zombieH / 1.8, so
|
||||
// a shambler is 1, a brute ~1.5 and a late chapter boss ~2.4: the thump under it drops from 150 Hz
|
||||
// to around 60, and the whole hit slows down with it.
|
||||
export function thud(vol = 0.6, size = 1) {
|
||||
try {
|
||||
const ac = audio(), t = ac.currentTime;
|
||||
const src = ac.createBufferSource(); src.buffer = noiseBuf(ac, 0.22);
|
||||
const lp = ac.createBiquadFilter(); lp.type = 'lowpass';
|
||||
lp.frequency.setValueAtTime(1400, t); lp.frequency.exponentialRampToValueAtTime(180, t + 0.2);
|
||||
const k = Math.max(0.7, Math.min(2.4, size));
|
||||
const dur = 0.15 * k;
|
||||
// the slap. The band FALLS, where pock's sits still — a collapsing resonance is the wet part,
|
||||
// and it is the same trick splat() uses, just shorter and much less of it.
|
||||
const src = ac.createBufferSource(); src.buffer = noiseBuf(ac, dur);
|
||||
const bp = ac.createBiquadFilter(); bp.type = 'bandpass'; bp.Q.value = 3.4;
|
||||
bp.frequency.setValueAtTime(780 / k, t);
|
||||
bp.frequency.exponentialRampToValueAtTime(190 / k, t + dur);
|
||||
const g = ac.createGain();
|
||||
g.gain.setValueAtTime(vol, t); g.gain.exponentialRampToValueAtTime(0.001, t + 0.22);
|
||||
src.connect(lp).connect(g).connect(ac.destination); src.start();
|
||||
const o = ac.createOscillator(), og = ac.createGain(); // body thud
|
||||
o.frequency.setValueAtTime(120, t); o.frequency.exponentialRampToValueAtTime(45, t + 0.15);
|
||||
og.gain.setValueAtTime(vol * 0.7, t); og.gain.exponentialRampToValueAtTime(0.001, t + 0.16);
|
||||
o.connect(og).connect(ac.destination); o.start(t); o.stop(t + 0.18);
|
||||
g.gain.setValueAtTime(vol * 0.8, t); g.gain.exponentialRampToValueAtTime(0.001, t + dur);
|
||||
src.connect(bp).connect(g).connect(ac.destination); src.start(t);
|
||||
// the weight behind it — THIS is what actually carries the size difference
|
||||
const o = ac.createOscillator(), og = ac.createGain();
|
||||
o.frequency.setValueAtTime(150 / k, t); o.frequency.exponentialRampToValueAtTime(52 / k, t + 0.11 * k);
|
||||
og.gain.setValueAtTime(vol, t); og.gain.exponentialRampToValueAtTime(0.001, t + 0.13 * k);
|
||||
o.connect(og).connect(ac.destination); o.start(t); o.stop(t + 0.15 * k);
|
||||
// one short bright tick just behind the slap: something gave. Quiet on purpose — this is a hit,
|
||||
// not a burst, and at splat()'s spatter level every rally would sound like a kill.
|
||||
const d = 0.03 * k, sp = ac.createBufferSource(); sp.buffer = noiseBuf(ac, 0.07 * k);
|
||||
const hp = ac.createBiquadFilter(); hp.type = 'highpass'; hp.frequency.value = 1100;
|
||||
const spg = ac.createGain();
|
||||
spg.gain.setValueAtTime(vol * 0.18, t + d);
|
||||
spg.gain.exponentialRampToValueAtTime(0.001, t + d + 0.07 * k);
|
||||
sp.connect(hp).connect(spg).connect(ac.destination); sp.start(t + d);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Wet burst — a body meeting a first serve. `size` is roughly zombieH / 1.8, so 1 is a shambler,
|
||||
// ~1.5 a brute and ~2.4 a late chapter boss: everything below pitches DOWN and lasts LONGER with it.
|
||||
// A runner pops; a boss comes apart. Same sound, and that is the point — it should read as the same
|
||||
// event happening to a bigger body, not as a different event.
|
||||
export function splat(vol = 0.7, size = 1) {
|
||||
try {
|
||||
const ac = audio(), t = ac.currentTime;
|
||||
const k = Math.max(0.7, Math.min(2.4, size)); // clamp: nothing gets so big it stops being a splat
|
||||
const dur = 0.22 * k;
|
||||
// The wetness lives HERE. The old filter was a plain lowpass with no resonance, which is a dry
|
||||
// hiss with the top taken off; a RESONANT band collapsing down the spectrum is a squelch.
|
||||
const src = ac.createBufferSource(); src.buffer = noiseBuf(ac, dur);
|
||||
const bp = ac.createBiquadFilter(); bp.type = 'bandpass'; bp.Q.value = 3.2;
|
||||
bp.frequency.setValueAtTime(1100 / k, t);
|
||||
bp.frequency.exponentialRampToValueAtTime(130 / k, t + dur * 0.85);
|
||||
const g = ac.createGain();
|
||||
g.gain.setValueAtTime(vol, t); g.gain.exponentialRampToValueAtTime(0.001, t + dur);
|
||||
src.connect(bp).connect(g).connect(ac.destination); src.start(t);
|
||||
const o = ac.createOscillator(), og = ac.createGain(); // body thud, heavier on a heavier body
|
||||
o.frequency.setValueAtTime(120 / k, t); o.frequency.exponentialRampToValueAtTime(45 / k, t + 0.15 * k);
|
||||
og.gain.setValueAtTime(vol * 0.7, t); og.gain.exponentialRampToValueAtTime(0.001, t + 0.16 * k);
|
||||
o.connect(og).connect(ac.destination); o.start(t); o.stop(t + 0.18 * k);
|
||||
// ...and what came out of it lands a moment later. This second, brighter burst is most of what
|
||||
// reads as "wet" — one clean hit is a thud, a thud followed by spatter is a body.
|
||||
const d = 0.06 * k, sp = ac.createBufferSource(); sp.buffer = noiseBuf(ac, 0.18 * k);
|
||||
const hp = ac.createBiquadFilter(); hp.type = 'highpass'; hp.frequency.value = 900;
|
||||
const spg = ac.createGain();
|
||||
spg.gain.setValueAtTime(0.001, t + d);
|
||||
spg.gain.exponentialRampToValueAtTime(vol * 0.3, t + d + 0.02);
|
||||
spg.gain.exponentialRampToValueAtTime(0.001, t + d + 0.18 * k);
|
||||
sp.connect(hp).connect(spg).connect(ac.destination); sp.start(t + d);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
@@ -217,6 +270,43 @@ export function boom(vol = 0.9) { // bomb ball — sub-bass thump under a filter
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// The laser on release. It was borrowing boom() — the BOMB — which is why the showiest weapon in
|
||||
// the game sounded like the least showy one. This is a plane of light cutting a crowd in half, so:
|
||||
// a crack, a dive, weight underneath it, and a ring after it has gone.
|
||||
export function zap(vol = 0.5) {
|
||||
try {
|
||||
const ac = audio(), t = ac.currentTime, dur = 0.42;
|
||||
const src = ac.createBufferSource(); src.buffer = noiseBuf(ac, 0.09); // the crack: air leaving
|
||||
const hp = ac.createBiquadFilter(); hp.type = 'highpass'; hp.frequency.value = 2200;
|
||||
const ng = ac.createGain();
|
||||
ng.gain.setValueAtTime(vol * 0.85, t); ng.gain.exponentialRampToValueAtTime(0.001, t + 0.09);
|
||||
src.connect(hp).connect(ng).connect(ac.destination); src.start(t);
|
||||
// the body: two detuned saws DIVING through a resonant lowpass. The direction is the whole
|
||||
// reading — a rising sweep is a charge-up, and this thing never charges, it discharges.
|
||||
const lp = ac.createBiquadFilter(); lp.type = 'lowpass'; lp.Q.value = 12;
|
||||
lp.frequency.setValueAtTime(5200, t); lp.frequency.exponentialRampToValueAtTime(220, t + dur * 0.8);
|
||||
const bg = ac.createGain();
|
||||
bg.gain.setValueAtTime(vol, t); bg.gain.exponentialRampToValueAtTime(0.001, t + dur);
|
||||
lp.connect(bg).connect(ac.destination);
|
||||
for (const det of [1, 1.008]) { // the detune IS the width: one saw alone is a game-show buzzer
|
||||
const o = ac.createOscillator(); o.type = 'sawtooth';
|
||||
o.frequency.setValueAtTime(1300 * det, t);
|
||||
o.frequency.exponentialRampToValueAtTime(120 * det, t + dur * 0.7);
|
||||
o.connect(lp); o.start(t); o.stop(t + dur);
|
||||
}
|
||||
const s = ac.createOscillator(), sg = ac.createGain(); // weight, or it is only hiss on a phone
|
||||
s.frequency.setValueAtTime(150, t); s.frequency.exponentialRampToValueAtTime(48, t + 0.3);
|
||||
sg.gain.setValueAtTime(vol * 0.7, t); sg.gain.exponentialRampToValueAtTime(0.001, t + 0.34);
|
||||
s.connect(sg).connect(ac.destination); s.start(t); s.stop(t + 0.36);
|
||||
const r = ac.createOscillator(), rg = ac.createGain(); // ...and it rings after it has gone
|
||||
r.type = 'triangle'; r.frequency.setValueAtTime(3100, t);
|
||||
r.frequency.exponentialRampToValueAtTime(1900, t + dur);
|
||||
rg.gain.setValueAtTime(0.001, t); rg.gain.exponentialRampToValueAtTime(vol * 0.2, t + 0.05);
|
||||
rg.gain.exponentialRampToValueAtTime(0.001, t + dur + 0.12);
|
||||
r.connect(rg).connect(ac.destination); r.start(t); r.stop(t + dur + 0.14);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
export function shriek(vol = 0.35) { // a runner announces itself — harsh rising cry, the fairness contract
|
||||
try {
|
||||
const ac = audio(), t = ac.currentTime, dur = 0.7;
|
||||
|
||||
Reference in new issue
Block a user