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

+96 -1
View File
@@ -491,6 +491,98 @@ zombie (now an occasional opponent, `TUNE.racketEvery`, not the every-game fixtu
`RETURNED`, `DOUBLE KILL`) is a footnote to something you already watched happen. Sharing the loud
FACE with the milestone cards is right; sharing their SIZE made every footnote read as a milestone.
## Session log: THE LOB, AND TWO THINGS THE MULTIPLAYER TEST FOUND (2026-08-22, same day)
### THE LOB IS THE FLICK YOU ALREADY HAVE — option (c), picked by Oliver after a written comparison
Oliver: *"when you're close to the net in a duel with another player there should be a gesture
allowing to make a ball flying high from below (lupfen)... this likely needs to be discussed."*
Three candidates were put to him — (a) slow drag vs fast flick, (b) a down-then-up hook, (c) context
arms it — and he picked (c): *"i think this is the easiest. no additional gesture to learn."* He also
corrected the premise: *"the opponent should come to the net too — in zombie tennis the zombie often
comes to the net"*, and he is right (racketAI chases up to `COURT.svcFar`, the classic CPU to z 9.5).
**The rule: a forward flick means "put it deep behind him", and the game picks the trajectory that
actually does that.** With him on his baseline that is the smash it has always been; with him in, it
is over his head. `oppAtNet()` in gameplay.js is the whole switch. Four things it rests on:
1. **It reads the BODY, not O.** In the duel and the match the opponent you play is the racket
zombie; O is the invisible tennis body the serve machinery is driven off and it only moves
between points, so asking O would have answered "on his mark" all rally, in the one mode Oliver
plays most. `farBody()` picks the 'r' zombie whenever there is one and is not netplay.
2. **`TUNE.lobArm` 7.0 m is not taste.** A lob peaking at `lobApex` passes a man 7 m from the net at
almost exactly `CONTACT.maxY` (2.3 m, the height above which nothing can be struck) — so the shot
arms EXACTLY as far out as it can still clear him. Measured, not guessed; the numbers are in the
suite. Move one of the three and re-measure the other two.
3. **The pace is DERIVED from the height, never chosen.** `vy = sqrt(2g(apex - y))` fixes the hang
time and the ground still to cover fixes the speed, so the same arc comes off the baseline (9.5
m/s) and off mid-court (5.8 m/s). A fixed speed was tried on paper first and is a fine lob from
one place and a flat drive from the other. It also carries NO topspin multiplier — the dive that
makes a hard flat ball land in would pull a lob into the net.
4. **`SHOT_DEPTH.lob` is DEEPER than `deep` (2.0 vs 3.0 m).** At the ordinary deep target the ball
passes only a metre or two behind the man it is going over, and at the far edge of the arm band
that was 2.05 m — inside his reach. The shot is defined by where it comes DOWN.
Measured, nominal (no scatter), from the baseline: over a man at the far service line it passes at
**3.25 m**, lands 2 m inside his baseline, 2.17 s of hang. Over one at z 9.5 (a real net-rusher):
5.4 m. The AI can and does run it down — a lob is not a free point in tennis either — but it pulls
him off the net, which is what the shot is for. The word `LOB` pops at the contact: (c) buys "no
gesture to learn" by letting context change what the flick means, and something has to say which
shot you just asked for.
### "THE OPPONENT SEES THE BALL REALLY FAST — THE PLAYER BARELY SEES THE SPEED INCREASE"
Measured first, because the report has two possible causes and only one of them is a bug.
**The ball is the same ball on both phones** — `netSendShot` copies vx/vy/vz/g verbatim and the
receiver mirrors them, no re-simulation. What differs is the CAMERA: it sits behind your baseline,
so your own ball's screen speed decays into the distance while the same shot expands toward the man
receiving it. That asymmetry cannot be fixed by changing pace.
The pace ladder, measured: **serve 49.2 plain / 50.8-52.4 with the hard flick** (the +9 m/s
`serveHard` asks for 56-58, and `serveArc`'s net-clearance loop takes 10% of it back in one cut);
**rally 19.2 + 14.4×timing, ×1.42 for a smash at flush timing** — so the smash is a real 42% when
you time it and nothing when you do not, and the hard SERVE is only ever ~3-6%.
Nothing about the pace was changed. Three feedback channels were added instead, all on the striker's
side, none of them touching a ball's physics:
- **The ghost streak now reads PACE.** It was three ghosts at every speed, so it said fast/not-fast
and nothing else; it is 3 to 8 now, sampled every frame, so a quick ball is both longer and more
spread out. A lob stays clean.
- **The crack is pitched by pace** (`strikeRate`, 0.93 to 1.09 — past a tenth the sample stops
sounding like the same racket).
- **A fast ball kicks the camera** (`strikeKick`, up to ~2.2 px for 0.12 s, YOUR racket only, and
never over a shake already running).
**Still open, and it is Oliver's call.** If he wants the hard serve to actually BE harder, the knob
is `serveArc`'s loft margin: it charges `0.06 + descent×0.04` (about 0.46 m of extra net clearance
on a fast serve) against a sampling error the sim does not really make any more — the net test in
`update()` already interpolates the crossing height, and semi-implicit Euler's real lag at the tape
measures ~2-17 cm. Cutting that margin gives the hard serve its full +9 back (58 vs 49, a difference
you could see). Not done unasked: a serve into the tape is worse than a slow one, and that margin
exists because balls were clipping it.
### THE DEATH MATCH: THE BURST IS THE DEATH, AND ONLY BLOOD IS
Oliver: *"each player has 8 blood points but the player explodes for the winner already at 0:40 —
the winner sees the zombie explode but the player on the other side is not dead yet. this needs to
be aligned."* Two clocks were running side by side: **a game is four points, a life is eight**, and
`matchGameWon` burst the friend's body on the game. Both were true on their own phone and they
disagreed with each other.
- **A won game is only a game now** (netplay branch of `matchGameWon`): the flash, the serve change
and the heal it pays, then the next point. No burst, no walk-on, no mop beat — there is no roster
body to walk back on in a death match anyway.
- **The burst moved to the death**, in `netMatchWon`, which fires on the `dead` message the dying
phone sends when its own blood runs out. Both sides are then agreeing about the same event.
- **The summary waits `TUNE.netWinBurst` (1.4 s) for the gore to fly.** `update()` freezes the whole
world on `G.done` — no particles — so setting it on the same frame as the burst hangs a still
frame of an explosion behind the trophy screen. Ticked in `tickMatchFall`, like every other beat
in beats.js that has to outlive the frame that started it.
- **The friend's bleeding tracks THEIR blood.** `bleedO` read points-won-this-game, which resets
every game and ignores the heal — a friend two points from death looked unhurt. Their hp now rides
the 10 Hz `pos` stream (one number; a dropped one corrects itself 100 ms later) and the flow reads
it. The old count survives as the fallback for a build that sends none.
## Session log: THE SERVICE RULES ARRIVE IN THE OTHER MODES (2026-08-22)
Three notes from Oliver, two of them fixed here; the third (the lob) is under discussion in OPEN.
@@ -3941,7 +4033,10 @@ Nothing here is broken; these are known gaps and unverified risks.
from a tin). At 12 rounds/s a per-pellet sample will machine-gun the mixer, not just the wave.
- **boss sounds** — footfall, wind-up, throw, wound, death. Three designs in `BOSS_ART`; decide
whether they share one set or get one each before recording anything.
14. **THE LOB ("lupfen") — asked for 2026-08-22, UNDER DISCUSSION, do not build it yet.** Oliver:
14. ~~**THE LOB ("lupfen")**~~ SHIPPED 2026-08-22 as option (c) — see the session log. The rest of
this entry is the discussion it came out of, kept because the two rejected options are the
argument for the one that shipped.
**~~asked for 2026-08-22, UNDER DISCUSSION, do not build it yet.~~** Oliver:
*"when you're close to the net in a duel with another player there should be a gesture allowing
to make a ball flying high from below... I don't know how to implement this gesture wise — this
likely needs to be discussed."* The shot itself is nearly free: `trySwing` already solves every