The flick stops losing the point, and the corpse falls and serves
THE FLICK. "when returning a ball with a spin it seems too sensitive, 8/10 times the ball lands out." Arithmetic, not feel. The singles line is 4.115 m; an aimed shot targeted a flat ax*3.3 and THEN added err() on top — up to +-2.4 m at quality 0 — so a flicked return landed as wide as 5.7 m, and the only clamp in the path sat at +-5, outside the court. An UNAIMED return targets 0 + the same scatter and is therefore always in: aiming was the thing losing the point, which is backwards for the mode's one act of skill. TUNE.flickReach scales the aim width by the same quality the scatter shrinks with, so the two can never sum past the line — worst case 1.49+2.4 = 3.89, and a perfectly timed flick still gets the full 3.3 with nothing left to scatter. The test asserts that sum across the whole quality range rather than sampling rallies. Not named `aimReach`: that key already exists further down TUNE (the slingshot's 22 m aim ray) and the duplicate silently won until a test caught it. The placement ring carries `q` now and is the flick's receipt — tight and hot when your timing earned the full width, wide and dim when you barely got a racket on it. Without it the rule is invisible: the shot just goes less wide. THE FALL (handoff item 4). A point that takes a limb knocks the body down and it drags itself up short of it. The three collapse frames wear the stage it STILL WAS and the rise wears the stage it has BECOME — that order is the whole beat, and it is what turns a popping wound stage into something the point did. Own clock beside the horde's warning, for the reason this file keeps relearning (a beat that moves the phase under a latch strands it); the 'over' beat it sets outlives the fall so it cannot expire into setupPoint() mid-collapse. Only fires when the stage actually moved — a quarter off a full pool is 0.75, so point one has no limb to explain and a second of collapse for nothing is a beat the player waits out. THE OPPONENT SERVES (handoff item 7), and it was mostly switched off rather than missing. setupPoint already placed a non-player server behind its own baseline and armed aiServeT; update() already tossed and struck for O. Both were gated on `G.mode === 'tennis'` and the match runs in splatter under a permanent interlude — one condition. Genuinely new: service alternates at each game boundary (the match calls advancePoint directly and the flip lives in tennis's pointTo, which it bypasses — the same shape as the G.serveNum bug), the zombie takes O's mark so picture and ballistics agree on where it stands, and the serve poses are named onto it because O is invisible here. racketStage() moves to config: drawZombie and the fall must not answer the same question twice, which is how the boss's skins and its blood drifted apart. Docs: PROJECT.md gains THE ZOMBIE FAMILY PIPELINE — the measured API contract (4K is a parameter and the default is 1K; n is 1 so "return both" is not a thing; aspect is an enum so pad to it), why magenta stays despite green being the documented key, and the two things _zsheet.py must do globally. Verified in the browser: brute in the match, z_brfall2 -> z_brw1rise on a wounding point, and bounce -> toss -> serve off the opponent's own baseline. 58 tests green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cba2fb8a8b
commit
c1f7268a04
9 files changed
+412
-37
No files matched your search
+52
-14
@@ -70,11 +70,16 @@ communicating its own rules. Model A only works if the player can see they are b
|
||||
*The art is now there for it:* every wound stage has its own ready stance, shuffle pair and
|
||||
forehand (`z_rw1*`, `z_rw2*`), so a wounded opponent still plays rather than reverting to a walk.
|
||||
|
||||
4. **The fall animation** *(art pass — Oliver's idea, and a good one)* — point lost → the corpse
|
||||
COLLAPSES → drags itself up minus the limb. It explains the wound stage popping between points and
|
||||
gives every point a payoff beat. `matchReady()` is the natural slot. **Constraint: draw frames,
|
||||
do not fake them** (settled rule) — this needs a small fall/rise sheet per stage through
|
||||
`assets/_regen.py`, not a JS rotate/sink. The `racket_*` batches are the template.
|
||||
4. ~~**The fall animation**~~ **DONE** (2026-08-12). Point won → the body COLLAPSES → drags itself up
|
||||
minus the limb. `beats.tickMatchFall`, on its OWN clock beside the horde's warning (a beat that
|
||||
moves the phase under a latch strands it — the mgT lock, again); the 'over' beat it also sets is
|
||||
only there to keep the court quiet and is deliberately set to OUTLIVE the fall, so it cannot
|
||||
expire into `setupPoint()` mid-collapse and hand the player a serve over a body on the ground.
|
||||
The contract, and the whole point of the beat: **the three collapse frames wear the stage it
|
||||
STILL WAS, the rise wears the stage it has BECOME.** Frames are named through `zb.showFrame`
|
||||
(the bench's own override, which already degrades a missing stage to the healthy frame).
|
||||
**Only when the stage actually moved** — a quarter off a full pool is 0.75, so point one crosses
|
||||
nothing and a second of collapse for nothing is a beat the player waits out.
|
||||
|
||||
5. ~~**Menu entry**~~ **DONE.** Three modes on the setup screen, from `config.MODES`. The back room
|
||||
keeps its door (MATCH TEST still works) — the menu did not replace it, it just stopped being the
|
||||
@@ -85,8 +90,17 @@ communicating its own rules. Model A only works if the player can see they are b
|
||||
nowhere on touch. The placement ring (`G.placeMark`) already shows the result; nothing teaches
|
||||
the cause.
|
||||
|
||||
7. **Opponent serve games** *(bigger, later)* — the zombie never serves today. Alternating service
|
||||
is real tennis and doubles the skill surface (return timing).
|
||||
7. ~~**Opponent serve games**~~ **DONE** (2026-08-12), and it was almost entirely switched off rather
|
||||
than missing. `setupPoint`'s tennis branch already placed a non-player server behind its own
|
||||
baseline and armed `G.aiServeT`; `update()` already tossed and struck for O. Both were gated on
|
||||
`G.mode === 'tennis'`, and the match runs in splatter under a permanent interlude — one condition.
|
||||
What was genuinely new: **service alternates at each game boundary** (the match calls
|
||||
`advancePoint` directly, and the flip lives in tennis's `pointTo`, which the match bypasses —
|
||||
the same shape as the `G.serveNum` bug above), the zombie takes O's mark when it serves so the
|
||||
picture and the ballistics agree on where it stands, and the serve POSES are named onto it
|
||||
(`bounce` → `toss` → `serve`, held briefly after contact) because O is invisible here.
|
||||
*Known simplification:* the interlude's line-call branch has no serve box and no second serve, so
|
||||
a missed opponent serve is simply the player's point. Add faults when it matters.
|
||||
|
||||
---
|
||||
|
||||
@@ -169,6 +183,25 @@ he ran, where the ball died. That triple diagnosed every defect in the contact c
|
||||
|
||||
---
|
||||
|
||||
## THE FLICK WAS LOSING THE POINT (fixed 2026-08-12)
|
||||
|
||||
Oliver: *"when returning a ball with a spin on the screen that seems to be too sensitive, 8/10 times
|
||||
the ball lands out."* It was arithmetic, not feel. The singles line is **4.115 m**. An aimed shot
|
||||
targeted a flat `ax * 3.3` and THEN added `err()` on top — up to `±aimError` (2.4 m) at quality 0 —
|
||||
so a flicked return landed as wide as **5.7 m**, and the only clamp in the path was at ±5, outside
|
||||
the court. An UNAIMED return targets `0 + the same scatter` and is therefore always in. **Aiming was
|
||||
the thing that lost the point**, which is exactly backwards for the mode's one act of skill.
|
||||
|
||||
`TUNE.flickReach` (0.45) scales the aim width by the same quality the scatter shrinks with, so the
|
||||
two can never sum past the line: worst case `1.49 + 2.4 = 3.89` at quality 0, and a perfectly timed
|
||||
flick still gets the full 3.3 with no scatter left on it. A test asserts that sum across the whole
|
||||
quality range rather than sampling rallies. **Not named `aimReach`** — that key already exists further
|
||||
down TUNE (the slingshot's aim-ray length, 22 m) and the duplicate silently won, which a test caught.
|
||||
|
||||
The placement ring carries `q` now and reads as the flick's receipt: **tight and hot when your timing
|
||||
earned the full width, wide and dim when you barely got a racket on it.** Without it the new rule is
|
||||
invisible — the shot simply goes less wide and nothing says why.
|
||||
|
||||
## Traps this mode has already sprung (do not re-learn these)
|
||||
|
||||
- **`autoAssist` is 0 on `pointer: fine`.** The desktop pane has no auto-run. The match floors it
|
||||
@@ -201,14 +234,19 @@ he ran, where the ball died. That triple diagnosed every defect in the contact c
|
||||
|
||||
## The art pipeline
|
||||
|
||||
Everything learned generating this mode's sprites is in **PROJECT.md, "THE ART PIPELINE, corrected"**
|
||||
— read it before touching `assets/_regen.py`, `_fine.py` or `_master.py`. The one-line version: the
|
||||
generator does identity and pose given REFERENCE IMAGES, the pipeline does exact colour with
|
||||
arithmetic, and the gates only catch layout accidents. A day was spent discovering that the middle
|
||||
one is not a prompting problem.
|
||||
A new opponent FAMILY is now **one API call and one script** — read **PROJECT.md, "THE ZOMBIE FAMILY
|
||||
PIPELINE"** before generating one. The one-line version: ask for all 31 figures in a single sheet
|
||||
(one roll, one character, by construction), poses from an existing family and identity from one
|
||||
reference frame with each image's JOB named in the prompt, then `assets/_zsheet.py` cuts it back down
|
||||
with ONE global scale and ONE global palette.
|
||||
|
||||
The match opponent's own art is complete — 33 frames, three wound stages, the serve and the fall —
|
||||
and `assets/_master.py` turns that pose set into the master sheets a new opponent is restyled from.
|
||||
The older nine-batch flow (`_regen.py`, `_master.py`) is still right for single sheets, remasters and
|
||||
the boss roster, and **PROJECT.md, "THE ART PIPELINE, corrected"** still governs it. Run
|
||||
`python3 assets/_regen.py --verify <batches>` before trusting any chained batch: it is what proved
|
||||
27 of the parked brute's 31 frames were never art.
|
||||
|
||||
The match opponent's own art is complete — 31 frames, three wound stages, the serve and the fall —
|
||||
and the BRUTE is live beside it (`ZFAMS` has `'br'`, the KEEPER wears it), generated by the new path.
|
||||
|
||||
## Do not
|
||||
|
||||
|
||||
Reference in new issue
Block a user