The dead club doubles its staff, and the draw is made where you can see it
Six new opponent families (head coach, stringer, strawberry seller, chair umpire, spectator, roller man) through the famroll/zsheet route, placed by hierarchy into an 18-rung ladder; the champion steps up behind them. THE DRAW: picking the tournament now rolls its own bracket in front of you — only the final was ever settled — and what lands is what walks on. Netplay, off Oliver's cellular report: the mid-flight early-decide was missing its !G.net gate, so the striker judged its own outbound flight and the two scoreboards drifted a point apart into a court nobody could serve on. The host's court now rides the room, its scoreboard rides the pretoss stream, and a called ball rolls to its own halt instead of freezing where the call caught it. Plus the night court, and somebody in the stand is taking pictures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4c47295a73
commit
9cc3ac54b3
207 files changed
+632
-81
No files matched your search
+119
-2
@@ -20,8 +20,9 @@ anything, so if you add a `return` for an awkward deal, check the body still run
|
||||
the state-machine contracts that have historically produced the show-stoppers. The Dockerfile runs
|
||||
it as a build stage, so a red suite cannot deploy — but finding out from Coolify is the slow way.
|
||||
|
||||
Last updated 2026-08-14 (the live 1v1 socket lands: VS A FRIEND on the mode screen, the relay in
|
||||
server.py, the receiver-is-judge netcode in src/net.js — phone verdict pending). **Start with "How to work on this", "OPEN — leftovers" and "LATER —
|
||||
Last updated 2026-08-15 (the batch below: six new opponent families and an 18-rung ladder, the
|
||||
tournament DRAW screen, three netplay fixes off Oliver's cellular report, the night court, and
|
||||
camera flashes in the stand). **Start with "How to work on this", "OPEN — leftovers" and "LATER —
|
||||
shipping it"** (all three just above the CLOSED sections) — the first tells you how Oliver's feedback
|
||||
should be handled, the second is everything known to be outstanding in the game, the third is the
|
||||
demo/store/campaign plan, which is decided but entirely unbuilt. All four playtest rounds are closed
|
||||
@@ -411,6 +412,122 @@ 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 roster doubles, the draw is made on screen, and netplay stops locking (2026-08-15)
|
||||
|
||||
Oliver's seven-item batch. All of it is on STAGING and pending his phone verdict; items 3-5 in
|
||||
particular need two phones on cellular, which is the only place the bug they fix was ever seen.
|
||||
|
||||
### 1. SIX NEW OPPONENT FAMILIES, and the ladder goes 12 -> 18 rungs
|
||||
|
||||
*"expand the roster for the zombie tennis, use the proven way of generating the roster with the
|
||||
refcheck... head coach, stringer, strawberry seller, chair umpire, spectator, roller man."*
|
||||
|
||||
Rolled through `_famroll.py` -> `_zsheet.py` exactly as the WHICH-TOOL table says, keys `hc sg sb
|
||||
cu sp rm`, each anchored on the body it already has in the gore mode (`z_coach1`, `z_string1`,
|
||||
`z_berry1`, `z_chair1`, `z_s1`, `z_k1`) — the rung and the thing that hunts you stay one person.
|
||||
186 new sprites, 6 new head icons, `_heads.py` SRC/BOX rows for all of them. Placed by HIERARCHY
|
||||
rather than stacked on the end (spectator 3, roller man 5, seller 9, stringer 10, chair umpire 12,
|
||||
head coach 14), so the ladder still reads as the club's staff from the bottom up, and **the
|
||||
champion took the step up Oliver asked for**: 1.45 speed / 1.42 pace / 1.36 width / 0.38 flub,
|
||||
against 1.35 / 1.30 / 1.30 / 0.50. `CUP_ROUNDS` bands widened to match, `LADDER` is exported so the
|
||||
ramp test and the draw screen read the table instead of a pinned 12.
|
||||
|
||||
**TWO PIPELINE LESSONS, both paid for in rolls:**
|
||||
|
||||
- **THE RACKET IS THE POSE, NOT A PROP — say it out loud.** All six of these characters carry
|
||||
something in their anchor frame (a ball basket, a coil of string, a tray, an umpire's chair, a
|
||||
folding chair, a court roller), so each description had to say the thing is gone — and the model
|
||||
read "he carries nothing in either hand" as covering the RACKET too. Two families came back
|
||||
playing tennis bare-handed and the strawberry seller returned every shot with his tray. There is
|
||||
now a permanent clause in `_famroll.prompt()` saying the racket belongs to image 1's pose and is
|
||||
never removed or replaced. Describe a prop's REMOVAL by naming the prop ("the tray image 2 shows
|
||||
is NOT part of him"), never by talking about what his hands hold.
|
||||
- **A prop at head height defeats `_heads.py`'s shoulder-widening rule every time.** All six needed
|
||||
a hand `BOX`; the automatic crops came back 88x18, 36x17 and 35x16 — a letterbox of scalp. If a
|
||||
new family's head icon is a thin strip, that is the diagnosis, and a `BOX` row is the fix.
|
||||
- The club pro finally has a head icon too (`z_rhead.png`, cut from `z_c1` — the turned opponent
|
||||
IS him), so `SPR` preloads all of ZFAMS and the draw screen's portrait column is consistent.
|
||||
- **`ENEMIES['L' + i]` was reading BOSS_ART for the NAME and OPPONENTS for the line** — the same
|
||||
answer only while the two lists were the same list, and they were decoupled on 2026-08-13. The
|
||||
body-count screen has been printing THE PHYSIO over the mascot's line ever since. It reads one
|
||||
list now, which is also what stops rungs 11-18 having no row at all.
|
||||
|
||||
### 2. THE DRAW — the tournament's bracket, settled in front of you
|
||||
|
||||
*"when starting the tournament there should be a view of the tournament hierarchy but only the
|
||||
finale is settled... random browsing through the roster in the individual spots which begins fast,
|
||||
gets slower and then settles on a random character, so that every tournament is new."*
|
||||
|
||||
A new menu stage (`#bracket`, `main.buildBracket`), not a court-owning mode: SERVE UP lands here
|
||||
when the picked mode is `cup`, four rows roll their own band of the ladder at a gap that grows with
|
||||
p², they land in order at 1.5 / 2.05 / 2.6 s, and ENTER THE DRAW is `disabled` until the last one
|
||||
stops. The final wears its settled look from the first frame — it was never in doubt.
|
||||
|
||||
**THE SCREEN IS THE DRAW, NOT A TEASER.** `startMatch(cup, draw)` takes the settled rungs and
|
||||
`cupDraw` reads them off instead of rolling its own, so the face you watched land in ROUND ONE is
|
||||
the body across the net. A screen that dealt a different tournament from the one it just showed you
|
||||
would teach the player that the whole ceremony is a lie. `cupDraw` still validates each rung
|
||||
against its round's band and rolls honestly if it does not fit, so a stale or hand-passed draw
|
||||
cannot deal THE CHAMPION in round one. `#cup` (the back door) passes no draw and behaves as before.
|
||||
|
||||
### 3-5. NETPLAY: the deathmatch lock, the court, and the ball that froze
|
||||
|
||||
**The lock — "sometimes both players stand on the same side and nobody can serve" (cellular).**
|
||||
ROOT CAUSE FOUND AND IT IS ONE LINE: the mid-flight early-decide in `update()` was missing the
|
||||
`!G.net` gate its bounce-1 sibling in `onBounce` has carried since the netplay session. It exists to
|
||||
predict THE CPU'S LEGS; against a human on another phone it made the STRIKER a judge of its own
|
||||
outbound flight (`resolveInterlude` sends the verdict), so on a slow link both phones scored the
|
||||
same rally. The two scoreboards then sat a point apart forever — and that is not cosmetic, because
|
||||
`setupPoint` reads `score.pts` for the deuce/ad side and `score.server` for who tosses. One point of
|
||||
drift puts both players on the same half of their own court, each waiting for the other to serve,
|
||||
with no way out. **The one rule is that the receiving phone judges; this was the last place still
|
||||
breaking it.** The 2026-08-14 log claims the early-decide was gated — only the bounce-1 half was.
|
||||
|
||||
Belt and braces, because that failure mode is a soft-lock rather than a wrong number: **the HOST's
|
||||
10 Hz position stream now carries its scoreboard while it sits in `pretoss`, and the guest adopts
|
||||
it mirrored** (`netSync`). Gated to `pretoss` ONLY — nothing is mid-beat there and any verdict in
|
||||
flight has long landed — so a live rally is never reconciled underneath itself. One authority, no
|
||||
new message type. `score.games` is deliberately NOT synced (ponytail): it costs a wrong number on a
|
||||
board rather than a court nobody can serve on.
|
||||
|
||||
**The court rides the room.** `?court=<id>` at creation, stored beside `death` on the host's room in
|
||||
server.py, carried in the `start` message, applied by `netStart` -> `selectCourt`. Safe mid-run: the
|
||||
bounce pair is copied into TUNE and the backdrop cache keys on `court.id`. Two phones playing one
|
||||
rally on two surfaces disagreed about the bounce as well as the picture, and the guest never chose.
|
||||
|
||||
**A called ball plays out.** *"as soon as the ball goes out the gameloop stops — it should just play
|
||||
until it came to a natural halt."* Every scoring path used to switch the ball off where it stood,
|
||||
which on the STRIKER's phone is a whole court and a latency away from the call. `ball.scored` is the
|
||||
new flag: the flight is decided, physics keeps running, and the ball dies where a real one would —
|
||||
by rolling to a stop under the existing `|vy| < 0.8` dead-ball rule. `endFlight()` in gameplay.js is
|
||||
the one place that decides which it is (netplay rolls, everything else stops dead, so no
|
||||
single-player feel changed), and `onBounce` returns on `ball.scored` at the top, which covers every
|
||||
call site in one line. The three consequences further down the physics block (the dead-roll branch,
|
||||
the baseline crossing, the retire) carry the same guard. `setupPoint` clears it.
|
||||
|
||||
### 6. THE NIGHT COURT
|
||||
|
||||
A `COURTS` row (`gloom`, THE NIGHT COURT) plus one generated menu tile. It needed no new palette
|
||||
machinery — its TENNIS palette is simply darker than every other court's SPLATTER one, and
|
||||
`paintBackdrop` already paints every line in `UI.cream`, so the lines read as chalk under
|
||||
floodlights with no new knob. It borrows the hard court's stands and boards rather than growing two
|
||||
more sheets. **The one thing it did need is `dim`**: a fraction the STAND is taken down by before
|
||||
the rot even starts (`render.standDim`, 0.62 here and absent everywhere else). Without it a
|
||||
floodlit blue crowd sat above a near-black court and the picture stopped being a night. `standDim`
|
||||
is ONE function because the number is applied twice — to the cached sheet and to the live clap
|
||||
columns drawn over it — and two copies of it is a stand whose clapping columns pop brighter than
|
||||
the rest of it, the same class of bug as the two crowd frames drifting apart.
|
||||
|
||||
### 7. CAMERA FLASHES IN THE STAND
|
||||
|
||||
`render.tickFlashes`, an OVERLAY like the clap ripple and for the same reason: the backdrop is one
|
||||
cached blit and anything animated inside it repaints the whole scene every frame. Clocked off
|
||||
`G.idleT` (the function has no dt, and idleT is the clock that never stops). A flash is a hot core
|
||||
plus a cold bloom, `TUNE.flashLife` 0.16 s — deliberately short, because a flash you can look AT is
|
||||
a lamp and one you only catch is a photograph being taken. The first cut drew a 6 px disc and read
|
||||
as a floodlight; it is 1-2 px now. `TUNE.flashEvery` (5 s, jittered 0.35x..1.95x) is the one knob,
|
||||
halved again while `crowd.cheer` runs — which is the "slightly increase during applause".
|
||||
|
||||
## Session log: MULTIPLAYER — the live 1v1 socket (2026-08-14). PHONE VERDICT PENDING
|
||||
|
||||
Oliver: *"alright multiplayer"* — and, offered the recorded ghost-match step one, he picked the
|
||||
|
||||
Reference in new issue
Block a user