Make the bosses move, and put six of them and three new bodies on court

A chapter boss had no AI at all: it matched no branch in the variant chain and
fell through to the crowd's aimless wobble, so "the boss AI owns its x" was a
comment describing code that did not exist. Movement is now data on the BOSS_ART
row -- pace, shadow, surge, charge, bounce -- and bossAI is the one place it
happens. Every behaviour field is optional, so a row carrying only art behaves
exactly as a boss did before.

Three earlier drafts of those styles failed the same way: written as "walk toward
the player's x", which in splatter means arrive in four seconds and stand still,
because the player is rooted in pretoss for nearly all of a boss fight. Every
style now moves under its own power and only biases toward him. Measured over a
simulated minute each: 50-136 m travelled, none static.

Three new designs -- THE HEAD COACH (a 5-shot drill 0.14s apart), THE PHYSIO
(charges, and withdraws less than it charges, so it reaches you at 38s and that
is the fight's clock), THE SPONSOR MASCOT (gap 0, so the whole wall of five
leaves in ONE frame and is dodged rather than parried). Harder to kill: 16 -> 20
hp, 9 -> 10 per chapter, plus per-design multipliers.

Three new bodies: the ball boy (3 hp, 2 skins, from game 3), the bouncer (6/3,
game 8) and the roller man (9/4, game 15), each with a 4-frame walk and a drawn
two-frame lurch for every wound stage.

Three wound rules became one. The brute had a hardcoded stage array, everything
else a single skin behind 'abd'.includes(...), and the boss a fraction of hpMax.
The boss's rule reproduces the other two exactly -- verified: the brute still
draws z_gw/gw2/gw3/gw4 at hp 4/3/2/1 -- and keeps working when hp is not
one-per-skin, which all three new bodies need. So a multi-hit body is now a row
in config.ZWOUND and no code. bleed() reads the same call, which also gave the
steward the trickle his wound skin had always implied.

Fixes a real crash on the way past: killPlayer empties shots[] while the shots
loop is mid-iteration, so every remaining index read undefined and the frame
threw. The mascot's five-at-once volley makes it easy to hit; the shipped 3-shot
volley could always have hit it.

_regen.py gains two style blocks and a check tier. One constant used to say
"keep every colour exactly as the reference" -- right for a remaster, and it
fights every word of the subject otherwise. newart is for a new character, state
for the same character wounded or mid-throw; using newart for the wound sheets
licensed a repaint and got one, five came back with living tan skin. And the
detail gate ratchets against the file it replaces, so check: any exists for
re-rolling already-fine art to fix something the gate cannot see, such as colour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-03 11:45:35 +02:00
1 parent 0774a2a31d
commit 4cda6bde76
64 files changed
+694 -78

No files matched your search

