Make it readable: 2x sprites, a flatter camera, honest hit timing

The umpire is situational again. Each line is tied to the thing that justifies
it — a new variant appearing, the court filling, one crossing your baseline, a
game lost, a horde, a changeover — behind one shared cooldown, so several
triggers at once still yield one remark. The random timer pool is gone, the
interval went 26s to 55s, and ump_out_of_order, ump_let and ump_no_appeal are
deleted.

"Visually the ball is not yet at the player but somehow the player already is
hit" was real and measurable. The hostile-ball hit test reused the parry's
hypot(dx, dz*0.8), and weighting depth shrinks it: contact fired while the ball
was still 0.68 m up-court and drawn up around his head. It has its own
unweighted test now, and contact lands at 0.25 m with the ball on his chest.
The parry keeps its weighting — a racket sweep really is wider than it is deep.

On "everything is a tad too small": measured first, because higher-resolution
sprites are only half the answer. At RES 3 the player rasterises into 170
device pixels from a 112px source — upscaled 1.5x — while a far shambler is
downscaled to 78. So near bodies needed resolution and far bodies needed the
camera.

Camera: pushed back and lengthened (camZ 42->48, camF 580->760) to flatten the
perspective so distant bodies grow relative to near ones rather than everything
zooming, camY raised for a steeper look-down, horizon re-centred. Far shambler
26->30px, boss 39->45px, player unchanged, court in the same band.

Sprites: the original sheets were still in the repo at full resolution, so
nothing needed regenerating — IDLE_TARGET_H went to 224 and every sheet was
re-sliced. The sheet-to-output mapping was never recorded, so it was recovered
by matching sliced frames to existing assets by image signature: 58 came back
from sheets, the 29 without a confident match were 2x-upscaled to stay on the
shared scale, since a sprite off that scale renders at half height.

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-07-30 16:23:04 +02:00
1 parent 8bc57e9358
commit 49d391dc64
95 files changed
+119 -21

No files matched your search

