Commit Graph
100 Commits
Author SHA1 Message Date
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 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 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 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 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 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 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
wuwiandClaude Opus 5 4d6b47dadd Die the way people die in the films
The old death was a red tint and a shake for half a second, then a puff of gore.
That reads as the sprite having a moment, not as being eaten.

Now it takes four and a half seconds and it is a crowd doing it. You are dragged
to your knees fighting it (new frames, p_die1/p_die2, alternating), blood coming
out of you the whole time — a constant spray and a stain that keeps widening,
with the wet work on a loop underneath. Every body on court drops what it was
doing and converges: each claims a spot in a ring around you, kept from wherever
it was standing, so they crowd in from all sides instead of stacking on one
pixel. Then you are pressed flat into the ground while the ring closes over you —
you are drawn INSIDE the crowd's z order, so the ones between you and the camera
hide you exactly as they arrive — and what is left is the mound, with them still
feeding over it when the summary finally fades in.

Also: the wounded shamblers were stepping their two frames at half the cadence of
the healthy four, which reads as frozen next to the crowd they walk in. Same
clock now. (They were animating — verified frame by frame — just too slowly.)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 20:36:25 +02:00
wuwiandClaude Opus 5 b7899f122c Stop the brute pulsing, and let a boss die where you can see it
The pulse was drawStatic being told a height instead of reading one. _slice.py
common-scales a sheet, so a crouched frame is genuinely shorter — 211 px against
its neighbours' 224 — and forcing every frame to the same on-screen height
stretched it back up, so the body swelled and shrank once per stride. Worst on
the brute because it is the biggest and slowest. The rule already existed for the
wound stages; it now applies to every frame, once, which also settles the
shamblers and the steward (their cycles vary by up to 9%).

A chapter boss now dies in its own beat instead of under the card announcing it:
the world drops to a third speed for 1.2 s, the screen kicks, a shockwave ring
blows out through 90 extra particles and six stains, and the CHAPTER DOWN card
holds off until the pieces have landed — about 1.4 s, measured.

Cards can hold off in general now (`wait`). First cut only delayed the card's
countdown while still drawing it, which is worse than no delay: caught it in a
screenshot before it shipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 20:22:55 +02:00
wuwiandClaude Opus 5 7d307d90b9 Four frames for the chair spectator and the turned CPU
The last two bodies still shuffling on two frames. Both get their in-between
strides, generated against their own art and fitted per channel onto it.

The chair-down stagger was walk frame 3, which a four-frame cycle would have
walked straight through — it is its own sprite now (z_sdown), picked by state
rather than by where the cycle happens to be.

Also fixed a strobe that predates all of this: z_c2's racket was drawn BLUE
while every other frame of the same zombie carries a brown one, so it flashed
once per cycle. Remapped onto z_c1's racket palette.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 20:10:20 +02:00
wuwiandClaude Opus 5 661ad90be5 Let the ball machine roll, dent, and blow up
It was furniture in the most literal sense: bolted to one spot, three hits from
scrap, and when it died it bled and a ball boy came out to mop it.

- It trundles to a new spot every seven seconds or so, and CANNOT fire while it
  rolls. That is honest (a rolling cart cannot aim) and it is the window you use
  to close on it — but it also means it stops being a landmark you can ignore.
- Six hits, not three, and it wears them: dented at two thirds, split open and
  sparking at one third, smoking where it stands once it is that far gone.
- It comes apart as MACHINERY. No blood decal, no gore mound, no mop boy — a
  flash, sparks, torn metal and a column of dark smoke over a scorch mark. Smoke
  is a new particle flavour that rises and spreads instead of falling.

Damaged sheets keep the healthy cart's footprint: the generator drew the smoke
plume into the sprite, which would have shrunk the cart as it took damage, so
the plume is cropped off and the smoke is drawn procedurally where it can move.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 20:05:44 +02:00
wuwiandClaude Opus 5 1168e9ee5b Keep the wounded walking, and make the steward worth his size
A wound skin was a single still, so anything hurt froze on one frame and slid
across the court. Every skin has its …b twin now — the stride swapped, stepped
off the same clock as the healthy walk — for both shamblers, both racket-zombie
stages, and the steward, who had no wounded state at all.

The steward also died like a shambler at 3 hp, which made a bigger body read as
decoration. Five now, with a wound skin to show the second half of that. hpMax
is recorded on every body that has hp, so "hurt = show the wound" is one rule
instead of a per-variant constant to remember.

