Splatter: clear the wave by hand, tougher 2-hit shamblers, set cards, free depth

Feedback batch from a phone playtest:

- Winning a game no longer auto-sweeps the court. The survivors stay and must
  be killed by hand (no new spawns, no scoring during the mop-up); only once the
  court is clear does play advance to the changeover / next game / set. Replaces
  sweepCourt() with a G.clearing gate + proceedAfterClear(). "CLEAR THE COURT".
- Classic shamblers (a/b) now soak one splat-quality wound before they drop
  (shamblerHp=2) — swapping to new z_aw/z_bw wound skins that bleed the thin
  dynamic trickle plus a small impact pop. Only the runner stays one-shot glass;
  brute/boss unchanged. Ball, pellets and triple side-balls all respect hp.
- Winning a SET shows a centered milestone card (a real pause) before the horde;
  one reusable drawCard() now backs both it and the reworded sudden-death card.
- Player is no longer pinned to the baseline: free to advance up-court on
  keyboard (bounded at the net); touch keeps the baseline (drag is the serve-aim)
  and the horde/GUN burst re-brace there.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 4.8 committed 2026-07-17 22:13:59 +02:00
1 parent 13416ee749
commit 6df0ddb00f
10 files changed
+91 -42

No files matched your search

+18 -9
View File
@@ -68,7 +68,7 @@ for (const n of ['p_idle1','p_idle2','p_bounce','p_bounce2','p_toss','p_smash','
'x_ump','x_boyrun1','x_boyrun2','x_boyrun3','x_boyrun4','x_crowd','x_ads',
'z_a1','z_a2','z_a3','z_a4','z_b1','z_b2','z_b3','z_b4','z_c1','z_c2',
'z_f1','z_f2','z_f3','z_f4','z_g1','z_g2','z_gw','z_s1','z_s2','z_s3',
'z_cw1','z_cw2', // wounded skins for multi-hit enemies (brute, racket boss)
'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
'scoreboard']) {
const img = new Image();
@@ -153,6 +153,7 @@ function drawZombie(zb) {
// zombies.js). Fall back to the healthy sprite until the wounded PNG has loaded.
let name = 'z_' + v + frame;
if (zb.variant === 'g' && zb.hp < TUNE.bruteHp && SPR.z_gw) name = 'z_gw';
else if ((zb.variant === 'a' || zb.variant === 'b') && zb.hp < TUNE.shamblerHp && SPR['z_' + zb.variant + 'w']) name = 'z_' + zb.variant + 'w'; // gashed shambler
else if (rk && zb.hp <= 1 && SPR.z_cw2) { name = 'z_cw2'; h = 1.52; } // headless: a head shorter, so the shoulders land where a whole zombie's do
else if (rk && zb.hp === 2 && SPR.z_cw1) name = 'z_cw1';
drawStatic(name, zb.x, zb.z, h, Math.floor(zb.walkT * 5.2) % 2);
@@ -315,6 +316,17 @@ function drawRack() {
ctx.restore();
}
// the centered milestone 'block': a real pause, front and center. Shared by the sudden-death duel
// and the SET announcement so both read as the same interruption. Assumes textAlign is already set.
function drawCard(title, sub1, sub2, color) {
ctx.fillStyle = 'rgba(10,6,8,0.66)'; ctx.fillRect(0, H / 2 - 36, W, 64);
ctx.textAlign = 'center';
ctx.fillStyle = color; ctx.font = 'bold 18px monospace'; ctx.fillText(title, W / 2, H / 2 - 26);
ctx.fillStyle = '#e8e8dc'; ctx.font = 'bold 8px monospace';
if (sub1) ctx.fillText(sub1, W / 2, H / 2 - 2);
if (sub2) ctx.fillText(sub2, W / 2, H / 2 + 12);
}
// ------------------------------------------------------------------ frame ----
export function render() {
ctx.setTransform(RES, 0, 0, RES, 0, 0); // logical W x H coordinates at RES density
@@ -523,14 +535,11 @@ export function render() {
ctx.font = 'bold 14px monospace'; ctx.fillStyle = '#ffffff';
ctx.fillText(G.flash.text, W / 2, 60 + Math.sin(G.flash.t * 20) * 1);
}
if (G.interlude && G.interlude.introT > 0) { // the duel card: a real pause, front and center
ctx.fillStyle = 'rgba(10,6,8,0.66)'; ctx.fillRect(0, H / 2 - 36, W, 64);
ctx.fillStyle = '#ff5050'; ctx.font = 'bold 18px monospace';
ctx.fillText('SUDDEN DEATH', W / 2, H / 2 - 26);
ctx.fillStyle = '#e8e8dc'; ctx.font = 'bold 8px monospace';
ctx.fillText('ONE POINT · THE LINES ARE LIVE', W / 2, H / 2 - 2);
ctx.fillText('WIN IT FOR THE NEXT TIN', W / 2, H / 2 + 12);
}
// the sudden-death duel card, then (below it in priority) the SET milestone card — same block
if (G.interlude && G.interlude.introT > 0)
drawCard('SUDDEN DEATH', 'WIN THE POINT · THE LINES ARE LIVE', 'THE POINT WINS YOU A NEW WEAPON', '#ff5050');
else if (G.card && G.card.t > 0)
drawCard(G.card.title, G.card.sub1, G.card.sub2, G.card.color);
if (TUNE.missHud && G.lastMiss && G.idleT - G.lastMiss.at < 3.5) { // last miss reason, live
ctx.font = 'bold 8px monospace'; ctx.fillStyle = '#ff9060';
ctx.fillText(G.lastMiss.text.toUpperCase(), W / 2, H - 28);