Polish round: modal system, softened palette, pixel font, and the 14 plan items

The three foundations first, since everything else leans on them:

- G.modal is the only thing that stops the world. update() early-returns on it
  so no zombie steps, no ball flight, no spawn timers run while a popup is on
  screen — you can read it without being eaten. Dismissal swallows the gesture;
  the pointerdown dismisser is registered only on pointer:fine, because on touch
  pointerdown beats touchstart and a shared handler cleared the modal and then
  served with the same thumb.
- UI in config.js is the accent palette. The arcade primaries were shouting over
  the artwork; the gore reds deliberately stay loud.
- Every canvas font string now goes through F(). Bare `monospace` resolved to
  Courier — a slab serif — which was the "generic serif" bug, not taste.

Then: weapons are only ever WON. deliverTin() has exactly one caller now
(resolveInterlude('P')); the old game schedule calls the DUEL instead, fired off
proceedAfterClear() where the court is guaranteed empty. Four popups (splatter
tutorial with a generated illustration, first duel, weapon received, horde),
wCap 9 with the tin amounts tunable, a 5-hp brute that comes apart over four
wound skins, fire that ignites the neighbours and chains outward, a three-stage
death where you become the biggest gore mound on the court, and a 7s changeover
with a ball boy who finally has a mop.

New art: assets/logo.png, x_howto, z_gw2-4, x_boymop1-4, plus assets/_key.py for
standalone art (chroma-key + crop, no frame splitting, no forced sprite height).

Benchmarks re-run and holding: CPU returns 23/24 serves, 30 human-tapped points
= 50 hits / 0 whiffs, 26 simulated games with no exceptions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-07-27 05:28:04 +03:00
1 parent 5faaa99467
commit a09a5d7559
23 files changed
+392 -103

No files matched your search

+37
View File
@@ -106,6 +106,37 @@ gives clean manual stepping. `T.press('Space')` = tap. Dismiss the intro overlay
human-like tapping (tap when incoming ball d<2.2–3.2) = 50 hits 0 whiffs; feet-bounce
tap sweep ≈ 19/30 winning frames.
## POLISH ROUND — all 14 items of POLISH-PLAN.md are DONE (2026-07-27)
New foundations you must know before touching anything:
- **`G.modal`** (state.js) is the only thing that stops the world: `update()` early-returns on it
after the flash/card decay, so no zombie steps, no ball flight, no spawn timers — only `idleT`
ticks (for the blink). `showModal`/`dismissModal` in gameplay.js; `drawModal` is the LAST thing
render() draws. Dismissal must SWALLOW the gesture: input.js `touchstart` returns on it, keydown
returns on it, and a `pointerdown` dismisser is registered **only on `pointer: fine`** — on touch,
pointerdown fires *before* touchstart, so a shared handler cleared the modal and let the
touchstart underneath serve. `G.seen.<key>` gates first-time-only modals.
- **`UI` in config.js** is the accent palette (hot/warm/pick/cream/dim/bad/ball). Every HUD colour
goes through it. Gore reds are deliberately NOT in it — the splatter stays loud.
- **`F(size, bold)` in render.js** builds every canvas font string. Bare `monospace` resolves to
Courier, a slab SERIF — that was the "generic serif" bug, not taste. Never write `ctx.font =` by
hand. Widest string checked: `SERVE FIRES: … TAP THE RACK TO SWITCH` at 259px of 270 portrait.
- **Weapons are only ever WON.** `deliverTin()` has exactly one caller: `resolveInterlude('P')`.
The old game schedule now calls the DUEL instead (`TUNE.duelFirst`/`duelEvery`, `G.duelDue`),
fired from `proceedAfterClear()` — the one beat where the court is guaranteed empty. The beat it
interrupts is parked in `G.afterDuel` and resumed when the duel resolves. `G.shameGame` is gone.
- **Fire chains.** `zb.burnT`/`burnD`; `ignite()` is read-only over `zombies[]` so it is safe inside
a kill loop; `burnTick()` returns true when the fire killed the body and the caller MUST `continue`.
Flames are drawn procedurally (`drawFlames`), no sprite sheet.
- **Sprite heights follow the PNG now.** `drawBoy` and the brute wound stages scale by
`img.height / SPRITE_H`, because `_slice.py` common-scales a sheet: a deep mopping lean and a
headless brute are genuinely shorter and were being stretched back to full height.
- Benchmarks re-run and holding: CPU returns 23/24 serves; 30 human-tapped points = **50 hits /
0 whiffs** — but you MUST set `T.TUNE.autoAssist = 1.15` first, the Browser pane reports
`pointer: fine` so the assist is off and every ball reads as "never reachable".
- Brute is 5 hp: worst-case (max ramp) it still needs 22.5s to cross, vs ~7s of serves to kill.
- `TUNE.burnSpread` 1.3 m chains reliably through shoulder-to-shoulder shamblers (~1.0 m apart);
at 1.2 m spacing the tail of a line can survive. It is a T-panel knob if Oliver wants it hotter.
## Gotchas learned the hard way
- Browser-pane verification: python http.server sends no cache headers, so edited ES
modules often load STALE even after location.reload(). Reliable fix: preview_stop +
@@ -122,6 +153,12 @@ gives clean manual stepping. `T.press('Space')` = tap. Dismiss the intro overlay
half with --largest. Passing the existing sheet as --input (edit mode) nails the style.
- drawChar mirrors sprites by ch.facing — any stance that must not mirror needs facing reset
(setupPoint pins facing=1).
- Standalone art (logo, popup panels) uses `assets/_key.py` instead of `_slice.py`: chroma-key +
crop to the content bbox + optional downscale, with NO frame splitting and NO forced sprite
height. `python3 assets/_key.py sheet.png out.png [target_width]`.
- If a generated sheet's frames TOUCH (a mop handle bridging the gap), `_slice.py` finds 1 frame
and there is no column of magenta to split on — re-prompt for "at least 150px of empty magenta
between frames" and "the prop must stay inside its own frame". That worked; hand-splitting did not.
- The scratchpad venv/scripts get wiped between sessions — recreate venv (requests+pillow)
and slice.py lives only in scratchpad; its logic is described above if gone.
- Don't trust single-sample gameplay sims; randomness (aiFlub 9%, aim error) needs ~12 runs