First pass at the wound frames was rejected on sight and deserved it: the
generator drew small figures with fine pixels on a GRADIENT background, and a
nearest-neighbour downscale to sprite height shredded the outlines. Regenerated
two at a time, drawn large, chunky pixels, flat background, hard black outline —
then fitted per channel onto each frame's own healthy twin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 19:57:45 +02:00
wuwiandClaude Opus 5 a7835d367c Stop bodies blinking on their way in
They spawn BEHIND the far baseline, at negative z, and the frames that step off
distance walked used a plain %, which keeps the sign of the dividend:
floor(-1.5 / 0.25) % 4 is -2, so the pick asked for 'z_g-1'. drawStatic draws
nothing for a sprite that does not exist, so the body flickered until it walked
past z=0 — worst on exactly the ones Oliver noticed, because only the heavy
bodies step off distance. The chapter boss had it too, spawning at z=-1.2 and
falling through to an undefined frame name.

One positive-modulo helper at the four picks. Verified across the whole approach
from z=-1.6: every frame resolves to a real sprite now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 19:47:38 +02:00
wuwiandClaude Opus 5 85e08ab4b8 Give the brute a four-frame lumber
Last body still walking on two frames, and the slowest thing on court, so the
lurch was the most visible. Same treatment as the player: the whole cycle
regenerated in one pass, then fitted per channel onto the mean and sd of the
frames that stay — the four wound skins — so a healthy brute and a wounded one
are the same colour (mean error about 1 of 255).

Its cycle steps off DISTANCE WALKED, not time, the same rule its wound lurch
already used: at brute pace a time-driven cycle is pure foot-slide. Four frames
over the same metre, so the cadence is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 19:05:31 +02:00
wuwiandClaude Opus 5 94136ddf8c Run the player on four frames instead of two
Two frames is a lurch, and he is on screen the whole run — during a horde he is
the only thing moving. Regenerated the whole cycle in one pass rather than
bolting two new frames onto the old pair, so the four are consistent with each
other, then fitted them per channel onto the mean and sd of the frames that
stay (idle + swing): mean error under 3 of 255, so he does not change colour
when he starts moving.

The pick is frame-count driven, not hardcoded — the opponent has no run3/run4
and stays on two, and the cycle RATE is unchanged either way: more frames
subdivide the stride, they do not speed it up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 19:02:15 +02:00
wuwiandClaude Opus 5 d62478e8b1 Put a steward on the court
A new ordinary body between the shambler and the brute: a stadium security
steward in a hi-vis vest, 2.15 m, three hp, a touch slower. Deliberately the
mildest thing in the advanced pool — no wound skins, no special rule to learn —
so he can unlock early (game 4) and thicken the ordinary crowd, which a mix of
two shamblers never did. A fifth of horde spawns come out as stewards too, so a
grown wave is not one body dying at one range.

Sheet generated against the existing shambler walk (assets/_sheet_dwalk.png),
sliced to a 4-frame cycle, so he shambles as smoothly as the crowd he walks in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 18:57:03 +02:00
wuwiandClaude Opus 5 614e1ee469 Use 100vh and the viewport-fit toggle for the installed app
Researched it instead of guessing again. Launched from the homescreen, WebKit
resolves the DYNAMIC viewport and every env() inset once against a provisional
geometry and does not revisit them until the geometry is "exercised" — a
rotation. That is the whole bug, and it also explains why listening for a signal
could never fix it: nothing fires, so there is nothing to observe.

Two known workarounds, both applied. Standalone has no toolbars, so 100vh is
correct AND stable from the first frame — dvh only exists for the browser's
disappearing toolbars, so it now applies only there. And toggling viewport-fit
off and back on at boot forces WebKit to recalculate, without the user rotating
anything.

Countdown moved up off the ad boards — the strip's own projected line is its
ceiling now, so it stays in the stands whatever the phone does to the layout,
with a 1px shadow to carry it over the crowd.

Sources: gist.github.com/fozzedout/5e77925381991a9570151550992baf14

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 09:32:36 +02:00
wuwiandClaude Opus 5 9a4d921706 Pin the canvas height to what is actually visible, and three fixes
Viewport, again — the homescreen app was the case that broke it. Watching for a
signal cannot work there: launched standalone, iOS resolves the CSS viewport once
against a provisional box and never re-resolves it, so no resize, no layout
change, nothing for a ResizeObserver to observe. Only a rotation forced the issue.
So the CSS box is now a proposal: visualViewport says what is really visible and
the height gets pinned in px to match, re-read on every fit — plus a short settle
salvo after launch, since that launch announces nothing.

- The countdown skipped the transition, where overT is parked at 9999 to hold the
  point flow. It read as RESUMING IN 9999 after the first game you ever win.
- Countdown moved off the HUD line into the open, at a readable size.
- A returned shot is a glancing blow now, not a mirror. Reversing the incoming
  line sent every parry straight back up it — into the thrower — so a boss handed
  you a free wound per volley. Contact across the racket face sets the angle:
  dead centre still goes back at him, which is a placement you earn.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 09:18:55 +02:00
