Let an early tap be a weak ball, and let the CPU put a sitter away
Step 4 (b), Oliver's pick. v372 measured why the ladder could not climb: nearly every lost point was the player's own early whiff, the CPU barely missed, and the auto-run reached everything, so no opponent dial decided a point. - The follow-through: a groundstroke swing meets a bounced ball up to 60 ms after its window at q 0.05. The early whiff edge goes from ~70 ms back to ~130 ms, where the volley window had it before step 1. - A sitter (a player ball under q 0.5) is put away with a chance that is the ladder (0.2 at the bottom of the Climb, 0.85 at the Champion, by difficulty in classic): clean, at the line, bounded by the rung's width. - The player is on his heels for 0.5 s after a put-away of his own sitter. Without it the auto-run covered the whole court and no put-away won. - Fixed a step-2 regression this surfaced: the drop's dig-out cap fired on every slice drive, so every low contact against the slicer was a sitter. Measured at level 26: the Climb now slopes from 95% of points at the Shambler to 28-39% at the Champion. The net rushers are still easier than their neighbours. The log names put-aways. Three new tests; v373. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5dd5d71c33
commit
4406aeafdb
8 files changed
+231
-31
No files matched your search
+46
-3
@@ -5585,6 +5585,48 @@ sein, dass durch die geänderte Mechanik alles etwas schwerer geworden ist"*.
|
||||
- (c) **Widen the early whiff edge only** (a smaller tail), keeping the error race: undoes most of the
|
||||
"harder" without changing the model.
|
||||
|
||||
### Step 4 (b), Oliver's pick: the follow-through, and a CPU that can WIN a point (2026-10-14, v373)
|
||||
Oliver: *"b"* (options above). Two halves, each useless alone (both were measured alone above):
|
||||
- **THE FOLLOW-THROUGH** (`TUNE.swingTail` 0.06 s, rally modes, player, bounced balls only). A swing
|
||||
meets a ball arriving up to 60 ms after its window closed at q 0.05. The early whiff edge goes from ~70
|
||||
ms back to ~130 ms (where the volley window had it before step 1); past it, it is still a whiff. The
|
||||
log says `q=0.05 · early 115ms`. Volleys keep their window: a tail on balls in the air raised the
|
||||
back-court volley share.
|
||||
- **THE SITTER IS PUNISHED** (`gameplay.punishes`): a player ball under `punishBelow` (0.5) is answered
|
||||
with a put-away at a chance from `punishMin` 0.2 to `punishMax` 0.85 along the Climb's own progress
|
||||
(`matchStep`, so its brisk bottom counts), classic by `DIFFS[].punish` (0.2/0.5/0.8). A put-away is
|
||||
struck at `punishQ` 0.95, skips the style's soft shots, aims at `punishX` 3.5 m (the line; the rung's
|
||||
width dial still bounds it, so the bottom of the ladder punishes without painting lines), and only
|
||||
counts if no cap took it down (a low volley stays a block). The log says `THEM PUT-AWAY of your weak ball`.
|
||||
- **...AND HE IS ON HIS HEELS** (`ball.heels`, `punishRead` 0.5 s): the auto-run stands for that long on a
|
||||
put-away of his own sitter. This is the half that makes it work. The auto-run started on the strike's
|
||||
frame with a perfect read, ~7 m/s plus a 1.75 m reach, and covered the whole court: a q 0.95 put-away
|
||||
to the open court won 0 of 21, at the line 0-2 of 20. Read 0.25 s: ~5%; 0.4: ~20%; 0.55: ~70%.
|
||||
**Feel risk:** he visibly stands still for half a second. If that reads as a bug on the phone, the
|
||||
alternatives are a slowed (not frozen) run, or a shorter read with the put-away hit harder.
|
||||
- **A step-2 regression it surfaced:** the drop's dig-out cap read `ball.spin < 1`, and the slicer hits
|
||||
EVERY rally ball at spin 0.8, so every low contact against him (both sides) came back at `lowVolleyQ`.
|
||||
Now `ball.soft && spin < 1`. THE SELLER went from 35% to 47-54% and normal rally lengths.
|
||||
- **Measured** (Climb, level 26, 8 seeds × 500 s, point win %):
|
||||
|
||||
| rung | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| σ40 | 95 | 88 | 88 | 87 | 75 | 58 | 52 | 45 | 54 | 48 | 47 | 77 | 42 | 46 | 64 | 40 | 65 | 39 |
|
||||
| σ60 | 95 | 88 | 83 | 78 | 62 | 55 | 53 | 43 | 47 | 46 | 36 | 61 | 30 | 43 | 57 | 28 | 55 | 28 |
|
||||
|
||||
The ladder slopes now, down to the Champion at 28-39%. Classic: easy ~80, medium ~57, hard ~30 (σ60, level 1).
|
||||
- **Still open:**
|
||||
- (1) **The net rushers** (Umpire, Club Pro, Director) are 15-25 points easier than their neighbours.
|
||||
They win only ~35-55% of their net points, because the bot ALWAYS passes or lobs a net man, which is
|
||||
their tag's answer played perfectly. The phone decides whether a human does that; if so, the dial is
|
||||
their split-step read (`netReact`) or their put-away from the net.
|
||||
- (2) The bottom three rungs are ~90%. That is easy for level 26, but the Climb is a marathon.
|
||||
- Tests (167, nine seeds): the follow-through meets an ~80 ms early tap (and the old edge did not); the
|
||||
put-away share climbs the ladder, goes for the line, never answers a good ball; a low slice drive is not
|
||||
a dug-out drop. The baseline-shape test excludes the answer to a put-away (a man on his heels takes it in
|
||||
the air legitimately), and synthetic returns in the suite now carry `pow: 1` (a stale `pow` from the
|
||||
previous test read as a sitter).
|
||||
|
||||
# OPEN — leftovers, in rough priority order
|
||||
|
||||
Nothing here is broken; these are known gaps and unverified risks.
|
||||
@@ -5592,9 +5634,10 @@ Nothing here is broken; these are known gaps and unverified risks.
|
||||
0. **THE RALLY GOES FLAT (2026-10-13), the top item.** Every match converged on serve, both at the net,
|
||||
reflex volleys, won by a wrong-foot. Step 1 (the court gets its length) SHIPPED v361, awaiting the
|
||||
phone verdict; the tap timing + leg knee (v362), the soft ball (v363, drop/lob split v364) and the
|
||||
opponent styles (v364) followed; the swing-log fixes ran to v371. **Step 4 (v372) is BLOCKED on a
|
||||
design decision**: the ladder cannot climb while points are decided by the player's whiffs. See "Step
|
||||
4, measured first" above for options (a)/(b)/(c). Fallback tag `rally-baseline-v360`.
|
||||
opponent styles (v364) followed; the swing-log fixes ran to v371. Step 4: v372 measured why the ladder
|
||||
could not climb (points decided by the player's whiffs); Oliver picked (b), shipped v373 (the follow-
|
||||
through, put-aways, on his heels). Awaiting the phone verdict on v373, especially the half-second
|
||||
flat-footed read and the net rushers. See "Step 4 (b)" above. Fallback tag `rally-baseline-v360`.
|
||||
|
||||
1. **`RES` 4 is unverified on a real phone under load.** It nearly doubled the fill rate (810 -> 1080
|
||||
device px across). Oliver has not reported stutter, but he also has not played a late horde since.
|
||||
|
||||
+1
-1
@@ -1140,6 +1140,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
||||
</div>
|
||||
</div>
|
||||
<span id="recLamp" hidden>REC</span>
|
||||
<script type="module" src="src/main.js?v=372"></script>
|
||||
<script type="module" src="src/main.js?v=373"></script>
|
||||
</body>
|
||||
</html>
|
||||
+12
-3
@@ -153,6 +153,15 @@ export const TUNE = {
|
||||
// screen: a short swipe is a safe half-court ball, and only a committed
|
||||
// one goes hunting the line, which is what makes going OUT a chosen risk.
|
||||
perfectMs: 45, // |offset from window center| for PERFECT
|
||||
swingTail: 0.06, // s: rally modes, a groundstroke swing still meets a ball arriving this long after
|
||||
// its window closes — the follow-through, q 0.05 (a weak short ball, not a whiff).
|
||||
// 0 = the old edge: a tap ~70 ms early whiffed (step 4, see gameplay.trySwing)
|
||||
punishBelow: 0.5, // a player ball under this q is a SITTER the CPU may put away (gameplay.punishes)
|
||||
punishMin: 0.2, // ...the chance it does at the bottom of the ladder
|
||||
punishMax: 0.85, // ...and at the CHAMPION (classic: DIFFS[].punish)
|
||||
punishQ: 0.95, // the put-away's quality, played to the open court...
|
||||
punishX: 3.5, // ...at this x, inside the singles line (4.1), bounded by the rung's width dial
|
||||
punishRead: 0.5, // s the player's auto-run stands flat-footed on a put-away of his own sitter
|
||||
qSeamless: 1, // rally modes: past perfectMs, q falls from 1 to the window edge with no step.
|
||||
// 0 = the old curve, 1 - off/half, which dropped a groundstroke 1 ms past
|
||||
// PERFECT straight to q 0.43 (step 4, 2026-10-14 — see gameplay.trySwing)
|
||||
@@ -919,9 +928,9 @@ export const MENU = [{ mode: 'zombie' }, { mode: 'train' }, { mode: 'classic' }]
|
||||
export const DIFFS = [
|
||||
// EASY's first cut (flub 0.30, quality cap 0.62, reaction 0.62) was "way too easy" once HARD
|
||||
// taught us which dials bite — it is a mild step below MEDIUM now, not a training dummy
|
||||
{ id: 'easy', name: 'EASY', flub: 0.15, quality: 0.80, qMin: 0.45, speed: 0.95, reaction: 0.50, reach: 1.55, serve: 0.95, smart: 0, recover: 0.48 },
|
||||
{ id: 'medium', name: 'MEDIUM', flub: 0.06, quality: 0.92, qMin: 0.56, speed: 1.18, reaction: 0.34, reach: 1.70, serve: 1.05, smart: 0.35, recover: 0.66 },
|
||||
{ id: 'hard', name: 'HARD', flub: 0.02, quality: 1.0, qMin: 0.66, speed: 1.38, reaction: 0.27, reach: 1.80, serve: 1.15, smart: 0.7, recover: 0.80 },
|
||||
{ id: 'easy', name: 'EASY', flub: 0.15, quality: 0.80, qMin: 0.45, speed: 0.95, reaction: 0.50, reach: 1.55, serve: 0.95, smart: 0, recover: 0.48, punish: 0.2 },
|
||||
{ id: 'medium', name: 'MEDIUM', flub: 0.06, quality: 0.92, qMin: 0.56, speed: 1.18, reaction: 0.34, reach: 1.70, serve: 1.05, smart: 0.35, recover: 0.66, punish: 0.5 },
|
||||
{ id: 'hard', name: 'HARD', flub: 0.02, quality: 1.0, qMin: 0.66, speed: 1.38, reaction: 0.27, reach: 1.80, serve: 1.15, smart: 0.7, recover: 0.80, punish: 0.8 },
|
||||
];
|
||||
|
||||
// THE TOURNAMENT (Oliver's pick over the endless ladder, 2026-08-13): four rounds up the club's
|
||||
|
||||
+43
-9
@@ -268,6 +268,9 @@ function doHit(ch, quality, aim) {
|
||||
const bound = Math.max(-TUNE.flickWidth, Math.min(TUNE.flickWidth, ch.x));
|
||||
tx = sway < 0 ? Math.min(tx, bound) : Math.max(tx, bound);
|
||||
}
|
||||
// a PUT-AWAY goes for the line, not the rally lane (the match's width dial below still bounds it,
|
||||
// so the bottom of the ladder cannot paint lines even when it punishes)
|
||||
if (aim && aim.kill) tx = (aim.ax || (P.x > 0 ? -1 : 1)) * TUNE.punishX + err() * 0.3;
|
||||
// THE COURT HAS ITS LENGTH: in a rally mode the ball goes DEEP, and bad timing costs DEPTH — a
|
||||
// mistimed ball sits up short, which is the ball a real opponent attacks — never length, so
|
||||
// depth adds no out. The down-flick keeps its old short ball (it becomes the soft ball next).
|
||||
@@ -384,6 +387,7 @@ function doHit(ch, quality, aim) {
|
||||
ball.approach = rallyFeel() && ball.rx && ball.rx.short && !(ball.from < TUNE.netFrom) ? ch.side : null;
|
||||
ball.from = Math.abs(ch.z - COURT.netZ);
|
||||
ball.lastHit = ch.side; ball.bounces = 0; ball.rx = null; // a new shot: the receiver plans it afresh
|
||||
ball.heels = aim && aim.kill ? TUNE.punishRead : 0; // a put-away: he is on his heels (see the auto-run)
|
||||
// THE SWING LOG's other half: what came at you. A LOST "passed you" line is unreadable without it —
|
||||
// was that ball reachable at all? And "never swung" is judged per incoming ball, not once a point.
|
||||
if (ch.side !== 'P' && swingLog()) {
|
||||
@@ -393,7 +397,7 @@ function doHit(ch, quality, aim) {
|
||||
const out = Math.abs(land.x) > COURT.W / 2 || land.z > COURT.L || land.z < COURT.netZ;
|
||||
return ` · lands ${r2(Math.abs(dx))}m ${dx < 0 ? 'left' : 'right'} of you, ${inside}m inside your line${out ? ' (OUT)' : ''}`;
|
||||
})();
|
||||
const kind = soft === null ? (aim && aim.az < 0 ? 'deep' : 'drive') + (ball.spin > 1 ? ' topspin' : ball.spin < 1 ? ' slice' : '')
|
||||
const kind = aim && aim.kill ? 'PUT-AWAY of your weak ball' : soft === null ? (aim && aim.az < 0 ? 'deep' : 'drive') + (ball.spin > 1 ? ' topspin' : ball.spin < 1 ? ' slice' : '')
|
||||
: soft === 0 ? 'drop' : soft >= 1 ? 'lob' : 'moonball';
|
||||
logSwing({ result: 'THEM', reason: `${kind} q=${r2(quality)} · ${Math.round(Math.hypot(ball.vx, ball.vz))}m/s${mark}`
|
||||
+ (ball.approach === ch.side ? ' · coming in' : ''), px: r2(P.x), pz: r2(P.z) }); // where HE stood when it left
|
||||
@@ -613,7 +617,7 @@ export function hitServe(sv, quality, aim = null, perfect = quality > 0.999) {
|
||||
perfect && G.mode === 'splatter' && !G.interlude && G.weapon === 0 && !G.temp);
|
||||
ball.g = g;
|
||||
ball.vx = v.vx; ball.vy = v.vy; ball.vz = v.vz;
|
||||
ball.lastHit = sv.side; ball.bounces = 0; ball.rx = null; ball.spin = 1; ball.soft = false;
|
||||
ball.lastHit = sv.side; ball.bounces = 0; ball.rx = null; ball.heels = 0; ball.spin = 1; ball.soft = false;
|
||||
// a serve-and-volleyer follows its serve in (STYLES.sv) — the serve is its approach shot
|
||||
ball.approach = sv.side === 'O' && styleOf(zombies.find(z => z.variant === 'r'))?.sv ? 'O' : null; ball.from = Math.abs(sv.z - COURT.netZ);
|
||||
ball.pow = quality;
|
||||
@@ -1650,6 +1654,25 @@ function logWhiff() {
|
||||
...(P.tgt ? { runTo: r2(P.tgt.x) + ',' + r2(P.tgt.z) } : {}) }); // where the auto-run was taking him
|
||||
}
|
||||
|
||||
// THE FOLLOW-THROUGH (step 4 (b), 2026-10-14). A tap a little EARLY on a baseline ball starts the swing
|
||||
// while the ball is still in the air; the swing waits for the bounce (letBounce), and by then the
|
||||
// groundstroke window has run out — a clean whiff at ~70 ms early, while a tap 120 ms LATE still met
|
||||
// the ball. Before step 1 the same ball was volleyed in a 1.8x window, so it never came up. In the
|
||||
// rally modes the swing now carries on for swingTail after its window and meets a ball arriving there
|
||||
// at q 0.05: a weak short ball the opponent has to punish. Whether he does is what the ladder is.
|
||||
const swingTail = isAI => !isAI && rallyFeel() ? TUNE.swingTail : 0;
|
||||
// ...and the other half, without which the follow-through only made rallies endless: the CPU had NO
|
||||
// way to win a point. It returned a floater exactly like a drive, its whiff rate is near zero past
|
||||
// mid-ladder, and every point it took was a whiff of the player's — which no opponent dial touches, so
|
||||
// the ladder could not climb. A sitter (the player's ball under punishBelow) is now PUT AWAY with a
|
||||
// chance that IS the ladder: punishMin at the shambler to punishMax at the champion, classic by
|
||||
// difficulty. Rally modes, solo only.
|
||||
function punishes(ch) {
|
||||
if (!rallyFeel() || G.net || ch.side === 'P' || (ball.pow ?? 1) >= TUNE.punishBelow) return false;
|
||||
// the Climb's own progress (matchStep: its brisker bottom included), so a level-26 run is not filler
|
||||
const k = G.diff ? (G.diff.punish ?? 0) : G.match ? TUNE.punishMin + (TUNE.punishMax - TUNE.punishMin) * Math.min(1, matchStep() / (LADDER - 1)) : 0;
|
||||
return Math.random() < k;
|
||||
}
|
||||
function trySwing(ch, isAI, qCap = TUNE.aiQualityMax) {
|
||||
// Contact happens if the ball is inside reach while the swing window is open.
|
||||
// Player volleys (pre-bounce) get wider reach + window: full-speed ball, no bounce cue.
|
||||
@@ -1663,7 +1686,7 @@ function trySwing(ch, isAI, qCap = TUNE.aiQualityMax) {
|
||||
: TUNE.reach * (volley ? VOLLEY.reach : 1) * (rallyFeel() ? TUNE.matchReach : 1);
|
||||
const win = TUNE.swingWindow * (volley ? VOLLEY.window : 1) * (isAI ? 1 : winLevel());
|
||||
const t = ch.swingT;
|
||||
if (t < TUNE.swingWindup || t > TUNE.swingWindup + win) return;
|
||||
if (t < TUNE.swingWindup || t > TUNE.swingWindup + win + (volley ? 0 : swingTail(isAI))) return; // the tail meets a BOUNCED ball
|
||||
if (!ball.active || G.phase !== 'rally') return;
|
||||
// splatter: your OWN ball is fair game. It rebounds off the backstop and comes back at you —
|
||||
// volleying it is how you keep firing without walking it down. (The duel is tennis: one touch.)
|
||||
@@ -1681,7 +1704,7 @@ function trySwing(ch, isAI, qCap = TUNE.aiQualityMax) {
|
||||
// bounce frame (his v367 log, 8.98 s).
|
||||
if (letBounce()) {
|
||||
const gg = ball.g || TUNE.gravity, tb = (ball.vy + Math.sqrt(ball.vy * ball.vy + 2 * gg * ball.y)) / gg;
|
||||
if (t + tb < TUNE.swingWindup + TUNE.swingWindow * winLevel() - 0.03) return;
|
||||
if (t + tb < TUNE.swingWindup + TUNE.swingWindow * winLevel() + swingTail(isAI) - 0.03) return;
|
||||
}
|
||||
// quality: how centered in the window the contact lands
|
||||
const center = TUNE.swingWindup + win / 2;
|
||||
@@ -1697,6 +1720,7 @@ function trySwing(ch, isAI, qCap = TUNE.aiQualityMax) {
|
||||
if (rallyFeel() && !isAI && TUNE.qSeamless && half > p)
|
||||
q = Math.max(0.05, 1 - (Math.abs(t - center) - p) / (half - p));
|
||||
if (Math.abs(t - center) * 1000 < TUNE.perfectMs) q = 1;
|
||||
if (t > TUNE.swingWindup + win) q = 0.05; // the follow-through: met, barely
|
||||
ch.facing = dx >= 0 ? 1 : -1; // forehand right (righty), backhand left → mirrored sprite
|
||||
// the LIVE swipe outranks the released one: since the crossing fires the swing (input.js), the
|
||||
// finger is usually still down at contact, and the vector under it NOW is the aim the player
|
||||
@@ -1708,7 +1732,9 @@ function trySwing(ch, isAI, qCap = TUNE.aiQualityMax) {
|
||||
// makes a hard CPU feel like it is PLAYING you. ax is an absolute court lane (see doHit).
|
||||
const smartChance = G.diff ? (G.diff.smart || 0) : G.match && !G.net ? (ch.foe?.smart || 0) : 0;
|
||||
// Keep the old random sequence for classic and for foes without smart aim.
|
||||
const smart = isAI && (G.diff || smartChance) && Math.random() < smartChance;
|
||||
// THE SITTER IS PUNISHED: rolled first and only on a sitter, so every other ball keeps its old sequence
|
||||
const kill = isAI && punishes(ch);
|
||||
const smart = kill || isAI && (G.diff || smartChance) && Math.random() < smartChance;
|
||||
const aim = isAI
|
||||
? { ax: smart ? (P.x > 0.6 ? -1 : P.x < -0.6 ? 1 : (Math.random() < 0.5 ? -1 : 1))
|
||||
: [ -1, 0, 1 ][Math.floor(Math.random() * 3)],
|
||||
@@ -1717,7 +1743,7 @@ function trySwing(ch, isAI, qCap = TUNE.aiQualityMax) {
|
||||
// THE STYLES (step 2): what this body DOES with the ball. A lob when you are at the net, a drop
|
||||
// when you hang back and it is inside its own court, a moonball as a habit — otherwise the rally
|
||||
// ball it always hit, carrying its spin. One roll, so the shares are exactly the table's.
|
||||
const st = isAI ? styleOf(ch) : null;
|
||||
const st = isAI && !kill ? styleOf(ch) : null;
|
||||
if (st) {
|
||||
const atNet = Math.abs(P.z - COURT.netZ) < TUNE.netFrom, r = Math.random();
|
||||
const inside = Math.abs(ch.z - COURT.netZ) < COURT.netZ - 1.5;
|
||||
@@ -1730,15 +1756,19 @@ function trySwing(ch, isAI, qCap = TUNE.aiQualityMax) {
|
||||
// luckiest returns. Classic and solo match traits own their bands; gore keeps its own floor.
|
||||
const qMin = G.diff ? G.diff.qMin : G.match && !G.net ? (ch.foe?.qMin ?? TUNE.aiQualityMin) : TUNE.aiQualityMin;
|
||||
if (isAI) q = Math.min(q, qMin + Math.random() * (Math.max(qCap, qMin) - qMin));
|
||||
if (kill) q = Math.max(q, TUNE.punishQ);
|
||||
const q0 = q; // (the log names whichever cap below took it down)
|
||||
// a volley struck BELOW THE TAPE can only be blocked up: soft, short, little width. This is what
|
||||
// the net costs — the passing shot dipped at your feet. ...and a DROP dug out at the ankles after
|
||||
// its bounce comes back the same way: reaching it is not enough. Backspin balls only — any ball met
|
||||
// on its first frame in reach is often low, and capping those made every rally ball a sitter.
|
||||
if (rallyFeel() && (ball.bounces === 0 && ball.y < COURT.netH || ball.spin < 1 && ball.y < TUNE.scoopY)) q = Math.min(q, TUNE.lowVolleyQ);
|
||||
// ...and SOFT backspin balls only (step 4): step 2's slicer hits every rally ball at spin 0.8, so this
|
||||
// capped every low contact against him, his own included — invisible until a sitter got punished.
|
||||
if (rallyFeel() && (ball.bounces === 0 && ball.y < COURT.netH || ball.soft && ball.spin < 1 && ball.y < TUNE.scoopY)) q = Math.min(q, TUNE.lowVolleyQ);
|
||||
// ...and a LOBBED net man who ran it down comes back on his heels: a defensive ball, not a reply.
|
||||
// That is the lob's real reward — rarely the winner itself, nearly always the point.
|
||||
if (rallyFeel() && ball.rx && ball.rx.lob) q = Math.min(q, TUNE.lowVolleyQ);
|
||||
if (kill && q >= TUNE.punishQ) aim.kill = true; // a put-away only if no cap took it down (a low volley is a block)
|
||||
if (!isAI && swingLog()) {
|
||||
G.swingSnaps = null; G.rallyNear = null;
|
||||
const kind = aim.az > 0 && rallyFeel() ? (aim.mag === undefined || aim.mag >= TUNE.dropMax ? ' lob' : ' drop')
|
||||
@@ -2271,7 +2301,11 @@ export function update(dt) {
|
||||
const chasing = ball.lastHit === 'O' || (G.mode === 'splatter' && !G.interlude && ball.vz > 0);
|
||||
// no `slow` here: tapping must not freeze your legs, or anticipating a shot loses it
|
||||
// ...and lobbed at the net he backpedals, exactly as slowly as the CPU does (TUNE.lobTurn)
|
||||
const rate = TUNE.playerSpeed * aK * dt * (chasing ? 1 : 0.55) * (chasing && ball.rx && ball.rx.lob ? TUNE.lobTurn : 1);
|
||||
let rate = TUNE.playerSpeed * aK * dt * (chasing ? 1 : 0.55) * (chasing && ball.rx && ball.rx.lob ? TUNE.lobTurn : 1);
|
||||
// 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; }
|
||||
// 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
|
||||
@@ -2368,7 +2402,7 @@ export function update(dt) {
|
||||
// 0.24 s and closes TUNE.parryWindow (0.34 s) before it has had its chance
|
||||
const win = TUNE.swingWindow * ((shots.length || (ball.active && ball.bounces === 0)) ? VOLLEY.window : 1)
|
||||
* winLevel(); // the clock must outlive the widened window or the level's gift is dead time
|
||||
if (P.swingT > TUNE.swingWindup + win + 0.02) { P.swingT = -1; if (G.phase === 'rally') swish(); logWhiff(); } // log even if the point ended mid-swing
|
||||
if (P.swingT > TUNE.swingWindup + Math.max(win, TUNE.swingWindow * winLevel() + swingTail(false)) + 0.02) { P.swingT = -1; if (G.phase === 'rally') swish(); logWhiff(); } // log even if the point ended mid-swing
|
||||
}
|
||||
expirePose(P, dt);
|
||||
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ export function netSendShot() {
|
||||
// the opponent struck: their ball, mirrored, becomes the live ball — and the authority flips here
|
||||
export function netApplyShot(m) {
|
||||
const s = mirror(m);
|
||||
ball.active = true; ball.collected = false; ball.bounces = 0; ball.rx = null; ball.approach = null;
|
||||
ball.active = true; ball.collected = false; ball.bounces = 0; ball.rx = null; ball.heels = 0; ball.approach = null;
|
||||
ball.x = s.x; ball.y = m.y; ball.z = s.z;
|
||||
ball.vx = s.vx; ball.vy = m.vy; ball.vz = s.vz;
|
||||
ball.g = m.g; ball.pow = m.pow; ball.spin = m.spin || 1; // the soft ball's backspin: both phones bounce it alike
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ import { gore } from './zombies.js'; // runtime-only cycle (zombies imports stat
|
||||
export const P = { x: 1.3, z: 22.8, side: 'P', pose: 'idle', poseT: 0, swingT: -1, facing: 1, runT: 0 };
|
||||
export const O = { x: -1.3, z: 1.0, side: 'O', pose: 'idle', poseT: 0, swingT: -1, facing: 1, runT: 0 };
|
||||
export const ball = { x: 0, y: 1, z: 20, vx: 0, vy: 0, vz: 0, g: 9.2,
|
||||
active: false, lastHit: 'P', bounces: 0, rx: null, approach: null, spin: 1, soft: false, collected: false, kills: 0, bomb: false, flame: false,
|
||||
active: false, lastHit: 'P', bounces: 0, rx: null, heels: 0, approach: null, spin: 1, soft: false, collected: false, kills: 0, bomb: false, flame: false,
|
||||
// NETPLAY: THE CALLED BALL PLAYS OUT. A verdict used to freeze the ball where it was — on the
|
||||
// striker's phone that is mid-roll, a whole court away from the call, and it read as the game
|
||||
// loop stopping. `scored` means "this flight is decided": physics keeps running, every scoring
|
||||
@@ -206,7 +206,7 @@ export const other = s => s === 'P' ? 'O' : 'P';
|
||||
export const deuceSide = () => (score.pts[0] + score.pts[1]) % 2 === 0; // even points → deuce (right) side
|
||||
|
||||
export function setupPoint() {
|
||||
G.phase = 'pretoss'; ball.active = false; ball.bounces = 0; ball.scored = false; ball.rx = null; ball.approach = null;
|
||||
G.phase = 'pretoss'; ball.active = false; ball.bounces = 0; ball.scored = false; ball.rx = null; ball.heels = 0; ball.approach = null;
|
||||
G.swingSnaps = null; G.rallyNear = null; G.swung = false;
|
||||
if (G.mode === 'splatter' && !G.match) { // always your serve, from wherever you're standing
|
||||
// (the MATCH falls through to the tennis branch below:
|
||||
|
||||
@@ -911,7 +911,7 @@ test('league 5 has no speed dip below league 4, including serve and wrong-footed
|
||||
G.match.opp = n; G.match.surprised = surprised; G.match.expected = false;
|
||||
zb.foe = cfg.matchFoe(n); zb.x = -3; zb.z = 2; zb.swingCool = 999;
|
||||
G.phase = 'rally'; G.wasServe = serve;
|
||||
Object.assign(ball, { active: true, lastHit: 'P', bounces: 0,
|
||||
Object.assign(ball, { active: true, lastHit: 'P', pow: 1, bounces: 0,
|
||||
x: 3, z: 5, y: 2, vx: 0, vy: 4, vz: -8 });
|
||||
update(DT);
|
||||
return Math.hypot(zb.vx, zb.vz);
|
||||
@@ -946,7 +946,7 @@ test('league 5 sustains strong returns and hunts the open court; league 4 stays
|
||||
G.match.surprised = G.match.expected = false;
|
||||
P.x = i % 2 ? 3 : -3; P.z = 23; P.swingT = -1;
|
||||
zb.x = 0; zb.z = 2; zb.swingT = -1; zb.swingCool = 0; zb.serveHold = 0;
|
||||
Object.assign(ball, { active: true, lastHit: 'P', bounces: 1,
|
||||
Object.assign(ball, { active: true, lastHit: 'P', bounces: 1, pow: 1, // a good ball: no put-away
|
||||
x: 0, z: 2.4, y: 1.2, vx: 0, vy: 0, vz: -1 });
|
||||
update(DT);
|
||||
if (ball.lastHit !== 'O') continue; // actual flubs remain part of the opponent
|
||||
@@ -1012,7 +1012,7 @@ test('match low contact: scoop rising balls, reject falling balls and contact ac
|
||||
const rz = oppOf();
|
||||
Object.assign(rz, { x: 0, z: Math.min(z, COURT.netZ - 1.2), swingT: -1, swingCool: 0 });
|
||||
G.phase = 'rally'; G.wasServe = false;
|
||||
Object.assign(ball, { active: true, lastHit: 'P', bounces: 1,
|
||||
Object.assign(ball, { active: true, lastHit: 'P', pow: 1, bounces: 1,
|
||||
x: 0, y, z, vx: 0, vy, vz: -1, g: TUNE.gravity });
|
||||
update(DT);
|
||||
assert.equal(ball.lastHit === 'O', returns, `contact at z=${z}, y=${y}, vy=${vy}`);
|
||||
@@ -1076,7 +1076,7 @@ test('THE COURT HAS ITS LENGTH: a match rally is played from the baselines, not
|
||||
oppOf().foe = cfg.matchFoe(11); // the BODY plays the style, not the rung number
|
||||
for (let i = 0; i < 2.2 * 60; i++) update(DT);
|
||||
const hits = [];
|
||||
let prev = ball.lastHit;
|
||||
let prev = ball.lastHit, putAway = false;
|
||||
for (let i = 0; i < 120 * 60; i++) {
|
||||
G.modal = null; G.modalQ.length = 0; G.hp = G.hpMax; G.match.opp = 11; // keep playing: the shape is the subject
|
||||
if (G.phase === 'pretoss' && !(G.interlude && G.interlude.introT > 0)) { if (i % 40 === 0) keyPressed('Space'); }
|
||||
@@ -1085,11 +1085,14 @@ test('THE COURT HAS ITS LENGTH: a match rally is played from the baselines, not
|
||||
const d = Math.hypot(ball.x - P.x, (ball.z - P.z) * 0.8);
|
||||
if (d < 2.9 && d > 2.2) keyPressed('Space');
|
||||
}
|
||||
const b0 = ball.bounces;
|
||||
const b0 = ball.bounces, kill = putAway;
|
||||
update(DT);
|
||||
if (ball.active && ball.lastHit !== prev && !G.wasServe) {
|
||||
const ch = ball.lastHit === 'P' ? P : oppOf();
|
||||
if (ch) hits.push({ back: Math.abs(ch.z - COURT.netZ) >= TUNE.netFrom, volley: b0 === 0, t: i * DT });
|
||||
// the answer to a PUT-AWAY (step 4) is a defence, not the rally's shape: a man caught on his heels
|
||||
// takes it where he can, in the air included
|
||||
if (ch) hits.push({ back: Math.abs(ch.z - COURT.netZ) >= TUNE.netFrom, volley: b0 === 0, t: i * DT, kill });
|
||||
putAway = ball.heels > 0;
|
||||
}
|
||||
prev = ball.active ? ball.lastHit : null;
|
||||
}
|
||||
@@ -1098,7 +1101,7 @@ test('THE COURT HAS ITS LENGTH: a match rally is played from the baselines, not
|
||||
// the autopilot mistimes into short balls and a body is MEANT to come in on those, so the share of
|
||||
// net contacts follows its timing; the back-court volley share below is the guard against the old T
|
||||
assert.ok(back.length / hits.length >= 0.5, `most of the rally is played from the back: ${back.length}/${hits.length}`);
|
||||
const volleys = back.filter(h => h.volley).length;
|
||||
const volleys = back.filter(h => h.volley && !h.kill).length;
|
||||
assert.ok(volleys / back.length <= 0.1, `a baseliner lets it bounce: ${volleys}/${back.length} back-court volleys`);
|
||||
const gaps = [];
|
||||
for (let k = 1; k < hits.length; k++) if (hits[k].back && hits[k - 1].back && hits[k].t - hits[k - 1].t < 3)
|
||||
@@ -1143,6 +1146,105 @@ test('a SHORT ball is an approach, a deep one keeps the baseliner back, and nobo
|
||||
} finally { TUNE.matchFlub = flub; }
|
||||
});
|
||||
|
||||
test('THE FOLLOW-THROUGH: a tap ~80 ms early on a baseline ball is met weakly, not whiffed', () => {
|
||||
// Step 4 (b), 2026-10-14. An early tap starts the swing while the ball is in the air, the swing waits
|
||||
// for the bounce, and the groundstroke window had run out by then: a whiff at ~70 ms early, while a
|
||||
// tap 120 ms LATE still met it. swingTail lets the swing meet it at q 0.05 — a sitter, not a miss.
|
||||
const assist = TUNE.autoAssist; TUNE.autoAssist = 1.15;
|
||||
const run = () => {
|
||||
startMatch(); G.interlude.introT = 0; G.match.opp = 6;
|
||||
for (let i = 0; i < 2.2 * 60; i++) update(DT);
|
||||
let armed = false, incoming = 0, met = 0;
|
||||
for (let i = 0; i < 90 * 60; i++) {
|
||||
G.modal = null; G.modalQ.length = 0; G.hp = G.hpMax; G.match.opp = 6;
|
||||
if (G.phase === 'pretoss' && !(G.interlude && G.interlude.introT > 0)) { if (i % 40 === 0) keyPressed('Space'); }
|
||||
else if (G.phase === 'toss') { if (ball.vy < 0.4 && ball.y > 2.4) keyPressed('Space'); }
|
||||
else if (G.phase === 'rally' && ball.active && ball.lastHit === 'O' && ball.vz > 0 && !armed) {
|
||||
const c = ringCue();
|
||||
if (c && c.eta - c.sweet <= 5 / 60 + 1e-6 && c.eta - c.sweet > 4 / 60) { armed = true; incoming++; keyPressed('Space'); }
|
||||
}
|
||||
const before = ball.lastHit;
|
||||
update(DT);
|
||||
if (armed && before === 'O' && ball.lastHit === 'P') { met++; armed = false; }
|
||||
else if (armed && (!ball.active || G.phase !== 'rally')) armed = false;
|
||||
}
|
||||
endMatch();
|
||||
return { incoming, met };
|
||||
};
|
||||
const tail = TUNE.swingTail;
|
||||
try {
|
||||
const on = run();
|
||||
assert.ok(on.incoming >= 20, 'a real sample: ' + on.incoming);
|
||||
assert.ok(on.met / on.incoming >= 0.8, `an ~80 ms early tap is met: ${on.met}/${on.incoming}`);
|
||||
TUNE.swingTail = 0;
|
||||
const off = run();
|
||||
assert.ok(off.met / off.incoming < 0.6, `...where the old edge whiffed it: ${off.met}/${off.incoming}`);
|
||||
} finally { TUNE.autoAssist = assist; TUNE.swingTail = tail; }
|
||||
});
|
||||
|
||||
test('THE SITTER IS PUNISHED: the put-away chance climbs the ladder, goes for the line, and finds him on his heels', () => {
|
||||
// Step 4 (b): before it, the CPU had no way to win a point — a q 0.05 floater came back like a drive
|
||||
// and every point it took was the player's own whiff, which no opponent dial touches.
|
||||
startMatch(); G.interlude.introT = 0;
|
||||
const zb = oppOf();
|
||||
const share = (n, pow) => {
|
||||
G.match.opp = n; zb.foe = cfg.matchFoe(n);
|
||||
let hits = 0, kills = 0, wide = 0, heels = 0;
|
||||
for (let i = 0; i < 1200; i++) {
|
||||
G.phase = 'rally'; G.wasServe = false;
|
||||
G.match.surprised = G.match.expected = false;
|
||||
P.x = 0; P.z = 23; P.swingT = -1;
|
||||
zb.x = 0; zb.z = 2; zb.swingT = -1; zb.swingCool = 0; zb.serveHold = 0;
|
||||
Object.assign(ball, { active: true, lastHit: 'P', bounces: 1, pow, rx: null, spin: 1, soft: false,
|
||||
x: 0, z: 2.4, y: 1.2, vx: 0, vy: 0, vz: -1 });
|
||||
update(DT);
|
||||
if (ball.lastHit !== 'O') continue;
|
||||
hits++;
|
||||
if (ball.heels > 0) {
|
||||
kills++; heels++;
|
||||
const t = (ball.vy + Math.sqrt(ball.vy * ball.vy + 2 * ball.g * ball.y)) / ball.g;
|
||||
if (Math.abs(ball.x + ball.vx * t) > 2.9) wide++;
|
||||
assert.ok(ball.pow >= TUNE.punishQ, 'a put-away is struck clean');
|
||||
}
|
||||
ball.active = false;
|
||||
}
|
||||
return { hits, kills: kills / hits, wide: kills ? wide / kills : 0 };
|
||||
};
|
||||
const champ = share(cfg.LADDER, 0.1), first = share(1, 0.1), good = share(cfg.LADDER, 0.9);
|
||||
assert.ok(Math.abs(champ.kills - TUNE.punishMax) < 0.06, `the champion puts a sitter away ${champ.kills.toFixed(2)}`);
|
||||
assert.ok(first.kills < champ.kills - 0.3 && first.kills > 0.1, `the first rung does it rarely ${first.kills.toFixed(2)}`);
|
||||
assert.equal(good.kills, 0, 'a good ball is never put away');
|
||||
assert.ok(champ.wide > 0.9, `the champion's put-away goes for the line ${champ.wide.toFixed(2)}`);
|
||||
endMatch();
|
||||
});
|
||||
|
||||
test('a low SLICE drive is not a dug-out drop: the scoop cap is for soft backspin balls only', () => {
|
||||
// Step 2's slicer hits every rally ball at spin 0.8, and the drop's dig-out cap read `spin < 1`, so every
|
||||
// low contact against him came back at lowVolleyQ — found when step 4 started punishing sitters.
|
||||
startMatch(); G.interlude.introT = 0;
|
||||
const zb = oppOf(); G.match.opp = 6; zb.foe = cfg.matchFoe(6);
|
||||
const flub = TUNE.matchFlub; TUNE.matchFlub = 0;
|
||||
try {
|
||||
for (const soft of [false, true]) {
|
||||
let n = 0, capped = 0;
|
||||
for (let i = 0; i < 300; i++) {
|
||||
G.phase = 'rally'; G.wasServe = false; G.match.surprised = G.match.expected = false;
|
||||
P.x = 0; P.z = 23; P.swingT = -1;
|
||||
zb.x = 0; zb.z = 2; zb.swingT = -1; zb.swingCool = 0; zb.serveHold = 0;
|
||||
Object.assign(ball, { active: true, lastHit: 'P', bounces: 1, pow: 1, rx: null, spin: 0.8, soft,
|
||||
x: 0, z: 2.4, y: 0.3, vx: 0, vy: 0.5, vz: -1 });
|
||||
update(DT);
|
||||
if (ball.lastHit !== 'O') continue;
|
||||
n++; if (ball.pow <= TUNE.lowVolleyQ + 1e-9) capped++;
|
||||
ball.active = false;
|
||||
}
|
||||
assert.ok(n > 200, 'returns: ' + n);
|
||||
if (soft) assert.equal(capped, n, 'a dug-out drop is still a weak reply');
|
||||
else assert.equal(capped, 0, 'a low slice drive is an ordinary ball');
|
||||
}
|
||||
} finally { TUNE.matchFlub = flub; endMatch(); }
|
||||
});
|
||||
|
||||
test('a groundstroke a little off PERFECT is a little worse, not a weak ball (no cliff past perfectMs)', () => {
|
||||
// Step 4's measurement (2026-10-14): q was 1 inside perfectMs and 1 - off/half outside it, so a
|
||||
// groundstroke (half-window ~80 ms) fell from 1 to 0.43 a millisecond past PERFECT, and a tap 30 ms
|
||||
@@ -1302,7 +1404,7 @@ function styleReturns(rung, pz, aiZ = 2) {
|
||||
G.phase = 'rally'; G.wasServe = false; G.match.surprised = G.match.expected = false;
|
||||
P.x = 0; P.z = pz; P.swingT = -1;
|
||||
zb.x = 0; zb.z = aiZ; zb.swingT = -1; zb.swingCool = 0; zb.serveHold = 0;
|
||||
Object.assign(ball, { active: true, lastHit: 'P', bounces: 1, rx: null, spin: 1, soft: false,
|
||||
Object.assign(ball, { active: true, lastHit: 'P', bounces: 1, rx: null, spin: 1, soft: false, pow: 1, // no sitter: no put-away
|
||||
x: 0, z: aiZ + 0.4, y: 1.2, vx: 0, vy: 0, vz: -1 });
|
||||
update(DT);
|
||||
if (ball.lastHit !== 'O') continue;
|
||||
@@ -1505,7 +1607,7 @@ test('rally depth: deep by default, mistiming lands it SHORTER, never longer', (
|
||||
G.phase = 'rally'; G.wasServe = false; G.match.surprised = G.match.expected = false;
|
||||
P.x = 0; P.z = 24; P.swingT = -1;
|
||||
zb.x = 0; zb.z = 2; zb.swingT = -1; zb.swingCool = 0; zb.serveHold = 0;
|
||||
Object.assign(ball, { active: true, lastHit: 'P', bounces: 1, rx: null,
|
||||
Object.assign(ball, { active: true, lastHit: 'P', pow: 1, bounces: 1, rx: null,
|
||||
x: 0, z: 2.4, y: 1.2, vx: 0, vy: 0, vz: -1 });
|
||||
update(DT);
|
||||
if (ball.lastHit !== 'O' || ball.soft) continue; // a style's moonball has its own depth (and tests)
|
||||
@@ -1532,7 +1634,7 @@ test('The Climb opens briskly, converges at opponent eight, and leaves cup and t
|
||||
G.match.surprised = G.match.expected = false;
|
||||
zb.foe = cfg.matchFoe(n); zb.x = -3; zb.z = 2; zb.swingCool = 999;
|
||||
G.phase = 'rally'; G.wasServe = false;
|
||||
Object.assign(ball, { active: true, lastHit: 'P', bounces: 0,
|
||||
Object.assign(ball, { active: true, lastHit: 'P', pow: 1, bounces: 0,
|
||||
x: 3, z: 5, y: 2, vx: 0, vy: 4, vz: -8 });
|
||||
update(DT);
|
||||
const chase = Math.hypot(zb.vx, zb.vz);
|
||||
@@ -1543,7 +1645,7 @@ test('The Climb opens briskly, converges at opponent eight, and leaves cup and t
|
||||
G.phase = 'rally'; G.wasServe = false;
|
||||
P.x = 0; P.z = 23; P.swingT = -1;
|
||||
zb.x = 0; zb.z = 2; zb.swingT = -1; zb.swingCool = 0; zb.serveHold = 0;
|
||||
Object.assign(ball, { active: true, lastHit: 'P', bounces: 1,
|
||||
Object.assign(ball, { active: true, lastHit: 'P', pow: 1, bounces: 1,
|
||||
x: 0, z: 2.4, y: 1.2, vx: 0, vy: 0, vz: -1 });
|
||||
update(DT);
|
||||
if (ball.lastHit !== 'O') continue;
|
||||
|
||||
+13
-1
@@ -84,6 +84,7 @@ const DT = 1 / 60;
|
||||
const RUNGS = (process.env.RUNGS || '1,6,12,18').split(',').map(Number);
|
||||
const SECS = Number(process.env.SECS || 240);
|
||||
TUNE.autoAssist = 1.15;
|
||||
for (const [k, v] of Object.entries(process.env)) if (k.startsWith('T_')) TUNE[k.slice(2)] = Number(v); // T_punishRead=0.4 etc.
|
||||
const pct = (a, f) => a.length ? Math.round(100 * a.filter(f).length / a.length) : 0;
|
||||
const med = a => { const s = [...a].sort((x, y) => x - y); return s.length ? s[Math.floor(s.length / 2)] : NaN; };
|
||||
// BOT=human (the default): taps on the timed gold ring like a player (JIT ms of timing spread, a
|
||||
@@ -92,7 +93,7 @@ const med = a => { const s = [...a].sort((x, y) => x - y); return s.length ? s[M
|
||||
const HUMAN = (process.env.BOT || 'human') === 'human';
|
||||
const JIT = Number(process.env.JIT ?? 60) / 1000, LAG = 7;
|
||||
const gauss = () => Math.sqrt(-2 * Math.log(1 - Math.random())) * Math.cos(2 * Math.PI * Math.random());
|
||||
const probe = [];
|
||||
const probe = [], kills = []; let kOpen = null, kpow = 1;
|
||||
const bot = { armed: false, fire: -1, jit: null, swipe: null };
|
||||
const sw = (dx, dy, mag) => { const n = Math.hypot(dx, dy) || 1, fx = dx / n, fy = dy / n;
|
||||
return { ax: Math.abs(fx) > 0.38 ? Math.sign(fx) : 0, az: Math.abs(fy) > 0.38 ? Math.sign(fy) : 0, fx, fy, mag }; };
|
||||
@@ -172,6 +173,16 @@ for (const rung of RUNGS) {
|
||||
update(DT);
|
||||
if (process.env.PROBE && lh === 'O' && ball.lastHit === 'P' && bot.land) { const L = bot.land; probe.push({ late: Math.round(((i - L.i) / 60 - L.eta) * 1000), sw: Math.round(L.sw * 1000), q: +ball.pow.toFixed(2), bAtLand: L.b, bAtHit: L.bb, d: +L.d.toFixed(2), y: +L.y.toFixed(2) }); bot.land = null; }
|
||||
if (ball.lastHit !== 'O') G.swipe = null;
|
||||
// KILL=1: every put-away (a q >= .95 CPU ball straight after a player ball under .5) — where it went,
|
||||
// how far the player had to go, how long he had, and whether he got it back
|
||||
if (process.env.KILL) {
|
||||
if (lh === 'P' && ball.lastHit === 'O' && kpow < 0.5 && ball.pow >= 0.95) {
|
||||
const L = gp.predictLanding(); kills.push({ px: +P.x.toFixed(1), pz: +P.z.toFixed(1), lx: L && +L.x.toFixed(1), lz: L && +L.z.toFixed(1), tl: L && +L.t.toFixed(2), v: +Math.hypot(ball.vx, ball.vz).toFixed(1), got: null }); kOpen = kills[kills.length - 1];
|
||||
}
|
||||
if (kOpen && ball.lastHit === 'P' && lh === 'O') { kOpen.got = +ball.pow.toFixed(2); kOpen = null; }
|
||||
if (kOpen && (!ball.active || G.phase !== 'rally')) { kOpen.got = 'LOST'; kOpen.gap = +(G.rallyNear ? G.rallyNear.d : -1).toFixed(2); kOpen = null; }
|
||||
if (ball.lastHit === 'P') kpow = ball.pow;
|
||||
}
|
||||
const rz = process.env.CLASSIC ? state.O : zombies.find(z => z.variant === 'r');
|
||||
if (ball.active && ball.lastHit !== prevLast && (ball.lastHit === 'P' || ball.lastHit === 'O')) {
|
||||
const ch = ball.lastHit === 'P' ? P : rz;
|
||||
@@ -221,6 +232,7 @@ for (const rung of RUNGS) {
|
||||
rows.push({ rung, side: 'rally', shots: rallies.length, medDistNet: med(rallies), '<6.4m(svcline)%': '', '<3.5m(net)%': '', 'volley%': '', medTimeToHit_s: 'shots/pt mean ' + (rallies.reduce((a, b) => a + b, 0) / rallies.length).toFixed(1) });
|
||||
}
|
||||
if (!process.env.SUMMARY) { console.table(rows); console.table(land); }
|
||||
if (process.env.KILL) { console.table(kills.slice(0, 30)); const l = kills.filter(k => k.got === 'LOST').length; console.log('put-aways', kills.length, 'won by CPU', l); }
|
||||
if (process.env.PROBE) { const l = probe.map(p => p.late).sort((a, b) => a - b); console.log("contact after predicted entry, ms: n", l.length, "mean", (l.reduce((a, b) => a + b, 0) / l.length).toFixed(1), "p10", l[l.length * 0.1 | 0], "p50", l[l.length / 2 | 0], "p90", l[l.length * 0.9 | 0]); }
|
||||
if (process.env.JSON) { console.log(JSON.stringify(pts)); process.exit(0); }
|
||||
console.table(pts);
|
||||
|
||||
Reference in new issue
Block a user