The receiver stands where the serve can actually go
The trick: hard serve, slight slide, and the first ten opponents died where they stood. Measured headless — that serve was returned 0 times out of 24 at EVERY rung. Three things were wrong and all three were the same thing: he waited on the tennis mark 1.3 m off the centre line (covering the T, conceding the whole corner), he took no first step, and he chased the ball's LIVE position, which during a serve is still up by the server's baseline. Now: he waits on the bisector of the serves he has to cover, takes a returner's first step (serve-scoped — the rally is untouched), and runs at the bounce instead of at the ball. The slid serve comes back from rung one; a flush serve into the very corner of the box still aces the bottom of the ladder, which is the reward placement is supposed to earn. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ece27827e5
commit
86b8275f5d
6 files changed
+77
-10
No files matched your search
+29
-7
@@ -365,7 +365,10 @@ export function sliceCarom(tgt, speed) {
|
||||
// `perfect` is passed SEPARATELY from `quality` because the two serves earn it differently: the toss
|
||||
// ritual earns it on a 45 ms timing window, while the slingshot's quality saturates at pullFull and
|
||||
// so can never fail the old `quality > 0.999` test once you have pulled far enough.
|
||||
function hitServe(sv, quality, aim = null, perfect = quality > 0.999) {
|
||||
// exported for the headless suite only: it is the one honest way to serve without a thumb, and a
|
||||
// serve-defence test that fakes its own ball is testing a ball the game never hits (it did, once —
|
||||
// the fake was a flat rocket that skidded out the back and nothing could have returned it).
|
||||
export function hitServe(sv, quality, aim = null, perfect = quality > 0.999) {
|
||||
const toFar = sv.side === 'P';
|
||||
// {sx, hard} is the TENNIS strike aim (see keyPressed's toss branch): sx sways the target in
|
||||
// SCREEN direction — flick right, serve right — and hard buys pace and depth, paid for in
|
||||
@@ -1010,8 +1013,16 @@ function matchFallWillPlay() {
|
||||
return racketStage({ hp: after, hpMax: rz.hpMax }) !== racketStage(rz); // a limb
|
||||
}
|
||||
|
||||
// How fast this body chases a ball. ONE definition, because racketAI's legs and the two
|
||||
// out-of-reach tests that end a rally early must never disagree about what it can get to.
|
||||
// THE RETURN DASH is the returner's first step: a serve is the one ball in the game that is
|
||||
// hit before he is moving at all, and against a hard one into the corner the walk-up simply
|
||||
// could not start in time (Oliver: the first ten opponents can be killed the moment you serve).
|
||||
// It lasts exactly as long as the serve does — G.wasServe is cleared at the first bounce and at
|
||||
// any return — so a rally is chased at the ordinary ladder speed and nothing else moves.
|
||||
const rzChaseSpeed = rz => TUNE.racketSpeed
|
||||
* (TUNE.matchSpeedBase + (G.match.opp - 1) * TUNE.matchSpeedPer) * ((rz.foe && rz.foe.speed) || 1);
|
||||
* (G.match ? (TUNE.matchSpeedBase + (G.match.opp - 1) * TUNE.matchSpeedPer) * ((rz.foe && rz.foe.speed) || 1) : 1)
|
||||
* (G.wasServe && ball.lastHit === 'P' ? TUNE.aiReturnDash : 1);
|
||||
|
||||
// Can the match opponent still reach this ball before its second bounce? Closed-form flight from
|
||||
// the POST-BOUNCE state (velocities already damped when the line call runs) against the body's own
|
||||
@@ -1279,16 +1290,27 @@ function racketAI(rz, dt) {
|
||||
const incoming = ball.active && ball.lastHit === 'P' && ball.vz < 0
|
||||
&& (G.interlude || (ball.pow || 0) < TUNE.zSplatQ); // in the duel it plays EVERY ball — it's tennis now
|
||||
if (!incoming) { if (rz.pose === 'run') rz.pose = 'idle'; return; }
|
||||
const tz = Math.max(1.5, Math.min(COURT.svcFar, ball.z)); // chase laterally, hold deep on the far court
|
||||
const dx = ball.x - rz.x, dz = tz - rz.z, d = Math.hypot(dx, dz);
|
||||
// ANTICIPATION. He used to run at the ball's LIVE position, and against a serve that is a
|
||||
// trap: for the first third of the flight the ball is still up by YOUR baseline, so he ran
|
||||
// deep and straight while the ball was already leaving sideways, and only started sliding
|
||||
// across once it was past him. Measured headless: a flush, hard serve into the corner of the
|
||||
// box was returned 0 times out of 40 at every rung of the ladder — Oliver's "you can kill the
|
||||
// first ten of them the moment you serve". He reads the BOUNCE now and runs at that instead.
|
||||
// aiRead is how much of the read he gets: 1 would be a machine that is never wrong-footed, and
|
||||
// being wrong-footed is most of what makes the rally a rally. Serves only in spirit but applied
|
||||
// to every flight — the same lag was there in a rally, just hidden by the shorter distances.
|
||||
const pred = ball.y > 0.35 && ball.vy < 3 ? predictLanding() : null; // not while it is still climbing off the racket
|
||||
const aimX = pred ? ball.x + (pred.x - ball.x) * TUNE.aiRead : ball.x;
|
||||
const aimZ = pred ? ball.z + (pred.z - ball.z) * TUNE.aiRead : ball.z;
|
||||
const tz = Math.max(1.5, Math.min(COURT.svcFar, aimZ)); // chase laterally, hold deep on the far court
|
||||
const dx = aimX - rz.x, dz = tz - rz.z, d = Math.hypot(dx, dz);
|
||||
if (d > 0.15) {
|
||||
// THE MATCH ESCALATES HERE, not in hp: with regicide gone the pool is pure scoreboard,
|
||||
// so a harder opponent is a FASTER one that flubs less. The BASE is boosted too — the duel's
|
||||
// 2.8 m/s lumber was tuned for one sudden-death point against a fresh player, and across a
|
||||
// whole game it read as "very weak and slow": opponent one already moves at matchSpeedBase.
|
||||
const spd = TUNE.racketSpeed * (G.match ? (TUNE.matchSpeedBase + (G.match.opp - 1) * TUNE.matchSpeedPer)
|
||||
* (rz.foe?.speed || 1) : 1);
|
||||
const step = Math.min(spd * dt, d);
|
||||
// (the dials themselves live in rzChaseSpeed, which the rally-ending reach tests share)
|
||||
const step = Math.min(rzChaseSpeed(rz) * dt, d);
|
||||
rz.dist = (rz.dist || 0) + step; // ground covered — his walk cycle steps off this, not the clock
|
||||
rz.x += dx / d * step; rz.z += dz / d * step; rz.runT += dt;
|
||||
if (rz.pose !== 'swing') rz.pose = 'run';
|
||||
|
||||
Reference in new issue
Block a user