Make the parry reachable, and stop a boss dying to one flame ball

Six reports from Oliver's first playtest of the boss build. Two mattered.

A boss sprite turned up in the shambler crowd because z_B1.png and z_b1.png
are the same file on a case-insensitive filesystem — writing the boss frames
silently overwrote the 'b' shambler's first two frames, and `ls` only ever
showed the original lowercase name. Restored from HEAD~1, boss frames renamed
z_boss*, and the rule is now in the asset pipeline notes.

The parry was unreachable, which killed the entire new mechanic: P.swingT is
only set in keyPressed's `phase === 'rally'` branch, and splatter sits in
pretoss almost all the time, so every tap aimed at an incoming shot tossed the
serve ball instead of swinging. An incoming shot inside parryArm now owns the
tap in every phase, with its own generous reach and window — this is a phone,
one thumb, against something aimed at you. The three fail cases (too early,
too late, no tap) are what keep it a timing skill. The swish no longer eats
shots: it runs first in keyPressed and was swallowing the very ball the parry
was about to return.

One flame ball killed a 16 hp boss, and that was a bug rather than tuning. A
flame ball is not consumed on contact (full pierce, so it can cut a lane) but a
boss is not removed when hit, so the same ball re-wounded it every frame. Boss
bodies now take a contact cooldown and flame spends itself on them. Chapter 1
costs 4 flame balls or 16 clean smashes, and bossHp goes 10 to 16.

The boss had no wounded state while throwing — correct, the wound skins were
standing poses only. New sheet gives all four damage states their own overarm
pitch. The ball machine's four painted rects are replaced with a real sheet,
idle and firing, keeping only the muzzle glow procedural because the telegraph
has to read at 20px tall. And the player's blood no longer sits on him as a
painted layer: live drip and the screen-edge wash, nothing else.

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-07-30 13:50:47 +02:00
1 parent 4a066e5cc6
commit 295470b73c
21 files changed
+137 -85

No files matched your search

