Thumb-scale draw meter, looser aim magnet, a bomb worth the tin

- The draw meter is a fixed bar across the bottom, ~90% of the width and 10px
  tall, with the kill notch on it. It was a 26x4 stub floating on the apron by
  the player's feet, which is nothing under a thumb at arm's length. The serve
  hint is suppressed while pulling since they share the slot. pullFull 95 -> 130:
  at 95 every pull maxed out by accident, so the meter never meant anything.
- The aim magnet took the nearest body ALONG the lane, so a close shambler a
  little off-axis stole shots clearly aimed past it. It now picks the best
  ALIGNED body, with TUNE.aimTie of slack in which a nearer one still wins — a
  straight-ahead lane still eats the nearest body first, which is the settled
  feel. aimTie: 1 restores the old behaviour exactly, for A/B.
- Bomb radius 2.5 -> 4.2 (2 kills -> 5 in a scattered ten-wave), a 120-particle
  eruption, a ring of thrown stains and a full-size pile. The blinding white
  frame is now a brief warm bloom — it was hiding the very gore it announced.
- Rack tiles 22 -> 30 tall, and the touch band is padded above and runs to the
  screen edge below. At 22 logical px they were ~30 CSS px on a phone, well under
  the ~44 a thumb expects; now ~62.

26-game soak clean with heavy bomb use, arrays stay bounded. Serve returns
136/150 = 90.7% +/-2.4%, 30 human-tapped points = 50 hits / 0 whiffs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-07-28 09:51:24 +03:00
1 parent 55ddf5932e
commit 21b9dd53fa
7 files changed
+90 -35

No files matched your search

