Scoreboard: Wimbledon styling — green board, purple flash, serif names

Bottle-green board with a purple flash down the left, cream Georgia serif
names, a yellow ball marking the server, and the live game in a purple
points cell. Sets/games in divided columns; the kills-to-next-point strip
tucks into a darker well below. Verified in tennis and splatter modes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 4.8 committed 2026-07-13 15:51:45 +02:00
1 parent ae8c04d413
commit 31fa473ec8
1 file changed
+29 -21
+29 -21
View File
@@ -385,45 +385,53 @@ export function render() {
ctx.setTransform(RES, 0, 0, RES, 0, 0); // un-shake: the HUD and overlays stay rock steady 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); } 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 — broadcast scoreboard: two player rows (server ball · name · sets · games · points), // HUD — Wimbledon-style scoreboard: bottle-green board with a purple flash down the left,
// then a splatter status strip (kills-to-next-point pips + weapon stock). // 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.
const splat = G.mode !== 'tennis'; const splat = G.mode !== 'tennis';
const sbW = 132, sbH = splat ? 37 : 25; const sbX = 6, sbY = 6, sbW = 150, sbH = splat ? 40 : 28, rowsB = splat ? 29 : sbH - 3;
ctx.fillStyle = 'rgba(10,14,22,0.85)'; ctx.fillRect(6, 6, sbW, sbH); const board = () => { ctx.beginPath(); ctx.roundRect ? ctx.roundRect(sbX, sbY, sbW, sbH, 3) : ctx.rect(sbX, sbY, sbW, sbH); };
ctx.strokeStyle = '#42506e'; ctx.strokeRect(6.5, 6.5, sbW - 1, sbH - 1); 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 ptTxt = i => score.pts[0] >= 3 && score.pts[1] >= 3 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') ? (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)]; : ['0', '15', '30', '40'][Math.min(score.pts[i], 3)];
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
const names = splat ? ['YOU', 'THEM'] : ['YOU', 'CPU']; const names = splat ? ['YOU', 'THEM'] : ['YOU', 'CPU'];
for (let i = 0; i < 2; i++) { for (let i = 0; i < 2; i++) {
const rowY = 12 + i * 11, serving = (score.server === 'P') === (i === 0); const rowY = 13 + i * 11, serving = (score.server === 'P') === (i === 0);
ctx.textAlign = 'left'; 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 = '#f8e048'; ctx.beginPath(); ctx.arc(15, rowY, 2.3, 0, 7); ctx.fill(); } // the server holds the ball 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
ctx.font = 'bold 9px monospace'; ctx.fillStyle = serving ? '#ffffff' : '#9aa6ba'; ctx.textAlign = 'left'; ctx.font = 'bold 9px Georgia, "Times New Roman", serif';
ctx.fillText(names[i], 21, rowY + 0.5); ctx.fillStyle = serving ? '#f6f1e3' : '#9db3a7'; ctx.fillText(names[i], 21, rowY + 0.5);
ctx.textAlign = 'center'; ctx.textAlign = 'center';
if (splat) { ctx.fillStyle = '#78849a'; ctx.font = 'bold 8px monospace'; ctx.fillText(String(G.sets[i]), 56, rowY + 0.5); } // sets column if (splat) { ctx.fillStyle = '#8fa79a'; ctx.font = 'bold 8px Georgia, serif'; ctx.fillText(String(G.sets[i]), 73, rowY + 0.5); } // sets
ctx.fillStyle = '#c2ccda'; ctx.font = 'bold 9px monospace'; ctx.fillText(String(score.games[i]), 72, rowY + 0.5); // games column ctx.fillStyle = '#dfe7df'; ctx.font = 'bold 9px Georgia, serif'; ctx.fillText(String(score.games[i]), 91, rowY + 0.5); // games
ctx.fillStyle = serving ? 'rgba(248,224,72,0.15)' : 'rgba(255,255,255,0.05)'; ctx.fillRect(84, rowY - 6, 30, 12); // points cell ctx.fillStyle = serving ? '#f6f1e3' : '#b9cabf'; ctx.font = 'bold 9px Georgia, serif'; ctx.fillText(ptTxt(i), 126, rowY + 0.5); // current game
ctx.fillStyle = serving ? '#f8e048' : '#dfe6f2'; ctx.font = 'bold 9px monospace'; ctx.fillText(ptTxt(i), 99, rowY + 0.5);
} }
if (splat) { // status strip under the two rows if (splat) { // status strip: kills-to-next-point pips + weapon stock
const sy = 34; const sy = 35;
ctx.textAlign = 'left'; ctx.textAlign = 'left';
if (TUNE.killPips && TUNE.killsPerPoint > 1) { // "how many zombies to the next point" — empty pips are kills to go if (TUNE.killPips && TUNE.killsPerPoint > 1) { // "how many zombies to the next point" — empty pips are kills to go
ctx.fillStyle = '#6b768a'; ctx.font = 'bold 6px monospace'; ctx.fillText('NEXT', 12, sy + 0.5); ctx.fillStyle = '#7f9488'; ctx.font = 'bold 6px Georgia, serif'; ctx.fillText('NEXT', 12, sy + 0.5);
for (let k = 0; k < TUNE.killsPerPoint; k++) { for (let k = 0; k < TUNE.killsPerPoint; k++) {
ctx.beginPath(); ctx.arc(35 + k * 7, sy, 2.2, 0, 7); ctx.beginPath(); ctx.arc(36 + k * 7, sy, 2.2, 0, 7);
if (k < G.killCount) { ctx.fillStyle = '#e8f03a'; ctx.fill(); } if (k < G.killCount) { ctx.fillStyle = '#e8f03a'; ctx.fill(); }
else { ctx.strokeStyle = '#54607a'; ctx.lineWidth = 1; ctx.stroke(); } else { ctx.strokeStyle = '#54756a'; ctx.lineWidth = 1; ctx.stroke(); }
} }
} }
const wx = 66; // weapon stock, moved off the player rows so the board reads clean const wx = 80; // 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.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 = '#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.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); } 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 = '#333'; ctx.fillRect(wx, sy - 2.5, 40, 5); ctx.fillStyle = '#f8e048'; ctx.fillRect(wx, sy - 2.5, 40 * Math.min(1, G.mgT / TUNE.mgPickupLen), 5); } 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); }
} }
ctx.textBaseline = 'top'; ctx.textAlign = 'center'; ctx.textBaseline = 'top'; ctx.textAlign = 'center';
if (G.mode === 'eaten') { // the run summary — permadeath makes this scoreboard the whole game if (G.mode === 'eaten') { // the run summary — permadeath makes this scoreboard the whole game