Die the way people die in the films

The old death was a red tint and a shake for half a second, then a puff of gore.
That reads as the sprite having a moment, not as being eaten.

Now it takes four and a half seconds and it is a crowd doing it. You are dragged
to your knees fighting it (new frames, p_die1/p_die2, alternating), blood coming
out of you the whole time — a constant spray and a stain that keeps widening,
with the wet work on a loop underneath. Every body on court drops what it was
doing and converges: each claims a spot in a ring around you, kept from wherever
it was standing, so they crowd in from all sides instead of stacking on one
pixel. Then you are pressed flat into the ground while the ring closes over you —
you are drawn INSIDE the crowd's z order, so the ones between you and the camera
hide you exactly as they arrive — and what is left is the mound, with them still
feeding over it when the summary finally fades in.

Also: the wounded shamblers were stepping their two frames at half the cadence of
the healthy four, which reads as frozen next to the crowd they walk in. Same
clock now. (They were animating — verified frame by frame — just too slowly.)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-07-31 20:36:25 +02:00
1 parent b7899f122c
commit 4d6b47dadd
8 files changed
+88 -24

No files matched your search

+37 -1
View File
@@ -3,7 +3,7 @@
// 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 } from './config.js';
import { TUNE, UI, COURT, VOLLEY, CONTACT, BOSS_ART, EATEN_BURST, 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 } from './audio.js';
import { splatPoint, cheer, spentBall, resolveInterlude, showModal, endBossFight, aside } from './gameplay.js';
@@ -280,6 +280,21 @@ function burnTick(zb, i, dt) {
return true;
}
// Phase A, every frame: they are tearing at you and it shows. A thin constant spray out of the
// pile plus a stain that keeps widening — "blood and gore flowing out" is a CONTINUOUS thing, not
// one burst, and the burst at EATEN_BURST lands on top of it.
export function eatenBleed(dt) {
const at = G.eatenAt; if (!at) return;
for (let k = 0; k < 2; k++) parts.push({
x: at.x + (Math.random() * 2 - 1) * 0.35, y: 0.5 + Math.random() * 0.8, z: at.z + (Math.random() * 2 - 1) * 0.3,
vx: (Math.random() * 2 - 1) * 3.2, vy: 1.5 + Math.random() * 3.5, vz: (Math.random() * 2 - 1) * 3.2,
t: 0.4 + Math.random() * 0.5, dark: Math.random() < 0.45 });
const g = gore.find(d => d.spreading);
if (g) g.r = Math.min(2.1, g.r + dt * 0.7);
else { gore.push({ x: at.x, z: at.z, r: 0.7, a: 0.9, spreading: true }); if (gore.length > 40) gore.shift(); }
if ((G.eatSfxT = (G.eatSfxT || 0) - dt) <= 0) { splat(0.45); G.eatSfxT = 0.28; } // wet work, on a loop
}
// phase B of the death animation (EATEN_BURST, config.js): the player IS the gore now. Reuses killZombie's machinery —
// the biggest mound on the court, and it stays visible under the summary scrim. That's the point.
export function playerGore() {
@@ -555,6 +570,27 @@ export function updateZombies(dt) {
pock(0.3, 1500); // the tell: a mechanical click as it loads
}
}
if (G.mode === 'eaten' && G.eatenAt && zb.variant !== 'm') {
// You are down. Nothing else matters to them: every body on court converges on the spot and
// packs into a ring around it, and the ones already there feed (the bob is in drawZombie).
// This is the whole point of the death — you go under a CROWD, not under a puff of gore.
// Each one claims a SPOT around the body, kept from wherever it happened to be standing, so
// they form a ring and crowd in from all sides. Sending them all at the one point instead
// stacked every body on the same pixel and read as one fat zombie, not a swarm.
if (zb.eatA === undefined) {
zb.eatA = Math.atan2(zb.z - G.eatenAt.z, zb.x - G.eatenAt.x) + (Math.random() * 2 - 1) * 0.4;
zb.eatR = EATEN_FEED_R * (0.8 + Math.random() * 0.7);
}
const tx = G.eatenAt.x + Math.cos(zb.eatA) * zb.eatR;
const tz = G.eatenAt.z + Math.sin(zb.eatA) * zb.eatR * 0.55; // squashed in depth: the camera is low
const dx = tx - zb.x, dz = tz - zb.z, d = Math.hypot(dx, dz);
zb.feeding = d < 0.12;
if (!zb.feeding) {
const sp = zb.v * 1.8 * dt; // they hurry for a kill
zb.x += dx / d * Math.min(sp, d); zb.z += dz / d * Math.min(sp, d);
}
continue; // no shambling, no eating you twice, no crossing the baseline for a point
}
if (zb.variant === 'm') { // the machine wheels itself around instead of being bolted down
if (!held && G.mode === 'splatter' && !G.interlude) {
if (zb.rollTo !== undefined) {