Let the ball machine roll, dent, and blow up
It was furniture in the most literal sense: bolted to one spot, three hits from scrap, and when it died it bled and a ball boy came out to mop it. - It trundles to a new spot every seven seconds or so, and CANNOT fire while it rolls. That is honest (a rolling cart cannot aim) and it is the window you use to close on it — but it also means it stops being a landmark you can ignore. - Six hits, not three, and it wears them: dented at two thirds, split open and sparking at one third, smoking where it stands once it is that far gone. - It comes apart as MACHINERY. No blood decal, no gore mound, no mop boy — a flash, sparks, torn metal and a column of dark smoke over a scorch mark. Smoke is a new particle flavour that rises and spreads instead of falling. Damaged sheets keep the healthy cart's footprint: the generator drew the smoke plume into the sprite, which would have shrunk the cart as it took damage, so the plume is cropped off and the smoke is drawn procedurally where it can move. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1168e9ee5b
commit
661ad90be5
7 files changed
+70
-11
No files matched your search
+48
-5
@@ -40,8 +40,9 @@ export function spawnZombie(x, z, variant) {
|
||||
if (zb.hp) zb.hpMax = zb.hp;
|
||||
if (variant === 's') zb.shieldT = 0; // chair up; > 0 = seconds of stagger left
|
||||
if ('fgsd'.includes(variant) && !G.seen['v' + variant]) { G.seen['v' + variant] = true; aside('ump_dress_code', 0.8); }
|
||||
if (variant === 'm') { // the rogue ball machine: furniture, not a body. It does not walk at all.
|
||||
zb.v = 0; zb.hp = TUNE.machHp; zb.fireT = TUNE.machCycle; zb.windT = 0;
|
||||
if (variant === 'm') { // the rogue ball machine: furniture, not a body. It never SHAMBLES...
|
||||
zb.v = 0; zb.hp = zb.hpMax = TUNE.machHp; zb.fireT = TUNE.machCycle; zb.windT = 0;
|
||||
zb.repoT = TUNE.machRepo * (0.4 + Math.random()); // ...but it wheels itself somewhere else now and then
|
||||
}
|
||||
zombies.push(zb);
|
||||
}
|
||||
@@ -159,10 +160,36 @@ const hitR = zb => zb.variant === 'g' ? 0.75 : zb.variant === 'd' ? 0.64 : zb.va
|
||||
// how absurdly a variant dies: classics splash, the advanced roster DETONATES
|
||||
const GORE_MUL = { a: 1, b: 1, f: 1.8, s: 2.5, g: 4, r: 3, m: 1.5, B: 6 }; // a chapter boss comes apart like nothing else on the court
|
||||
|
||||
// dark smoke: one puff, drifting up and spreading. Not gravity-bound like gore (see the parts loop).
|
||||
function smokePuff(x, y, z, spread = 0.5) {
|
||||
parts.push({ x: x + (Math.random() * 2 - 1) * spread, y, z: z + (Math.random() * 2 - 1) * spread * 0.5,
|
||||
vx: (Math.random() * 2 - 1) * 0.35, vy: 0.5 + Math.random() * 0.5, vz: (Math.random() * 2 - 1) * 0.35,
|
||||
t: 0.9 + Math.random() * 0.9, smoke: true });
|
||||
}
|
||||
|
||||
// The machine is the one thing on court that is not made of meat, and it used to burst like a body:
|
||||
// blood decal, gore mound, the wet splat, a ball boy sent out to mop it. It comes apart as MACHINERY
|
||||
// now — a flash, sparks, torn metal, and a column of dark smoke over a scorch mark.
|
||||
function wreckMachine(zb) {
|
||||
boom(); G.shake = { t: 0.3, mag: 5 }; G.boomFlash = 0.09;
|
||||
gore.push({ x: zb.x, z: zb.z, r: 1.1, a: 0.95, scorch: true }); // burnt clay, not blood
|
||||
if (gore.length > 40) gore.shift();
|
||||
for (let k = 0; k < 26; k++) parts.push({ // sparks and burning debris
|
||||
x: zb.x, y: 0.8, z: zb.z,
|
||||
vx: (Math.random() * 2 - 1) * 4, vy: 1.5 + Math.random() * 4, vz: (Math.random() * 2 - 1) * 4,
|
||||
t: 0.3 + Math.random() * 0.5, fire: true });
|
||||
for (let k = 0; k < 26; k++) smokePuff(zb.x, 0.5 + Math.random() * 1.6, zb.z, 0.7); // a column, not a puff
|
||||
}
|
||||
|
||||
function killZombie(i, quiet) {
|
||||
const zb = zombies[i];
|
||||
const mul = GORE_MUL[zb.variant] || 1;
|
||||
zombies.splice(i, 1);
|
||||
if (zb.variant === 'm') { // furniture, not a body: no blood, no mound, no mop boy
|
||||
wreckMachine(zb);
|
||||
if (!G.horde && !G.clearing) splatPoint('P', quiet);
|
||||
return;
|
||||
}
|
||||
if (G.interlude && zb.variant === 'r') resolveInterlude('P'); // regicide ends the duel your way — any weapon counts
|
||||
if (zb.variant === 'B') endBossFight(); // the chapter is over
|
||||
const stains = Math.round(Math.sqrt(mul)); // advanced kills splash a scatter of stains, not one puddle
|
||||
@@ -487,7 +514,8 @@ export function updateZombies(dt) {
|
||||
if (zb.hitT > 0) zb.hitT -= dt; // boss contact cooldown (see TUNE.bossHitCool)
|
||||
// ---- the two RANGED attackers. Both wind up visibly before firing (TUNE.machWind / drawZombie's
|
||||
// charge glow) because a shot you cannot see coming is not a fairness contract, it is a gotcha.
|
||||
if (!held && !G.interlude && (zb.variant === 'm' || zb.variant === 'B') && G.mode === 'splatter') {
|
||||
if (!held && !G.interlude && (zb.variant === 'm' || zb.variant === 'B') && G.mode === 'splatter'
|
||||
&& zb.rollTo === undefined) { // a cart on the roll cannot aim — that is the window to close on it
|
||||
const boss = zb.variant === 'B';
|
||||
if (zb.salvo > 0) { // mid-volley: the rest of the balls are still coming
|
||||
if ((zb.salvoT -= dt) <= 0) {
|
||||
@@ -506,7 +534,20 @@ export function updateZombies(dt) {
|
||||
pock(0.3, 1500); // the tell: a mechanical click as it loads
|
||||
}
|
||||
}
|
||||
if (held || zb.variant === 'r' || zb.variant === 'm') { /* boss AI owns its x; the machine is bolted down */ }
|
||||
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) {
|
||||
const d = zb.rollTo - zb.x;
|
||||
if (Math.abs(d) < 0.06) { zb.rollTo = undefined; zb.repoT = TUNE.machRepo * (0.7 + Math.random() * 0.6); }
|
||||
else {
|
||||
zb.x += Math.sign(d) * Math.min(Math.abs(d), TUNE.machRoll * dt);
|
||||
if ((zb.rollT -= dt) <= 0) { pock(0.16, 150); zb.rollT = 0.4; } // castors grinding on the clay
|
||||
}
|
||||
} else if ((zb.repoT -= dt) <= 0) { zb.rollTo = (Math.random() * 2 - 1) * 3.6; zb.rollT = 0; }
|
||||
}
|
||||
if (zb.hp < zb.hpMax * 0.5 && Math.random() < dt * 6) smokePuff(zb.x, 1.1, zb.z, 0.25); // wrecked: it smokes where it stands
|
||||
}
|
||||
else if (held || zb.variant === 'r') { /* boss AI owns its x; a duelling opponent owns his */ }
|
||||
else if (G.mode === 'splatter' && zb.z > COURT.netZ) { // on YOUR half they close in — each on its own lane, not a column
|
||||
const tx = P.x + zb.homeOff;
|
||||
zb.x += Math.sign(tx - zb.x) * Math.min(Math.abs(tx - zb.x), zb.v * 0.7 * dt)
|
||||
@@ -667,7 +708,9 @@ export function updateZombies(dt) {
|
||||
|
||||
for (let i = parts.length - 1; i >= 0; i--) {
|
||||
const p = parts[i];
|
||||
p.t -= dt; p.vy -= 14 * dt;
|
||||
p.t -= dt;
|
||||
if (p.smoke) { p.vy = 0.55; p.vx *= 0.97; p.vz *= 0.97; } // smoke rises and spreads; it does not fall
|
||||
else p.vy -= 14 * dt;
|
||||
p.x += p.vx * dt; p.y += p.vy * dt; p.z += p.vz * dt;
|
||||
if (p.y < 0) { p.y = 0; p.vy = 0; p.vx *= 0.5; p.vz *= 0.5; }
|
||||
if (p.t <= 0) parts.splice(i, 1);
|
||||
|
||||
Reference in new issue
Block a user