Splatter: still camera, and the aim reads as arrows + a notched draw meter

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 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-07-26 16:15:43 +03:00
1 parent e85345e0d8
commit f32cac6a70
2 files changed
+45 -17

No files matched your search

+42 -15
View File
@@ -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);
}
}