Give the run strip a panel, round the rack, fix the duel's hint

The NEXT/GAME row was bare text on the court. That was legible over one dark
green lawn and is not over a sunlit clay crowd or a floodlit blue one, so it
gets its own rounded panel — the same thing the scoreboard art above it already
relies on. It states the sets banked now too; they had only ever appeared in the
scoreboard's own per-row column.

The weapon rack's five slots are rounded, which is what they are: buttons.

The duel is played under tennis rules — no slingshot, no rack, serve to the box
(see the !G.interlude guards in solveShot and the aim reticle) — but the bottom
hint was still telling you to pull back to aim, which is the one thing that does
nothing there. It reads as a tennis serve now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-01 12:21:53 +02:00
1 parent af30db2ff3
commit 60f7ce2cf1
2 files changed
+28 -8

No files matched your search

+1 -1
View File
@@ -117,6 +117,6 @@
<div id="courts"><h2>CHOOSE YOUR COURT</h2></div> <div id="courts"><h2>CHOOSE YOUR COURT</h2></div>
</div> </div>
</div> </div>
<script type="module" src="main.js?v=89"></script> <script type="module" src="main.js?v=90"></script>
</body> </body>
</html> </html>
+27 -7
View File
@@ -208,6 +208,14 @@ function quadXZ(x1, z1, x2, z2, color) { // filled court-plane rect (screen trap
ctx.fillStyle = color; ctx.beginPath(); ctx.fillStyle = color; ctx.beginPath();
ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.lineTo(c.x, c.y); ctx.lineTo(d.x, d.y); ctx.fill(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.lineTo(c.x, c.y); ctx.lineTo(d.x, d.y); ctx.fill();
} }
// Rounded panel. ctx.roundRect is native everywhere this game runs (Safari 16.4+), and the square
// fallback only matters on a browser that could not render the rest of this anyway.
function panel(x, y, w, h, r, fill, stroke) {
ctx.beginPath();
if (ctx.roundRect) ctx.roundRect(x, y, w, h, r); else ctx.rect(x, y, w, h);
if (fill) { ctx.fillStyle = fill; ctx.fill(); }
if (stroke) { ctx.strokeStyle = stroke; ctx.stroke(); }
}
function drawShadow(px, py, rx, ry, alpha = 0.28) { function drawShadow(px, py, rx, ry, alpha = 0.28) {
ctx.fillStyle = `rgba(0,0,0,${alpha})`; ctx.fillStyle = `rgba(0,0,0,${alpha})`;
ctx.beginPath(); ctx.ellipse(px, py, rx, Math.max(1, ry), 0, 0, 7); ctx.fill(); ctx.beginPath(); ctx.ellipse(px, py, rx, Math.max(1, ry), 0, 0, 7); ctx.fill();
@@ -601,8 +609,10 @@ function drawRack() {
ctx.save(); ctx.lineWidth = 1; ctx.save(); ctx.lineWidth = 1;
for (let i = 0; i < g.n; i++) { for (let i = 0; i < g.n; i++) {
const x = g.x0 + i * g.tw, sel = G.weapon === i, dim = stock[i] <= 0 && i > 0; const x = g.x0 + i * g.tw, sel = G.weapon === i, dim = stock[i] <= 0 && i > 0;
ctx.fillStyle = sel ? '#123a24' : '#0a2418'; ctx.fillRect(x + 1, g.y0 + 1, g.tw - 2, g.h - 2); // sunk slot // rounded slots: five separate tiles read as five buttons, which is what they are
ctx.strokeStyle = sel ? UI.pick : '#05160e'; ctx.strokeRect(x + 1.5, g.y0 + 1.5, g.tw - 3, g.h - 3); // gold frame when live panel(x + 1.5, g.y0 + 1.5, g.tw - 3, g.h - 3, 4,
sel ? '#123a24' : '#0a2418', // sunk slot
sel ? UI.pick : '#05160e'); // gold frame when live
// icon: left, vertically centred, padded off the edge // icon: left, vertically centred, padded off the edge
const box = g.h - pad * 2, icx = x + pad + box / 2, icy = g.y0 + g.h / 2; const box = g.h - pad * 2, icx = x + pad + box / 2, icy = g.y0 + g.h / 2;
ctx.globalAlpha = dim ? 0.35 : 1; ctx.globalAlpha = dim ? 0.35 : 1;
@@ -1009,12 +1019,19 @@ export function render() {
ctx.font = F(8); ctx.fillStyle = bd.cream; ctx.fillText(String(score.games[i]), 90, cy); // games ctx.font = F(8); ctx.fillStyle = bd.cream; ctx.fillText(String(score.games[i]), 90, cy); // games
ctx.font = F(8); ctx.fillStyle = svr ? bd.gold : bd.cream; ctx.fillText(ptTxt(i), 126, cy); // points, in the art's slot ctx.font = F(8); ctx.fillStyle = svr ? bd.gold : bd.cream; ctx.fillText(ptTxt(i), 126, cy); // points, in the art's slot
} }
if (splat) { // which game you're in, permanently — the 1.1s GAME N flash was the only place it showed if (splat) {
// This row used to be bare text on the court. That was fine over one dark green lawn and is
// unreadable over a sunlit clay crowd or a floodlit blue one, so it gets its own panel — the
// same trick the scoreboard art above it already relies on.
ctx.lineWidth = 1;
panel(0.5, bH + 1.5, bW - 1, 12, 3, 'rgba(8,26,18,0.82)', hexA(bd.hi, 0.35));
// which game you're in, and the sets banked, permanently — the 1.1s GAME N flash and the
// scoreboard's own per-row sets column were the only places either showed.
ctx.textAlign = 'right'; ctx.font = F(6, 0); ctx.fillStyle = bd.dim; ctx.textAlign = 'right'; ctx.font = F(6, 0); ctx.fillStyle = bd.dim;
ctx.fillText('GAME ' + (G.runGames + 1), bW, bH + 7); ctx.fillText(`GAME ${G.runGames + 1} · ${G.sets[0]} SET${G.sets[0] === 1 ? '' : 'S'}`, bW - 3, bH + 7);
} }
if (splat && TUNE.killPips && TUNE.killsPerPoint > 1) { // kills-to-next-point pips, just under the board — no box if (splat && TUNE.killPips && TUNE.killsPerPoint > 1) { // kills-to-next-point pips, on the panel above
ctx.textAlign = 'left'; ctx.font = F(6, 0); ctx.fillStyle = bd.dim; ctx.fillText('NEXT', 2, bH + 7); ctx.textAlign = 'left'; ctx.font = F(6, 0); ctx.fillStyle = bd.dim; ctx.fillText('NEXT', 4, bH + 7);
for (let k = 0; k < TUNE.killsPerPoint; k++) { for (let k = 0; k < TUNE.killsPerPoint; k++) {
ctx.beginPath(); ctx.arc(32 + k * 8, bH + 7, 2.4, 0, 7); ctx.beginPath(); ctx.arc(32 + k * 8, bH + 7, 2.4, 0, 7);
if (k < G.killCount) { ctx.fillStyle = bd.gold; ctx.fill(); } if (k < G.killCount) { ctx.fillStyle = bd.gold; ctx.fill(); }
@@ -1068,7 +1085,10 @@ export function render() {
ctx.fillStyle = UI.warm; ctx.font = F(8); ctx.fillStyle = UI.warm; ctx.font = F(8);
const touch = IS_TOUCH, switchHint = touch ? 'TAP THE RACK TO SWITCH' : '1-5 TO SWITCH'; const touch = IS_TOUCH, switchHint = touch ? 'TAP THE RACK TO SWITCH' : '1-5 TO SWITCH';
const sel = ['PLAIN', 'BOMB', 'TRIPLE', 'FIRE', 'GUN'][G.weapon]; const sel = ['PLAIN', 'BOMB', 'TRIPLE', 'FIRE', 'GUN'][G.weapon];
const txt = G.mode === 'splatter' // The DUEL is played under tennis rules — no slingshot, no rack, serve to the box (see the
// !G.interlude guards in solveShot and the aim reticle). It was still being told to pull back
// to aim, which is the one thing that does nothing there.
const txt = G.mode === 'splatter' && !G.interlude
? (G.weapon === 4 ? (touch ? 'TAP TO UNLOAD THE GUN' : 'SPACE UNLOADS THE GUN') ? (G.weapon === 4 ? (touch ? 'TAP TO UNLOAD THE GUN' : 'SPACE UNLOADS THE GUN')
: G.aim ? 'PULL BACK: FURTHER = HARDER' : G.aim ? 'PULL BACK: FURTHER = HARDER'
: G.weapon > 0 ? `SERVE FIRES: ${sel} · ${switchHint}` : G.weapon > 0 ? `SERVE FIRES: ${sel} · ${switchHint}`