Commit Graph
169 Commits
Author SHA1 Message Date
wuwiandClaude Opus 5 4cda6bde76 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>
2026-08-03 11:45:35 +02:00
wuwiandClaude Opus 5 0774a2a31d Delete the wake variant; say PULL DOWN and point only downward
The switch and the losing branch are gone — a fired band is a short streak
behind the ball, full stop. TUNE.shotTail, WAKE and the tailMode branching all
removed; the aiming band is untouched as ever.

THE IDLE DEMO NO LONGER POINTS UP. It grew chevrons up-court as the pull grew,
because the shot going the opposite way to the thumb was meant to be the
lesson — but an up arrow on screen is an instruction to swipe up, and swiping
up is the one gesture this game does not have. The only direction drawn now is
the one you actually make: the dotted track, the thumb sliding down it, and a
down arrowhead at the end, clear of where the thumb lands at full draw.

And it says so in words. The mime alone was too abstract to read as "drag your
thumb down", so PULL DOWN sits under the arrow in the pixel font.

Which makes the text slot below it redundant while the demo runs, so it shuts
up rather than printing the same instruction twice 40 px apart. The wording
everywhere else moves BACK -> DOWN too: "back" is the ambiguous word that could
be read as up-court, which is exactly the confusion here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 22:02:41 +02:00
wuwiandClaude Opus 5 dbc0d5baee Default the fired band to the tail behind the ball
shotTail 0 -> 1. The panel is keyboard-only ('T'), so on a phone the default
IS the setting — noted next to the knob so the next person doesn't assume it
can be flipped in play.

Verified on the default build: the drawn slice runs from z 21.4 back at the
racket side to z 18.84, which is the ball to the centimetre — entirely behind
it, ending on it. Nothing ahead. 600 frames clean.

Keeping the losing branch until a real playtest confirms the pick.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 21:44:14 +02:00
wuwiandClaude Opus 5 6ce52a6e85 Two ways to spend the aim band, switchable in the panel
The laser look came from carrying the AIMING picture unchanged into the flight.
While you pull, a bright saturated line spanning you to the target is exactly
right — it is the information you are deciding on. Once fired it is spent, and
a projectile drawing its own future in a continuous saturated line from itself
to what it is about to hit is the visual definition of a targeting beam. No
real ball shows where it is going.

So a fired band now desaturates toward dust, collapses, and shrinks to a short
streak. TUNE.shotTail picks which side of the ball that streak sits on:
  0  a WAKE ahead of it, dissolving — your erase idea, beam cut down to a wisp
  1  a TAIL behind it, nothing ahead at all — conventional projectile language
The shadow follows the same slice, because "nothing ahead" has to include the
ground or it still gives the future away. The target mark fades with it: a mark
still burning on the shambler is the other half of the laser.

Both leave the aiming band untouched.

Built the collapse squared first and it was wrong — both variants were gone by
a third of the flight, which is not "less like a laser", it is no streak at all,
and the strobing at release is what this trail exists to prevent. Linear now.
The SHORT SLICE is what kills the beam; the fade only has to make sure nothing
is still burning when the ball lands.

Flip it live on the phone: T panel, shotTail, type 0 or 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 21:13:34 +02:00
wuwiandClaude Opus 5 7284ca08f8 Pace back to 1.2x, and unweld the racket from over his head
PACE 1.3x -> 1.2x on all four numbers: 19.2 / 14.4 rally, 22.8 / 26.4 serve.

The strength-to-velocity relationship you suspected was already there is: a
splatter serve runs 26 m/s at the minimum pull, 36 at the kill threshold and
49.2 at a full draw. The draw very nearly DOUBLES the pace (1.89x), because
serveQualitySpeed carries more of the pair than serveSpeed does. Nothing to
change unless you want it steeper, and that is the one knob.

THE RACKET-ALWAYS-UP LOOK WAS A LATCH, not the art. hitServe sets pose 'serve'
with poseT 0; that only expires after POSE_TIME (0.38 s); and p_pull, the
wind-up frame, is only ever selected from pose 'idle'. So firing faster than
0.38 s reset the timer before it could expire, he never returned to idle, the
pull frame was never drawn once, and he stayed welded into p_smash — arm
extended overhead with balls appearing out of the strings. Exactly the late
game, because that is where the fast successions are.

serveToss now clears a stale 'serve' pose: a new wind-up ends the last shot's
follow-through. At ~4.6 shots/sec the cycle is now pull x5 frames, smash x8,
pull x5 — a real swing. Slow fire still plays the whole ritual: bounce, pull,
smash, idle, bounce.

Caveat: past roughly 8 shots/sec each pose gets 1-2 frames and will read as a
flicker rather than a swing. There is no fix for that short of drawn
follow-through frames; the poses are stills.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 19:14:12 +02:00
wuwiandClaude Opus 5 19c13986c5 Ball 30% faster, all four pace numbers together
baseShotSpeed 16 -> 20.8, qualityShotSpeed 12 -> 15.6, serveSpeed 19 -> 24.7,
serveQualitySpeed 22 -> 28.6. All exactly 1.3x, so nothing about the relative
feel moves: rally and serve, tennis and splatter, his ball and the CPU's, which
share baseShotSpeed.

Swept the full-pull serve down the court: 53.3 m/s against the old 41 at every
range, flight times ~23% shorter (30% more pace is 23% less time in the air).

