The card's punchline wraps, and the bulk answers to the name the stats gave him

Six head icons recut by hand; the new cuts are square, and a 1:1 portrait beside a
long punchline pushed the head against the left edge while the line itself ran off
both. The sub lines now wrap to the same room the title already shrank to fit, and
the plate grows with them — one line each still lands on 30/44, so every card that
fit is pixel-identical.

THE BULK is THE BRUTE: the gore mode's stats screen has called that body THE BRUTE
since it dragged a pole, and one character may not carry two names.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-16 09:29:13 +02:00
1 parent 86b8275f5d
commit 932673ff5d
9 files changed
+33 -8

No files matched your search

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

+1 -1
View File
@@ -659,6 +659,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') {
</div>
</div>
</div>
<script type="module" src="src/main.js?v=274"></script>
<script type="module" src="src/main.js?v=275"></script>
</body>
</html>
+3 -1
View File
@@ -1020,7 +1020,9 @@ const OPPONENTS = [
line: 'HE TAUGHT YOU EVERY SHOT YOU HAVE.', speed: 1.20, pace: 1.20, wide: 1.22, flub: 0.72 },
{ short: 'CLUB PRO', name: 'THE CLUB PRO',
line: 'HE GAVE LESSONS ON THIS COURT.', speed: 1.16, pace: 1.22, wide: 1.20, flub: 0.70 },
{ short: 'THE BULK', name: 'THE BULK', fam: 'br', h: 2.0,
// THE BRUTE, not "THE BULK": the gore mode's stats screen has called this body THE BRUTE since it
// was the pole-dragger (ENEMIES.g below), and one character may not carry two names.
{ short: 'BRUTE', name: 'THE BRUTE', fam: 'br', h: 2.0,
line: 'NOTHING GETS PAST HIM TWICE.', speed: 1.02, pace: 1.34, wide: 1.24, flub: 0.66 },
{ short: 'DIRECTOR', name: 'THE DIRECTOR', fam: 'dr',
line: 'HE CAN HAVE YOU REMOVED.', speed: 1.26, pace: 1.28, wide: 1.28, flub: 0.60 },
+29 -6
View File
@@ -226,7 +226,7 @@ function farB(z) {
// Asset cache-buster — bump with main.js?v=N (see the img.src note below). EXPORTED because the
// menu screens hang <img> tags on the same files, and two copies of this number is one copy too
// many: the one that does not get bumped is the one that serves a cached 404.
export const SPRV = 138;
export const SPRV = 139;
// The boot bar in main.js watches this. Nothing here CHANGES when things load — every sprite was
// already requested at import — it just makes the wait visible and holds the menu back until the
// last sheet has landed. See the onerror note below for the other half of "sometimes missing".
@@ -1957,8 +1957,15 @@ function cardBody(c, dx) {
const tw = ctx.measureText(title).width;
const tSize = tw > room ? Math.max(13, Math.floor(22 * room / tw)) : 22;
const lines = [[title, B(tSize), 0, color]];
if (sub1) lines.push([sub1, F(8), 30, UI.cream]);
if (sub2) lines.push([sub2, F(8), 44, UI.cream]);
// THE SUB LINES WRAP. The title shrinks to fit `room` (above), but the sub lines did neither —
// "HE MOWED THIS LAWN FOR THIRTY YEARS." ran off both edges of a phone, and because the head icon
// is placed off the widest line, an over-long sub also shoved the portrait against the left edge.
// Wrapping to the same `room` fixes both. One line each still lands on 30 / 44 exactly, so every
// card that already fit is untouched — this only ever adds a row where one was overflowing.
const subs = [];
if (sub1) subs.push(...wrapText(sub1, F(8), room));
if (sub2) subs.push(...wrapText(sub2, F(8), room));
subs.forEach((t, i) => lines.push([t, F(8), 30 + i * 14, UI.cream]));
const s = inkSpan(lines);
plate(Math.max(s.bot - s.top, ih));
const y = Math.round(CARD_MID - (s.top + s.bot) / 2);
@@ -1974,10 +1981,26 @@ function cardBody(c, dx) {
for (const [t, f, rel, col] of lines) { ctx.font = f; shadowText(t, cx, y + rel, col); }
ctx.textAlign = 'center'; ctx.restore(); // the band's neighbours (flash, countdown) assume centred
}
// The plate, sized to the `h` px of content it carries and centred on the axis. Capped at CARD_H:
// the three-line SET card is what that number was tuned for and it still fills it exactly.
// Greedy word wrap to `max` px in `font`. A word longer than the whole line gets its own row rather
// than being broken mid-word — nothing in the roster is that long, and a hyphenless break would read
// as a typo on a card that is up for four seconds.
function wrapText(t, font, max) {
ctx.font = font;
if (ctx.measureText(t).width <= max) return [t]; // the common case allocates nothing extra
const out = [];
let line = '';
for (const w of t.split(' ')) {
const next = line ? line + ' ' + w : w;
if (line && ctx.measureText(next).width > max) { out.push(line); line = w; } else line = next;
}
if (line) out.push(line);
return out;
}
// The plate, sized to the `h` px of content it carries and centred on the axis. It GROWS with a
// wrapped card (see wrapText) — the CARD_H cap was tuned when three lines was the maximum a card
// could hold, and clamping there now would leave a wrapped fourth row hanging outside its own plate.
function plate(h) {
const ph = Math.min(CARD_H, Math.round(h) + CARD_PADY * 2);
const ph = Math.round(h) + CARD_PADY * 2;
ctx.fillStyle = 'rgba(10,6,8,0.72)';
ctx.fillRect(0, Math.round(CARD_MID - ph / 2), W, ph);
}