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:
wuwiandClaude Opus 5 committed 2026-08-03 23:23:47 +02:00
1 parent 0c12157702
commit 5a4c6e4359
4 files changed
+126 -23

No files matched your search

+18 -12
View File
@@ -3,9 +3,9 @@
// your baseline — one crossing it is a point for THEM.
// (zombies <-> gameplay is a runtime-only import cycle, same pattern as input.)
import { TUNE, UI, COURT, VOLLEY, CONTACT, BOSS_ART, WEAPONS, BARREL_WEAPONS, woundStage, EATEN_BURST, EATEN_SUMMARY, EATEN_FEED_R } from './config.js';
import { TUNE, UI, COURT, VOLLEY, CONTACT, BOSS_ART, WEAPONS, BARREL_WEAPONS, woundStage, zombieH, EATEN_BURST, EATEN_SUMMARY, EATEN_FEED_R } from './config.js';
import { G, P, ball, sendBoy, setupPoint } from './state.js';
import { splat, moan, pock, voice, boom, swish, shriek, aww, sfx , whoosh } from './audio.js';
import { splat, moan, pock, thud, zap, voice, boom, swish, shriek, aww, sfx , whoosh } from './audio.js';
import { splatPoint, cheer, spentBall, resolveInterlude, showModal, endBossFight, aside, grantWeapon, weaponModal, endBurst } from './gameplay.js';
export const zombies = []; // {x, z, v, walkT, variant: 'a'|'b'|'c'}
@@ -296,6 +296,9 @@ const hitR = zb => zb.variant === 'g' ? 0.75 : zb.variant === 'd' ? 0.64 : zb.va
: zb.variant === 'B' ? 0.55 * (zb.scale || 1) * 1.4 // a chapter boss is a barn door, as it should be
: zb.variant === 'm' ? 0.8 // the machine is a wide low box
: 0.55;
// How big it sounds. 1 = a shambler, and every impact sound scales its pitch and length off this,
// so a ball landing on a brute is audibly a bigger event than one landing on a runner.
const bodySize = zb => zombieH(zb) / 1.8;
// THE LASER. Not a projectile: the serve gesture you already know, and instead of a ball you get a
// widening plane of light down the lane you aimed at, killing everything standing inside the wedge
@@ -307,7 +310,7 @@ export function fireBeam(dirX, dirZ) {
// out of the PLAYER at chest height — same point the cone is measured from, so what you see and
// what it kills are one geometry
G.beam = { x: P.x, y: TUNE.beamY + TUNE.beamOriginLift, z: P.z, ux, uz, t: TUNE.beamT };
boom(0.55); G.shake = { t: 0.18, mag: 3 }; G.boomFlash = 0.08;
zap(0.55); G.shake = { t: 0.18, mag: 3 }; G.boomFlash = 0.08;
for (let i = zombies.length - 1; i >= 0; i--) {
const zb = zombies[i];
if (!zb) continue;
@@ -376,7 +379,7 @@ function bossDeath(zb) {
G.slowT = TUNE.bossSlowT; // real seconds; main.js scales dt while it runs
G.bossBurst = { x: zb.x, z: zb.z, t: 0.75 }; // the shockwave ring render.js draws
G.shake = { t: 0.5, mag: 7 }; G.boomFlash = 0.12;
boom(); splat(1); cheer(4, 1); voice('unbelievable', 0.5);
boom(); splat(1, bodySize(zb)); cheer(4, 1); voice('unbelievable', 0.5); // the biggest body in the game, at its lowest
const h = 1.8 * (zb.scale || 1.5);
for (let k = 0; k < 90; k++) parts.push({ // on top of killZombie's own burst: a full body's worth
x: zb.x, y: h * (0.2 + Math.random() * 0.7), z: zb.z,
@@ -426,7 +429,7 @@ function killZombie(i, quiet) {
}); }
if (ash) { for (let k = 0; k < 5; k++) smokePuff(zb.x, 0.4 + Math.random() * 1.1, zb.z, 0.45); pock(0.35, 150); }
else {
splat();
splat(1, bodySize(zb)); // the burst is pitched by the body too, so a boss comes apart LOW
sfx('headshot-' + (Math.random() < 0.5 ? 1 : 2), 0.6); // random headshot crack layered on the gore burst
}
sendBoy(zb.x, zb.z, true); // a boy comes out with the mop
@@ -458,7 +461,7 @@ function wound(zb, amount, fx = true) {
zb.chip = (zb.chip || 0) + amount;
let took = 0;
while (zb.chip >= 1 && zb.hp > 0) { zb.chip -= 1; zb.hp--; took++; }
if (took && fx) { splat(0.5); woundBurst(zb); }
if (took && fx) { splat(0.5, bodySize(zb)); woundBurst(zb); }
return zb.hp <= 0;
}
@@ -945,20 +948,23 @@ export function updateZombies(dt) {
if ((zb.hpMax || 0) > 1) {
const meat = zb.variant !== 'm'; // the machine rings and does not bleed
const heavy = 'gmhk'.includes(zb.variant); // a heavy body is not shoved back by a ball
const sz = bodySize(zb);
if ((ball.pow || 0) >= TUNE.zSplatQ) {
if (--zb.hp <= 0) { splatter(i); continue; } // the killing blow
if (meat) woundBurst(zb); // a bloody wound, not a death
splat(0.5); pock(0.6, meat ? (heavy ? 220 : 280) : 700); // meat thuds, machine rings
// meat THUDS (wet, and pitched by how big the body is), the machine still rings
if (meat) { thud(0.65, sz); splat(0.3, sz); } else pock(0.6, 700);
if (!heavy) zb.z -= TUNE.zPushback * 0.5;
ball.vx *= 0.3; ball.vz *= 0.3; ball.lastHit = 'X'; continue; // ball spent on the wound
}
if (!heavy) zb.z -= TUNE.zPushback; // soft ball staggers the light ones, bounces off the rest
pock(0.6, 300); ball.vx *= 0.6; ball.vz *= 0.6; ball.lastHit = 'X'; continue;
if (meat) thud(0.4, sz); else pock(0.6, 500); // a soft ball off a body is a duller version of the same hit
ball.vx *= 0.6; ball.vz *= 0.6; ball.lastHit = 'X'; continue;
}
// chair-down shield zombies (and anything hit from behind) obey the normal rules:
if ((ball.pow || 0) >= TUNE.zSplatQ) { splatter(i); continue; }
zb.z -= TUNE.zPushback; // weak sitters only stagger them
pock(0.6, 300);
thud(0.4, bodySize(zb));
ball.vx *= 0.6; ball.vz *= 0.6; ball.lastHit = 'X'; // spent — one stagger per ball
}
}
@@ -1087,13 +1093,13 @@ export function updateZombies(dt) {
// TRIPLE serve read as "he can't be hurt". They wound him like the main ball now.
if (isBoss(zb)) {
if (e.pow >= TUNE.zSplatQ) { if (wound(zb, TUNE.dmgSide)) killZombie(j, !!G.horde); }
else pock(0.5, 300);
else thud(0.4, bodySize(zb));
}
else if (zb.variant === 's' && e.vz < 0 && !(zb.shieldT > 0)) { zb.shieldT = TUNE.shieldDown; pock(0.9, 1800); } // chair blocks these too
else if (e.pow >= TUNE.zSplatQ || zb.variant === 'f') {
if (zb.hp > 1) { zb.hp--; splat(0.4); } // wounds brutes AND shamblers
if (zb.hp > 1) { zb.hp--; thud(0.55, bodySize(zb)); splat(0.25, bodySize(zb)); } // wounds brutes AND shamblers
else killZombie(j, !!G.horde);
} else { zb.z -= TUNE.zPushback; pock(0.5, 300); }
} else { zb.z -= TUNE.zPushback; thud(0.4, bodySize(zb)); }
dead = true; break; // a side ball is spent on its first touch
}
}