diff --git a/assets/z_brhead.png b/assets/z_brhead.png index 61192ae..eb6e1ed 100644 Binary files a/assets/z_brhead.png and b/assets/z_brhead.png differ diff --git a/assets/z_chhead.png b/assets/z_chhead.png index 2ad747b..fc993fd 100644 Binary files a/assets/z_chhead.png and b/assets/z_chhead.png differ diff --git a/assets/z_drhead.png b/assets/z_drhead.png index 352799a..86b0795 100644 Binary files a/assets/z_drhead.png and b/assets/z_drhead.png differ diff --git a/assets/z_gkhead.png b/assets/z_gkhead.png index 71040ed..d2cbdcc 100644 Binary files a/assets/z_gkhead.png and b/assets/z_gkhead.png differ diff --git a/assets/z_mahead.png b/assets/z_mahead.png index d79bf87..2877e4d 100644 Binary files a/assets/z_mahead.png and b/assets/z_mahead.png differ diff --git a/assets/z_rhead.png b/assets/z_rhead.png index 869a28a..cfbd7b7 100644 Binary files a/assets/z_rhead.png and b/assets/z_rhead.png differ diff --git a/index.html b/index.html index 70df955..e8a6351 100644 --- a/index.html +++ b/index.html @@ -659,6 +659,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') { - + diff --git a/src/config.js b/src/config.js index b3655be..58dade0 100644 --- a/src/config.js +++ b/src/config.js @@ -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 }, diff --git a/src/render.js b/src/render.js index 9294e48..3a38d60 100644 --- a/src/render.js +++ b/src/render.js @@ -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 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); }