Scoreboard: redraw as a 16-bit Wimbledon manual board
Replace the rounded-panel version with a bevelled bottle-green box (light top/left, dark bottom/right), a purple club spine, cream Georgia serif names, and the live game sunk into a recessed slot — purple with gold digits for the server. Sets/games stay quiet in divided columns; the kills-to-next-point strip sits in a recessed well. Hard edges, no radius. Designed in an offline harness, then ported; verified in tennis and splatter modes. (Local const W/H were shadowing the module canvas W/H — renamed to bW/bH.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
31fa473ec8
commit
88e46a62b8
1 file changed
+37
-30
@@ -385,54 +385,61 @@ export function render() {
|
||||
ctx.setTransform(RES, 0, 0, RES, 0, 0); // un-shake: the HUD and overlays stay rock steady
|
||||
if (G.boomFlash > 0) { ctx.fillStyle = `rgba(255,255,255,${Math.min(1, G.boomFlash / 0.08) * 0.85})`; ctx.fillRect(0, 0, W, H); }
|
||||
|
||||
// HUD — Wimbledon-style scoreboard: bottle-green board with a purple flash down the left,
|
||||
// cream serif names, a yellow ball on the server, sets/games in divided columns and the
|
||||
// current game in a purple points cell. Splatter adds a darker status strip below.
|
||||
// HUD — Wimbledon manual scoreboard as a 16-bit panel: bevelled bottle-green box (light
|
||||
// top/left, dark bottom/right), a purple club spine, cream serif names, and the LIVE game
|
||||
// sunk into a recessed slot — purple for the server, gold digits. Splatter adds a recessed
|
||||
// status well (kills-to-next-point pips + weapon stock). Hard edges, no radius = retro.
|
||||
const splat = G.mode !== 'tennis';
|
||||
const sbX = 6, sbY = 6, sbW = 150, sbH = splat ? 40 : 28, rowsB = splat ? 29 : sbH - 3;
|
||||
const board = () => { ctx.beginPath(); ctx.roundRect ? ctx.roundRect(sbX, sbY, sbW, sbH, 3) : ctx.rect(sbX, sbY, sbW, sbH); };
|
||||
board(); ctx.fillStyle = '#0b3b2a'; ctx.fill(); // Wimbledon green
|
||||
ctx.save(); board(); ctx.clip();
|
||||
ctx.fillStyle = '#4b2e83'; ctx.fillRect(sbX, sbY, 5, sbH); // the purple flash
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.05)'; ctx.fillRect(sbX, sbY, sbW, 1); // top sheen
|
||||
if (splat) { ctx.fillStyle = 'rgba(0,0,0,0.28)'; ctx.fillRect(sbX, 29, sbW, sbH - 23); } // status strip well
|
||||
ctx.restore();
|
||||
ctx.strokeStyle = 'rgba(244,239,225,0.1)'; ctx.lineWidth = 1; // column dividers
|
||||
for (const cx of (splat ? [64, 82, 100] : [82, 100])) { ctx.beginPath(); ctx.moveTo(cx, sbY + 3); ctx.lineTo(cx, rowsB); ctx.stroke(); }
|
||||
board(); ctx.strokeStyle = 'rgba(244,239,225,0.22)'; ctx.stroke(); // board edge
|
||||
const bd = { face: '#1e5b3a', hi: '#3f9560', lo: '#0e3624', slot: '#0b2b1c', slotSvr: '#2b1a46',
|
||||
spine: '#472a70', spineHi: '#5f3d94', cream: '#f2ead6', dim: '#89a08f', gold: '#e6ee3a', engHi: '#2c7a4f' };
|
||||
const R = (x, y, w, h, c) => { ctx.fillStyle = c; ctx.fillRect(x, y, w, h); };
|
||||
const ptTxt = i => score.pts[0] >= 3 && score.pts[1] >= 3
|
||||
? (score.pts[i] === score.pts[1 - i] ? '40' : score.pts[i] > score.pts[1 - i] ? 'AD' : '40')
|
||||
: ['0', '15', '30', '40'][Math.min(score.pts[i], 3)];
|
||||
ctx.save(); ctx.translate(6, 6);
|
||||
const bW = 150, bH = splat ? 42 : 30, rowsB = splat ? 28 : bH - 2;
|
||||
R(0, 0, bW, bH, bd.face); // panel face
|
||||
R(0, 0, bW, 1, bd.hi); R(0, 0, 1, bH, bd.hi); // bevel highlight (top/left)
|
||||
R(0, bH - 1, bW, 1, bd.lo); R(bW - 1, 0, 1, bH, bd.lo); // bevel shadow (bottom/right)
|
||||
R(1, 1, 5, bH - 2, bd.spine); R(1, 1, 5, 1, bd.spineHi); R(6, 1, 1, bH - 2, bd.lo); // purple club spine
|
||||
for (const cx of (splat ? [58, 74, 90] : [74, 90])) { R(cx, 3, 1, rowsB - 4, bd.lo); R(cx + 1, 3, 1, rowsB - 4, bd.engHi); } // engraved column dividers
|
||||
R(7, 13, 84, 1, bd.lo); R(7, 14, 84, 1, bd.engHi); // engraved row divider (name→games only)
|
||||
ctx.textBaseline = 'middle';
|
||||
const names = splat ? ['YOU', 'THEM'] : ['YOU', 'CPU'];
|
||||
const px = 92, pw = bW - px - 3; // recessed points slot
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const rowY = 13 + i * 11, serving = (score.server === 'P') === (i === 0);
|
||||
ctx.fillStyle = serving ? '#4b2e83' : 'rgba(244,239,225,0.05)'; ctx.fillRect(101, rowY - 5, 51, 10); // points cell (purple for the server)
|
||||
if (serving) { ctx.fillStyle = '#e8f03a'; ctx.beginPath(); ctx.arc(15, rowY, 2.4, 0, 7); ctx.fill(); ctx.strokeStyle = '#0b3b2a'; ctx.lineWidth = 0.6; ctx.stroke(); } // the ball marks the server
|
||||
const cy = 8 + i * 11, svr = (score.server === 'P') === (i === 0), sy = cy - 5;
|
||||
R(px, sy, pw, 11, svr ? bd.slotSvr : bd.slot); // slot well
|
||||
R(px, sy, pw, 1, '#05160e'); R(px, sy, 1, 11, '#05160e'); // inset shadow (top/left)
|
||||
R(px, sy + 10, pw, 1, bd.engHi); R(px + pw - 1, sy, 1, 11, svr ? bd.spineHi : bd.engHi); // inset light (bottom/right)
|
||||
if (svr) { ctx.fillStyle = bd.gold; ctx.beginPath(); ctx.arc(11, cy, 2.4, 0, 7); ctx.fill(); ctx.strokeStyle = '#0a2417'; ctx.lineWidth = 0.6; ctx.stroke(); } // server holds the ball
|
||||
ctx.textAlign = 'left'; ctx.font = 'bold 9px Georgia, "Times New Roman", serif';
|
||||
ctx.fillStyle = serving ? '#f6f1e3' : '#9db3a7'; ctx.fillText(names[i], 21, rowY + 0.5);
|
||||
ctx.fillStyle = svr ? bd.cream : bd.dim; ctx.fillText(names[i], 16, cy + 0.5);
|
||||
ctx.textAlign = 'center';
|
||||
if (splat) { ctx.fillStyle = '#8fa79a'; ctx.font = 'bold 8px Georgia, serif'; ctx.fillText(String(G.sets[i]), 73, rowY + 0.5); } // sets
|
||||
ctx.fillStyle = '#dfe7df'; ctx.font = 'bold 9px Georgia, serif'; ctx.fillText(String(score.games[i]), 91, rowY + 0.5); // games
|
||||
ctx.fillStyle = serving ? '#f6f1e3' : '#b9cabf'; ctx.font = 'bold 9px Georgia, serif'; ctx.fillText(ptTxt(i), 126, rowY + 0.5); // current game
|
||||
if (splat) { ctx.font = 'bold 8px Georgia, serif'; ctx.fillStyle = bd.dim; ctx.fillText(String(G.sets[i]), 66, cy + 0.5); } // sets
|
||||
ctx.font = 'bold 9px Georgia, serif'; ctx.fillStyle = bd.cream; ctx.fillText(String(score.games[i]), 82, cy + 0.5); // games
|
||||
const pt = ptTxt(i); ctx.font = 'bold ' + (pt.length > 1 ? 9 : 10) + 'px Georgia, serif';
|
||||
ctx.fillStyle = svr ? bd.gold : bd.cream; ctx.fillText(pt, px + pw / 2, cy + 0.5); // live game
|
||||
}
|
||||
if (splat) { // status strip: kills-to-next-point pips + weapon stock
|
||||
const sy = 35;
|
||||
if (splat) { // recessed status well: kills-to-next-point pips + weapon stock
|
||||
const y0 = 29, sy = 35;
|
||||
R(7, y0, bW - 9, bH - y0 - 2, '#0a2418'); R(7, y0, bW - 9, 1, '#05160e');
|
||||
ctx.textAlign = 'left';
|
||||
if (TUNE.killPips && TUNE.killsPerPoint > 1) { // "how many zombies to the next point" — empty pips are kills to go
|
||||
ctx.fillStyle = '#7f9488'; ctx.font = 'bold 6px Georgia, serif'; ctx.fillText('NEXT', 12, sy + 0.5);
|
||||
if (TUNE.killPips && TUNE.killsPerPoint > 1) { // empty pips = zombies still to go before the next point
|
||||
ctx.font = 'bold 6px Georgia, serif'; ctx.fillStyle = bd.dim; ctx.fillText('NEXT', 12, sy + 0.5);
|
||||
for (let k = 0; k < TUNE.killsPerPoint; k++) {
|
||||
ctx.beginPath(); ctx.arc(36 + k * 7, sy, 2.2, 0, 7);
|
||||
if (k < G.killCount) { ctx.fillStyle = '#e8f03a'; ctx.fill(); }
|
||||
else { ctx.strokeStyle = '#54756a'; ctx.lineWidth = 1; ctx.stroke(); }
|
||||
if (k < G.killCount) { ctx.fillStyle = bd.gold; ctx.fill(); }
|
||||
else { ctx.strokeStyle = '#3a5a4a'; ctx.lineWidth = 1; ctx.stroke(); }
|
||||
}
|
||||
}
|
||||
const wx = 80; // weapon stock
|
||||
const wx = 82; // weapon stock
|
||||
for (let b = 0; b < G.bombs; b++) { ctx.fillStyle = '#111'; ctx.beginPath(); ctx.arc(wx + b * 8, sy, 2.6, 0, 7); ctx.fill(); ctx.strokeStyle = '#ff8010'; ctx.lineWidth = 0.75; ctx.stroke(); }
|
||||
for (let b = 0; b < G.triples; b++) { ctx.fillStyle = '#e8f03a'; ctx.beginPath(); ctx.arc(wx + b * 8, sy, 2.6, 0, 7); ctx.fill(); ctx.fillRect(wx + b * 8 - 4, sy - 1, 1.5, 1.5); ctx.fillRect(wx + b * 8 + 3, sy - 1, 1.5, 1.5); }
|
||||
for (let b = 0; b < G.triples; b++) { ctx.fillStyle = bd.gold; ctx.beginPath(); ctx.arc(wx + b * 8, sy, 2.6, 0, 7); ctx.fill(); ctx.fillRect(wx + b * 8 - 4, sy - 1, 1.5, 1.5); ctx.fillRect(wx + b * 8 + 3, sy - 1, 1.5, 1.5); }
|
||||
for (let b = 0; b < G.flames; b++) { ctx.fillStyle = '#ff8010'; ctx.beginPath(); ctx.arc(wx + b * 8, sy, 2.6, 0, 7); ctx.fill(); ctx.fillStyle = '#ffe8a0'; ctx.fillRect(wx + b * 8 - 1, sy - 1, 2, 2); }
|
||||
if (G.mgT > 0) { ctx.fillStyle = '#0a2a1e'; ctx.fillRect(wx, sy - 2.5, 40, 5); ctx.fillStyle = '#e8f03a'; ctx.fillRect(wx, sy - 2.5, 40 * Math.min(1, G.mgT / TUNE.mgPickupLen), 5); }
|
||||
if (G.mgT > 0) { ctx.fillStyle = '#06180f'; ctx.fillRect(wx, sy - 2.5, 40, 5); ctx.fillStyle = bd.gold; ctx.fillRect(wx, sy - 2.5, 40 * Math.min(1, G.mgT / TUNE.mgPickupLen), 5); }
|
||||
}
|
||||
ctx.restore();
|
||||
ctx.textBaseline = 'top'; ctx.textAlign = 'center';
|
||||
if (G.mode === 'eaten') { // the run summary — permadeath makes this scoreboard the whole game
|
||||
ctx.fillStyle = 'rgba(10,4,6,0.72)'; ctx.fillRect(0, 0, W, H);
|
||||
|
||||
Reference in new issue
Block a user