Delete the wake variant; say PULL DOWN and point only downward

The switch and the losing branch are gone — a fired band is a short streak
behind the ball, full stop. TUNE.shotTail, WAKE and the tailMode branching all
removed; the aiming band is untouched as ever.

THE IDLE DEMO NO LONGER POINTS UP. It grew chevrons up-court as the pull grew,
because the shot going the opposite way to the thumb was meant to be the
lesson — but an up arrow on screen is an instruction to swipe up, and swiping
up is the one gesture this game does not have. The only direction drawn now is
the one you actually make: the dotted track, the thumb sliding down it, and a
down arrowhead at the end, clear of where the thumb lands at full draw.

And it says so in words. The mime alone was too abstract to read as "drag your
thumb down", so PULL DOWN sits under the arrow in the pixel font.

Which makes the text slot below it redundant while the demo runs, so it shuts
up rather than printing the same instruction twice 40 px apart. The wording
everywhere else moves BACK -> DOWN too: "back" is the ambiguous word that could
be read as up-court, which is exactly the confusion here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-02 22:02:41 +02:00
1 parent dbc0d5baee
commit 0774a2a31d
2 files changed
+29 -35

No files matched your search

-6
View File
@@ -168,12 +168,6 @@ export const TUNE = {
// court — so every downward pull picked the same body and the angle felt inert.
// 0.10 gives ±2.9 m out there: you have to actually point at a lane.
pullHintAfter: 1.5, // s of doing nothing in the serve stance before the pull gesture demos itself
shotTail: 1, // HOW THE AIM BAND BEHAVES ONCE FIRED. 0 = a short WAKE ahead of the ball,
// dissolving; 1 = a short TAIL behind it and nothing ahead at all. Both keep
// the aiming band itself untouched — full arc, full colour, that part is the
// information you act on. Read per frame, so the T panel flips it live — but
// the panel is keyboard-only ('T'), so on a phone this default is the setting.
// 1 is Oliver's pick; the losing branch stays until a real playtest confirms it.
aimTie: 0.12, // how much worse-aimed a NEARER body may be and still win the lane (0..1 of the
// cone half-width). 0 = pure best-aligned wins; 1 = the old "nearest in the
// lane always wins", which is what made the magnet grab the wrong shambler.
+29 -29
View File
@@ -460,12 +460,10 @@ function drawFlames(zb, hMeters) {
// information you are deciding on. Carrying that picture unchanged into the flight is what made
// the shot read as a laser — a bright, saturated, continuous line running from the projectile to
// the thing it is about to hit is the visual definition of a targeting beam, and no real ball ever
// shows its own future. So once fired the band is spent: it desaturates toward dust, collapses
// fast, and shrinks to a short streak. TUNE.shotTail picks which side of the ball that streak is on
// 0 a WAKE ahead of it, dissolving — the erase idea, with the beam cut down to a wisp
// 1 a TAIL behind it, nothing ahead at all — conventional projectile language, cannot beam
const WAKE = 0.16; // fraction of the arc drawn ahead of the ball (shotTail 0)
const TAIL = 0.15; // ...and behind it (shotTail 1)
// shows its own future. So once fired the band is spent: it desaturates toward dust, collapses,
// and shrinks to a short streak BEHIND the ball, with nothing at all drawn ahead of it.
// (A variant that put that streak ahead of the ball instead was built, compared and cut.)
const TAIL = 0.15; // fraction of the arc drawn behind the ball once it is flying
const DUST = [206, 200, 184];
const RGB = h => [parseInt(h.slice(1, 3), 16), parseInt(h.slice(3, 5), 16), parseInt(h.slice(5, 7), 16)];
const mix = (a, b, t) => a.map((v, i) => Math.round(v + (b[i] - v) * t));
@@ -476,10 +474,9 @@ function drawShotBand(a, u0, flying) {
// cool blue-white = this 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 hex = a.q >= TUNE.zSplatQ ? (a.zt ? UI.hot : UI.warm) : '#9db4d0';
const tailMode = flying && TUNE.shotTail >= 0.5;
// the slice of arc to draw, and how hard the whole thing is already gone
const lo = !flying ? 0 : tailMode ? Math.max(0, u0 - TAIL) : u0;
const hi = !flying ? 1 : tailMode ? u0 : Math.min(1, u0 + WAKE);
// the slice of arc to draw: everything while aiming, a short streak behind the ball in flight
const lo = flying ? Math.max(0, u0 - TAIL) : 0;
const hi = flying ? u0 : 1;
if (hi - lo < 0.001) return;
// Linear, not squared. Squared made both variants basically gone by a third of the flight, which
// is not "less like a laser", it is "no streak at all" — and the strobing at release is the thing
@@ -493,7 +490,7 @@ function drawShotBand(a, u0, flying) {
const uAt = i => lo + (hi - lo) * (i / N);
// s: 0 at the far end of the slice, 1 at the ball (or at the racket, while aiming) — so the
// streak is always brightest where the action is and dies off away from it.
const sAt = i => tailMode ? i / N : 1 - i / N;
const sAt = i => flying ? i / N : 1 - i / N;
// The shadow first, under everything: an arc hanging in the air is ambiguous about where it
// comes down until something pins it to the lawn. This is the whole 3D cue. It follows the same
// slice — "nothing ahead" has to mean the shadow too, or the ground still gives the future away.
@@ -786,22 +783,22 @@ function drawPullHint() {
const len = travel * draw;
ctx.save();
ctx.globalAlpha = gone ? 0 : 0.85;
// the dotted track the thumb travels, straight down-screen = "pull back"
// the dotted track the thumb travels, straight down-screen
ctx.fillStyle = UI.dim;
for (let d = 7; d < travel + 4; d += 7) ctx.fillRect(ax - 1, ay + d, 2, 2);
// chevrons growing UP-court as the pull grows — the inverse relationship is the whole lesson.
// They start above his HEAD, not at his feet: pinned to the apron anchor they were drawn across
// his own legs, which read as clutter on the sprite rather than as a shot leaving him.
const head = proj(P.x, 2.4, P.z).y;
// A DOWN arrowhead at the far end of the track, and nothing pointing up anywhere. The demo used
// to grow chevrons UP-court as the pull grew — the shot going the opposite way to the thumb was
// meant to be the lesson, but an up arrow on screen is an instruction to swipe up, and that is
// the one gesture this does not have. The only direction drawn is the one you actually make.
ctx.fillStyle = UI.warm;
for (let i = 0; i < 3; i++) {
const u = (i + 1) / 4;
if (draw < u * 0.9) continue;
const cy = head - 6 - i * 9, s = 5;
ctx.beginPath();
ctx.moveTo(ax, cy - s); ctx.lineTo(ax + s, cy + s * 0.7); ctx.lineTo(ax - s, cy + s * 0.7);
ctx.fill();
}
const av = ay + travel + 16, s = 5; // clear of where the thumb lands at full draw
ctx.beginPath();
ctx.moveTo(ax, av + s); ctx.lineTo(ax + s, av - s * 0.7); ctx.lineTo(ax - s, av - s * 0.7);
ctx.fill();
// ...and say it in words. The mime alone was too abstract to read as "drag your thumb down".
ctx.fillStyle = UI.warm; ctx.font = F(8);
ctx.textAlign = 'center'; ctx.textBaseline = 'top';
ctx.fillText('PULL DOWN', ax, av + 8);
// the ghost thumb
ctx.globalAlpha = gone ? 0 : 0.9;
ctx.fillStyle = UI.cream;
@@ -1193,16 +1190,19 @@ export function render() {
// 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.
// after a beat of inaction the apron demo takes over, and it says PULL DOWN in its own words —
// so this slot shuts up rather than printing the same instruction a second time 40 px below it
const demo = G.mode === 'splatter' && !G.interlude && G.weapon !== 4 && G.idleHintT > TUNE.pullHintAfter;
const txt = G.mode === 'splatter' && !G.interlude
? (G.weapon === 4 ? (touch ? 'TAP TO UNLOAD THE GUN' : 'SPACE UNLOADS THE GUN')
: G.aim ? 'PULL BACK: FURTHER = HARDER'
: G.aim ? 'PULL DOWN: FURTHER = HARDER'
: G.weapon > 0 ? `SERVE FIRES: ${sel} · ${switchHint}`
: touch ? 'PULL BACK TO AIM · RELEASE TO FIRE' : 'DRAG BACK TO AIM · SPACE SERVES')
: demo ? ''
: touch ? 'PULL DOWN TO AIM · RELEASE TO FIRE' : 'DRAG DOWN TO AIM · SPACE SERVES')
: 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, METER_Y()); // same slot the draw meter uses
// and after a beat of inaction, show the gesture instead of only naming it
if (G.mode === 'splatter' && !G.interlude && G.weapon !== 4 && G.idleHintT > TUNE.pullHintAfter) drawPullHint();
if (txt) ctx.fillText(txt, W / 2, METER_Y()); // same slot the draw meter uses
if (demo) drawPullHint(); // show the gesture instead of only naming it
}
if (G.flash) {
ctx.font = F(12); ctx.fillStyle = '#ffffff';