Stop the auto-run shaking while it chases a ball
Oliver: "wenn der Autoplay den Ball versucht zu bekommen, zittert der Player schnell hin und her". Measured: the run's target flipped by metres every other frame. intercept samples the flight every 10 ms against a 16.7 ms frame, so any candidate on a margin (a low bounce peaking on the 0.45 m floor, a point on the back boundary, a dying drop) passed one frame and failed the next. Frames with no candidate sent him home and back. 9-62 reversals inside 200 ms per five minutes. The chase now latches its plan: the flight is fixed at the strike, so the first point stays the answer while his legs still make it in time, net of any time he stood (split-step, heels). Reach is judged from where the chase began, and the target eases with a start/stop hysteresis. After: no reversals at any rung, timing unchanged. He wastes no steps, so he is a little stronger, mostly at the bottom of the ladder. New test; v375. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
599d5b0506
commit
bb72a01a3e
7 files changed
+145
-19
No files matched your search
+53
-15
@@ -1528,6 +1528,16 @@ function intercept(side) {
|
||||
// the bounce a kicking ball drifts on sideways long enough to plan him out to x 6.5-8.7 — he ran to
|
||||
// the edge of the frame and watched it pass (his v369 log, 65.77 s and 84.26 s)
|
||||
const xl = side === 'P' ? xLimit() : 99;
|
||||
// ...and the PLAYER's reach is judged from where his chase BEGAN plus the time he has run since (P.chase),
|
||||
// not from where he stands this frame. Judged live, his own step moved the answer: a deep point became
|
||||
// reachable one frame and not the next, and the plan flipped 2.2 m every other frame (the shake).
|
||||
const o = side === 'P' && P.chase ? P.chase : { x: ch.x, z: ch.z, t: 0 };
|
||||
// ...with HYSTERESIS: the flight is sampled every 10 ms against a 16.7 ms frame, so a candidate on a
|
||||
// margin passed one frame and failed the next as the grid slid — a low slice whose bounce peaks at ~0.46 m
|
||||
// against the 0.45 m floor below, measured flipping the plan 2.2 m every other frame. The point he is
|
||||
// already running to keeps runKeep of time and 6 cm of height slack.
|
||||
const near = (px, pz) => side === 'P' && P.goal && Math.hypot(px - P.goal.x, pz - P.goal.z) < 0.6;
|
||||
const can = (px, pz, i, k = 1) => Math.hypot(px - o.x, pz - o.z) / (spd * k) <= o.t + i * dt + (near(px, pz) ? TUNE.runKeep : 0);
|
||||
let fallback = null, deepest = null, anyHit = null, rise = null, nb = ball.bounces;
|
||||
// ...but the PLAYER lobbed at the net first looks for the overhead: the first point the lob comes down
|
||||
// into his smash band (smashMaxY) that his backpedal reaches in time. A short lob is smashed; one he
|
||||
@@ -1540,17 +1550,17 @@ function intercept(side) {
|
||||
vy -= g * dt; x += vx * dt; y += vy * dt; z += vz * dt;
|
||||
if (y <= 0 && vy < 0) { y = 0; vy = -vy * bY(); vx *= bXZ(); vz *= bXZ(); nb++; g = gAfter(g); }
|
||||
if (smashing && nb === 0 && vy < 0 && y <= TUNE.smashMaxY - 0.2 && y >= TUNE.smashMinY && Math.abs(x) <= xl
|
||||
&& Math.hypot(x - ch.x, z - ch.z) / (spd * TUNE.smashRun) <= i * dt) return (plan.over = { x, z });
|
||||
&& can(x, z, i, TUNE.smashRun)) return (plan.over = { x, z, ti: o.t + i * dt });
|
||||
// FROM THE BACK: meet it between its bounce and the next one — at the top or falling through
|
||||
// rallyHit+0.3, from as far as rallyBehind behind the line; a short ball the first moment it is
|
||||
// up off the grass. Only if nothing like that is reachable do the old candidates below apply.
|
||||
// (up to rallyTop since the dip ends at the bounce: a kicking ball is taken at the top of its
|
||||
// bounce, chest high, rather than chased forward and volleyed — 40% volleys was the first cut)
|
||||
if (back && nb === 1 && i > 5 && y > 0.45 && y < (plan.short ? TUNE.rallyHit + 0.6 : TUNE.rallyTop)) {
|
||||
const inB = (side === 'P' ? z <= COURT.L + TUNE.rallyBehind : z >= -TUNE.rallyBehind) && Math.abs(x) <= xl;
|
||||
if (inB && Math.hypot(x - ch.x, z - ch.z) / spd <= i * dt) {
|
||||
if (plan.short || vy < 1) return { x, z };
|
||||
rise ||= { x, z };
|
||||
if (back && nb === 1 && i > 5 && y > (near(x, z) ? 0.39 : 0.45) && y < (plan.short ? TUNE.rallyHit + 0.6 : TUNE.rallyTop)) {
|
||||
const inB = (side === 'P' ? z <= COURT.L + TUNE.rallyBehind + (near(x, z) ? 0.1 : 0) : z >= -TUNE.rallyBehind) && Math.abs(x) <= xl;
|
||||
if (inB && can(x, z, i)) {
|
||||
if (plan.short || vy < 1) return { x, z, ti: o.t + i * dt };
|
||||
rise ||= { x, z, ti: o.t + i * dt };
|
||||
}
|
||||
}
|
||||
// THE SHORT BALL (match): a slow return can bounce and DIE at z 18-20, which never enters the
|
||||
@@ -1559,21 +1569,24 @@ function intercept(side) {
|
||||
// losses. Remember the earliest reachable point ON HIS HALF where the ball is still at hitting
|
||||
// height: running forward onto the short ball is the tennis answer, and now it is the assist's.
|
||||
if (side === 'P' && rallyFeel() && !anyHit && i > 5 && z > COURT.netZ && y < 1.6 && y > 0.35) {
|
||||
const pt = { x, z: Math.min(24.5, z) };
|
||||
if (Math.abs(x) <= xl && Math.hypot(pt.x - ch.x, pt.z - ch.z) / spd <= i * dt) anyHit = pt;
|
||||
const pt = { x, z: Math.min(24.5, z), ti: o.t + i * dt };
|
||||
if (Math.abs(x) <= xl && can(pt.x, pt.z, i)) anyHit = pt;
|
||||
}
|
||||
if (i > 5 && inZone(z) && y < 1.6 && y > 0.3) {
|
||||
const pt = { x, z: side === 'O' ? Math.max(0.8, z) : Math.min(24.5, z) };
|
||||
const reachable = Math.abs(x) <= xl && Math.hypot(pt.x - ch.x, pt.z - ch.z) / spd <= i * dt;
|
||||
const pt = { x, z: side === 'O' ? Math.max(0.8, z) : Math.min(24.5, z), ti: o.t + i * dt };
|
||||
const reachable = Math.abs(x) <= xl && can(pt.x, pt.z, i);
|
||||
if (y > 0.7 && reachable && !back) return pt; // waist-high AND arrivable in time
|
||||
if (reachable && !fallback) fallback = pt; // low but arrivable
|
||||
deepest = pt; // last resort: run at the deepest hittable point
|
||||
}
|
||||
}
|
||||
if (smashing) plan.over = null; // no overhead in reach: he runs it down
|
||||
const to = rise || fallback || deepest || anyHit || home; // zone points first: meeting deep is still better
|
||||
// ...and a frame that finds NOTHING mid-chase holds the plan it had: a dying drop's bounce sits on the
|
||||
// 0.35 m floor, and every other frame without a candidate sent him home, 10 m away, and back again
|
||||
const hold = side === 'P' && P.chase && P.goal ? P.goal : null;
|
||||
const to = rise || fallback || deepest || anyHit || hold || home; // zone points first: meeting deep is still better
|
||||
// a ball no point of which he can reach still gets a run — to the nearest spot he is allowed to stand
|
||||
return Math.abs(to.x) > xl ? { x: Math.sign(to.x) * xl, z: to.z } : to;
|
||||
return Math.abs(to.x) > xl ? { x: Math.sign(to.x) * xl, z: to.z, ti: to.ti } : to;
|
||||
}
|
||||
|
||||
// THE GOLD RING, TIMED (2026-10-14). The approach ring used to go gold at a DISTANCE (matchRingSweet
|
||||
@@ -2346,7 +2359,11 @@ export function update(dt) {
|
||||
// ON HIS HEELS: a put-away of his own sitter is read late (step 4 (b)). Without this the auto-run
|
||||
// started on the strike's frame with a perfect read and covered the whole court — no put-away ever
|
||||
// won a point, and the CPU still had no way to take one
|
||||
if (chasing && ball.heels > 0) { ball.heels -= dt; rate = 0; }
|
||||
const key = ball.lastHit + (chasing ? 'c' : 'r');
|
||||
if (!chasing) P.chase = null;
|
||||
else if (!P.chase || P.goalKey !== key) P.chase = { x: P.x, z: P.z, t: 0, lost: 0, plan: null }; // where this chase began
|
||||
if (chasing && ball.heels > 0) { ball.heels -= dt; rate = 0; if (P.chase) P.chase.lost += dt; }
|
||||
else if (P.chase) P.chase.t += dt; // the time he has had to run, for intercept's reach
|
||||
// Run to the ball — then recover. WHERE you recover to is mode-specific and each rule exists
|
||||
// for a reason:
|
||||
// GORE SPLATTER recovers in DEPTH ONLY, holding your lane — recovering to HOME.P's x undid
|
||||
@@ -2358,12 +2375,33 @@ export function update(dt) {
|
||||
// unreachable": one wide return pulled you off centre and you STAYED there, so the next
|
||||
// ball to the opposite corner was lost before it was struck. Not a strong CPU — a player
|
||||
// who never walked back.
|
||||
const tgt = chasing ? intercept('P')
|
||||
// ...and the plan is LATCHED for the chase: the flight is fixed the moment the ball is struck, so the
|
||||
// point chosen first stays the answer while his legs still make it in time (net of any time he stood:
|
||||
// a split-step, a put-away's heels). Re-asked every frame, every margin in intercept — a bounce that
|
||||
// peaks on a height floor, a point on the back boundary — flipped the plan by metres as the 10 ms
|
||||
// sampling grid slid against the frame: the shake Oliver saw. Hysteresis there only moved the margin.
|
||||
const c = P.chase, spdP = TUNE.playerSpeed * aK;
|
||||
const latched = c && c.plan && c.plan.ti !== undefined
|
||||
&& Math.hypot(c.plan.x - c.x, c.plan.z - c.z) / spdP <= c.plan.ti - (c.lost - c.plan.lost0) + 0.02;
|
||||
const raw = chasing ? (latched ? c.plan : (c ? (c.plan = { ...intercept('P'), lost0: c.lost }) : intercept('P')))
|
||||
: G.mode === 'splatter' && !G.match ? { x: P.x, z: HOME.P.z }
|
||||
: rallyFeel() ? rallyHome(P) : HOME.P;
|
||||
// THE RUN, SMOOTHED (2026-10-14, Oliver: "wenn der Autoplay den Ball versucht zu bekommen, zittert der
|
||||
// Player schnell hin und her"). intercept asks every frame for the EARLIEST point he reaches in time,
|
||||
// and his own step moves that point: the target jumped ~10 cm a frame (p90 0.3 m) and flipped across
|
||||
// him, start/stop at the deadzone's edge, dozens of reversals a minute. It now EASES toward each plan
|
||||
// (runSmooth); a new shot or a re-plan further than runSnap (the lob read) is still taken at once.
|
||||
const jump = P.goal ? Math.hypot(raw.x - P.goal.x, raw.z - P.goal.z) : Infinity;
|
||||
if (!P.goal || P.goalKey !== key || jump > TUNE.runSnap) P.goal = { x: raw.x, z: raw.z };
|
||||
else { const k = Math.min(1, dt * TUNE.runSmooth); P.goal = { x: P.goal.x + (raw.x - P.goal.x) * k, z: P.goal.z + (raw.z - P.goal.z) * k }; }
|
||||
P.goalKey = key;
|
||||
const tgt = P.goal;
|
||||
P.tgt = tgt; // the timed gold ring (ringCue) predicts the contact from where this run ends
|
||||
const dx = tgt.x - P.x, dz = tgt.z - P.z, d = Math.hypot(dx, dz);
|
||||
if (d > 0.35) { // deadzone: stand ready near the spot instead of dithering over it
|
||||
// deadzone WITH HYSTERESIS: start at 0.35, and once running, carry on to runStop — a target hovering
|
||||
// at the edge no longer toggles him between a step and a stand every other frame
|
||||
P.running = d > (P.running ? TUNE.runStop : 0.35);
|
||||
if (P.running) { // deadzone: stand ready near the spot instead of dithering over it
|
||||
P.x += dx / d * Math.min(rate, d);
|
||||
P.z += dz / d * Math.min(rate, d);
|
||||
assist = dx;
|
||||
|
||||
Reference in new issue
Block a user