One exception, and it is geometry rather than tuning: a shambler standing right
on the tape is loft-limited. That shot has to come almost straight down, so the
net-clearance loop trades the extra pace straight back and it ends up only ~14%
quicker. Nothing to fix unless it reads as sluggish next to everything else, in
which case serveSpin is the knob — a harder dip buys pace over the tape.

Tennis serves 8/8 into the box at the new pace; 900 frames clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 18:53:33 +02:00
wuwiandClaude Opus 5 2edabe6c64 Aim the ball at the chest, and stop lobbing at a net that isn't there
THE BALL WAS SOLVED TO THE GROUND. solveShot's terminal height was a hardcoded
0.08 — ball-on-the-grass — so a splatter serve came down past the shambler and
registered at the ankles however the mark was drawn. It now takes an arrival
height, and a lane shot aims at the body: zombieH x Z_CHEST, scaled off the
same height table render draws with, so a brute is struck on the brute rather
than at the height a shambler's chest happens to be. Measured at the frame the
wound registers: shambler 1.11 m of 1.8, brute 1.64 m of 2.7, and an empty lane
still lands on the grass. The aim mark reads the same `ty`, so mark, arc end
and impact are now one point instead of three guesses.

AND A BUG FOUND ON THE WAY. The net-clearing loop ran for shots that never
reach the net. A shambler on your own side sits short of the tape, so `tN` is
still positive — the net is ahead in the direction of travel, the ball simply
lands first — the height test there is far below the tape, and the loop spent
all twelve cuts. Every same-side shot left the racket at 28% of nominal pace as
a slow loop. That is exactly the shot that needs to be fast: those are the ones
about to eat you. Guarded on the shot actually crossing the net, in serveArc
and in doHit's copy. Flight time to a body at z=16 drops 0.71 s -> 0.20 s.

THIS ONE IS A FEEL CHANGE YOU DID NOT ASK FOR — near shots are now fast and
flat where they used to loop. Say if you want the lob back.

Tennis and the duel still serve into the box, far-side shots still clear the
tape, 900 frames of splatter render clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 18:49:47 +02:00
wuwiandClaude Opus 5 ff9e09e3eb The body mark was behind the body; the yellow ring was never mine
Two bugs, one report.

THE MARK WAS DRAWN UNDER THE SPRITE. The whole band, target mark included, is
painted before the sprite pass — which is right for the arc, since a nearer
shambler should occlude the part of the flight behind it. But the mark is a
mark ON a shambler, so it went down first and the body was then drawn straight
over the top of it. It has never once been visible. Split out as drawAimMark
and called after the near-side actors, which is the only place a highlight on
a body can go.

THE YELLOW CIRCLE WAS G.outMarker, not the aim at all. It is the tennis in/out
call — "did that land in?" — and splatter never asks: `out` could not be true
there, so it was an unconditional yellow ring on the grass carrying no
information, sitting under the shambler for 1.3 s. That is longer than the gap
before the next pull, which is why it was there "most of the time" and why it
looked like the aim marking the feet. Now set only in tennis, from both the
serve and the rally swing.

Checked: tennis keeps its ring on serves AND rally shots, splatter has none
anywhere, and the mark now draws over the torso at every pull strength.

Note this leaves a splatter VOLLEY with no landing feedback at all. It had a
ring that said nothing before, so nothing was lost, but say if the volley now
feels blind.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 18:38:08 +02:00
wuwiandClaude Opus 5 0ce857fedc Let the ball eat its own aim band, mark the body, grow the machine
Four things from the phone.

THE BAND NO LONGER BLINKS OUT. Cutting it at the instant of release made a
fast rally strobe — band, gone, band, gone. hitServe now records the arc it
actually fired (error included, so the ball tracks what it is consuming to
within a centimetre) and the ball eats it from behind as it flies. The aim
resolves into the shot instead of vanishing. It ends early when the ball does,
because a shambler swallowing the shot is a cut you want.

THE TARGET MARK SITS ON THE BODY. A ring on the grass pointed at the one part
of a shambler the shot has nothing to do with — the ball strikes anywhere under
1.9 m and the wound lands on the torso. So the mark rides at chest height when
there is a body to hit and lies flat only when the lane is empty. Filled and
washed out in the band's own colour instead of a hard 2px ring, which stops it
reading as a decal stuck to the lawn.

x_howto REDRAWN for the current gesture: thumb dragging back to a print, the
shot leaving as a curved band onto the shambler's chest, shadow dots beneath.
The old one still taught a straight arrow.

MACH_H 1.15 -> 1.55. Waist high read as a prop you could ignore.

Verified on a cache-busted build (the pane was serving a stale config.js, so
the first pass measured nothing): preview velocity and gravity still identical
to the fired ball, same flight time in the trail, unravel smooth to the hit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 17:41:18 +02:00
wuwiandClaude Opus 5 3a91d87b08 Show the shot itself: one solver, one band, no meter
The reticle was a chevron trail on the ground and a draw meter across the
bottom. The trail was a RAY — it pointed at the target but said nothing about
the ball getting over the net — and the meter sat in the apron, which is
exactly where your thumb is while you pull, so the one readout that mattered
mid-gesture spent the whole gesture underneath your hand.

Now the aim IS the flight. previewServe runs the same targeting and the same
loft solve hitServe fires with (both extracted, neither duplicated), so the arc
you see is bit-identical to the ball you get — verified: vx/vy/vz/g all match
to zero at quality 1.0, where the aim error vanishes. A preview drawn from a
second copy of the maths is worse than no preview; it lies with confidence.

