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
@@ -1180,6 +1180,13 @@ this on any new pose with a raised prop.
|
|||||||
index, never a search — which is why the laser looked fine while the other four did not.
|
index, never a search — which is why the laser looked fine while the other four did not.
|
||||||
Note also that spending re-indexes the rack (`spendSlot` splices), so a slot number goes stale the
|
Note also that spending re-indexes the rack (`spendSlot` splices), so a slot number goes stale the
|
||||||
moment any tin empties. Look weapons up by ID, not by a slot you cached a frame ago.
|
moment any tin empties. Look weapons up by ID, not by a slot you cached a frame ago.
|
||||||
|
- **Every sound in audio.js swallows its own errors** (`try { ... } catch (e) {}`), so a clean
|
||||||
|
console proves NOTHING about whether one played. To verify a sound really built its graph, count
|
||||||
|
nodes — patch `AudioContext.prototype.createBufferSource` / `createOscillator` /
|
||||||
|
`createBiquadFilter` and diff around the event. To verify a PARAMETER reached the synthesis, patch
|
||||||
|
`AudioParam.prototype.setValueAtTime` and look for the frequencies you expect (that is how the
|
||||||
|
size scaling on `thud`/`splat` was checked: 150 Hz body thump on a shambler, 100 on a brute,
|
||||||
|
59 on a chapter-4 boss).
|
||||||
- **A test that walks the game forward will quietly kill the player.** Three separate diagnostic runs
|
- **A test that walks the game forward will quietly kill the player.** Three separate diagnostic runs
|
||||||
above returned nonsense (`stillCarried: true`, no laser fired at all) purely because `G.mode` had
|
above returned nonsense (`stillCarried: true`, no laser fired at all) purely because `G.mode` had
|
||||||
become `'eaten'` several steps earlier and every gameplay entry point bails on that. When a probe
|
become `'eaten'` several steps earlier and every gameplay entry point bails on that. When a probe
|
||||||
|
|||||||
@@ -185,19 +185,72 @@ export function pock(vol = 0.8, freq = 900) { // racket contact / bounce thock
|
|||||||
} catch (e) {}
|
} 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 {
|
try {
|
||||||
const ac = audio(), t = ac.currentTime;
|
const ac = audio(), t = ac.currentTime;
|
||||||
const src = ac.createBufferSource(); src.buffer = noiseBuf(ac, 0.22);
|
const k = Math.max(0.7, Math.min(2.4, size));
|
||||||
const lp = ac.createBiquadFilter(); lp.type = 'lowpass';
|
const dur = 0.15 * k;
|
||||||
lp.frequency.setValueAtTime(1400, t); lp.frequency.exponentialRampToValueAtTime(180, t + 0.2);
|
// 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();
|
const g = ac.createGain();
|
||||||
g.gain.setValueAtTime(vol, t); g.gain.exponentialRampToValueAtTime(0.001, t + 0.22);
|
g.gain.setValueAtTime(vol * 0.8, t); g.gain.exponentialRampToValueAtTime(0.001, t + dur);
|
||||||
src.connect(lp).connect(g).connect(ac.destination); src.start();
|
src.connect(bp).connect(g).connect(ac.destination); src.start(t);
|
||||||
const o = ac.createOscillator(), og = ac.createGain(); // body thud
|
// the weight behind it — THIS is what actually carries the size difference
|
||||||
o.frequency.setValueAtTime(120, t); o.frequency.exponentialRampToValueAtTime(45, t + 0.15);
|
const o = ac.createOscillator(), og = ac.createGain();
|
||||||
og.gain.setValueAtTime(vol * 0.7, t); og.gain.exponentialRampToValueAtTime(0.001, t + 0.16);
|
o.frequency.setValueAtTime(150 / k, t); o.frequency.exponentialRampToValueAtTime(52 / k, t + 0.11 * k);
|
||||||
o.connect(og).connect(ac.destination); o.start(t); o.stop(t + 0.18);
|
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) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,6 +270,43 @@ export function boom(vol = 0.9) { // bomb ball — sub-bass thump under a filter
|
|||||||
} catch (e) {}
|
} 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
|
export function shriek(vol = 0.35) { // a runner announces itself — harsh rising cry, the fairness contract
|
||||||
try {
|
try {
|
||||||
const ac = audio(), t = ac.currentTime, dur = 0.7;
|
const ac = audio(), t = ac.currentTime, dur = 0.7;
|
||||||
|
|||||||
+1
-1
@@ -184,6 +184,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="module" src="main.js?v=117"></script>
|
<script type="module" src="main.js?v=118"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+18
-12
@@ -3,9 +3,9 @@
|
|||||||
// your baseline — one crossing it is a point for THEM.
|
// your baseline — one crossing it is a point for THEM.
|
||||||
// (zombies <-> gameplay is a runtime-only import cycle, same pattern as input.)
|
// (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 { 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';
|
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'}
|
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 === '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
|
: zb.variant === 'm' ? 0.8 // the machine is a wide low box
|
||||||
: 0.55;
|
: 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
|
// 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
|
// 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
|
// 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
|
// what it kills are one geometry
|
||||||
G.beam = { x: P.x, y: TUNE.beamY + TUNE.beamOriginLift, z: P.z, ux, uz, t: TUNE.beamT };
|
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--) {
|
for (let i = zombies.length - 1; i >= 0; i--) {
|
||||||
const zb = zombies[i];
|
const zb = zombies[i];
|
||||||
if (!zb) continue;
|
if (!zb) continue;
|
||||||
@@ -376,7 +379,7 @@ function bossDeath(zb) {
|
|||||||
G.slowT = TUNE.bossSlowT; // real seconds; main.js scales dt while it runs
|
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.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;
|
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);
|
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
|
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,
|
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); }
|
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 {
|
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
|
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
|
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;
|
zb.chip = (zb.chip || 0) + amount;
|
||||||
let took = 0;
|
let took = 0;
|
||||||
while (zb.chip >= 1 && zb.hp > 0) { zb.chip -= 1; zb.hp--; took++; }
|
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;
|
return zb.hp <= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -945,20 +948,23 @@ export function updateZombies(dt) {
|
|||||||
if ((zb.hpMax || 0) > 1) {
|
if ((zb.hpMax || 0) > 1) {
|
||||||
const meat = zb.variant !== 'm'; // the machine rings and does not bleed
|
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 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 ((ball.pow || 0) >= TUNE.zSplatQ) {
|
||||||
if (--zb.hp <= 0) { splatter(i); continue; } // the killing blow
|
if (--zb.hp <= 0) { splatter(i); continue; } // the killing blow
|
||||||
if (meat) woundBurst(zb); // a bloody wound, not a death
|
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;
|
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
|
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
|
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:
|
// chair-down shield zombies (and anything hit from behind) obey the normal rules:
|
||||||
if ((ball.pow || 0) >= TUNE.zSplatQ) { splatter(i); continue; }
|
if ((ball.pow || 0) >= TUNE.zSplatQ) { splatter(i); continue; }
|
||||||
zb.z -= TUNE.zPushback; // weak sitters only stagger them
|
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
|
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.
|
// TRIPLE serve read as "he can't be hurt". They wound him like the main ball now.
|
||||||
if (isBoss(zb)) {
|
if (isBoss(zb)) {
|
||||||
if (e.pow >= TUNE.zSplatQ) { if (wound(zb, TUNE.dmgSide)) killZombie(j, !!G.horde); }
|
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 (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') {
|
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 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
|
dead = true; break; // a side ball is spent on its first touch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user