Splatter: one gesture always, a serve that clears the net, an aim camera
Three things from the phone test. The drag meant two different things depending on invisible state: aiming from pretoss, but walking the player around while the last ball was still in the air. That "mode between the hits" read as a bug, so it's gone — canAimServe now covers every phase of splatter, and firing over a live ball is allowed again (the old one flies on as a ballistic clone). That was the mid-rally re-toss that used to be the spam vector, and it's safe now because the brake moved into the pull: a rhythm of lazy 30px pulls fires 100 shots in 30s and kills NOTHING (the court fills up instead), while 85 committed full pulls kill 11. hitServe never had doHit's net-clearance loop, so every shot at a shambler hugging the tape died in the net — not balance, a missing loop. Both paths now trade pace for arc until the ball is genuinely over, and the ceiling goes 8 -> 12 tries because a target right behind the net needs a near-vertical drop (measured: clears the tape by 0.20m and connects). The trade is real: the loft arrives slower and the walk-lead was computed for the original pace, so a lob is honestly the less reliable shot. And the camera now rises and leans back a little while the slingshot is drawn. That steeper look is what makes the gap between the net and the bodies behind it readable — the shot that used to be blind. Eased once per frame before anything projects, since one sheared frame would be visible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ff973cf6bb
commit
b61004a18d
4 files changed
+42
-7
No files matched your search
+26
-4
@@ -96,7 +96,7 @@ function doHit(ch, quality, aim) {
|
||||
// low contact → trade pace for loft until the net is cleared; good timing gets a
|
||||
// comfortable margin, sloppy timing keeps some tape risk
|
||||
const margin = 0.06 + 0.22 * quality;
|
||||
for (let i = 0; i < 8; i++) {
|
||||
for (let i = 0; i < NET_LOFT_TRIES; i++) {
|
||||
const tN = (COURT.netZ - ball.z) / v.vz;
|
||||
const yN = ball.y + v.vy * tN - 0.5 * g * tN * tN;
|
||||
if (tN <= 0 || yN >= COURT.netH + margin) break;
|
||||
@@ -128,9 +128,13 @@ function tossBall(sv) {
|
||||
}
|
||||
export function serveApexY() { return TOSS.y0 + TOSS.vy * TOSS.vy / (2 * TUNE.gravity); } // ~2.95
|
||||
|
||||
// how many 10%-pace cuts a shot may take to get over the net. 8 wasn't enough for a target
|
||||
// sitting right behind the tape — that needs a near-vertical drop, so ~10 cuts (pace × 0.35).
|
||||
const NET_LOFT_TRIES = 12;
|
||||
|
||||
function hitServe(sv, quality, aim = null) {
|
||||
const toFar = sv.side === 'P';
|
||||
const s = TUNE.serveSpeed + quality * TUNE.serveQualitySpeed;
|
||||
let s = TUNE.serveSpeed + quality * TUNE.serveQualitySpeed;
|
||||
let boxX, tzs;
|
||||
const ft = G.mode !== 'tennis' && !G.interlude // the duel serves like tennis: to the box, not free-aim
|
||||
? (aim && aim.fx !== undefined ? flickTarget(sv, aim) : { zt: zombieTarget(aim) }) : null;
|
||||
@@ -148,7 +152,19 @@ function hitServe(sv, quality, aim = null) {
|
||||
tzs = boxZ + (Math.random() * 2 - 1) * (1 - quality) * 1.5;
|
||||
}
|
||||
const g = TUNE.gravity * (1 + quality * 0.35); // kick serves dip a little too
|
||||
const v = solveShot(ball.x, ball.y, ball.z, boxX, tzs, s, g);
|
||||
// CLEAR THE NET. A shambler standing just the other side of the tape is only reachable on a
|
||||
// loft, so trade pace for arc until the ball actually gets over. doHit has done this since
|
||||
// forever; the serve never did, which is why every shot at a net-hugger died in the tape.
|
||||
// The trade is real: the loftier it goes the slower it arrives, and the walk-lead below was
|
||||
// computed for the original pace — a lob is a genuinely less reliable shot.
|
||||
let v = solveShot(ball.x, ball.y, ball.z, boxX, tzs, s, g);
|
||||
const margin = 0.06 + 0.22 * quality;
|
||||
for (let i = 0; i < NET_LOFT_TRIES; i++) {
|
||||
const tN = (COURT.netZ - ball.z) / v.vz;
|
||||
const yN = ball.y + v.vy * tN - 0.5 * g * tN * tN;
|
||||
if (tN <= 0 || yN >= COURT.netH + margin) break;
|
||||
s *= 0.9; v = solveShot(ball.x, ball.y, ball.z, boxX, tzs, s, g);
|
||||
}
|
||||
ball.g = g;
|
||||
ball.vx = v.vx; ball.vy = v.vy; ball.vz = v.vz;
|
||||
ball.lastHit = sv.side; ball.bounces = 0;
|
||||
@@ -592,9 +608,15 @@ export function serveToss() {
|
||||
export function dragServe(aim) {
|
||||
if (G.mode !== 'splatter' || score.server !== 'P') return;
|
||||
if (G.interlude || G.horde || G.mgT > 0 || G.changeover) return; // the duel serves like tennis, not the reticle
|
||||
if (G.phase !== 'pretoss' && G.phase !== 'toss') return; // one ball: no re-serving over a live rally
|
||||
if (G.phase === 'over') { ball.active = false; setupPoint(); } // mid-beat pull: arm the next shot now
|
||||
if (G.phase !== 'pretoss' && G.phase !== 'toss' && G.phase !== 'rally') return;
|
||||
if (tryGunBurst()) return;
|
||||
if (aim.mag < TUNE.pullMin) return; // released without really drawing: cancel, nothing spent
|
||||
// Firing while the last ball still flies is fine again — the brake is the pull itself, not the
|
||||
// rally clock. This is what lets the drag mean ONE thing at all times: a second gesture that
|
||||
// walked the player around between shots was the confusing part, not the fire rate.
|
||||
if (ball.active && G.phase !== 'toss') // the old ball flies on as a ballistic clone
|
||||
extras.push({ x: ball.x, y: ball.y, z: ball.z, vx: ball.vx, vy: ball.vy, vz: ball.vz, pow: ball.pow || 0.5, t: 2.5 });
|
||||
ball.active = true; ball.collected = false; ball.bounces = 0; ball.firstLand = null;
|
||||
ball.x = P.x + 0.35; ball.z = P.z; ball.y = 2.2; ball.g = TUNE.gravity;
|
||||
hitServe(P, pullQuality(aim.mag), aim); // a full pull is quality 1.0 — PERFECT, hitstop and all
|
||||
|
||||
Reference in new issue
Block a user