One object now carries all three readings, up-court where you are looking:
the parabola is WHERE, the colour is WHETHER (cool blue still means it only
staggers), and the band's thickness and opacity are HOW MUCH — which is the
part a hue cannot carry on its own. Because the arc ends at the auto-picked
target, a weak pull and a full one at the same shambler draw the same curve in
different colours: "pulling further does not reach further" made visible.

A shadow tracks the arc along the lawn. An arc hanging in the air is ambiguous
about where it comes down, and that trail is what pins it.

The apron is left to the gesture demo alone, drawn bigger now that it owns the
space, with its chevrons lifted clear of the player's legs.

Tennis and the duel still serve to the box, not at shamblers — checked both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 17:16:21 +02:00
wuwiandClaude Opus 5 75936d3324 Fall the light off across the run-off, so it reads as ground and not margin
Riding the court up left a wide band below the near baseline, and `pal.out` is
one flat colour — so the perspective visibly stopped partway down the screen
and the rest read as dead margin rather than as the back of a court.

A gradient from the baseline to the bottom edge fixes it: the band gets depth,
the rack gets a frame, and what darkens is exactly the area the thumb covers
anyway. Starts at alpha 0 on the baseline, so there is no seam.

A procedural "trodden ground" variant (scuffs and drag marks) was built and
cut: random ellipses read as unexplained puddles, not as wear. That idea wants
authored detail and gore that accumulates over a run, not noise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 15:57:36 +02:00
wuwiandClaude Opus 5 0fe8f37a81 Ride the court high enough to actually pull under him
The slingshot never had room. On a 375x812 his feet sat 658 CSS px down an
812 px screen, leaving 112 px of clear court under him — and a full draw is
130. The wind-up physically did not fit, so the thumb was driven onto the iOS
bottom edge every rally, where a swipe down is Reachability and the system
takes the gesture. The gesture was never the problem; the geometry was.

So the portrait horizon is derived from the BOTTOM now: park his feet a full
draw plus the rack plus slack above the screen edge, and let the horizon fall
where it must. What gets shorter is the stand above the court, and that was
only ever crowd. 375x812: horizon 121 -> 51, the clear band under him 112 ->
210 px, and a full pull from his feet now ends 122 px clear of the edge.

Landscape is untouched. `horizon` is still the live knob in the T panel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 14:39:05 +02:00
wuwi 8ea749f380 Revert "Point at them, then wind up: the lane comes from where the thumb lands"
This reverts commit 1798fc67b8.
2026-08-02 14:35:25 +02:00
wuwiandClaude Opus 5 1798fc67b8 Point at them, then wind up: the lane comes from where the thumb lands
The lane used to come from the pull's own direction, so aiming up-court was
always a downward drag. Every shot ended lower than it began, the next touch
starts where the last one ended, and over a horde the thumb walks itself into
the iOS bottom edge — where a pull down is Reachability and the system takes
the gesture out from under you. A web page cannot refuse that.

So touch-down picks the lane now (a ray from his chest through the thumb) and
the pull after it is power and nothing else, in whatever direction there is
room for. Aiming is what drags the thumb UP the screen, because that is where
the shamblers are.

The cost, and the thing to feel: the lane locks when the thumb lands. You can
no longer sweep the aim while drawn.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 14:22:55 +02:00
wuwi 1337c1f47d Revert "Aim by dragging at them, so the thumb stops walking into the iOS edge"
This reverts commit d4bbcfc080.
2026-08-02 14:10:12 +02:00
wuwiandClaude Opus 5 d4bbcfc080 Aim by dragging at them, so the thumb stops walking into the iOS edge
The slingshot pulled back, which meant every shot ended with the thumb lower
than it started — and the next touch begins where the last one ended. Over a
horde it walks itself into the bottom edge strip, where a pull down is
Reachability and iOS takes the gesture. A web page cannot refuse that.

So the drag now points AT the target: the thumb ends high and drifts up-screen,
away from the edge. It also matches the flick-swing, which always aimed this way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 14:05:04 +02:00
wuwiandClaude Opus 5 b23a3a8aaf Turn the theme up, and let START GAME catch the light
Menu theme 0.5 -> 0.65.

The gold frame comes off START GAME — both buttons share the one slate border
now, so the pair reads as a pair, and a light sweeps across the START plate
instead: over in ~0.8s, then still for the rest of a 4.5s cycle. The sweep is a
::after with pointer-events:none so it can never eat the tap, and it is off
under prefers-reduced-motion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 17:44:21 +02:00
wuwiandClaude Opus 5 d3aabc4bd6 Fold the menu into the title screen, and stop the theme restarting
The menu stage was the same logo on the same backdrop with two buttons, so it
read as the title screen failing to advance. The buttons move onto the title
screen in place of the control list, which is the tutorial's job now.

Death no longer reloads the page. The reload was an honest full reset, but it
also tore down the AudioContext — so the theme restarted from silence every
time you died, when it should carry straight on from the death summary into the
title screen. restartRun() puts G, P, O, ball and score back from a
structuredClone taken at module load (all plain data, so the clone is the whole
job and a field added later is covered for free), and the two modules with
timers of their own reset themselves: every zombies array plus spawnT/moanT/
mgTick, and input's held keys and half-finished drags, which would otherwise be
inherited by the next run's first frame.

The overlay is hidden rather than removed so it can come straight back, which
needs its own [hidden] rule to beat the display:grid on #intro.

The theme starts on the first tap and never restarts: loopBed's beds.has guard
already makes every later menuMusic() a no-op.

