From f32cac6a70c52a211873a0cf1b94b6e214cd6005 Mon Sep 17 00:00:00 2001 From: Oliver Rieger Date: Sun, 26 Jul 2026 16:15:43 +0300 Subject: [PATCH] Splatter: still camera, and the aim reads as arrows + a notched draw meter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Camera dynamics off (aimCamY/aimCamZ 0) — the knobs stay, so either direction is a value away in the T panel, but the default holds still now. The aim graphics carry the read instead. A chevron trail runs down the lane and plants an arrowhead on the target: the chevrons multiply and fatten as the draw builds, so the trail reads as pressure rather than as a static dashed line, and the direction is unmistakable now that the angle actually selects a lane. The new part is the draw meter on the apron in front of your feet, because the kill threshold was the one rule with no picture: you could hear the click and see the colour flip, but never see the line COMING. Now a white notch marks it, the fill runs cold below and hot past it, and a full draw flashes. Anchored at the feet rather than over the head — up there it sat right in the lane with the chevrons running through it — and clamped so advancing up-court can't push it into the rack. Co-Authored-By: Claude Opus 5 --- config.js | 5 +++-- render.js | 57 ++++++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 45 insertions(+), 17 deletions(-) diff --git a/config.js b/config.js index 4ef2dc5..3446781 100644 --- a/config.js +++ b/config.js @@ -47,8 +47,9 @@ export const TUNE = { // pushes in — you crouch into the shot. POSITIVE lifts it and leans back for a steeper, more // top-down look, which reads the depth behind the net better but feels less like aiming. // Both 0 = the old fixed camera. - aimCamY: -1.2, // m of camera height change - aimCamZ: -1.2, // m of camera dolly (negative = closer) + aimCamY: 0, // m of camera height change (negative drops you into the shot) + aimCamZ: 0, // m of camera dolly (negative = closer). Both 0 = the camera holds still, + // which is where Oliver landed: the aim graphics carry the read on their own. backstopZ: -1.8, // z of the far backstop — must sit IN FRONT of the -2 retire line, or balls die before they reach it backstopKeep: 0.55, // pace a ball keeps after the rebound — still lethal, slower backstopPop: 1.8, // m/s of lift off the fence, so the rebound travels back instead of dying at the net posts diff --git a/render.js b/render.js index ed85bab..5c51637 100644 --- a/render.js +++ b/render.js @@ -403,26 +403,53 @@ export function render() { if (l) drawRing(l.x, l.z, 'rgba(255,255,255,0.85)'); } if (G.outMarker) drawRing(G.outMarker.x, G.outMarker.z, G.outMarker.out ? '#ff5050' : '#f8e048'); - // slingshot aim: the pull's direction is the lane, its length is POWER. Everything on screen - // reads power, because that's the decision — a dim thin line only staggers, a hot fat one kills. + // slingshot aim. Three readings, all of them power: a chevron trail running down the lane, an + // arrowhead on the target, and a notched meter over your head. The notch is the point of the + // whole thing — it puts the kill threshold on screen instead of leaving it to be discovered. if (G.aim && G.mode === 'splatter' && !G.interlude && (G.phase === 'pretoss' || G.phase === 'toss' || G.phase === 'rally')) { const ft = flickTarget(P, G.aim); const tx = ft.zt ? ft.zt.x : ft.tx, tz = ft.zt ? ft.zt.z : ft.tz; + const kills = ft.q >= TUNE.zSplatQ; + // cool blue-white = this pull only staggers. It stays READABLE on the dark lawn: you still + // need to see where you're pointing, you just need to know it won't kill. + const col = !kills ? '#9db4d0' : ft.zt ? '#ff5050' : '#f8e048'; if (tx !== undefined) { - const kills = ft.q >= TUNE.zSplatQ; - // cool blue-white = this pull only staggers. It has to stay READABLE on the dark lawn — - // the player still needs to see where they're aiming, they just need to know it's weak. - const col = !kills ? '#9db4d0' : ft.zt ? '#ff5050' : '#f8e048'; - const a = proj(P.x, 1.4, P.z), b = proj(tx, 0, tz); - // the drawn band: it grows BEHIND you as you pull, so the gesture has a visible cost - const bl = proj(P.x - G.aim.fx * ft.f * 3, 1.2, P.z - G.aim.fy * ft.f * 3); - ctx.strokeStyle = col; ctx.lineWidth = 1 + ft.f * 2; - ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(bl.x, bl.y); ctx.stroke(); - ctx.setLineDash([3, Math.max(2, 7 - ft.f * 5)]); // dashes tighten up as the shot gets hot - ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke(); - ctx.setLineDash([]); ctx.lineWidth = 1; + const a = proj(P.x, 1.4, P.z), b = proj(tx, 0.35, tz); + const dx = b.x - a.x, dy = b.y - a.y, len = Math.hypot(dx, dy) || 1; + const ux = dx / len, uy = dy / len, px = -uy, py = ux; // unit along the lane, and its normal + // chevrons pointing at the target: they multiply and grow as the draw gets stronger, so + // the trail reads as pressure building up rather than as a static line + const n = Math.round(3 + ft.f * 5), size = 2 + ft.f * 2.5; + ctx.fillStyle = col; + for (let i = 1; i <= n; i++) { + const d = len * (i / (n + 1)), cx = a.x + ux * d, cy = a.y + uy * d; + ctx.beginPath(); + ctx.moveTo(cx + ux * size, cy + uy * size); + ctx.lineTo(cx - ux * size * 0.4 + px * size, cy - uy * size * 0.4 + py * size); + ctx.lineTo(cx - ux * size * 0.4 - px * size, cy - uy * size * 0.4 - py * size); + ctx.fill(); + } + const head = 3 + ft.f * 3.5; // the arrowhead lands ON the target, bigger the harder you pull + ctx.beginPath(); + ctx.moveTo(b.x + ux * head, b.y + uy * head); + ctx.lineTo(b.x - ux * head + px * head * 0.8, b.y - uy * head + py * head * 0.8); + ctx.lineTo(b.x - ux * head - px * head * 0.8, b.y - uy * head - py * head * 0.8); + ctx.fill(); drawRing(tx, tz, col); - if (ft.zt) { const hp = proj(ft.zt.x, 2.2, ft.zt.z); ctx.fillStyle = col; ctx.fillRect(hp.x - 1, hp.y - 4, 3, 3); } + } + // 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); + 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, my, mw, mh); } }