+88 -4
View File
@@ -79,6 +79,29 @@ zombie (now an occasional opponent, `TUNE.racketEvery`, not the every-game fixtu
and only then extend, or you have silently nerfed the part of the game that was already fine.
The raw dials do run out around game 25 by design; bosses and ranged attacks (steps 4-5) are what
carry escalation past that, not more bodies.
- **`woundStage(zb, n)` in config.js is the ONE rule for how damage READS.** 0 = unhurt, 1..n = the
skins, spread evenly over `hpMax`. There used to be three rules: the brute's hardcoded stage array
indexed by absolute hp, a single skin behind `'abd'.includes(...)`, and the boss's fraction-of-hpMax.
The boss's rule reproduces the other two EXACTLY (5 hp over 4 skins is one skin per hit; 2 hp over
1 skin is the shambler) and keeps working when hp is not one-per-skin — which the ball boy (3 hp,
2 skins), bouncer (6/3) and roller man (9/4) all need. So a new multi-hit body is now one row in
`config.ZWOUND` (healthiest first, `…b` stride twin looked up automatically) and NO code.
`bleed()`'s anchors are read through the same call and are deliberately the same length as that
variant's ZWOUND row, so the blood and the drawing it comes out of cannot drift apart — they used
to be written by hand against different constants. **'r' and 'B' keep their own skins** (the boss's
on its design row) but go through the same `woundStage`.
- **A chapter boss's MOVEMENT is data on its `BOSS_ART` row** (`move`, plus optional `hpMul`,
`speedMul`, `volley`, `gap`, `spread`, `cycle`, `wind`, `shot`), and `bossAI` in zombies.js is the
only place it moves. Every field is optional, so a row carrying nothing but art behaves exactly as a
boss did before there were any. Three things that are load-bearing and were each a bug first:
**(a)** the generic forward step at the top of the body loop SKIPS `'B'` — bossAI owns both axes,
and leaving it in advanced the boss twice per frame; **(b)** the stride steps off `zb.dist`
(ground covered on both axes), not `zb.z`, or a boss strafing sideways slides with its feet frozen;
**(c)** a boss holds a z BAND (`bossZMin..bossZMax`) except while charging — uncapped, a fight ended
by the boss simply arriving after about a minute, and every extra hp made that MORE likely, not less.
**No style may rely on the player moving to create its own motion**: in splatter he is rooted in
`pretoss` for nearly all of a boss fight, so "close on the player's x" arrives, stops, and is static
for the rest of the fight. Every style moves under its own power and only BIASES toward him.
- **`wound(zb, amount, fx)` in zombies.js is the ONE place damage is applied to a multi-hit body.**
Fractional amounts are legal (a pellet chips `TUNE.dmgPellet` = 0.34 of a point); they bank on
`zb.chip` and only WHOLE points come off `zb.hp`, because the wound skins and `bleed()` anchors
@@ -384,6 +407,24 @@ Clear the intro first: `document.getElementById('intro')?.remove()`.
survives as a rim on a green court.
12. **Two-row "style reference / redraw this" input does NOT work.** The model returns both rows and
invents a background. Describe the target detail level in words instead.
12b. **`_regen.py` has THREE style blocks now, and picking the wrong one costs rolls.** The shared
block used to be one constant that always said *"keep every pose, every colour EXACTLY as in the
reference"* — correct for a remaster, and it fights every word of the subject for anything else.
- default (`STYLE`): remastering a sprite that already exists.
- `"newart": true` (`NEW_STYLE`): a NEW character drawn in the reference's style. Drops the
fidelity clause and says explicitly not to copy the reference's body or outfit.
- `"state": true` (`STATE_STYLE`): the SAME character in a new state — wounded, mid-throw. This is
the one that is easy to get wrong, because `newart` looks right and isn't: it licenses a repaint,
and it got one. Five wound sheets came back with living TAN skin and a redesigned shirt while the
healthy frame stayed a grey-green corpse, and the coach's navy tracksuit came back mid-blue
(torso RGB blue channel 111 -> 138). `STATE_STYLE` pins colours, clothing and build to the
reference and states outright that he is a corpse, never pink or tan.
**A brand-new sprite name must exist on disk before its batch runs** — `run()` measures `density()`
of the file it is about to replace, and `colour_match` opens it. Seed each new name by copying a
body of the right size and facing, and use `check: same`/`loose` so `colour_match` is skipped and
the seed's palette is NOT inherited. The trap in that: a batch that silently fails leaves a perfect
copy of another character behind, which renders as working art. Assert every new PNG actually
differs from its seed before shipping (the scratchpad `check_seeds.py` does exactly this).
13. **`assets/_derive.py` is where a frame that must MATCH another frame is made.** Three of them
live there now: `p_bounce2` (a per-column shear of `p_bounce` — the free arm bends, nothing else
moves), `p_toss` (the drawn-in ball cut out, canvas height KEPT so drawChar keeps his scale) and
@@ -409,6 +450,47 @@ Known generator quirks, all still true:
- **Colour-match any regenerated frame** of an existing pair before shipping: fit a per-channel
linear map onto the original's mean and sd, then verify with a mean-difference number.
## Session log: the roster grows — moving bosses, six of them, three new bodies (2026-08-03)
Oliver asked for five things. This is the first half: **enemies first, weapons second**, his call, so
the arsenal could be tuned against the harder game rather than the other way round.
**Bosses move now, and the style is data.** A boss previously matched NO branch in the variant chain
and fell through to the crowd's aimless ±0.15 m/s wobble — "the boss AI owns its x" was a comment
describing code that did not exist. Six styles, one per design: `pace` (groundskeeper, and the coach
at 1.9x), `shadow` (line judge — paces a band centred on YOU), `surge` (chair umpire — rolls up and
down the whole depth band), `charge` (physio), `bounce` (mascot). Measured over a simulated minute
each: 50–136 m travelled, none static. **Three earlier drafts of these styles failed the same way and
it is worth knowing why: they were written as "walk toward the player's x", which in splatter means
arrive in four seconds and stand still for the rest of the fight, because the player is rooted in
`pretoss` and never moves.** Measure `xSpan` over 3600 steps before believing any movement change.
**Three new bosses**: THE HEAD COACH (rapid 5-shot drill, 0.14 s apart), THE PHYSIO (charges — it
withdraws LESS than it charges, so it is a couple of metres nearer after each rush and reaches you at
about 38 s, which is the fight's clock), THE SPONSOR MASCOT (a `gap: 0` volley — the whole wall of
five leaves in ONE frame, so it is dodged, not parried; the salvo loop is a `while` for exactly this).
Harder to kill: `bossHp` 16 -> 20, `bossHpPerChapter` 9 -> 10, plus per-design `hpMul`.
**Chapter 6 is now 91 hp — the number most likely to want tuning after a real playtest.**
**Three new bodies**: ball boy `'e'` (3 hp, 2 skins, from game 3), bouncer `'h'` (6/3, game 8), roller
man `'k'` (9/4, game 15). Named "roller man" and not "groundsman" so he cannot be confused with the
GROUNDSKEEPER boss.
**One real bug found, and it was pre-existing.** `killPlayer()` empties `shots[]` while the shots loop
is mid-iteration, so every remaining index reads `undefined` and the frame throws. The mascot's
five-at-once volley makes it easy to hit; the shipped 3-shot volley could always have hit it. Fixed
with the same `if (!s) continue` guard the body loop above it already carries, and for the same reason.
**Verification that actually caught things** (all via `window.T`, RAF halted):
- `drawImage` instrumentation per design: all six bosses and all three new bodies draw their OWN
frames, both `…w`/`…wb` stride twins included. Nothing silently falls back to design 1.
- Differential check on the refactor: the brute still draws `z_gw/gw2/gw3/gw4` at hp 4/3/2/1 — the
new shared `woundStage` is byte-identical to the array it replaced.
- Foot-gap spread: ball boy 76% (the shipped shambler is 36%, the brute 39%). Boss stride-pair
difference 34–54%, inside the shipped range of 30–83%.
- 26-game soak: no exceptions, no stall, `mgT` back to 0, gore capped 40, piles capped 30, and
`e`/`h`/`k`/`B` all appeared in it.
## Session log: court types (2026-08-01)
Three courts, picked on a second start-screen stage. A court is pure DATA — `COURTS` in config.js
@@ -746,11 +828,13 @@ Nothing here is broken; these are known gaps and unverified risks.
scale but gained no detail in the remaster, and they are the two most-seen poses in the game.
Fix by hand-slicing `_sheet_player2/3` and `_sheet_opp2/3` at `IDLE_TARGET_H` 224 rather than going
through the signature matcher, which assigned those frames elsewhere.
4. **Boss designs are asymmetric.** Design 1 (GROUNDSKEEPER) has 3 wound stages and 4 throw frames;
designs 2 (LINE JUDGE) and 3 (CHAIR UMPIRE) have 1 wound skin and 2 throw frames each. Legal — the
4. **Boss designs are asymmetric — still, for two of the six.** Designs 1 (GROUNDSKEEPER), 4 (HEAD
COACH), 5 (PHYSIO) and 6 (SPONSOR MASCOT) have 3 wound stages and 4 throw frames each; designs 2
(LINE JUDGE) and 3 (CHAIR UMPIRE) still have 1 wound skin and 2 throw frames. Legal — the
`BOSS_ART` indices are clamped — but a chapter-2 boss degrades in one visible step instead of three.
5. **Only 3 boss designs, cycling.** Chapter 4 repeats the groundskeeper at 43 hp and 3.67 m. A fourth
design is a sheet plus one row in `BOSS_ART`.
Fixing them is now just two batches in the shape of `boss_coach_wounds` / `boss_coach_throw`.
5. ~~**Only 3 boss designs, cycling.**~~ CLOSED 2026-08-03 — six now, so a chapter does not repeat a
design until chapter 7. A seventh is still a sheet plus one row in `BOSS_ART`.
6. **Landscape is under-maintained.** The readability pass (camZ/camY/camF/horizon), the adaptive
height and the safe-area work are all portrait-only; landscape still runs the old camera and a fixed
480x270. Portrait is the stated primary target, but landscape has drifted and is untested.