Verified: one background-music fetch across launch -> run -> death -> title ->
second run, and a full second run through the transition after the in-place
reset.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 16:25:20 +02:00
wuwiandClaude Opus 5 2d4fa79258 Give the menu buttons room, a frame, and bigger type
The logo now sits exactly as it does on the title screen — same width, no menu
override — with 30px under it so the blood running off its bottom edge stops
touching the first button.

That button also had no top border to cut into: the crop was a BOTTOM band of a
taller plate, so it carried the plate's bottom border and nothing above. Both
plates are cropped to their interior now and CSS draws one consistent frame on
each, gold for START and slate for TUTORIAL.

Lettering stays live text and goes to 15px with wider tracking.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:25:54 +02:00
wuwiandClaude Opus 5 4a034773aa Draw the tutorial properly, and put the logo and gore on the menu
The tutorial was borrowing a court tile, the in-game how-to icon and a weapon
icon, none of which were drawn to be looked at. Four purpose-made 2:1 panels
now: the serve with a tapping hand, the slingshot pull with chevrons into a
zombie, the weapon rack, and the floodlit duel. They get the full card width
above Marvin instead of being squeezed into the text column beside him.

The menu wears the title logo instead of a Silkscreen headline, and the two
buttons wear generated plates — START GAME bloodied, TUTORIAL nearly clean.
The lettering stays Silkscreen on top: generated type is a lottery and would
not match the rest of the HUD.

Two things the art needed. Quantise the panels at 128 colours, not 64 — big
flat fields with saturated accents is where a small palette shows, and 64
turned the tennis ball beige. And find a plate by its BRIGHT BORDER, never by
the corner colour: one plate came back on a near-black margin, the other on a
transparency checkerboard that differs from itself everywhere and matched the
whole canvas.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 13:43:43 +02:00
wuwiandClaude Opus 5 9a65a50cc4 Put a menu in front of the game, with Marvin to explain it
The overlay is a four-stage machine now — splash -> menu -> {courts, tutorial}
— swapped by data-stage while the backdrop and card frame stay mounted, so the
live court never flashes through between screens.

Court tiles SELECT only. Tapping one used to start the match on the spot, which
gave you no way to compare the three and no way back out; START GAME and BACK
are their own buttons now. Headlines are 15px against the 11px body, so a screen
reads as a screen rather than as a list.

The tutorial is a data table: a Marvin pose, an optional picture already in
assets/, and his lines. Seven pages covering tennis, the turn, the slingshot,
the rack and the duel. Marvin is four new frames off x_boymop1 — waving,
pointing, shrugging, and clutching his mop in terror.

The theme no longer starts at boot. A title screen that plays at you before you
have touched it is rude, and the autoplay policy would have held it silent until
that first tap anyway.

Death returns to the menu instead of the title. The reload stays — it is the
honest full reset — and a sessionStorage flag is what picks the stage on the way
back in.

Machine-gun rounds are tennis balls, so they are drawn as balls and take the
perspective. A fixed 4x4 fillRect was exactly as big at the far baseline as at
your feet, which read as square confetti pinned to the screen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 13:31:00 +02:00
wuwiandClaude Opus 5 60f7ce2cf1 Give the run strip a panel, round the rack, fix the duel's hint
The NEXT/GAME row was bare text on the court. That was legible over one dark
green lawn and is not over a sunlit clay crowd or a floodlit blue one, so it
gets its own rounded panel — the same thing the scoreboard art above it already
relies on. It states the sets banked now too; they had only ever appeared in the
scoreboard's own per-row column.

The weapon rack's five slots are rounded, which is what they are: buttons.

The duel is played under tennis rules — no slingshot, no rack, serve to the box
(see the !G.interlude guards in solveShot and the aim reticle) — but the bottom
hint was still telling you to pull back to aim, which is the one thing that does
nothing there. It reads as a tennis serve now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 12:21:53 +02:00
wuwiandClaude Opus 5 af30db2ff3 Make the regen pipeline check the kit, and fix the swing and toss
Every roll drifts the palette and nothing measured it: a white headband on six
frames, pale grey shorts on the toss twice, brick-red shirts and brown shoes on
the swing all sailed past --tries. check: "same"/"loose" skips colour_match, and
colour_match is a global mean/sd fit far too weak to pull brick-red back to red
— a nearest-colour palette lock is worse still, it drags the duller reds into
the racket's browns.

