Fix match AI short-ball contact and recovery

This commit is contained in:
wuwi committed 2026-09-07 10:53:14 +02:00
1 parent fe64242090
commit a399e4929c
4 files changed
+129 -9

No files matched your search

+17 -8
View File
@@ -1488,10 +1488,15 @@ function racketAI(rz, dt) {
rz.swingCool -= 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
// Solo tennis recovers during its own flight, at half chase speed. The serve pose finishes
// first; dead balls and the gore racket keep their existing hold.
const recovering = G.match && G.phase === 'rally' && ball.active && !ball.scored
&& !G.match.decided && !G.match.ending && ball.lastHit === 'O' && ball.vz > 0
&& !(rz.serveHold > 0);
// ...and the sprint velocity dies WITH the chase. This return kept the last chase's vx/vz on
// the body, so the speed ghosts trailed a zombie that had already stopped ("teilweise bleiben
// die doppelten ghosts länger sichtbar") — the trail must read as motion, never as residue.
if (!incoming) { rz.vx = 0; rz.vz = 0; if (rz.pose === 'run') rz.pose = 'idle'; return; }
if (!incoming && !recovering) { rz.vx = 0; rz.vz = 0; if (rz.pose === 'run') rz.pose = 'idle'; return; }
// 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
@@ -1501,7 +1506,8 @@ function racketAI(rz, dt) {
// 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 pred = incoming && ((G.match && ball.bounces === 0) || (ball.y > 0.35 && ball.vy < 3))
? predictLanding() : null;
// THE READ rides on aiRead: an EXPECTED ball (you repeated your habit) is read almost perfectly
// — spam feeds the machine — and a SURPRISE ball collapses the read back toward the old pure
// pursuit, which is exactly the wrong-footing aiRead exists to permit. Both scale with the rung
@@ -1513,9 +1519,11 @@ function racketAI(rz, dt) {
: G.match ? TUNE.aiRead
// the DUEL's read climbs with the run (never to 1 — wrong-footing stays possible)
: TUNE.aiRead + (TUNE.duelReadMax - TUNE.aiRead) * duelSkill();
const aimX = pred ? ball.x + (pred.x - ball.x) * read : ball.x;
const aimZ = pred ? ball.z + (pred.z - ball.z) * read : ball.z;
const tz = Math.max(1.5, Math.min(COURT.svcFar, aimZ)); // chase laterally, hold deep on the far court
const aimX = recovering ? HOME.O.x : pred ? ball.x + (pred.x - ball.x) * read : ball.x;
// Read the landing depth immediately in a match, including rising returns. Blending with
// the ball's current depth would send every receiver towards the net before it arrives.
const aimZ = recovering ? HOME.O.z : pred ? (G.match ? pred.z : ball.z + (pred.z - ball.z) * read) : ball.z;
const tz = Math.max(1.5, Math.min(G.match ? COURT.netZ - 1.2 : COURT.svcFar, aimZ));
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,
@@ -1523,7 +1531,7 @@ function racketAI(rz, dt) {
// 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.
// (the dials themselves live in rzChaseSpeed, which the rally-ending reach tests share)
const step = Math.min(rzChaseSpeed(rz) * dt, d);
const step = Math.min(rzChaseSpeed(rz) * (recovering ? 0.5 : 1) * 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';
@@ -1547,9 +1555,10 @@ function racketAI(rz, dt) {
// as far, lost its head and missed exactly as often. Both dials read the same racketStage the
// SKINS do, so what you see it lose is what it loses. Match only — see config.matchWound.
const wnd = G.match ? matchWound(rz) : { reach: 1, flub: 1 };
if (rz.swingCool <= 0 && rz.swingT < 0
if (incoming && (!G.match || ball.z < COURT.netZ) && rz.swingCool <= 0 && rz.swingT < 0
&& Math.hypot(ball.x - rz.x, (ball.z - rz.z) * 0.8) < TUNE.aiReach * wnd.reach
&& ball.y < CONTACT.maxY && ball.y > CONTACT.minY) {
&& ball.y < CONTACT.maxY
&& (G.match ? ball.y >= CONTACT.minY || ball.vy > 0 : ball.y > CONTACT.minY)) {
// ...and the match has its OWN flub base: the duel's 0.2 hands a fifth of all returns away,
// which across a game is an opponent that beats itself.
const flub = (G.match ? TUNE.matchFlub * Math.pow(TUNE.matchFlubKeep, matchStep()) * (rz.foe?.flub || 1) * wnd.flub