wuwiandClaude Opus 5 e8116c6abb Re-fit the viewport whenever the canvas box moves
The portrait fit was right, it just ran once and too early: iOS Safari lays the
page out before its toolbars settle, so the box the first fit measured was short
by a strip — and no window resize follows, because the page never scrolls. The
height only came good after a rotate and back, which IS a resize. A
ResizeObserver on the canvas watches the box itself, so every cause is covered
(bars settling, rotation, split view, zoom) without guessing which event or delay
to listen for. It only rewrites the backing store, never the CSS box, so it
cannot feed itself.

Verified both directions: box grows -> logical H 585 -> 671, box back -> 585,
logical aspect matching the box aspect each time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 08:49:02 +02:00
wuwi 47d960e4ea Reapply "Sit the weapon rack on the bottom edge"
This reverts commit 22d1bf5443.
2026-07-31 08:34:54 +02:00
wuwi 22d1bf5443 Revert "Sit the weapon rack on the bottom edge"
This reverts commit 777f4b0a23.
2026-07-31 08:33:07 +02:00
wuwiandClaude Opus 5 777f4b0a23 Sit the weapon rack on the bottom edge
The 8px of breathing room under it read as padding, not as breathing room.
SAFE.bottom still lifts the rack clear of the home indicator — that gap is
the one that has to be there. The desktop key hint lived in the band that
just disappeared, so it moves to the empty top-right corner.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 08:29:56 +02:00
wuwiandClaude Opus 5 e904dfefa6 Let the horde gun keep up with the horde
The run ended on the second wave every time: the gun fired 6 flat rounds a
second, hit one body each, and every shambler soaked one — while hordeGrow
made each wave longer and denser. Doubled the rate and scaled it with the
wave (14/s at one set banked, 21/s at four), gave a round 0.5m of extra
reach, a wider cone, and enough damage to drop a shambler outright. Raised
the spawn rate to match, so surviving a wave still leaves a standing crowd
on your half — that pressure is the point of it.

Also: he no longer carries a second racket at his hip while holding one
forward (the run frames are the racket-less art), the umpire stopped talking
over himself and saying "remain seated" twice, and a timed freeze now shows
what it is counting down to instead of reading as dead controls.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 08:07:35 +02:00
wuwiandClaude Opus 5 571cd4c399 Undo the bad quad, and paint the page background where it shows
The triangle was mine. Extending the out-area quad to z=60 put it past camZ
(48), and proj() divides by (camZ - z) — so the scale explodes at the camera
plane and goes negative beyond it, folding the quad's far edge back above the
screen. The quad goes back to z=30; the full-frame clear is what covers the
screen, so it never needed extending in the first place.

The bottom still not being green was a second bug, in the previous commit. The
gradient was set on body, but the ROOT element's background is what paints the
viewport, and index.html gives html a background too — so body's never
propagated and painted only its own box, which has no height because the canvas
is position:fixed. It goes on documentElement now, and html gets an explicit
height, because a gradient is laid out in the root's own box and that box
collapses for the same reason.

Verified with the canvas deliberately ending 40px short: root box fills the
viewport, gradient's lower stop is rgb(41,92,45), the canvas's bottom row is
the same, and the seam is invisible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 19:33:12 +02:00
wuwiandClaude Opus 5 c9699da6f1 Clear the frame to the apron colour
That band at the bottom was not a background mismatch — the canvas was
transparent there. render() never cleared: the stands cover the top and the
out-area quad covers the middle, so it looked complete, but the quad only
reached z=30 and everything below where that projects was see-through. The
page showed through it, which is why it read as a slightly duller green.

The frame is now cleared to pal.out before anything draws, oversized by 8px so
a camera shake cannot expose an edge, and the out-area quad runs to z=60 so the
perspective floor reaches the bottom on any aspect. Verified by sampling alpha
down the bottom third: opaque everywhere, one uniform colour, matching the page
gradient behind it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 19:22:23 +02:00
wuwiandClaude Opus 5 7b15e10494 Carry the court colour into the page background
A phone's rounded display corners clip slivers out of the canvas, and the flat
dark body showed through them as a dark notch where the bottom corners curve in.
The page background is a two-stop gradient now: dark down to the top safe-area
inset, where it sits behind the status bar, then the court's out-area colour for
everything below.

That colour is read live from the palette rather than hardcoded, because it
lerps between the tennis and splatter courts — bright green to dark olive — so
a fixed hex would only match in one mode. Written in resolved pixels rather
than env() so the declaration can't be invalid and drop the background
entirely, and only assigned when the string actually changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 19:15:43 +02:00
wuwiandClaude Opus 5 c601035eab Run the art to the bottom edge, lift only the rack
The remaining gap was the home-indicator inset being reserved on the canvas
element. Unlike the status bar that is a slim translucent bar, not opaque UI,
so the canvas now runs to the physical bottom edge and the court art fills it —
only the weapon rack and the draw meter lift clear of it, through SAFE.bottom.
The top keeps its element inset, since the scoreboard genuinely has to clear
the status bar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 18:37:09 +02:00