+64 -22
View File
@@ -5,10 +5,11 @@ 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. The **late-game depth work is complete**: steps 1-5 of the plan agreed after
Oliver's 50+ game playtest are all built and verified in-browser. Step 6 (crowd turning, ball-boy
catcher) is PARKED by his decision. **Awaiting his phone playtest of the boss/ranged build.**
What is still owed is art, not mechanics — see "Art still owed" at the bottom.
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.**
## What this is
SNES-style pseudo-3D tennis (Super Tennis reference) that transitions into a zombie splatter game
@@ -122,6 +123,27 @@ zombie (now an occasional opponent, `TUNE.racketEvery`, not the every-game fixtu
(it keeps its residence) and NOT the ball machine (furniture — a standing threat you deal with when
you choose). Making the machine gate progression turned "decide what to prioritise" into a chore
every four games.
- **The parry is bound to the TAP, in every phase, and that is load-bearing.** `keyPressed` checks
`shots` for anything within `TUNE.parryArm` FIRST and turns the tap into a swing if so. Before that,
the parry sat behind the `phase === 'rally'` branch at the bottom of `keyPressed` — and splatter is
in `pretoss` nearly all the time, so a tap aimed at an incoming shot tossed the serve ball instead
and the return was unreachable in practice. Oliver could not land one in a whole session.
`TUNE.parryReach`/`parryWindow` are deliberately much more generous than a normal volley (2.4 m /
0.34 s), because this is a phone, one thumb, against something aimed at you. The fail cases still
exist and are what keep it a timing skill: tap before the shot is inside `parryArm`, tap after it
has passed, or don't tap. **`parryWindow` must stay <= `swingWindup + swingWindow * VOLLEY.window`**
(0.348 s) or update()'s swing clock expires before the window closes.
`meleeSwipe` deliberately does NOT touch `shots`: it runs first in `keyPressed`, so it used to eat
the very shot the parry was about to return.
- **`zb.hitT` (`TUNE.bossHitCool`) stops a LINGERING ball multi-wounding a boss.** A flame ball has
full pierce — it is not consumed on contact, so it can cut a lane — but a boss is not removed when
hit, so the same ball sat inside its radius and re-wounded it *every frame*. One flame ball killed a
16 hp chapter boss. Flame now also spends itself (`lastHit = 'X'`) on a boss specifically. Any new
weapon that is not consumed on contact must set `hitT` too.
- **No blood is painted onto the player.** Oliver: *"the player blood looks odd, appears as a painted
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.
- **`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
@@ -227,6 +249,12 @@ 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
`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
ORIGINAL lowercase name, which is why it is easy to miss. Boss frames are `z_boss*` now. Use
all-lowercase, whole-word names.
Known generator quirks, all still true:
- It sometimes ignores the magenta (harmless — the key uses the corner colour).
@@ -264,26 +292,40 @@ Known generator quirks, all still true:
---
# ART STILL OWED (mechanics are done)
# CLOSED — Oliver's first boss/ranged playtest (2026-07-30)
The only outstanding work on the boss/ranged build is art. Everything below is playable now.
Six reports, all fixed. Two were serious.
- **The chapter boss has no B-frames for its wound stages.** `assets/_sheet_boss.png` is a 5-frame
sheet: `z_B1`/`z_B2` (a real 2-frame stride, driven off `zb.z / TUNE.bruteStride` per the
distance-not-time invariant) plus three static wound skins `z_Bw1` (arm torn off) / `z_Bw2` (ribs
out) / `z_Bw3` (headless, still throwing). The brute precedent has an A **and** B frame per wound
stage (`z_gw` + `z_gwb`, etc.), so a wounded boss currently stops striding mid-fight. Fix by
generating a second sheet of the three wound poses with the stride swapped, named `z_Bw1b`..`z_Bw3b`,
and extending drawZombie's stage pick the way the brute's does.
**Generating it: the model laid the first sheet out in TWO ROWS, which `_slice.py` cannot cut (it
segments on column gaps).** Don't fight the prompt — detect the blobs and re-flow them into one
baseline-aligned row before slicing. The reflow script is in this session's git history.
- **The ball machine is drawn procedurally** (`drawMachine` in render.js) — a hopper, a barrel and two
wheels, with the muzzle glowing hotter through the wind-up. That was a choice, not a stopgap: it is
furniture, a box is four rects, and the readable parts are the barrel and the charge. Replace with a
sheet only if it reads as a crate on a phone.
- **Chapter cards reuse `drawCard`,** which still cuts in with no entrance while modals now swoosh
(`TUNE.modalIn`). Giving the card a matching entrance was offered and not yet asked for.
### A boss sprite appeared in the shambler crowd — CASE COLLISION
`z_B1.png` / `z_B2.png` overwrote the 'b' shambler's `z_b1.png` / `z_b2.png` on a case-insensitive
filesystem. Restored from `HEAD~1`; boss frames renamed `z_boss1`, `z_boss2`, `z_bossw1..3`. The rule
is now in the asset pipeline section — this is a trap that leaves no obvious trace.
### The parry was unreachable — the whole mechanic was dead
*"I could not return a single ball of the ballmachine.. I tried it with taps near the hit - it never
worked"*, and the same for the boss's volleys. Cause: `P.swingT` is only set in `keyPressed`'s
`phase === 'rally'` branch, and splatter is in `pretoss` almost all of the time, so the tap tossed
the serve ball instead of swinging. An incoming shot now owns the tap in every phase — see the
invariant above. Verified returning from `pretoss`, `rally` and `over`, at 0.9 m through 5.4 m, with
the three fail cases (too early / too late / no tap) intact.
### One flame ball killed a 16 hp boss
Not tuning — a real bug. The flame ball is not consumed on contact (full pierce) and a boss is not
removed when hit, so it re-wounded every frame. `zb.hitT` + spending the ball on a boss. Chapter 1
now costs 4 flame balls or 16 clean smashes, and `bossHp` went 10 -> 16 (`bossHpPerChapter` 8 -> 9),
so chapter 4 is 43 hp.
### The boss had no wounded state while throwing
Correct — the wound skins were standing poses only. New sheet `_sheet_bossthrow.png` gives all four
damage states their own overarm pitch (`z_bosst0..3`), used whenever `windT > 0`.
### The ball machine was "naively painted"
It was — four rects. Replaced with a generated sheet (`z_mach1` idle, `z_mach2` firing with a muzzle
flash): a dented hopper cart on wheels with a barrel aimed down-court. The muzzle GLOW stays
procedural on top, because the telegraph has to read at 20 px tall.
### The player's blood read as a painted layer
Removed entirely. See the invariant above.
# CLOSED — chapter bosses + ranged attacks, steps 4-5 (2026-07-30)