diff --git a/assets/_sheet_dwalk.png b/assets/_sheet_dwalk.png new file mode 100644 index 0000000..07077cc Binary files /dev/null and b/assets/_sheet_dwalk.png differ diff --git a/assets/z_d1.png b/assets/z_d1.png new file mode 100644 index 0000000..a7cc9f9 Binary files /dev/null and b/assets/z_d1.png differ diff --git a/assets/z_d2.png b/assets/z_d2.png new file mode 100644 index 0000000..8e81df8 Binary files /dev/null and b/assets/z_d2.png differ diff --git a/assets/z_d3.png b/assets/z_d3.png new file mode 100644 index 0000000..e5975dc Binary files /dev/null and b/assets/z_d3.png differ diff --git a/assets/z_d4.png b/assets/z_d4.png new file mode 100644 index 0000000..33988f5 Binary files /dev/null and b/assets/z_d4.png differ diff --git a/config.js b/config.js index 6b52989..cde0c64 100644 --- a/config.js +++ b/config.js @@ -205,6 +205,9 @@ export const TUNE = { runnerSpeed: 3.2, // runner ('f') speed multiplier over the shamble runnerVMax: 4.2, // hard runner speed cap — the late-game ramp must stay humanly reactable shamblerHp: 2, // splat-quality hits a classic shambler ('a'/'b') soaks before it dies — only the runner ('f') is still one-shot glass + stewardHp: 3, // the steward ('d'): one hit tougher than the crowd he walks in. Deliberately NOT + // a brute — no wound skins, no special rule, just a bigger body that takes one more + hordeSteward: 0.2, // share of horde spawns that come out as stewards (from game 4) // fire: the flame ball no longer just kills what it touches, it starts an epidemic. burnTime: 3.0, // s a body burns before the fire goes out burnDps: 2.0, // hp lost per second while burning — burnTime*burnDps must exceed bruteHp or tanks walk out of the fire diff --git a/index.html b/index.html index 1c992ff..facb77b 100644 --- a/index.html +++ b/index.html @@ -97,6 +97,6 @@

- + diff --git a/render.js b/render.js index 93936f6..6d69c62 100644 --- a/render.js +++ b/render.js @@ -155,7 +155,7 @@ export function proj(x, y, z) { // ---------------------------------------------------------------- sprites ---- // assets/.png; game falls back to code-drawn figures until sprites load. -const SPRV = 50; // asset cache-buster — bump with main.js?v=N (see the img.src note below) +const SPRV = 51; // 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_swing1','p_swing2', 'o_idle1','o_idle2','o_bounce','o_bounce2','o_toss','o_smash','o_run1','o_run2','o_swing1','o_swing2', @@ -169,6 +169,7 @@ for (const n of ['p_idle1','p_idle2','p_bounce','p_bounce2','p_toss','p_smash',' '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', + 'z_d1','z_d2','z_d3','z_d4', // the steward: bigger body, hi-vis vest, 4-frame walk 'z_f1','z_f2','z_f3','z_f4','z_g1','z_g2','z_gw','z_s1','z_s2','z_s3', 'z_cw1','z_cw2','z_aw','z_bw', // wounded skins for multi-hit enemies (racket boss, brute, and now the classic shamblers) 'w_ball','w_bomb','w_triple','w_fire','w_gun', // weapon-rack icons @@ -257,7 +258,7 @@ function drawBoy(b) { ctx.drawImage(img, -w / 2, -h, w, h); ctx.restore(); } -const ZFRAMES = { a: 4, b: 4, f: 4 }; // 4-frame walk cycles where sheets exist; the rest stay 2 +const ZFRAMES = { a: 4, b: 4, f: 4, d: 4 }; // 4-frame walk cycles where sheets exist; the rest stay 2 // The rogue ball machine, on its own generated sheet: a dented hopper cart on wheels with a barrel // aimed down-court, and a second frame with the muzzle flashing. Oliver called the first, procedural // version "naively painted" — it was four rects, which is fine for fire but not for furniture the @@ -288,7 +289,7 @@ function drawZombie(zb) { // same CYCLE frequency as before — more frames subdivide the stride, they don't speed it up let frame = rk && zb.pose === 'swing' ? 2 : Math.floor(zb.walkT * (zb.variant === 'f' ? 6 : 2.6) * N / 2) % N + 1; if (zb.variant === 's' && zb.shieldT > 0) frame = 3; // chair dropped — hit it NOW - let h = zb.variant === 'g' ? 2.7 : rk ? 1.85 : 1.8; // the brute towers half again over the crowd + let h = zb.variant === 'g' ? 2.7 : zb.variant === 'd' ? 2.15 : rk ? 1.85 : 1.8; // the brute towers half again over the crowd; the steward stands between him and it if (zb.variant === 'B') h = 1.8 * (zb.scale || 1.5); // ...and a chapter boss towers over the brute // wounded skins: multi-hit enemies show their damage as hp drops (arm off → head gone, still // shambling). The blood is NOT painted in — open wounds gush live particles (see bleed() in diff --git a/zombies.js b/zombies.js index 0531334..6ef279d 100644 --- a/zombies.js +++ b/zombies.js @@ -33,8 +33,9 @@ export function spawnZombie(x, z, variant) { if (variant === 'f') { zb.v = Math.min(TUNE.runnerVMax, zb.v * TUNE.runnerSpeed); shriek(); } // shriek = the fairness contract; capped so late runners stay reactable if (variant === 'a' || variant === 'b') zb.hp = TUNE.shamblerHp; // classic shamblers take a wound before they drop — only the runner is one-shot glass if (variant === 'g') { zb.v *= 0.55; zb.hp = TUNE.bruteHp; } // slow, but soaks hits + if (variant === 'd') { zb.v *= 0.8; zb.hp = TUNE.stewardHp; } // the steward: a head taller and a hit tougher than the crowd, nothing more exotic than that if (variant === 's') zb.shieldT = 0; // chair up; > 0 = seconds of stagger left - if ('fgs'.includes(variant) && !G.seen['v' + variant]) { G.seen['v' + variant] = true; aside('ump_dress_code', 0.8); } + 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; } @@ -130,6 +131,10 @@ function pickVariant() { if (g >= 2 && !zombies.some(z => z.variant === 'f')) pool.push('f'); // at most one runner alive if (g >= 6) pool.push('g'); if (g >= 12) pool.push('s'); + // The steward is the mildest of the pool on purpose — bigger and one hit tougher, no special rule + // to learn — so he can arrive EARLY and thicken the ordinary crowd, which the shambler mix alone + // never did. He is also the only advanced body that shows up in a horde (see spawnHordeZombie). + if (g >= 4) pool.push('d'); const nasty = Math.min(TUNE.nastyMax, g * TUNE.nastyPerGame); if (pool.length && Math.random() < nasty) return pool[Math.floor(Math.random() * pool.length)]; return Math.random() < 0.5 ? 'a' : 'b'; @@ -140,7 +145,7 @@ function pickVariant() { // A BOSS BODY: takes wound damage from everything instead of being deleted by it. The brute ('g') is // deliberately NOT one — melee/bomb/fire one-shotting a brute is a settled "power exception". const isBoss = zb => zb.variant === 'r' || zb.variant === 'B'; -const hitR = zb => zb.variant === 'g' ? 0.75 : zb.variant === 'r' ? 0.7 +const hitR = zb => zb.variant === 'g' ? 0.75 : zb.variant === 'd' ? 0.64 : zb.variant === 'r' ? 0.7 : 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 : 0.55; @@ -302,7 +307,10 @@ function splatter(i, fanfare = true) { // the MAIN ball hit a zombie // ---- horde wave: dense spawns + an auto-firing machine-gun racket (pellets, not the match ball) function spawnHordeZombie() { // all from the far baseline, across the court — never off-screen at the sides - spawnZombie((Math.random() * 2 - 1) * 5, -1.5 - Math.random(), Math.random() < 0.5 ? 'a' : 'b'); + // a few stewards in the mix: the wave needs bodies that survive one pellet, or a grown horde is a + // uniform wall of the same thing dying at the same range + const v = Math.random() < TUNE.hordeSteward && G.runGames >= 4 ? 'd' : Math.random() < 0.5 ? 'a' : 'b'; + spawnZombie((Math.random() * 2 - 1) * 5, -1.5 - Math.random(), v); } let mgTick = 0; function firePellet() {