The lob is the flick you already have, and a game stops being a death

Three from Oliver.

THE LOB. He picked option (c) from the written comparison — "no additional
gesture to learn" — and corrected the premise: the zombie does come to the
net (racketAI chases to the far service line, the classic CPU to z 9.5). So
the forward flick keeps its one meaning, "put it deep behind him", and the
game picks the trajectory that actually does that: a smash while he is back,
over his head once he is in. oppAtNet() is the whole switch, and it reads the
BODY, not O — in the match O is the invisible tennis body and only moves
between points, so it would have answered "on his mark" all rally.

lobArm 7.0 m is measured, not taste: a lob peaking at lobApex passes a man 7 m
from the net at almost exactly CONTACT.maxY, so the shot arms exactly as far
out as it can still clear him. The pace is derived from the height, so the same
arc comes off the baseline and off mid-court, and SHOT_DEPTH.lob is deeper than
deep — at the ordinary deep target the ball passed 2.05 m over him at the edge
of the band, which is inside his reach.

PACE, SEEN FROM THE STRIKER. "the opponent sees the ball really fast - the
player barely sees the speed increase." Measured: the ball is the same ball on
both phones, the camera is not — it compresses your own ball into the distance
and expands theirs as it arrives. Pace untouched; three channels added on the
striker's side: the ghost streak now reads pace (3-8 ghosts, was 3 at every
speed), the crack is pitched by it, and a fast ball kicks the camera. The
serve's own ladder is 49.2 plain / 50.8-52.4 hard, because serveArc's loft
margin takes 10% of the +9 back — widening that is his call, and PROJECT.md
records the knob and the risk.

THE DEATH MATCH. "each player has 8 blood points but the player explodes for
the winner already at 0:40." Two clocks: a game is four points, a life is
eight. A won game is only a game now (flash, serve, heal); the burst moved to
the death itself, on the dead message, where both phones agree. The summary
waits 1.4 s for the gore, because update() freezes the world on G.done and
would have hung a still frame of an explosion behind the trophy screen. And
bleedO tracks THEIR blood off the pos stream instead of our points-this-game,
which reset every game and ignored the heal.

4 new tests, 113 pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-22 12:58:48 +02:00
1 parent 6e07dc74bb
commit 4f4534e272
9 files changed
+355 -38

No files matched your search

+17 -1
View File
@@ -74,6 +74,16 @@ 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 LOB ("lupfen", Oliver 2026-08-22). Same forward flick as the smash — the game picks which
// shot that gesture means from WHERE THE OTHER MAN IS STANDING, so there is no second gesture to
// learn. He is "in" when he is closer to the net than this, in metres:
lobArm: 7.0, // ...and 7.0 is not a taste number. A lob peaking at lobApex passes a man
// 7 m from the net at almost exactly CONTACT.maxY (2.3 m), so the shot
// arms EXACTLY where it can still go over him. Raise one, raise the other.
lobApex: 6.0, // m the lob peaks at, whatever the distance. The pace is DERIVED from this
// (see doHit): a fixed speed would be a fine lob from the baseline and a
// flat drive from mid-court, because the same speed over half the ground
// is half the hang time. Height is the shot; speed is what it costs.
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
@@ -587,6 +597,8 @@ export const TUNE = {
// ("still a LOT of unreachable balls"); now line-painting is the late ladder
matchWidePer: 0.25, // ...earning this much more width per opponent felled (softened with speedPer)
matchHeal: 2, // hp back for a game won (the duel's healPerDuel rate)
netWinBurst: 1.4, // s the friend's burst plays before the win screen covers it. update() freezes
// the world on G.done, so this beat is the only thing that lets the gore fly.
matchBoomHold: 0.5, // s the game-winning body lies prone before it bursts — the fall must be SEEN
killGush: 3, // prone bleed-out during the kill fall: particles ATTEMPTED per frame (gushProne)
// — the body visibly bleeds out on the ground before it bursts
@@ -1397,7 +1409,11 @@ export const P_BOUNDS = { x: 6.5, zMin: COURT.netZ + 1.2, zMax: 26 };
export const TOSS = { y0: 1.8, vy: 4.6, dx: -0.44, oDx: 0.42 };
// shot target depth from the receiving baseline, by aim (up = deep, none = mid, down = short)
export const SHOT_DEPTH = { deep: 3.0, mid: 5.4, short: 8.6 };
// `lob` is DEEPER than deep on purpose: a lob that lands where an ordinary drive lands passes only
// a metre or two behind the man it is going over, and a ball that close to him is still inside his
// 2.3 m reach (measured — at SHOT_DEPTH.deep it cleared him by nothing at the far edge of the arm
// band). The shot is defined by where it comes DOWN, not by how high it goes.
export const SHOT_DEPTH = { deep: 3.0, mid: 5.4, short: 8.6, lob: 2.0 };
// how long swing/serve poses hold before returning to idle (s)
export const POSE_TIME = 0.38;