UI polish: logo start screen, broadcast scoreboard, kill pips, best-run
- start screen: SPLATTER TENNIS logo + slogan, controls kept - scoreboard redrawn as a real broadcast board (server ball, name, sets, games, highlighted points cell); weapon stock moved to a status strip - kills-to-next-point pips (NEXT ○○○) so the K-kill gate is visible; TUNE.killPips toggles it - miss-debug HUD off by default (TUNE.missHud 0), dev-only now - eaten screen crows "NEW BEST" when the run beats the stored high score Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
aa8d2fff36
commit
ae8c04d413
5 files changed
+79
-48
No files matched your search
@@ -385,46 +385,62 @@ 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 — scoreboard
|
||||
ctx.fillStyle = 'rgba(12,16,24,0.82)'; ctx.fillRect(6, 6, 132, 30);
|
||||
ctx.strokeStyle = '#5a6a85'; ctx.strokeRect(6.5, 6.5, 131, 29);
|
||||
ctx.font = 'bold 9px monospace'; ctx.textBaseline = 'top';
|
||||
const ptTxt = i => score.pts[i] >= 3 && 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'))
|
||||
// HUD — broadcast scoreboard: two player rows (server ball · name · sets · games · points),
|
||||
// then a splatter status strip (kills-to-next-point pips + weapon stock).
|
||||
const splat = G.mode !== 'tennis';
|
||||
const sbW = 132, sbH = splat ? 37 : 25;
|
||||
ctx.fillStyle = 'rgba(10,14,22,0.85)'; ctx.fillRect(6, 6, sbW, sbH);
|
||||
ctx.strokeStyle = '#42506e'; ctx.strokeRect(6.5, 6.5, sbW - 1, sbH - 1);
|
||||
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)];
|
||||
const setsTxt = i => G.mode === 'tennis' ? '' : G.sets[i] + '·'; // splatter scoreboard: SETS·GAMES PTS
|
||||
ctx.fillStyle = score.server === 'P' ? '#f8e048' : '#c8d0dc';
|
||||
ctx.fillText('YOU ' + setsTxt(0) + score.games[0] + ' ' + ptTxt(0), 12, 10);
|
||||
for (let b = 0; b < G.bombs; b++) { // stocked bomb balls: black pips with an orange fuse rim
|
||||
ctx.fillStyle = '#111'; ctx.beginPath(); ctx.arc(96 + b * 8, 14, 2.6, 0, 7); ctx.fill();
|
||||
ctx.strokeStyle = '#ff8010'; ctx.lineWidth = 0.75; ctx.stroke();
|
||||
ctx.textBaseline = 'middle';
|
||||
const names = splat ? ['YOU', 'THEM'] : ['YOU', 'CPU'];
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const rowY = 12 + i * 11, serving = (score.server === 'P') === (i === 0);
|
||||
ctx.textAlign = 'left';
|
||||
if (serving) { ctx.fillStyle = '#f8e048'; ctx.beginPath(); ctx.arc(15, rowY, 2.3, 0, 7); ctx.fill(); } // the server holds the ball
|
||||
ctx.font = 'bold 9px monospace'; ctx.fillStyle = serving ? '#ffffff' : '#9aa6ba';
|
||||
ctx.fillText(names[i], 21, rowY + 0.5);
|
||||
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
|
||||
ctx.fillStyle = '#c2ccda'; ctx.font = 'bold 9px monospace'; ctx.fillText(String(score.games[i]), 72, rowY + 0.5); // games column
|
||||
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 ? '#f8e048' : '#dfe6f2'; ctx.font = 'bold 9px monospace'; ctx.fillText(ptTxt(i), 99, rowY + 0.5);
|
||||
}
|
||||
for (let b = 0; b < G.triples; b++) { // triple serves: a yellow pip flanked by two dots
|
||||
ctx.fillStyle = '#e8f03a'; ctx.beginPath(); ctx.arc(96 + b * 8, 14, 2.6, 0, 7); ctx.fill();
|
||||
ctx.fillRect(96 + b * 8 - 4, 13, 1.5, 1.5); ctx.fillRect(96 + b * 8 + 3, 13, 1.5, 1.5);
|
||||
if (splat) { // status strip under the two rows
|
||||
const sy = 34;
|
||||
ctx.textAlign = 'left';
|
||||
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);
|
||||
for (let k = 0; k < TUNE.killsPerPoint; k++) {
|
||||
ctx.beginPath(); ctx.arc(35 + k * 7, sy, 2.2, 0, 7);
|
||||
if (k < G.killCount) { ctx.fillStyle = '#e8f03a'; ctx.fill(); }
|
||||
else { ctx.strokeStyle = '#54607a'; ctx.lineWidth = 1; ctx.stroke(); }
|
||||
}
|
||||
}
|
||||
const wx = 66; // weapon stock, moved off the player rows so the board reads clean
|
||||
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.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); }
|
||||
}
|
||||
for (let b = 0; b < G.flames; b++) { // flame balls: orange pip, hot core
|
||||
ctx.fillStyle = '#ff8010'; ctx.beginPath(); ctx.arc(96 + b * 8, 14, 2.6, 0, 7); ctx.fill();
|
||||
ctx.fillStyle = '#ffe8a0'; ctx.fillRect(96 + b * 8 - 1, 13, 2, 2);
|
||||
}
|
||||
if (G.mgT > 0) { // machine-gun time: a draining ammo bar
|
||||
ctx.fillStyle = '#333'; ctx.fillRect(96, 11, 34, 5);
|
||||
ctx.fillStyle = '#f8e048'; ctx.fillRect(96, 11, 34 * Math.min(1, G.mgT / TUNE.mgPickupLen), 5);
|
||||
}
|
||||
ctx.fillStyle = score.server === 'O' ? '#f8e048' : '#c8d0dc';
|
||||
ctx.fillText((G.mode === 'tennis' ? 'CPU ' : 'THEM ') + setsTxt(1) + score.games[1] + ' ' + ptTxt(1), 12, 22);
|
||||
ctx.textAlign = 'center';
|
||||
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);
|
||||
ctx.fillStyle = '#c02020'; ctx.font = 'bold 16px monospace';
|
||||
ctx.fillText('GAME, SET, MASSACRE.', W / 2, H / 2 - 32);
|
||||
ctx.fillText('GAME, SET, MASSACRE.', W / 2, H / 2 - 40);
|
||||
ctx.fillStyle = '#c8d0dc'; ctx.font = 'bold 10px monospace';
|
||||
ctx.fillText('SURVIVED ' + G.runGames + ' GAMES · ' + G.sets[0] + ' SETS', W / 2, H / 2 - 6);
|
||||
if (G.best) {
|
||||
ctx.fillText('SURVIVED ' + G.runGames + ' GAMES · ' + G.sets[0] + ' SETS', W / 2, H / 2 - 14);
|
||||
if (G.newBest) { // a fresh record crows; otherwise show the mark to beat
|
||||
const pulse = 0.55 + 0.45 * Math.abs(Math.sin(G.eatenT * 4));
|
||||
ctx.fillStyle = `rgba(248,224,72,${pulse})`; ctx.font = 'bold 11px monospace';
|
||||
ctx.fillText('★ NEW BEST ★', W / 2, H / 2 + 4);
|
||||
} else if (G.best && G.best.games > 0) {
|
||||
ctx.fillStyle = '#8fa0b8'; ctx.font = 'bold 8px monospace';
|
||||
ctx.fillText('BEST ' + G.best.games + ' GAMES · ' + G.best.sets + ' SETS', W / 2, H / 2 + 10);
|
||||
ctx.fillText('BEST ' + G.best.games + ' GAMES · ' + G.best.sets + ' SETS', W / 2, H / 2 + 4);
|
||||
}
|
||||
if (G.eatenT > 0.7) { ctx.fillStyle = '#f8e048'; ctx.font = 'bold 8px monospace'; ctx.fillText('TAP TO PLAY AGAIN', W / 2, H / 2 + 28); }
|
||||
if (G.eatenT > 0.7) { ctx.fillStyle = '#f8e048'; ctx.font = 'bold 8px monospace'; ctx.fillText('TAP TO PLAY AGAIN', W / 2, H / 2 + 26); }
|
||||
}
|
||||
if (G.phase === 'pretoss' && score.server === 'P' && G.mode !== 'eaten' && !G.horde && !(G.mgT > 0)) {
|
||||
ctx.fillStyle = '#f8e048'; ctx.font = 'bold 8px monospace';
|
||||
|
||||
Reference in new issue
Block a user