Put a steward on the court

A new ordinary body between the shambler and the brute: a stadium security
steward in a hi-vis vest, 2.15 m, three hp, a touch slower. Deliberately the
mildest thing in the advanced pool — no wound skins, no special rule to learn —
so he can unlock early (game 4) and thicken the ordinary crowd, which a mix of
two shamblers never did. A fifth of horde spawns come out as stewards too, so a
grown wave is not one body dying at one range.

Sheet generated against the existing shambler walk (assets/_sheet_dwalk.png),
sliced to a 4-frame cycle, so he shambles as smoothly as the crowd he walks in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-07-31 18:57:03 +02:00
1 parent 7803c83564
commit d62478e8b1
9 files changed
+19 -7

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

+3
View File
@@ -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
+1 -1
View File
@@ -97,6 +97,6 @@
<p id="build"></p>
</div>
</div>
<script type="module" src="main.js?v=55"></script>
<script type="module" src="main.js?v=56"></script>
</body>
</html>
+4 -3
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 = 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
+11 -3
View File
@@ -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() {