So _regen.py measures three numbers and a batch opts in with palette_ref:
redness (r minus mean(g,b) over saturated warm pixels — brightness alone cannot
tell brick-red from red), shorts brightness (charcoal 61 vs the drift's 83), and
the near-white fraction that is the shoes and socks (brown drops it to 0.9%,
pale shorts push it to 7.9%, correct frames sit at 2.8%). Frames known to be
right cluster tightly on all three; every drifted frame fell outside on at least
one. It rejected five bad rolls in a row, then passed a good one.

The swing also kept re-inheriting its own drift: ref_sheet is built from
[lead] + the sprites being replaced, so it was showing the generator the very
brick-red frame it was about to overwrite. Copying a canonical frame over the
targets first breaks the loop — it passed on the next roll.

Swing is now a real backswing and follow-through (silhouettes differ by 1.0,
racket low behind vs up over the shoulder), both at full standing height, and
the toss and smash wear charcoal shorts again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 11:51:27 +02:00
wuwiandClaude Opus 5 494bab1b69 Stop the opponent shrinking, skating and sprinting on the spot
The "smaller when swinging or tossing" was an anchor bug, not art. _fine.py
scales a whole sheet so the --anchor frame is 224 px, and drawChar draws every
sprite at img.height/SPRITE_H — so anchoring opponent_play on the TOSS, with
arm and ball above the head, made 224 px cover the raised arm and shrank every
body on that sheet. o_swing1 was 162 px against a 224 px standing pose. Each
opponent batch now leads with a standing `ref` frame, which _fine.py anchors on
and _regen.py discards. Correctly anchored the toss is 278 px and the man is
full size; the swing is 221.

The receiver no longer slides 0.12 m side to side while waiting on a serve —
sliding a sprite sideways reads as skating, and the split-step swap to the run
frames (removed earlier) had already been the other half of that.

The idle bounce ball had one hardcoded offset for both men. They reach to
opposite sides of the screen — the player is drawn from behind, the opponent
from the front — so the opponent's ball sat out by his racket. dx and apex are
per-man now, and o_bounce/o_bounce2 were redrawn with a patting hand to put it
under.

Also: the opponent wears a RED headband, and a prompt of mine that said "white"
had quietly repainted it on six frames.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 11:37:16 +02:00
wuwiandClaude Opus 5 cd1c7a0cd8 Encode the splatter bed too, and give the opponent his racket back
zombie-mode: 5.9 MB of 24-bit WAV down to 287 KB. Its master is 982597 frames
at 48 kHz, so loopEnd is the un-round 20.47077 — the decoder hands back
20.4787, and those 7.9 ms of AAC padding would tick once a loop. Nothing in
assets/audio/ is a multi-megabyte WAV any more.

o_run1/o_run2 had no racket in either hand. The cause was the batch prompt:
opponent_run never said "racket", and its reference sheet was built from the
racket-less frames it was replacing, so there was nothing to copy one from. It
is a ref_only batch off o_idle1 now — same man, front-facing, racket in his
right hand — with the subject spelling out that he still carries it mid-stride.
The stride reads as well as the old one did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 10:55:18 +02:00
wuwiandClaude Opus 5 cf3498c049 Encode the menu theme as AAC
8.3 MB of WAV down to 768 KB, on a file that downloads on every page load
before the player has decided to play anything.

loopBed takes the full filename now, because the beds are a mix of .wav and
.m4a — fadeBed's key has to match it exactly, extension included, or a duck
silently no-ops.

It also takes an explicit loopEnd, and the theme passes 48. AAC pads its last
packet, so Chrome decodes this 48.000s master to a 48.0174s buffer: 17ms of
silence spliced in at every seam, once a loop, on the title screen. Trimming
the seam is what keeps the loop seamless.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 10:48:19 +02:00
wuwiandClaude Opus 5 c567ff68a6 Turn the menu theme up
0.3 -> 0.5. Nothing else is playing over the menus, so it can sit forward.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 10:42:30 +02:00
wuwiandClaude Opus 5 60a74f6a99 Give the menus a theme
background-music.wav loops under the title screen and the court picker, ducks
out when a court is picked, and fades back in at the death summary — the end
screen is a menu too, so the zombie bed ducks out as this one comes in.

It starts at BOOT, not on the first tap, so the title screen already has music.
That needed loopBed to call audio() before its already-started early return: the
theme plays into a context the autoplay policy holds suspended, and the call
that wakes it is a second menuMusic() from the first tap, which used to return
at the beds.has guard without reaching AC.resume().

fadeBed also now records a duck for a bed that is still loading, and loopBed
starts that bed silent. An 8 MB file ducked mid-fetch would otherwise come up at
full level, seconds later, over the match. Same latent bug applied to zombieDuck.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 10:42:10 +02:00
wuwiandClaude Opus 5 fa30fb94c4 Let the player choose a court before the match
Three courts, picked on a second start-screen stage: CENTRE COURT (grass),
TERRE ROUGE (clay) and THE SLAB (hard). A court is a row in config.COURTS and
four PNGs — render() never learns their names.

Each row carries two palettes, because every court still rots: what "drained"
means on orange clay is not what it means on green lawn. Mow stripes became a
flag rather than a colour — recolouring the lawn and leaving the bands on still
read as grass. Bounce is two numbers copied into TUNE on selection, so the three
bounce sites in gameplay.js and the T panel stay untouched: grass skids low and
fast, clay sits up and drags, hard is the old baseline.

Per-court crowd sheets and ad strips. Each court's clap frame is generated from
its OWN seated sheet, not from x_crowd2 — pairing one court's crowd with
another's applause cross-fades between two different crowds.

The picker is a second panel in the existing #intro overlay, rows built from the
table. Last pick is remembered and comes up pre-highlighted; 1..N or Enter on a
keyboard.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 10:25:30 +02:00
wuwiandClaude Opus 5 6378ff2a1b Cut the serve down to three poses
Oliver, after playing the four-stage version on the phone: idle, then the racket
dropping on the pull, then the drive on release — "that's all, forget all the
others". At thumb speed the middle stages are never read, so they are deleted
rather than kept around: p_pull is the wind-up, p_smash is the drive, and both
come out of the photo-referenced sheet, so the whole serve is one drawing style.

The dead batches and the arm_left guard go with them. assets/_pose_ref_serve.png
is kept as the recipe: his photo sequence over our own sprites, which is what
turned a fifteen-generation prose argument into one generation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 01:09:57 +02:00
wuwiandClaude Opus 5 f6dfc57a06 Draw the wind-up from the photographed serve, not from prose
Oliver's idea, and it is the fix for the process as much as for the frames: give
the generator the real serve sequence as an IMAGE and let it copy the poses,
instead of describing a pose in prose and re-rolling until it lands. One
generation — a composite of his five photographed frames over two of our sprites
— returned a magenta row of the whole serve in the game's own character and kit.
The four stages are sliced straight out of it, anchored on the standing figure.

_fine.py gets --defringe: its two de-fringe passes each eat a one-pixel line, and
a racket frame IS a one-pixel line — that is what had been leaving the men holding
a grip and nothing else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 01:00:56 +02:00
wuwiandClaude Opus 5 bcff17812f Draw the whole serve right-handed, and let the pull wind it up
The racket kept changing hands because the art disagreed with itself: p_idle,
p_bounce and p_toss held it in his LEFT hand while p_smash and p_swing used his
right, so every new frame copied whichever reference it was handed. The idle, the
bounce pair, the toss and the four wind-up frames are all regenerated with the
side named — drawn from behind, his right hand is the right-hand side of the
image, and only the racket HEAD crosses behind his back.

What separates the four wind-up stages is how far the racket has fallen, and that
needed the racket head pinned to a body landmark per frame — above his head,
beside his ear, between his shoulder blades, at the back of his thighs. Described
as "further into it each time" it drew four near-identical poses twice running.
They are wired late in the pull, Oliver's call: frame 1 owns the first half and
the three coiled frames are packed into the top half.

assets/_derive.py is deleted. Fixing generated art by mangling it afterwards is
always worse than regenerating; three guards in _regen.py turn the bad rolls that
used to install silently into automatic re-rolls — a portrait reference canvas
(the generator answered with a 2x2 grid), a non-magenta background (a green sheet
keyed away four racket heads), and a mirrored frame.

The bounce pair is one drawing from one sheet now, differing only in the free
hand, so BOUNCE_IDLE moved to his left side and was re-measured off the sprite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 00:49:28 +02:00
wuwiandClaude Opus 5 231cd8650a Keep the racket in one hand through the wind-up
Oliver, off the file browser: "the racket changes the hand in 3 and 4". It did —
the generator drew frames 1-2 right-handed and 3-4 left-handed. Both mirrored into
line with the rest of the serve (a back-view figure mirrors for free), and the
batch prompt now names the side, since the generator will not hold a prop in one
hand across a sheet unless told to.

The convention the rest of the art already uses: racket on the LEFT-HAND SIDE OF
THE IMAGE, free arm on the RIGHT — the side the game drops the bounced ball and
tosses the served one. p_smash keeps the racket on the right: that is the frame
where it has come over the top to meet the ball it is hitting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 23:54:27 +02:00
wuwiandClaude Opus 5 b1f8ea3240 Wind the serve up with the pull, and stop the frames flickering
Four new frames (p_pull1..4) turn the slingshot drag into the serve's wind-up:
racket above the head, behind the head, behind the back, fully coiled, picked off
pullFrac — the same 0..1 that sets the shot's power — so the gesture and the man
are one movement and the release snaps to the smash.

The two bounce frames are one drawing now. p_bounce2 is p_bounce with the free
arm sheared up 12px and nothing else touched, so the swap cannot flicker, and it
follows the ball instead of its rising phase; BOUNCE_IDLE was re-measured off the
sprite so the ball tops out under the palm. p_toss had the tossed ball drawn into
it — cut, canvas height kept, because the game scales him off img.height.

The runner: hip centres sat -2.0/+7.1/-6.0/-0.1 px off the sprite centre, a 13px
lateral jump at 12 frames a second, and Oliver's diagnosis was right that all four
frames leaned the same way and lifted the same leg. Frames 3 and 4 mirrored, all
four padded to one width with the hip on the centre line.

assets/_derive.py holds the three derivations, since "the same drawing with one
small difference" is the one thing the generator cannot draw.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 23:51:30 +02:00
wuwiandClaude Opus 5 f71d9293fc Keep the pole in one hand, and write the session up
Oliver spotted it before testing: a whole-frame mirror swaps the DRAGGED POLE
into the other hand as well as the legs. True for the one wound stage that still
has an arm holding something, so that stage gets a leg-band mirror about the hip
centre instead — legs swap, pole and stump stay put. The other three stages have
no arms and no props, so a full mirror is right for them.

The honest cost: a leg mirror leaves both feet in the same two places, so that
one frame reads as a leg swap rather than a stride. Written up with the fix that
worked for the shamblers (a passing pose, feet together) and the measurement that
tells them apart.

HANDOFF gets the session: what changed, the two lessons that cost the most (the
prompt sets the art resolution; the reference sheet is what the generator
copies), the rule that anything which must stay identical between two frames
belongs in code, and the four known limitations ranked for whoever picks it up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 23:10:52 +02:00
wuwiandClaude Opus 5 2f8aca69d9 Rebuild the brute's damage, and stop generating its second frames
All four stages drawn from the healthy body in two sheets, so he keeps the same
bulk the whole way down instead of slimming as the wounds get worse — one arm and
his pole, both arms, ribs open, then headless.

The second frame of each stage is now a MIRROR of the first, not a generation.
Every attempt at drawing them moved the injury between the two feet — this is the
third fix for the same complaint — and a mirror physically cannot: the wound
stays on the same part of the body, and flipping a standing figure IS the stride
swap the B frame exists for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 23:06:52 +02:00
wuwiandClaude Opus 5 86c08bf4b1 Serve at full size, and bounce the ball with the free hand
Oliver had the diagnosis exactly right: the raised racket is part of the sprite,
so the toss and smash frames were 29% racket by height — and the game scales a
sprite off its TOTAL height, which shrank the body to fit. Measured 71% body
against the standing frame's 98%. Both are scaled so the BODY lands at standing
size and the racket now extends above the frame instead of squeezing him into it.

The bounce frames are redrawn too: racket in the left hand, right hand empty and
reaching down to the side the game actually drops the ball on, and no ball drawn
into the sprite at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 23:05:18 +02:00
wuwiandClaude Opus 5 d1c584f473 Keep the brute's torn-off arm on one side
The generator drew the second frame of that wound stage as a MIRROR of the first,
so the missing arm and the dragged pole swapped sides on every step — which is
the "wound on the left arm on one foot but not the other" Oliver saw, and it was
on the sheet, not in the frame assignment. Flipping the frame back puts the
injury where it belongs, and a horizontal flip IS the leg swap a second stride
frame wants, so the walk still reads.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 22:48:54 +02:00
wuwiandClaude Opus 5 e4c59e2c26 Turn the player around, point the runner at you, match the wounds
The playtest list, art side. The generator had been copying the very frames I
wanted changed, so:

- The player is drawn FROM BEHIND again in every pose — standing, bouncing,
  serving, swinging. He had turned to face the camera in the redraw, which is
  also why he "looked at you" in different poses.
- The serve frames are back at full size. That batch had been anchored on the
  toss frame, so the whole serve set came out about a fifth too small.
- The runner CHARGES AT YOU down the court instead of sprinting sideways across
  it, which is what he actually does in the game.
- The brute's wound stages are drawn a pair at a time from their own A frame, so
  the injury stops moving between the two feet.
- The bounce frames had a ball painted into them while the game draws a live one
  on top — he was bouncing two. The painted one is gone, and the live one now
  falls on the side the sprite actually reaches for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 22:46:19 +02:00
wuwiandClaude Opus 5 67a410e707 Show the generator what to copy, not what to replace
The reference sheet is what it imitates, so building that sheet out of the very
sprites I was trying to change is why the front-facing player came back
front-facing and the side-on runner came back side-on. A batch can now name
`ref_only` — the frames it looks at — separately from the frames it replaces:
the back view is drawn from the one genuine back-view frame, the charging runner
from a front-facing zombie, and each brute wound pair from its own A frame so the
injury cannot wander between the two.

Also stopped colour-matching a REPOSE. Fitting a differently-posed frame onto the
old one's colour statistics is what greyed out the serve shorts; when the pose
changes the generator's own palette is the better answer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 22:36:13 +02:00
wuwiandClaude Opus 5 01498eddad Turn the bodies around, calm the racket zombie, break up the pack
Three from the playtest, all code:

Every side-on sprite is DRAWN facing left, and nothing ever mirrored them, so a
zombie walking right moonwalked — obvious the moment they close on you across
your own half. They face where they last walked now. The front-facing ones (the
turned CPU, the machine, a chapter boss) have no left or right and are left alone.

The racket zombie holds a position and only moves when the ball makes him, so a
time-driven walk cycle flicked him between two frames on the spot. His stride
steps off ground covered, like the brute's — standing still is standing still.

And the pack no longer stands on the body twitching for as long as the summary is
up: once it appears they break up and shamble away up-court. Measured — feeding
at 3s, dispersed to 3m by 6.5s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 22:24:11 +02:00
wuwiandClaude Opus 5 7edd623372 Finish the roster: brute wounds, the chair spectator, the last boy frames
The two that kept failing land as two-figure sheets — a dragged pole or a
held-up chair is wide enough that four to a sheet either merge into one frame or
run off the canvas whatever margin the prompt asks for. The chair spectator came
out the best of the lot (0.15-0.24 to 0.42-0.52).

Also caught x_boyrun3 and x_boyrun4 sitting at the old fidelity next to the two
frames of the same run I had already redrawn — my batch list only covered half
the cycle.

The roster now averages 0.341 inner-edge density across 111 character sprites,
against 0.237 for z_mach2. What is deliberately left alone: the chapter bosses,
the judge, the ball machine and the weapon icons — Oliver named those as the ones
that were already right.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 21:57:26 +02:00
wuwiandClaude Opus 5 79ff783033 Redraw the racket zombie's wounds, the umpire and the ball boys
Four more batches: the racket zombie's two wound stages with their stride twins,
the umpire and the ball boy's sprint, his mop-carry walk and his mopping cycle.
Detail roughly doubles on all of them.

Also stripped the last dozen key-colour pixels off p_die5's rim — edge pixels
only, because a racket's strings are legitimately pink and a blanket filter
would eat them.

The brute's wound stages and the chair spectator are re-running as smaller
sheets: four figures carrying a dragged pole or a held-up chair either merge
into one frame or run off the canvas whatever margin the prompt demands, so
they go two at a time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 21:52:34 +02:00
wuwiandClaude Opus 5 1253537737 Write the art-resolution lesson into the handoff
The prompt is what sets how fine the drawing is, and this session set it wrong
for eleven sheets before Oliver named it. That, plus what _regen.py and _fine.py
are for and the checks that stop a bad sheet reaching the repo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 21:48:20 +02:00
wuwiandClaude Opus 5 ff6515670f Redraw the player and the opponent
Both full sets through the same pipeline: stances and ball-bounce, serve toss,
smash and both swing frames, the four-frame run, and the five-frame death. The
opponent's ten frames with them. Inner-edge density roughly doubles across the
board — the serve frames go 0.22-0.32 to 0.41-0.54.

_regen.py retries a rejected batch itself now (default three attempts). A
rejection is usually just a bad roll — two figures drawn close enough that the
slicer sees one frame, or a limb off the canvas — and re-rolling fixes it without
me sitting in the loop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 21:47:29 +02:00
wuwiandClaude Opus 5 0c499b60b5 Automate the redraw, and run the first eight batches through it
assets/_regen.py plus a batch file: build a reference sheet from the sprites in
the repo, ask for a HIGH-resolution redraw, slice with _fine.py, colour-match
each frame onto the one it replaces, install. The point of the whole thing is the
style block — the old prompts specified coarse art and got it.

It refuses to install a bad sheet. Wrong frame count, any figure touching a
canvas edge (that is what clipped z_f1 in the first place), or a redraw that is
not measurably finer than what it replaces, and the batch is left alone with a
reason printed. Re-running a batch is the fix; three of the first eleven needed
it. The bar is 1.25x inner-edge density rather than 1.5x, because a sheet that
already has blood on it starts high and a genuinely finer wounded body still
scores a smaller gain.

Done so far: both shamblers and their wound skins, the brute's walk and its two
sets of wound stages, the steward and his wound skins, the turned CPU.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 21:43:58 +02:00
wuwiandClaude Opus 5 f2bf63d546 Redraw the runner at real pixel-art resolution
I had been measuring the wrong thing. The complaint is not the PNG's size or its
upscale factor, it is how fine the DRAWING is — how many pixels make up the
figure, and whether there is room in it for teeth, fingers, seams and folds. On
that, my own prompts were the cause: every sheet this session ordered "every art
pixel is a big square block about 8 screen pixels across" and "flat blocks of
colour, no shading", which is a specification for coarse art. The generator
followed it exactly.

New recipe, proven on the runner's four frames: ask for HIGH resolution — "build
the figure out of 90 to 110 pixels of height instead of 40" — and enumerate the
detail it should spend them on (teeth, sunken eyes, individual fingers, tracksuit
seams and side stripe, torn edges, ribs, laced shoes, four or five shades per
material with hatched shading). Inner-edge density went 0.15-0.22 to 0.43-0.49;
z_mach2, which Oliver holds up as the good one, sits at 0.24.

z_f1 has been clipped on its right edge since it was made — the generator ran the
figure off the sheet. The new sheets demand 150px of empty margin at every edge
and it is whole now.

The rest of the roster is still the coarse style and wants the same pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 21:29:41 +02:00
wuwiandClaude Opus 5 1e4370f530 Re-cut the half-resolution sprites at full detail
Audited every sprite by detecting the art-pixel BLOCK SIZE each one was actually
drawn at. The set splits cleanly in two: 29 sprites are 112 art-pixels tall on a
2-pixel block — literal 2x upscales, carrying half the detail — against 200-224
for everything drawn since. z_f3 and z_bw are in the first group, z_chairt and
z_mach2 in the second, exactly as Oliver called it.

Regenerated at full detail so far: the player's idle pair, toss and smash (he is
on screen the whole game, and his own run and death frames were already fine, so
he was mismatched against himself), plus the two wounded shamblers and the
runner's three frames — where the wound skins were the worse case, since their
newly drawn second frames were full-detail and their A frames were not.

