Give the rally its length back: deep balls, baseliners, approaches off short balls
Every match had collapsed into serve, both at the net, reflex volleys at ~0.7 s per shot. The rally ball landed on the service line by definition, the match CPU stood on the bounce and volleyed 90-100% of everything, and the auto-run chased the earliest contact. Rally modes only (classic + every match; gore and the duel untouched): rally balls land deep and mistiming only shortens them (no new long outs); each shot latches the receiver's plan as it leaves the racket — from the back he lets it bounce and meets it falling, up to 2.2 m behind the line, a short ball pulls him in and he follows it to the net, nobody approaches a man already there, and a volley below the tape is a weak block. Measured: contacts 10.5-13.7 m from the net, back-court volleys 0-8%, 0.8-1.0 s per baseline shot. Three new tests pin the shape; v361. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f33d46366a
commit
4e7c816a25
8 files changed
+292
-29
No files matched your search
@@ -74,6 +74,24 @@ export const TUNE = {
|
||||
smashBoost: 0.42, // extra pace on a FORWARD flick (aim.az < 0), scaled by timing. It does not
|
||||
// send the ball longer — solveShot still lands it on the same spot — it
|
||||
// gets there flatter and sooner, which is what a smash is.
|
||||
// THE COURT HAS ITS LENGTH (2026-10-13, PROJECT.md "THE RALLY GOES FLAT"). RALLY MODES ONLY
|
||||
// (classic + every match). Gore's tennis prologue and the duel keep SHOT_DEPTH and HOME. The rally
|
||||
// ball used to land ON the service line (SHOT_DEPTH.mid 5.4) and both sides met it as early as
|
||||
// they could, so every point became a volley exchange at the T at ~0.7 s per shot.
|
||||
rallyMid: 2.8, // m inside the receiver's baseline a plain rally ball aims for (gore: 5.4)
|
||||
rallyDeep: 1.9, // ...and a forward flick (gore: 3.0). Mistiming only ever SHORTENS these,
|
||||
// so depth adds no long out
|
||||
shortK: 3.0, // m of depth a quality-0 ball loses (rolled x0.5..1.5): a mistimed ball
|
||||
// sits up SHORT, which is the ball a real opponent attacks
|
||||
shortBall: 4.9, // m from the receiver's baseline past which a landing is SHORT: he runs
|
||||
// in and takes it on the rise (serves never count)
|
||||
netFrom: 7.0, // m from the net inside which a body is AT THE NET: it volleys, and it
|
||||
// recovers to netStand instead of the baseline
|
||||
netStand: 3.2, // m from the net a net player recovers to
|
||||
rallyBack: 0.6, // m behind the baseline a baseliner recovers to
|
||||
rallyBehind: 2.2, // m behind the baseline he will drop back to let a deep ball come down
|
||||
rallyHit: 1.0, // m: a baseliner meets the ball falling through this height
|
||||
lowVolleyQ: 0.3, // quality cap on a volley struck below the tape: you can only block it up
|
||||
aimError: 2.4, // m of scatter at quality 0
|
||||
aimTrust: 0.3, // an aimed rally shot TRUSTS its flick: the scatter may still push the
|
||||
// ball further OUT (the chosen out-risk keeps its exact shape) but only
|
||||
|
||||
+93
-16
@@ -243,8 +243,14 @@ 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);
|
||||
}
|
||||
const tzBase = aim.az < 0 ? SHOT_DEPTH.deep : aim.az > 0 ? SHOT_DEPTH.short : SHOT_DEPTH.mid;
|
||||
tz = (toFar ? tzBase : COURT.L - tzBase) + err() * 0.8;
|
||||
// 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).
|
||||
const deep = rallyFeel() && aim.az <= 0;
|
||||
const tzBase = aim.az < 0 ? (deep ? TUNE.rallyDeep : SHOT_DEPTH.deep)
|
||||
: aim.az > 0 ? SHOT_DEPTH.short : deep ? TUNE.rallyMid : SHOT_DEPTH.mid;
|
||||
const dz = deep ? (1 - quality) * TUNE.shortK * (0.5 + Math.random()) : err() * 0.8;
|
||||
tz = deep ? (toFar ? tzBase + dz : COURT.L - tzBase - dz) : (toFar ? tzBase : COURT.L - tzBase) + dz;
|
||||
// A FORWARD FLICK IS A SMASH. Up-screen is the far court, so flicking INTO the court asked for
|
||||
// depth and nothing else — the gesture had a direction with no consequence behind it. It buys
|
||||
// PACE now, scaled by timing like the width is. Note what it deliberately does not do: solveShot
|
||||
@@ -315,7 +321,12 @@ function doHit(ch, quality, aim) {
|
||||
}
|
||||
ball.g = g;
|
||||
ball.vx = v.vx; ball.vy = v.vy; ball.vz = v.vz;
|
||||
ball.lastHit = ch.side; ball.bounces = 0;
|
||||
// ...and a short ball taken is an APPROACH: he follows it in (rallyHome reads this while it flies).
|
||||
// Never against a man already at the net — that ball is a PASS, and two volleyers is the old
|
||||
// reflex duel at a quarter of a second per shot. ball.from is the striker's distance from the net.
|
||||
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
|
||||
G.wasServe = false; // a serve volleyed back pre-bounce must not fault-check the return
|
||||
ball.pow = quality; // carried pace: zombies splatter only above zSplatQ
|
||||
ball.kills = 0; // fresh shot: multi-kill streak restarts
|
||||
@@ -531,7 +542,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.lastHit = sv.side; ball.bounces = 0; ball.rx = null; ball.approach = null; ball.from = Math.abs(sv.z - COURT.netZ);
|
||||
ball.pow = quality;
|
||||
ball.kills = 0; // fresh serve: multi-kill streak restarts
|
||||
ball.praise = null; // ...and any word a spent ball never got to pop dies with it
|
||||
@@ -1346,6 +1357,43 @@ export const winLevel = () => G.match && !G.net ? bonus().window * (1 + TUNE.pee
|
||||
export const assistK = () => (rallyFeel() ? Math.max(TUNE.autoAssist, TUNE.matchAssist) : TUNE.autoAssist)
|
||||
* legLevel();
|
||||
|
||||
// THE COURT HAS ITS LENGTH. How the receiver plays THIS shot, decided once as it leaves the racket
|
||||
// (every strike clears ball.rx): at the net he volleys; from the back he lets it bounce, and a SHORT
|
||||
// landing pulls him in to take it on the rise. Latched, so a man running in for a short ball does not
|
||||
// turn volleyer the moment he crosses netFrom. Rally modes only — the callers gate it.
|
||||
function rxPlan(ch) {
|
||||
if (!ball.rx) {
|
||||
const land = ball.bounces === 0 ? predictLanding() : null;
|
||||
const depth = land ? (ch.side === 'P' ? COURT.L - land.z : land.z) : 0;
|
||||
ball.rx = { net: Math.abs(ch.z - COURT.netZ) < TUNE.netFrom, short: !G.wasServe && depth > TUNE.shortBall };
|
||||
}
|
||||
return ball.rx;
|
||||
}
|
||||
// ...a baseliner lets a low ball bounce: only a man at the net takes it out of the air below the tape
|
||||
const letBounce = () => ball.bounces === 0 && ball.y < COURT.netH && ball.rx && !ball.rx.net;
|
||||
// ...and where he recovers to is where he IS: at the net he holds the net, otherwise he goes back
|
||||
// behind his baseline. Read live — recovery always moves him away from netFrom, so it cannot dither.
|
||||
// The one exception is the APPROACH: having taken a short ball, he follows it in.
|
||||
const rallyHome = ch => ({ x: 0, z: Math.abs(ch.z - COURT.netZ) < TUNE.netFrom || ball.approach === ch.side
|
||||
? COURT.netZ + (ch.side === 'P' ? TUNE.netStand : -TUNE.netStand)
|
||||
: ch.side === 'P' ? COURT.L + TUNE.rallyBack : -TUNE.rallyBack });
|
||||
// Where a baseliner meets this ball: after the bounce, falling through rallyHit, or at the top of a
|
||||
// bounce that never gets that high. A SHORT ball is met on the RISE instead, moving in. Closed form
|
||||
// off predictLanding, for racketAI (intercept walks the flight for itself).
|
||||
function bounceContact(short) {
|
||||
const g = ball.g || TUNE.gravity;
|
||||
let { x, y, z, vx, vy, vz } = ball;
|
||||
if (ball.bounces === 0) {
|
||||
const p = predictLanding(); if (!p) return null;
|
||||
x = p.x; z = p.z; y = 0;
|
||||
vy = -(vy - g * p.t) * TUNE.bounceY; vx *= TUNE.bounceXZ; vz *= TUNE.bounceXZ;
|
||||
}
|
||||
const apex = y + Math.max(0, vy) ** 2 / (2 * g), h = Math.min(TUNE.rallyHit, apex);
|
||||
const r = Math.sqrt(Math.max(0, vy * vy - 2 * g * (h - y)));
|
||||
const t = Math.max(0, (short && vy > 0 ? vy - r : vy + r) / g);
|
||||
return { x: x + vx * t, z: z + vz * t };
|
||||
}
|
||||
|
||||
function intercept(side) {
|
||||
const ch = side === 'O' ? O : P;
|
||||
// G.diff is CLASSIC's difficulty chip (config.DIFFS) — null everywhere else, so only the
|
||||
@@ -1354,11 +1402,22 @@ function intercept(side) {
|
||||
let { x, y, z, vx, vy, vz } = ball;
|
||||
const g = ball.g || TUNE.gravity, dt = 0.01;
|
||||
const inZone = side === 'O' ? zz => zz <= ZONE_DEPTH : zz => zz >= COURT.L - ZONE_DEPTH;
|
||||
const home = HOME[side];
|
||||
let fallback = null, deepest = null, anyHit = null;
|
||||
const home = rallyFeel() ? rallyHome(ch) : HOME[side];
|
||||
const plan = rallyFeel() ? rxPlan(ch) : null, back = plan && !plan.net; // a baseliner lets it bounce
|
||||
let fallback = null, deepest = null, anyHit = null, rise = null, nb = ball.bounces;
|
||||
for (let i = 0; i < 300; i++) {
|
||||
vy -= g * dt; x += vx * dt; y += vy * dt; z += vz * dt;
|
||||
if (y <= 0 && vy < 0) { y = 0; vy = -vy * TUNE.bounceY; vx *= TUNE.bounceXZ; vz *= TUNE.bounceXZ; }
|
||||
if (y <= 0 && vy < 0) { y = 0; vy = -vy * TUNE.bounceY; vx *= TUNE.bounceXZ; vz *= TUNE.bounceXZ; nb++; }
|
||||
// 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.
|
||||
if (back && nb === 1 && i > 5 && y > 0.45 && y < TUNE.rallyHit + (plan.short ? 0.6 : 0.3)) {
|
||||
const inB = side === 'P' ? z <= COURT.L + TUNE.rallyBehind : z >= -TUNE.rallyBehind;
|
||||
if (inB && Math.hypot(x - ch.x, z - ch.z) / spd <= i * dt) {
|
||||
if (plan.short || vy < 1) return { x, z };
|
||||
rise ||= { x, z };
|
||||
}
|
||||
}
|
||||
// THE SHORT BALL (match): a slow return can bounce and DIE at z 18-20, which never enters the
|
||||
// near zone below — so nothing ever chased it, it rolled dead at the player's feet (below the
|
||||
// legal contact height by then) and the point went to the zombie. Seven of seven recorded
|
||||
@@ -1371,12 +1430,12 @@ function intercept(side) {
|
||||
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.hypot(pt.x - ch.x, pt.z - ch.z) / spd <= i * dt;
|
||||
if (y > 0.7 && reachable) return pt; // waist-high AND arrivable in time
|
||||
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
|
||||
}
|
||||
}
|
||||
return fallback || deepest || anyHit || home; // zone points first: meeting deep is still better
|
||||
return rise || fallback || deepest || anyHit || home; // zone points first: meeting deep is still better
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------- miss diagnostics ----
|
||||
@@ -1449,6 +1508,7 @@ function trySwing(ch, isAI, qCap = TUNE.aiQualityMax) {
|
||||
const dx = ball.x - ch.x, dz = ball.z - ch.z;
|
||||
if (Math.hypot(dx, dz * 0.8) > reach || ball.y > CONTACT.maxY) return;
|
||||
if (ball.y < CONTACT.minY && ball.vy <= 0) return; // a DROPPING ball below the ankles is past you — but a rising one is a half-volley, always scoopable
|
||||
if (letBounce()) return; // planned from the back: the swing waits for the bounce instead of a weak pick-up
|
||||
// quality: how centered in the window the contact lands
|
||||
const center = TUNE.swingWindup + win / 2;
|
||||
const off = Math.abs(t - center) / (win / 2);
|
||||
@@ -1475,6 +1535,9 @@ 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));
|
||||
// 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.
|
||||
if (rallyFeel() && ball.bounces === 0 && ball.y < COURT.netH) q = Math.min(q, TUNE.lowVolleyQ);
|
||||
if (!isAI && G.mode === 'tennis') {
|
||||
G.swingSnaps = null; G.rallyNear = null;
|
||||
logSwing({ result: 'HIT', reason: `contact q=${Math.round(q * 100) / 100}`, d: Math.round(Math.hypot(dx, dz * 0.8) * 100) / 100, y: Math.round(ball.y * 100) / 100 });
|
||||
@@ -1521,11 +1584,25 @@ 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 = recovering ? HOME.O.x : pred ? ball.x + (pred.x - ball.x) * read : ball.x;
|
||||
// THE COURT HAS ITS LENGTH (match only): from the back he waits BEHIND the bounce for the ball to
|
||||
// come down — standing ON it made him volley 90-100% of everything from the T — and a short ball
|
||||
// pulls him in. At the net he holds his depth on the ball's line, unless it will clear his racket:
|
||||
// that is a lob, and he turns and runs it down like a baseliner.
|
||||
const plan = G.match && incoming ? rxPlan(rz) : null;
|
||||
if (plan && plan.net && ball.vz < 0) {
|
||||
const tc = (rz.z - ball.z) / ball.vz, gg = ball.g || TUNE.gravity;
|
||||
if (tc > 0 && ball.y + ball.vy * tc - 0.5 * gg * tc * tc > CONTACT.maxY) plan.net = false;
|
||||
}
|
||||
const c = plan && !plan.net ? bounceContact(plan.short) : null;
|
||||
const netX = plan && plan.net ? ball.x + ball.vx * Math.max(0, (rz.z - ball.z) / ball.vz) : null;
|
||||
const tgtX = c ? c.x : netX ?? (pred && pred.x);
|
||||
const home = G.match ? rallyHome(rz) : HOME.O;
|
||||
const aimX = recovering ? home.x : tgtX != null ? ball.x + (tgtX - 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 aimZ = recovering ? home.z : c ? c.z : netX !== null ? rz.z
|
||||
: pred ? (G.match ? pred.z : ball.z + (pred.z - ball.z) * read) : ball.z;
|
||||
const tz = Math.max(G.match ? -TUNE.rallyBehind : 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,
|
||||
@@ -1559,7 +1636,7 @@ function racketAI(rz, dt) {
|
||||
const wnd = G.match ? matchWound(rz) : { reach: 1, flub: 1 };
|
||||
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.maxY && !letBounce()
|
||||
&& (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.
|
||||
@@ -1976,7 +2053,7 @@ export function update(dt) {
|
||||
// who never walked back.
|
||||
const tgt = chasing ? intercept('P')
|
||||
: G.mode === 'splatter' && !G.match ? { x: P.x, z: HOME.P.z }
|
||||
: HOME.P;
|
||||
: rallyFeel() ? rallyHome(P) : HOME.P;
|
||||
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
|
||||
P.x += dx / d * Math.min(rate, d);
|
||||
@@ -2105,7 +2182,7 @@ export function update(dt) {
|
||||
// the flight shortens the earliest-reachable point jumps around its solution — each jump
|
||||
// flips his run direction, faster and faster. He commits to the stored point unless the
|
||||
// fresh solve moves it meaningfully (or the chase resets between shots).
|
||||
const fresh = incoming ? intercept('O') : HOME.O;
|
||||
const fresh = incoming ? intercept('O') : rallyFeel() ? rallyHome(O) : HOME.O;
|
||||
if (!incoming || !G.aiTgt || Math.hypot(fresh.x - G.aiTgt.x, fresh.z - G.aiTgt.z) > 0.35)
|
||||
G.aiTgt = fresh;
|
||||
const tgt = G.aiTgt;
|
||||
@@ -2123,7 +2200,7 @@ export function update(dt) {
|
||||
if (incoming && G.aiSwingCool <= 0 && O.swingT < 0) {
|
||||
// same radius trySwing uses for contact, so an armed swing actually connects
|
||||
const near = Math.hypot(ball.x - O.x, (ball.z - O.z) * 0.8) < TUNE.aiReach
|
||||
&& ball.y < CONTACT.maxY && ball.y > CONTACT.minY && ball.vz < 0;
|
||||
&& ball.y < CONTACT.maxY && ball.y > CONTACT.minY && ball.vz < 0 && !letBounce();
|
||||
if (near) {
|
||||
// rubber band: every game HE has taken makes him flub more — the opening match must not
|
||||
// be a wall a new player grinds against for rounds (see aiFlubLoss in config).
|
||||
|
||||
+2
-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.active = true; ball.collected = false; ball.bounces = 0; ball.rx = null; 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;
|
||||
@@ -84,6 +84,7 @@ export function netApplyShot(m) {
|
||||
G.wasServe = !!m.serve; // arms the receiver's own service-box call
|
||||
G.phase = 'rally'; G.swung = false; G.rallyNear = null;
|
||||
O.x = s.x; O.z = s.z; // the strike happened where the ball says it did
|
||||
ball.from = Math.abs(O.z - COURT.netZ); // ...which is what the receiver's approach rule reads
|
||||
O.pose = m.serve ? 'serve' : 'swing'; O.poseT = 0;
|
||||
// ...AND IT MAKES A NOISE (Oliver, 2026-09-11: "der ballsound des gegners wird nicht abgespielt").
|
||||
// Every other opponent in the game is simulated locally and cracks through gameplay's own strike
|
||||
|
||||
+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, collected: false, kills: 0, bomb: false, flame: false,
|
||||
active: false, lastHit: 'P', bounces: 0, rx: null, approach: null, 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
|
||||
@@ -205,7 +205,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;
|
||||
G.phase = 'pretoss'; ball.active = false; ball.bounces = 0; ball.scored = false; ball.rx = null; 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:
|
||||
|
||||
Reference in new issue
Block a user