Tighten the returns, give each chapter its own boss, and let the umpire talk

Five reports from Oliver's second playtest.

The parry was too generous — at 6 m of arm with a 0.34 s window the shot flew
into reach on its own, so any tap once it was vaguely close worked. 3.4 m /
0.2 s / 1.7 m reach now: a tap at 4.5 m is rejected and costs blood, 3.2 m and
in returns. The boss telegraphs in 0.5 s against the machine's 0.9 and throws
harder, because a boss should be worse than furniture.

His three balls were decoration: fanned ±1.9 m across space, so only the middle
one could ever reach you. They are a timed salvo now — three shots 0.42 s apart,
each aimed at where you are at that moment. hurtGrace still caps a whole volley
at one point of damage, which is what it is for.

Every chapter was the same boss. Three designs cycling now, each with its own
sheet, stride, throw pose, wrecked skin and wrecked-throw skin: the
GROUNDSKEEPER, the LINE JUDGE, the CHAIR UMPIRE with his chair strapped on. The
chapter card names them. Designs are data in BOSS_ART, so a fourth is a sheet
plus a row.

Healing announces itself with a first-aid tin and a red cross instead of a 1.4 s
flash. And 14 new umpire lines: ten sarcastic asides on a jittered timer, four
hooked to specific beats. He still has not acknowledged that the match became a
massacre, so he keeps officiating it.

Two traps found and written into the pipeline notes. Sprite loads now carry
?v=SPRV, because python http.server sends no cache headers and the browser
caches 404s — a name added before its PNG exists stays permanently missing for
that browser. And a scripted replace that silently didn't match left drawZombie
on the old hardcoded boss names while everything else moved to BOSS_ART; the
built-in fallback hid it perfectly, all four designs rendering as design 1 with
no error. Assert your rewiring edits matched.

Tennis untouched: 30 tapped points, 50 hits / 0 whiffs. 45-game soak clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-07-30 14:56:00 +02:00
1 parent 295470b73c
commit 8bc57e9358
34 files changed
+180 -40

No files matched your search