New pipeline step for it, assets/_fine.py: detect the generator's art-pixel grid
and snap to it, common-scale in ART pixels off a chosen anchor frame (so a smash
with the arm overhead stays legitimately taller), quantise back to the sheet's
own palette, and de-fringe the key colour twice — the generator does not always
return a pure magenta and the halo survives as a rim on a green court.

Still half-resolution, in order of how much they are seen: the brute's seven
wound stages, the seven opponent frames, the racket zombie's two wound skins,
and x_ump / x_boyrun1 / x_boycarry2 / x_boycarry3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 21:03:47 +02:00
wuwiandClaude Opus 5 17ae73682b Draw the frames instead of faking them
Two of the same mistake, both called out.

The wounded feet still did not move because a mirror cannot move them: flipping
the legs swaps WHICH leg leads but keeps the two feet in the same two places, so
the spacing never changes and the body still slides. Measured it — mirrored feet
sat at -35/59 against the original's -34.5/60.5. What a walk actually needs is
the PASSING pose, feet together under the body, so the gap opens and closes.
Drawn now for all three wounded skins: 75->65, 95->68, 79->69 px between the
feet. (The mirror also clipped the steward's hanging arm into a floating
fragment, which is what Oliver spotted. Gone with it.)

The death was one sprite scaled down, which miniaturises a man rather than
killing him. It is five drawn frames now — arched back, knees, hands, face down
in the pool, a flat heap — each genuinely shorter than the last (100/84/60/47/20%
of standing height, common-scaled off one sheet) and each drawn at its own
height. No scaling anywhere in the sequence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 20:52:52 +02:00
wuwiandClaude Opus 5 0a7633b671 Give the wounded frames legs that actually move
The pair alternated but the feet stayed put: the generator had swapped the arms
and torso and redrawn the same legs. Measured it rather than squinting — the
lower third of the two wounded frames differed by 3.7% and 4.6%, where a healthy
walk pair differs by 60-70%. That is a body sliding, which is exactly what Oliver
saw.

Regenerated with the stride as the entire brief (33% for the shambler), and where
the generator would not move them, mirrored the leg band in place instead: a
side-on walk swaps its stride under a horizontal flip of the legs alone, same
pixels, same palette, no dice roll. The coated shambler and the steward are that
(28% and 31%).

Audited every wound pair the same way while I was in there — the racket zombie's
two stages and the brute were already fine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 20:42:12 +02:00