Triage: always kill the body that would reach you first

The sequence drill tested nothing — the strip was always visible (no memory),
the batch was slow (no urgency), the aim was easy (no execution). A checklist
with gore, and Oliver said so: "not interesting enough to keep me playing".

Settled in discussion, he picked triage + streak. One rule: always kill the
body that would reach you first. Nothing is called, nothing shown — the court
is the puzzle. arrival = distance / speed down a held lane, and the roster's
speed spread means the answer is not the closest body: a runner at the far
baseline beats a roller at the service line. Court sense — and it trains the
exact target-priority read the gore mode rewards.

Wounds are free: only the kill is judged, so pre-softening a heavy while it
is third in line is legal and eventually necessary. The slack (0.9s) makes
near-ties legal both ways, or the rule is a coin flip. The streak is both
punishment and payout: x1..x5, a misread shows OUT OF TURN and resets it, and
nothing else happens — the queue keeps walking. Caught is the real setback.
Best score banks to localStorage; the world board can carry it if it ships.

drillMark draws a chevron over the queue head for playtest calibration. It
answers the exact question the mode asks, so it must be off in anything
shipped.

Deleted: the alphabet, the batch, the strip, the eaters. Shamblers are
first-class stream members with pts 20; a roller is 100.

30 tests, rewritten around arithmetic riggings (known speeds and distances)
instead of luck; 20 seeds clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-11 21:32:17 +02:00
1 parent 57e96d629e
commit 6a16e77480
8 files changed
+313 -459

No files matched your search

+30 -34
View File
@@ -4,6 +4,7 @@ import { cv, ctx } from './dom.js';
import { TUNE, UI, EATEN_BURST, EATEN_SUMMARY, EATEN_RESTART, COURT, P_BOUNDS, PLAYER_H, BOY_H, UMP_H, UMP_X, MACH_H, BOSS_ART, ZWOUND, woundStage, BOUNCE_IDLE, SPRITE_H, zombieH , WEAPONS, RACK_ROW, TOSS } from './config.js';
import { P, O, ball, score, crowd, G, court, tour } from './state.js';
import { predictLanding, previewServe, slotId, slotStock } from './gameplay.js';
import { drillQueue, drillMult } from './drill.js';
import { zombies, gore, piles, parts, pellets, extras, shots, pops } from './zombies.js';
// ---------------------------------------------------------------- palette ----
@@ -679,44 +680,39 @@ function drawZombie(zb) {
if (v === 'B' && zb.hpMax) drawBossHp(zb, h);
}
// THE CALLED LIST, on screen — and it is the ROSTER now, not a palette. Each chip is the body's own
// walk frame, which is the same picture you are reading on the court: no legend to learn, no colour
// to map, and no new art. Done ones dim and the next wears the cream frame, so the strip doubles as
// "where am I" and never has to be re-read from the start mid-round.
// THE TRIAGE READOUT: score, multiplier, best — and the calibration marker. There is no strip and
// no called list any more; the court is the puzzle, so the HUD only carries what the court cannot
// say: how much the run is worth and how hot the streak is.
function drawDrillHud() {
const d = G.drill; if (!d) return;
const n = d.seq.length; if (!n) return;
// Sized to the widest case the ramp can deal: six chips at 30 + five 3px gaps = 195 px, inside a
// 240 px portrait canvas with room either side. A silhouette is the whole readout here, so the
// chips take every pixel the band can spare — at 26 the steward, the ball boy and the bouncer were
// three similar green figures.
const w = 30, gap = 3, totalW = n * w + (n - 1) * gap;
let x = Math.round(W / 2 - totalW / 2);
const y = CARD_TOP + 4, h = 34;
for (let i = 0; i < n; i++, x += w + gap) {
const done = i < d.step;
ctx.globalAlpha = done ? 0.3 : 1;
ctx.fillStyle = 'rgba(8,6,10,0.82)'; ctx.fillRect(x - 1, y - 1, w + 2, h + 2);
ctx.fillStyle = 'rgba(30,26,34,0.9)'; ctx.fillRect(x, y, w, h);
// the body itself, fitted into the chip on its own aspect — a roller is wide, a runner is not,
// and that difference is exactly the thing being read
const img = SPR[SK('z_' + d.seq[i] + '1')];
if (img) {
const pad = 2, bw = w - pad * 2, bh = h - pad * 2;
const sc = Math.min(bw / img.width, bh / img.height);
const iw = Math.round(img.width * sc), ih = Math.round(img.height * sc);
ctx.drawImage(img, Math.round(x + (w - iw) / 2), Math.round(y + h - pad - ih), iw, ih);
}
if (i === d.step) { // the one you owe them right now
ctx.strokeStyle = UI.cream; ctx.lineWidth = 2;
ctx.strokeRect(x - 2, y - 2, w + 4, h + 4);
}
ctx.globalAlpha = 1;
}
const y = CARD_TOP + 4;
ctx.textAlign = 'center';
if (d.msg && d.msg.t > 0) { ctx.font = B(20); shadowText(d.msg.text, W / 2, y + h + 22, d.msg.bad ? UI.bad : UI.pick); }
ctx.font = B(24);
shadowText(String(d.score), W / 2, y, UI.cream);
const m = drillMult(d);
ctx.font = B(14);
// the multiplier is the streak made visible — gold once it is working, dim while it rebuilds
shadowText('x' + m + ' · ' + d.streak + ' STREAK', W / 2, y + 26, m > 1 ? UI.warm : UI.dim);
ctx.font = F(8);
shadowText(d.rounds + ' - ' + d.lost, W / 2, y + h + 5, UI.dim);
shadowText('BEST ' + Math.max(d.best, d.score), W / 2, y + 44, UI.dim);
if (d.msg && d.msg.t > 0) { ctx.font = B(20); shadowText(d.msg.text, W / 2, y + 60, d.msg.bad ? UI.bad : UI.pick); }
// THE CALIBRATION MARKER (TUNE.drillMark): a chevron over the body the rule says is next. For the
// playtest only — it exists so Oliver can check the game's read against his own, and it answers
// the exact question the mode is asking, so it must be OFF in anything shipped.
if (TUNE.drillMark >= 0.5) {
const head = drillQueue()[0];
if (head) {
const p = proj(head.x, zombieH(head) + 0.5, head.z);
if (p.s > 0) {
const bob = Math.sin(G.idleT * 6) * 2;
const px = Math.round(p.x), py = Math.round(p.y + bob);
ctx.fillStyle = UI.warm;
ctx.beginPath();
ctx.moveTo(px - 5, py - 7); ctx.lineTo(px + 5, py - 7); ctx.lineTo(px, py);
ctx.closePath(); ctx.fill();
}
}
}
}
// The boss's remaining life, over its head, in the scoreboard's own 16-bit language: a dark sunk