+71 -1
View File
@@ -5,7 +5,7 @@ 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 SECOND playtest of the boss/ranged build. Both playtest
Last updated 2026-07-30, after Oliver's THIRD playtest round (the readability pass). All playtest
rounds are closed at the bottom. Step 6 (crowd turning, ball-boy catcher) stays PARKED by his
decision. **Awaiting his next phone playtest.**
@@ -150,6 +150,22 @@ zombie (now an occasional opponent, `TUNE.racketEvery`, not the every-game fixtu
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).
- **Sprites are 2x (`SPRITE_H` 224, `_slice.py` `IDLE_TARGET_H` 224) and EVERY scale-critical sprite
must share that scale.** `RES` is 3, so the player at the baseline rasterises into ~170 DEVICE
pixels — a 112px source was being UPSCALED 1.5x, which is why near bodies looked soft. Sprites that
go through `drawChar`, `drawBoy`, `drawMachine` or the wound-stage `h *= img.height / SPRITE_H` rule
are all measured against `SPRITE_H`, so a sprite left at the old scale renders at HALF height.
That is why the remaster 2x-upscaled the ~29 frames it could not re-slice rather than leaving them.
Sprites drawn through plain `drawStatic(name, x, z, hMeters)` only care about ASPECT, so their
resolution is free.
**Heights above 224 are deliberate, not bugs**: a smash or toss pose SHOULD draw taller than idle
(`p_smash` is 322), and that ratio is exactly what `SPRITE_H` exists to express.
- **The umpire's asides are SITUATIONAL, never a timer pool.** `aside(name)` in gameplay.js is the only
entry point and enforces one shared cooldown (`TUNE.asideEvery`), so several triggers in the same
moment still yield one remark. The first version drew from a bag on a timer and Oliver's verdict was
"too much and not situation aware.. they seem random". Each line is now tied to the thing that
justifies it — a new variant appearing, the court filling up, one crossing your baseline, a game lost,
a tin delivered, a horde, a changeover, a machine, a boss, the trainer, taking a hit.
- **`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
@@ -313,6 +329,60 @@ Known generator quirks, all still true:
---
# CLOSED — Oliver's third playtest: audio, hit timing, readability (2026-07-30)
### The umpire talked too much and at random
Rebuilt as situational triggers behind one shared cooldown — see the invariant above. `asideEvery`
26 s -> 55 s. Removed `ump_out_of_order`, `ump_let`, and `ump_no_appeal` (he asked for
"ump_no_deal"; that line does not exist and `ump_no_appeal` is the one he meant — confirm if a
different line is still unwanted). 11 `ump_*` lines remain.
### "Visually the ball is not yet at the player but somehow the player already is hit"
Real, and measurable. The hostile-ball hit test reused the PARRY's `hypot(dx, dz * 0.8)` — weighting
depth by 0.8 SHRINKS it, so contact fired while the ball was still most of a metre up-court. Measured
at the moment of the hit: `dz = -0.68 m`, and the ball drawn at screen y 314 against a head at 305 —
up around his head, not on his body. Fixed with a separate unweighted test (`TUNE.shotHitR` 0.42,
`shotHitY` 1.75). Re-measured: contact at `dz ≈ -0.25 m`, ball at screen y 325 against head 305 /
feet 362 — squarely on the chest. **The parry keeps its 0.8 weighting on purpose**: a racket sweep
really is wider than it is deep.
### "Everything is a tad too small" (his wife, and the best note of the project)
Measured before changing anything, because "regenerate the sprites at higher resolution" is only half
right. With `RES` 3, drawn DEVICE pixels vs the 112px source were:
```
player at baseline 170 px UPSCALED 1.52x <- higher-res art helps
shambler your half 120 px upscaled 1.08x <- helps
shambler mid-court 98 px downscaled <- cannot help; needs to be BIGGER
shambler far 78 px downscaled 0.70x <- cannot help
```
So near bodies needed resolution and far bodies needed the camera. Both were done.
**Camera** (portrait only; landscape untouched): `camZ` 42->48, `camF` 580->760, `camY` 9->11,
`horizon` 90->45. Pushing the camera back while lengthening the lens FLATTENS the perspective, so
distant bodies grow relative to near ones instead of everything merely zooming; `camY` rises with it
for a steeper look-down so bodies overlap less at depth, and `horizon` re-centres the court after
both. Result: far shambler 26 -> 30 logical px, mid 33 -> 36, chapter-1 boss 39 -> 45, **player
unchanged at 57**, court band 200..390 (was 199..380). If you retune these four, check all four
outputs: far size, near size, court top, court bottom.
**Sprite remaster, at zero generation cost.** The original `assets/_sheet_*.png` files are retained at
full generated resolution (~1400px wide), so nothing needed regenerating — `IDLE_TARGET_H` went to 224
and every sheet was RE-SLICED. The sheet-to-output mapping was never written down, so it was recovered
automatically: slice each sheet to a temp dir, then greedily assign frames to existing assets 1:1 by
LANCZOS-normalised RGBA signature distance. 58 sprites came back from sheets; the 29 with no confident
sheet match were 2x-upscaled instead, which adds no detail but keeps them on the shared scale (see the
`SPRITE_H` invariant — a mismatched sprite renders at half height). `x_crowd`/`x_crowd2` were reverted
to originals: they never touch `SPRITE_H` and doubling them is pure memory.
**Known gap:** `p_idle1`/`p_idle2` and most `o_*` frames were among the 2x-upscaled set, so the two
most-seen poses gained scale but not detail. Re-slicing `_sheet_player2/3` and `_sheet_opp2/3` by hand
(rather than via the matcher) would fix that.
### Regression net
30 tapped tennis points = 50 hits / 0 whiffs. 45-game soak: no exceptions, all four chapters fired,
`mgT` drains, gore 40 / piles 30 / bodies capped at 34.
---
# CLOSED — Oliver's second boss/ranged playtest (2026-07-30)
### The parry was too generous