Trade the tutorial for a screen that counts the dead
The menus were placeholders next to the art. Three screens now: a title over an empty, dimmed, blood-stained court; a MATCH SETUP screen where each court wears meters read straight off its own bounce numbers; and BODY COUNT, which needed the game to start measuring anything at all. stats.js keeps one capped array of per-run records — all time, last match and seven days are three filters over it rather than three counters that can drift apart. Kills file under the body AND the weapon: killZombie only ever knew an index, so every damage loop stamps the cause on its way past. The tutorial is gone. Marvin explained what the in-game modals already teach, and his stage is where the stats live now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
411bff8724
commit
f3b0b791d6
21 files changed
+619
-210
No files matched your search
+53
-8
@@ -961,13 +961,28 @@ death summary is the end menu, and the zombie bed ducks out as it comes in. Two
|
||||
bed's gain at 0. Without it, a multi-megabyte file ducked mid-fetch comes up at full level several
|
||||
seconds later, over the match.
|
||||
|
||||
**The overlay is a three-stage machine**, not a screen per DOM node: `#intro[data-stage]` swaps
|
||||
`splash -> {courts, tutorial}` while the backdrop and card frame stay mounted, because a screen that
|
||||
unmounts the overlay flashes the live court through between menus. **The title screen IS the menu** —
|
||||
a separate menu stage behind it was the same logo on the same backdrop with two buttons, and read as
|
||||
the title screen failing to advance. Court tiles SELECT only; `START GAME` and `BACK` are their own
|
||||
buttons. The tutorial is a data table (`TUT` in main.js) of Marvin poses, an optional picture in
|
||||
`assets/`, and his lines — adding a page is a row.
|
||||
**The overlay is a four-stage machine**, not a screen per DOM node: `#intro[data-stage]` swaps
|
||||
`splash -> {setup, stats, ranks}` while the backdrop and card frame stay mounted, because a screen
|
||||
that unmounts the overlay flashes the live court through between menus. **The title screen IS the
|
||||
menu** — a separate menu stage behind it was the same logo on the same backdrop with two buttons,
|
||||
and read as the title screen failing to advance. Court tiles SELECT only; `SERVE UP` commits.
|
||||
|
||||
**THE TUTORIAL IS GONE** (was `TUT` in main.js, eight pages of Marvin). What it explained is already
|
||||
taught in-game by the first-time-only modals, and its stage is the stats screen now. Deleted with
|
||||
it: `tut_*.png`, `marvin1-4.png`, `btn_tut.png`, `btn_start.png`, and the `marvin` regen batch.
|
||||
`x_howto.png` and `x_cross.png` LOOK tutorial-ish and are not — they are in-game modal art.
|
||||
|
||||
**Every menu screen is three bands**: something pinned to the top, something pinned to the bottom,
|
||||
and the slack in between. `.card` is a full-height flex column, `.foot` uses `margin-top:auto` (drop
|
||||
to the bottom of a short screen) plus `position:sticky` (stay there on a long one), and `#intro` is
|
||||
the scrollport those stickies resolve against. `align-content` is **`safe center`**, never
|
||||
`place-items: center` — plain centring on a grid whose item overflows pushes the item's top out of
|
||||
the scrollport where nothing can reach it. Two registers, unchanged: **Bangers shouts** (screen
|
||||
titles, the crimson primary button), **Silkscreen states** (every number, every label).
|
||||
|
||||
**The court cards' SPEED and BOUNCE meters are derived from `COURTS[].bounceXZ` / `.bounceY`**, not
|
||||
from hand-written ratings — so a physics change moves the bars and the screen can never advertise a
|
||||
court that plays differently from what it says.
|
||||
|
||||
**DEATH NO LONGER RELOADS THE PAGE.** `location.reload()` was an honest full reset and for a long
|
||||
time the right call — but it also tore down the AudioContext, so the menu theme restarted from
|
||||
@@ -985,11 +1000,41 @@ the title screen. `gameplay.restartRun()` replaces it:
|
||||
|
||||
**If you add module-level mutable state anywhere, it belongs in one of those reset functions.**
|
||||
|
||||
**Menu and tutorial art is generated DIRECTLY, not through `_regen.py`.** That pipeline keys magenta
|
||||
**`stats.js` is the only thing that remembers anything.** One localStorage key, `splatterRuns`: an
|
||||
array of per-run records (`{t, g, s, d, k, mk, dead, z, w}`), newest last, capped at 300. ALL TIME /
|
||||
LAST MATCH / 7 DAYS are three filters over that one array, not three stored counters — a fourth
|
||||
period is a filter, not a migration. It replaced `splatterBest`, which it migrates on first read.
|
||||
|
||||
- **The live run is module-local, not a field on `G`.** `G` is deep-cloned into `PRISTINE` at load
|
||||
and restored wholesale by `restartRun()`; a counter living there would be reset by machinery that
|
||||
knows nothing about stats.
|
||||
- **`killZombie` takes an index, not a cause.** Eleven call sites reach it, so instead of threading
|
||||
a weapon id through all of them (and through `wound()`, and through `splatter()`), every damage
|
||||
loop in zombies.js stamps a module-level `CAUSE` before it can kill. Safe because a frame is one
|
||||
synchronous pass. **The trap**: a kill path that fires OUTSIDE a stamped loop inherits a stale
|
||||
cause — the flame ball's fly-past `ignite()` did exactly that and filed 'fire' kills under 'ball'.
|
||||
Burning bodies carry `zb.burnBy` for the same reason: they die long after the thing that lit them.
|
||||
- **`endRun` needs an explicit `banked` flag, not an "is the bag empty" inference.** The games count
|
||||
lives on `G` and survives the flush, so a second call sails past a nothing-happened guard and
|
||||
writes a duplicate with zero kills. `pagehide` really does fire more than once (bfcache dismiss
|
||||
and restore), which is how this was caught. `tally()` clears the flag, so a fresh run re-arms
|
||||
itself with no reset call for anyone to forget.
|
||||
- `T.stats.check()` is the runnable check: it asserts the period filter and the aggregation against
|
||||
a fixture, and `read()` takes an injected array precisely so it never touches a real save.
|
||||
|
||||
**Menu art is generated DIRECTLY, not through `_regen.py`.** That pipeline keys magenta
|
||||
and slices figures out of a sheet; these are whole illustrated panels with backgrounds, so they go
|
||||
through `generate_image.py` and a downscale/quantise pass, the same route the court tiles took. Two
|
||||
things learned making them:
|
||||
|
||||
- **`assets/menu_bg.png` (the splash backdrop) was made by EDITING `court_grass.png`**, not from a
|
||||
text prompt: `generate_image.py "<the changes>" --input assets/court_grass.png`. Edit mode holds
|
||||
the camera, the palette and the line weight for free, and asking for a portrait aspect in the same
|
||||
prompt worked. Then halve it exactly (`//2`, so the generator's ~4px art-pixel grid stays square —
|
||||
a non-integer divisor shreds this art) and quantise at 128. 768x1376 -> 384x688, 80 KB.
|
||||
- **No python3 on this machine has Pillow.** Run the downscale/quantise step as
|
||||
`~/.local/bin/uv run --with pillow python - <<'PY'`. `generate_image.py` itself needs `requests`,
|
||||
which `/opt/homebrew/bin/python3.11` has.
|
||||
- **Quantise these at 128 colours, not 64.** The panels are big flat fields with a few saturated
|
||||
accents (the ball, fire, blood), which is exactly where a small palette shows — 64 turned the
|
||||
tennis ball beige.
|
||||
|
||||
Reference in new issue
Block a user