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:
wuwiandClaude Opus 5 committed 2026-07-31 20:05:44 +02:00
1 parent 1168e9ee5b
commit 661ad90be5
7 files changed
+70 -11

No files matched your search

+15 -4
View File
@@ -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);