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:
1 parent
4a066e5cc6
commit
295470b73c
21 files changed
+137
-85
No files matched your search
+64
-22
@@ -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)
|
||||
|
||||
|
||||
Reference in new issue
Block a user