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
Binary file not shown.
|
After Width: | Height: | Size: 554 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
@@ -62,7 +62,12 @@ export const TUNE = {
|
||||
// deal with, which is the target-priority decision the game was missing.
|
||||
machEvery: 4, // games before the first machine can appear, and games between them after
|
||||
machMax: 2, // machines allowed on court at once
|
||||
machHp: 3, // splat-quality hits to wreck one
|
||||
machHp: 6, // splat-quality hits to wreck one. It is a steel cart, not a body — and it now
|
||||
// shows the damage on the way down (z_machw1 dented, z_machw2 split open)
|
||||
machRoll: 1.6, // m/s it trundles when it repositions. It CANNOT fire while rolling, which is
|
||||
// both honest (a rolling cart cannot aim) and the window you use to close on it
|
||||
machRepo: 7, // s between repositions, jittered. "Furniture that occasionally wheels itself
|
||||
// somewhere else" — it stops being a landmark you can ignore once it is aimed away
|
||||
machCycle: 3.4, // s between its shots
|
||||
machWind: 0.9, // s of TELEGRAPH before each shot — the fairness contract, same idea as shriek()
|
||||
machSpeed: 13, // m/s it fires at
|
||||
|
||||
+1
-1
@@ -97,6 +97,6 @@
|
||||
<p id="build"></p>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="main.js?v=60"></script>
|
||||
<script type="module" src="main.js?v=61"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -155,7 +155,7 @@ export function proj(x, y, z) {
|
||||
|
||||
// ---------------------------------------------------------------- sprites ----
|
||||
// assets/<name>.png; game falls back to code-drawn figures until sprites load.
|
||||
const SPRV = 55; // asset cache-buster — bump with main.js?v=N (see the img.src note below)
|
||||
const SPRV = 56; // asset cache-buster — bump with main.js?v=N (see the img.src note below)
|
||||
const SPR = {};
|
||||
for (const n of ['p_idle1','p_idle2','p_bounce','p_bounce2','p_toss','p_smash','p_run1','p_run2','p_run3','p_run4','p_swing1','p_swing2',
|
||||
'o_idle1','o_idle2','o_bounce','o_bounce2','o_toss','o_smash','o_run1','o_run2','o_swing1','o_swing2',
|
||||
@@ -165,7 +165,7 @@ for (const n of ['p_idle1','p_idle2','p_bounce','p_bounce2','p_toss','p_smash','
|
||||
'z_gw2','z_gw3','z_gw4','z_gwb','z_gw2b','z_gw3b','z_gw4b', // the brute comes apart in stages (4/5 hp = z_gw .. 1/5 = z_gw4), each with a 2-frame lurch (…b)
|
||||
'z_boss1','z_boss2','z_bossw1','z_bossw2','z_bossw3', // the CHAPTER BOSS: 2-frame stride + three wound stages (arm off -> ribs out -> headless)
|
||||
'z_bosst0','z_bosst1','z_bosst2','z_bosst3', // ...and each of those four states mid-THROW (windT)
|
||||
'z_mach1','z_mach2', // the ball machine: idle, and firing with a muzzle flash
|
||||
'z_mach1','z_mach2','z_machw1','z_machw2', // the ball machine: idle, firing, dented, split open
|
||||
'z_judge1','z_judge2','z_judgew','z_judget','z_judgewt', // boss design 2 (chapter 2, 5, ...)
|
||||
'z_chair1','z_chair2','z_chairw','z_chairt','z_chairwt', // boss design 3 (chapter 3, 6, ...)
|
||||
'z_a1','z_a2','z_a3','z_a4','z_b1','z_b2','z_b3','z_b4','z_c1','z_c2',
|
||||
@@ -271,7 +271,12 @@ const ZFRAMES = { a: 4, b: 4, f: 4, d: 4, g: 4 }; // 4-frame walk cycles where s
|
||||
// player has to recognise and prioritise.
|
||||
function drawMachine(zb) {
|
||||
const firing = zb.windT > 0;
|
||||
const name = firing && SPR.z_mach2 ? 'z_mach2' : 'z_mach1';
|
||||
// It wears its damage like every other multi-hit body on court: dented at two thirds, split open
|
||||
// and sparking at one. The healthy sheet keeps the muzzle-flash frame; a damaged machine flashes
|
||||
// procedurally instead of needing a firing frame per stage.
|
||||
const f = zb.hp / (zb.hpMax || TUNE.machHp);
|
||||
const dmg = f <= 0.34 ? 'z_machw2' : f <= 0.67 ? 'z_machw1' : null;
|
||||
const name = dmg && SPR[dmg] ? dmg : firing && SPR.z_mach2 ? 'z_mach2' : 'z_mach1';
|
||||
const img = SPR[name];
|
||||
if (!img) return; // sheet not loaded yet: draw nothing, not a box
|
||||
const h = MACH_H * (img.height / SPRITE_H);
|
||||
@@ -846,9 +851,15 @@ export function render() {
|
||||
ctx.fillStyle = UI.ball;
|
||||
ctx.beginPath(); ctx.arc(Math.round(sp.x), Math.round(sp.y), r, 0, 7); ctx.fill();
|
||||
}
|
||||
// splatter particles (fire embers ride the same array)
|
||||
// splatter particles (fire embers and machine smoke ride the same array)
|
||||
for (const pt of parts) {
|
||||
const pp = proj(pt.x, pt.y, pt.z);
|
||||
if (pt.smoke) { // a broken machine's smoke: dark, soft-edged, growing and thinning as it climbs
|
||||
const age = Math.min(1, pt.t / 1.8), r = Math.max(2, pp.s * 0.09 * (2 - age));
|
||||
ctx.fillStyle = `rgba(46,44,48,${0.62 * age})`;
|
||||
ctx.beginPath(); ctx.arc(pp.x | 0, pp.y | 0, r, 0, 7); ctx.fill();
|
||||
continue;
|
||||
}
|
||||
ctx.fillStyle = pt.fire ? (pt.t > 0.12 ? '#ffc030' : '#ff8010')
|
||||
: pt.dark ? 'rgba(96,14,14,0.9)' : 'rgba(170,22,22,0.9)';
|
||||
ctx.fillRect(pp.x | 0, pp.y | 0, 2, 2);
|
||||
|
||||
+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