+25 -18
View File
@@ -320,15 +320,20 @@ function drawMgRacket() { // horde: the player braces the racket forward and up-
// the courtside weapon rack along the bottom: PLAIN + the four tins. Tap a tile (or 1–5) to
// pick which one your next serve throws; empty tins dim out. Geometry is shared with input
// via rackHit so a tap lands on the tile you see.
const RACK_BOT = 10; // px of breathing room below the rack (floats it up off the screen edge)
const RACK_BOT = 8; // px of breathing room below the rack (floats it up off the screen edge)
const RACK_GRAB = 7; // px the TOUCH band extends above the drawn rack (below, it runs to the screen edge)
export const METER_Y = () => H - TUNE.rackH - RACK_BOT - 17; // the draw meter's fixed bottom slot
function rackGeom() {
const h = TUNE.rackH, n = 5, tw = Math.min(52, (W - 8) / n), rw = tw * n;
const h = TUNE.rackH, n = 5, tw = Math.min(64, (W - 4) / n), rw = tw * n;
return { x0: Math.round((W - rw) / 2), y0: H - h - RACK_BOT, tw, h, n, rw };
}
export function rackHit(nx, ny) { // normalized [0,1] canvas coords -> slot index, or -1
if (G.mode !== 'splatter' || G.interlude) return -1; // no rack during the tennis-rules duel
const g = rackGeom(), cx = nx * W, cy = ny * H;
if (cy < g.y0 || cy > g.y0 + g.h || cx < g.x0 || cx >= g.x0 + g.rw) return -1;
// The touch band is deliberately BIGGER than the art: up by RACK_GRAB and all the way down to
// the screen edge, reclaiming the dead margin below. At 22px tall the tiles were ~30 CSS px on a
// phone — well under the 44px everyone's thumb expects — which is why switching tins felt fiddly.
if (cy < g.y0 - RACK_GRAB || cx < g.x0 || cx >= g.x0 + g.rw) return -1;
return Math.max(0, Math.min(g.n - 1, ((cx - g.x0) / g.tw) | 0));
}
function drawRack() {
@@ -354,9 +359,9 @@ function drawRack() {
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.globalAlpha = dim ? 0.5 : 1;
ctx.font = F(8); ctx.fillStyle = sel ? UI.pick : '#e8f0dc';
ctx.fillText(i === 0 ? '∞' : '×' + stock[i], tx, g.y0 + pad + 5); // ball = ∞, else ×count
ctx.fillText(i === 0 ? '∞' : '×' + stock[i], tx, g.y0 + g.h * 0.33); // ball = ∞, else ×count
ctx.font = F(6, 0); ctx.fillStyle = sel ? '#f2ead6' : '#8fa89a';
ctx.fillText(labels[i], tx, g.y0 + g.h - pad - 3); // weapon label, under its count
ctx.fillText(labels[i], tx, g.y0 + g.h * 0.72); // weapon label, under its count
ctx.globalAlpha = 1;
if (i === 4 && G.mgT > 0) { ctx.fillStyle = UI.pick; ctx.fillRect(x + 2, g.y0 + g.h - 2, (g.tw - 4) * Math.min(1, G.mgT / TUNE.mgPickupLen), 1.5); } // burst countdown
}
@@ -521,18 +526,18 @@ export function render() {
ctx.globalAlpha = 1;
drawRing(tx, tz, col);
}
// the draw meter: it fills as you pull and the notch marks the kill line. Anchored on the
// apron JUST IN FRONT of your feet — over the head it sat right in the lane and the chevrons
// ran straight through it, and down here there is never anything else to collide with.
const hd = proj(P.x, 0, P.z + 1.2);
const mw = 26, mh = 4, mx = Math.round(hd.x - mw / 2);
// advanced up-court the apron runs out, so pin the meter clear of the rack and the hint line
const my = Math.min(Math.round(hd.y), H - TUNE.rackH - RACK_BOT - 28);
ctx.fillStyle = 'rgba(0,0,0,0.55)'; ctx.fillRect(mx - 1, my - 1, mw + 2, mh + 2);
// The draw meter: it fills as you pull and the notch marks the kill line. A fixed BAR ACROSS
// THE BOTTOM, ~90% of the width — it used to be a 26x4 px stub floating on the apron by your
// feet, which is nothing at arm's length with a thumb over it. Fixed position also means it
// never wanders behind the chevrons or off the top of the court.
const mw = Math.round(W * 0.9), mh = 10, mx = Math.round((W - mw) / 2);
const my = METER_Y();
ctx.fillStyle = 'rgba(0,0,0,0.62)'; ctx.fillRect(mx - 2, my - 2, mw + 4, mh + 4);
ctx.fillStyle = 'rgba(255,255,255,0.16)'; ctx.fillRect(mx, my, mw, mh); // empty channel
ctx.fillStyle = col; ctx.fillRect(mx, my, Math.round(mw * ft.f), mh);
const notch = Math.round(mw * (TUNE.zSplatQ - TUNE.pullQMin) / (1 - TUNE.pullQMin));
ctx.fillStyle = '#ffffff'; ctx.fillRect(mx + notch, my - 2, 1, mh + 4); // the kill line
if (ft.f >= 0.999 && Math.floor(G.idleT * 12) % 2) { // fully drawn: the meter flashes
ctx.fillStyle = '#ffffff'; ctx.fillRect(mx + notch, my - 3, 2, mh + 6); // the kill line
if (ft.f >= 0.999 && Math.floor(G.idleT * 12) % 2) { // fully drawn: the whole bar flashes
ctx.fillStyle = '#ffffff'; ctx.fillRect(mx, my, mw, mh);
}
}
@@ -597,7 +602,9 @@ export function render() {
if ((G.horde || G.mgT > 0) && G.mode === 'splatter') drawMgRacket(); // rambo racket over the player
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); }
// bomb bloom: a warm blood-orange wash, not a white-out. At 0.85 white it blinded you on a phone
// and hid the very gore eruption it was announcing.
if (G.boomFlash > 0) { ctx.fillStyle = `rgba(255,150,70,${Math.min(1, G.boomFlash / 0.06) * 0.3})`; ctx.fillRect(0, 0, W, H); }
// 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
@@ -665,7 +672,7 @@ export function render() {
if (G.eatenT > EATEN_RESTART) { ctx.fillStyle = UI.warm; ctx.font = F(8); ctx.fillText('TAP TO PLAY AGAIN', W / 2, H / 2 + 26); }
ctx.restore();
}
if (G.phase === 'pretoss' && score.server === 'P' && G.mode !== 'eaten' && !G.horde && !(G.mgT > 0)) {
if (G.phase === 'pretoss' && score.server === 'P' && G.mode !== 'eaten' && !G.horde && !(G.mgT > 0) && !G.aim) { // while pulling, the draw meter owns this slot
ctx.fillStyle = UI.warm; ctx.font = F(8);
const touch = IS_TOUCH, switchHint = touch ? 'TAP THE RACK TO SWITCH' : '1-5 TO SWITCH';
const sel = ['PLAIN', 'BOMB', 'TRIPLE', 'FIRE', 'GUN'][G.weapon];
@@ -676,7 +683,7 @@ export function render() {
: touch ? 'TAP TO SERVE · PULL BACK TO AIM' : 'SPACE SERVES · ARROWS AIM')
: G.serveNum === 2 ? (touch ? 'SECOND SERVE — TAP' : 'SECOND SERVE — SPACE')
: (touch ? 'TAP TO TOSS, TAP TO SERVE' : 'SPACE TO TOSS, SPACE TO SERVE');
ctx.fillText(txt, W / 2, H - TUNE.rackH - RACK_BOT - 17); // sit clear above the rack
ctx.fillText(txt, W / 2, METER_Y()); // same slot the draw meter uses
}
if (G.flash) {
ctx.font = F(12); ctx.fillStyle = '#ffffff';