Let the ball eat its own aim band, mark the body, grow the machine
Four things from the phone. THE BAND NO LONGER BLINKS OUT. Cutting it at the instant of release made a fast rally strobe — band, gone, band, gone. hitServe now records the arc it actually fired (error included, so the ball tracks what it is consuming to within a centimetre) and the ball eats it from behind as it flies. The aim resolves into the shot instead of vanishing. It ends early when the ball does, because a shambler swallowing the shot is a cut you want. THE TARGET MARK SITS ON THE BODY. A ring on the grass pointed at the one part of a shambler the shot has nothing to do with — the ball strikes anywhere under 1.9 m and the wound lands on the torso. So the mark rides at chest height when there is a body to hit and lies flat only when the lane is empty. Filled and washed out in the band's own colour instead of a hard 2px ring, which stops it reading as a decal stuck to the lawn. x_howto REDRAWN for the current gesture: thumb dragging back to a print, the shot leaving as a curved band onto the shambler's chest, shadow dots beneath. The old one still taught a straight arrow. MACH_H 1.15 -> 1.55. Waist high read as a prop you could ignore. Verified on a cache-busted build (the pane was serving a stale config.js, so the first pass measured nothing): preview velocity and gravity still identical to the fired ball, same flight time in the trail, unravel smooth to the hit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3a91d87b08
commit
0ce857fedc
4 files changed
+78
-40
No files matched your search
+19
-2
@@ -29,6 +29,9 @@ export function predictLanding() { // first-bounce point, closed form — exact,
|
||||
// lane is the target; the cone widens with range so distant lanes stay pickable.
|
||||
export const pullFrac = mag => Math.max(0, Math.min(1, (mag - TUNE.pullMin) / (TUNE.pullFull - TUNE.pullMin)));
|
||||
export const pullQuality = mag => TUNE.pullQMin + pullFrac(mag) * (1 - TUNE.pullQMin);
|
||||
// pullQuality inverted: hitServe is handed a quality, not a drag, and the band it leaves behind is
|
||||
// drawn at the same weight the live preview had.
|
||||
export const qualityFrac = q => Math.max(0, Math.min(1, (q - TUNE.pullQMin) / (1 - TUNE.pullQMin)));
|
||||
|
||||
export function flickTarget(ch, aim) { // exported: render draws the live reticle with it
|
||||
const q = pullQuality(aim.mag), f = pullFrac(aim.mag);
|
||||
@@ -199,8 +202,14 @@ export function previewServe(aim) {
|
||||
const st = splatServeTarget(P, aim, s, c.x, c.z);
|
||||
if (!st) return null;
|
||||
const { v, g } = serveArc(c.x, c.y, c.z, st.tx, st.tz, q);
|
||||
return { x0: c.x, y0: c.y, z0: c.z, v, g, tx: st.tx, tz: st.tz, zt: st.ft.zt,
|
||||
q, f: pullFrac(aim.mag) };
|
||||
return shotBand(c.x, c.y, c.z, v, g, st.tx, st.tz, !!st.ft.zt, q);
|
||||
}
|
||||
|
||||
// The drawable form of a shot: everything render needs and nothing that would pin a zombie object
|
||||
// alive (`zt` is a flag, not the body — the trail below outlives whatever it was aimed at).
|
||||
function shotBand(x0, y0, z0, v, g, tx, tz, zt, q) {
|
||||
const T = Math.hypot(tx - x0, tz - z0) / (Math.hypot(v.vx, v.vz) || 1); // flight time to target
|
||||
return { x0, y0, z0, v, g, tx, tz, zt, q, f: qualityFrac(q), T, t: 0 };
|
||||
}
|
||||
|
||||
function hitServe(sv, quality, aim = null) {
|
||||
@@ -218,6 +227,11 @@ function hitServe(sv, quality, aim = null) {
|
||||
tzs = boxZ + (Math.random() * 2 - 1) * (1 - quality) * 1.5;
|
||||
}
|
||||
const { v, g } = serveArc(ball.x, ball.y, ball.z, boxX, tzs, quality);
|
||||
// THE BALL ERASES ITS OWN AIM. Cutting the band at the instant of release made a fast rally
|
||||
// strobe — band, gone, band, gone. Instead the arc stays and the ball eats it from behind as it
|
||||
// flies, so the aim resolves into the shot rather than blinking out of existence. This is the
|
||||
// ACTUAL launch, error included, so the ball tracks the band it is consuming exactly.
|
||||
if (st) G.trail = shotBand(ball.x, ball.y, ball.z, v, g, boxX, tzs, !!st.ft.zt, quality);
|
||||
ball.g = g;
|
||||
ball.vx = v.vx; ball.vy = v.vy; ball.vz = v.vz;
|
||||
ball.lastHit = sv.side; ball.bounces = 0;
|
||||
@@ -993,6 +1007,9 @@ export function update(dt) {
|
||||
crowd.cheer = Math.max(0, crowd.cheer - dt);
|
||||
G.idleT += dt;
|
||||
if (G.asideT > 0) G.asideT -= dt; // the asides' shared cooldown (see aside())
|
||||
// the fired band unravels behind the ball. It also goes the moment the ball does — a shambler
|
||||
// eating the shot ends the band with the splat, which is a cut you want.
|
||||
if (G.trail && ((G.trail.t += dt) >= G.trail.T || !ball.active)) G.trail = null;
|
||||
// "Time." belongs to the changeover alone. It used to ride the generic 'over' expiry, so any
|
||||
// other beat that overwrote overT while the breather ran (a melee kill taking a game, say)
|
||||
// had the umpire calling time on a pause that was never a changeover.
|
||||
|
||||
Reference in new issue
Block a user