+73 -6
View File
@@ -5,11 +5,9 @@ everything they specified is shipped, and what still matters from them is record
long history, `~/.claude/projects/.../memory/splatter-tennis-history.md` is auto-loaded; for the
detail of any individual change, read the git log.
Last updated 2026-07-30, after Oliver's first playtest of the boss/ranged build. All six things he
reported are fixed (see CLOSED at the bottom) — including the one that made the whole new mechanic
unusable: **the parry was unreachable**, because a tap only ever became a swing in `phase === 'rally'`
and splatter sits in `pretoss` almost all the time. Step 6 (crowd turning, ball-boy catcher) stays
PARKED by his decision. **Awaiting his next phone playtest.**
Last updated 2026-07-30, after Oliver's SECOND playtest of the boss/ranged build. Both playtest
rounds are closed at the bottom. Step 6 (crowd turning, ball-boy catcher) stays PARKED by his
decision. **Awaiting his next phone playtest.**
## What this is
SNES-style pseudo-3D tennis (Super Tennis reference) that transitions into a zombie splatter game
@@ -144,6 +142,14 @@ zombie (now an occasional opponent, `TUNE.racketEvery`, not the every-game fixtu
layer on top"*. His health reads as `bleedPlayer()`'s live drip plus the screen-edge wash on impact,
and nothing else. This is the same settled rule that governs the zombies — blood is never painted
into a sprite — and a `drawPlayerWounds` overlay broke it.
- **Boss designs are DATA: `BOSS_ART` in config.js, one row per chapter, cycling.** Each row is
`{ name, walk: [2], wounds: [...], throws: [...] }`; `spawnChapterBoss` stamps the row onto the body
as `zb.art` and `drawZombie` reads only that. Damage picks a wound stage as a fraction of `hpMax`
spread over however many stages the design HAS, and `throws` is index-clamped, so a design with
fewer throw frames than wound stages just reuses its last one. Adding a boss is a sheet plus a row —
never a change to drawZombie. Missing/unloaded sprites degrade to design 1 rather than vanishing,
which is convenient and also means **a wiring bug looks exactly like working code** (see pipeline
note 7).
- **`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
@@ -249,7 +255,22 @@ Clear the intro first: `document.getElementById('intro')?.remove()`.
4. Standalone art (logo, popup panels) uses `assets/_key.py` instead: chroma-key + crop to the
content bbox + optional downscale, no frame splitting, no forced height.
5. Register new names in `render.js`'s `SPR` preload array.
6. **NEVER let two sprite names differ only by CASE.** macOS is case-insensitive: writing
6. **New sprite not showing up? Bump `SPRV` in render.js.** Sprite loads go through
`assets/<n>.png?v=SPRV` for the same reason index.html versions main.js: python http.server sends
no cache headers, so the browser caches sprite responses heuristically — **including 404s**. Add a
name to the `SPR` list before its PNG exists, load the page once, and that browser treats the
sprite as permanently missing while `drawZombie` silently falls back to another design. Bump SPRV
alongside `main.js?v=N`.
7. **When a scripted `str.replace` edit is meant to rewire logic, ASSERT it matched.** A silent
no-op replace left `drawZombie` on its old hardcoded boss names while everything else had moved to
the BOSS_ART table, and the built-in fallback then hid the failure perfectly — all four boss
designs rendered as design 1 and nothing errored. Cost a full debugging round. `drawImage`
instrumentation is what finally settled it:
```js
const o = CanvasRenderingContext2D.prototype.drawImage;
CanvasRenderingContext2D.prototype.drawImage = function (img, ...a) { log(img.src); return o.call(this, img, ...a); };
```
8. **NEVER let two sprite names differ only by CASE.** macOS is case-insensitive: writing
`assets/z_B1.png` for the chapter boss silently OVERWROTE the 'b' shambler's `assets/z_b1.png`
(and `z_B2` ate `z_b2`), so a 3-metre boss started showing up in the shambler crowd and `git`
reported the shambler frames as "modified" rather than reporting a new file. `ls` shows only the
@@ -292,6 +313,52 @@ Known generator quirks, all still true:
---
# CLOSED — Oliver's second boss/ranged playtest (2026-07-30)
### The parry was too generous
*"I can tap pretty early and it returns the ball."* At `parryArm` 6.0 / window 0.34 / reach 2.4 the
shot flew INTO reach during the window, so any tap once it was vaguely close worked. Now 3.4 / 0.2 /
1.7 — verified: a tap at 6.0 m or 4.5 m is rejected and costs blood, 3.2 m and closer returns.
### The boss's three balls were decoration
*"he throws 3 of them from which only ever one can reach me. that does not make sense."* Correct — they
were fanned +-1.9 m across space, so only the middle one was ever aimed at the player. They are a
**timed salvo** now: three shots `TUNE.bossSalvoGap` apart, each aimed at where you are AT THAT MOMENT
with only a hand's width of scatter. Verified firing at 0.02 / 0.45 / 0.88 s, all within 0.3 m of the
player. Note `hurtGrace` (1.1 s) still caps a whole volley at one point of damage by design.
Reaction time also tightened: bosses get `bossWind` 0.5 s of telegraph against the machine's 0.9, and
throw at `bossShotSpeed` 15.5 m/s against its 13.
### Every chapter was the same boss
Right — chapters only differed in hp and scale. There are three designs now, cycling, each with its
own sheet, a 2-frame stride, a throw pose, a wrecked skin and a wrecked-throw skin:
**THE GROUNDSKEEPER** (ch 1/4/7…, blazer + ball hopper, and the only one with three wound stages),
**THE LINE JUDGE** (ch 2/5/…, fat, striped officials' polo, cap, ball apron),
**THE CHAIR UMPIRE** (ch 3/6/…, tall and gaunt, panama hat, the umpire's chair strapped to his back).
The chapter card names the design. Chapter 4 repeats design 1 but at 43 hp and 3.67 m.
**The chair-umpire sheet came back on a WHITE background**, and it wears a cream blazer and a white
hat — a colour key would have eaten both. Fixed by flood-filling the background inward from the image
BORDER instead of keying by colour; that script is in this session's git history and is the right tool
whenever a generated sheet's background colour also appears on the character.
### Healing needed to announce itself
Winning a duel while wounded now opens a real modal — `x_cross` first-aid tin, "WOUNDS TREATED / THE
TRAINER PATCHED YOU UP / N OF M BACK" — plus an umpire line. It was a 1.4 s flash before.
### The umpire needed to comment on the new reality
14 new recorded lines (`say -v Daniel -r 168` -> `afconvert`), all `ump_*`. Ten are a rotating pool of
sarcastic asides fired on a jittered `TUNE.asideEvery` timer whenever nothing else owns the audio
(`umpireAside` in gameplay.js); four are hooked to specific beats — a machine arriving, a boss
arriving, the trainer, and taking a hit. The joke is that he has still never acknowledged that the
match became a massacre, so he keeps officiating it: *"Code violation. Unsportsmanlike conduct.
Eating."*
### Regression net
30 tapped tennis points = 50 hits / 0 whiffs. 45-game soak: no exceptions, `mgT` drains, gore 40 /
piles 30 / bodies capped at 34.
---
# CLOSED — Oliver's first boss/ranged playtest (2026-07-30)
Six reports, all fixed. Two were serious.