1fccc11b1ddafd345d452d631e65a4d864633598
81
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1fccc11b1d |
Name the opponent, and stop making phones squint
The transition turns your opponent into a zombie with variant 'c', and the roster table had no row for it — so the stats screen fell back to the raw key and the shambler sprite, and the list appeared to hold two shamblers. He is THE OPPONENT now, and he is not the racket zombie, who keeps coming back. Type across the menus was sized on a desktop and read tiny on a real phone: tabs, opponent names, counts and labels all go up a step, and the roster sprites go from 34x46 to 48x62. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f3b0b791d6 |
Trade the tutorial for a screen that counts the dead
The menus were placeholders next to the art. Three screens now: a title over an empty, dimmed, blood-stained court; a MATCH SETUP screen where each court wears meters read straight off its own bounce numbers; and BODY COUNT, which needed the game to start measuring anything at all. stats.js keeps one capped array of per-run records — all time, last match and seven days are three filters over it rather than three counters that can drift apart. Kills file under the body AND the weapon: killZombie only ever knew an index, so every damage loop stamps the cause on its way past. The tutorial is gone. Marvin explained what the in-game modals already teach, and his stage is where the stats live now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0ca43bf68f |
Let the ball follow the mirror, and stop the serve shrinking her
Four things Oliver caught on the phone. The screen title is the ANNOUNCEMENT face now. A menu heading is a thing that shouts, the same register the canvas gives SET and CHAPTER — it was wearing the HUD font, which is the voice for scores and key hints. Bangers carries its own weight, so it takes size and air instead of Silkscreen's letter-spacing, plus the padding under it he asked for. Her toss and smash were shrinking the whole woman. The slice anchors frame 0 to 224 px, and frame 0 was the toss — whose arm is straight up, so the BODY got squeezed to make room for it. This is the trap already written down for the raised racket; it just moved to a different sheet. Re-sliced the retained sheet at the men's toss height (250) instead, so toss/smash/pull come out 250/293/191 against his 250/320/213 and her body matches her own idle again. No re-roll. The pre-serve ball did not follow the MIRROR. drawChar flips the whole sprite when a server faces left, so dodging during the bounce swapped his hands over and left the ball hanging out by the racket — one `* sv.facing` on the offset. It was wrong in the men's world the whole time, it just took a second body standing next to it before anyone saw it. And she reaches further out and higher than he does, so BOUNCE_IDLE's pair is overridable per tour now (0.46/0.78 against his 0.36/0.68). Measured off the drawn hand by eye, which is the only way those two numbers have ever been set. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
14a1e5d261 |
Let the tour be a choice, and give her a body of her own
The court screen picks WHO as well as WHERE now: two tiles above the court rows, men's or women's singles, remembered like the court is. No new stage — three rows and a pair of buttons already fill a phone, so the tiles go side by side and the tap count from splash to play does not change. Everything downstream is one line: drawChar already built every frame name off a single prefix, so SK() appends '_f' when the women's tour is picked and falls straight through to the men's frame for anything without a twin. The horde, the officials and the ball boys are shared — they are staff, not the draw — but the turned CPU is not: a male corpse opposite a female player reads as a bug. Her frames came back as a different woman in every batch — blonde in one, auburn in the next, greyscale line art in a third — because each was anchored on the MAN whose pose it was. "newart" repaints the character and "state" is welded to the corpse palette, so neither could say "the same living woman, a new pose". That mode now exists, and every batch anchors on her own settled frame. The death is drawn to frame 3 and recoloured after it. The generator will not draw a dead woman in any wording worth using; frames 4 and 5 survive that because by then she is flat and then a heap of cloth, so they are the men's drawing with the blue shorts painted into her white (see assets/_die_f.py). The collapse still falls 224/159/117/103/43 straight through the handoff. Her first bounce frames reached to the shoulder, which is a catching pose, not patting a ball — redrawn low rather than bending BOUNCE_IDLE to fit a wrong drawing, so both tours share one apex and the ball meets her palm. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ebce08199f |
Turn the praise down, and stop the footnotes shouting
The band made PERFECT earnable, which was the fix — but you learn a band, and then the HUD is talking on every other serve again whatever the word says. So the praise goes behind a shared cooldown (TUNE.praiseEvery, 14s), the same model the umpire's asides have used all along for exactly this problem. Eight band-perfect serves back to back now yield one word. The bright click in the pull is the reliable feedback for a good draw; the word was only ever garnish, and garnish on every plate is just food. And the free-floating slot drops 24 -> 18. It shares that slot with CLEAR THE COURT, DOUBLE KILL, RETURNED and GAME N — half of which are a footnote to something you already watched happen. Sharing the loud FACE with the milestone cards is right; sharing their SIZE made every footnote read as a milestone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9c7ee42bfa |
Earn the praise, and give the quiet face a shadow too
PERFECT fired on almost every serve, and it was not a threshold that had drifted — it was the wrong test entirely. pullFrac CLAMPS at pullFull, so pullQuality is exactly 1.0 for any pull of 130 px or more, and 130 px is an ordinary comfortable thumb drag. "quality > 0.999" therefore meant "you pulled far enough", which is not an achievement and is why it read as noise. So hitServe takes `perfect` as its own argument now. The toss ritual keeps earning it the honest way, on the 45 ms perfectMs window. The slingshot earns it by STOPPING in a band at the top of the draw (pullFull..pullFull*perfectBand, 130-166 px) — a target you can miss, rather than a ceiling you cannot. The pull already clicks when it crosses the splatter threshold; it clicks brighter when it enters the band and goes silent when you pull past, so the window is learnable by feel instead of being invisible skill. And the word rotates: PERFECT, TEXTBOOK, IMMACULATE, OH LOVELY, UNPLAYABLE, SHOT OF THE DAY. Cycled rather than random — the same word twice running is what makes one read as a stuck HUD element rather than as praise. The umpire has still never acknowledged the massacre, so a clean shot gets the line it would get at Wimbledon. Also: the card band grew 72 -> 80 px, because the second sub line ended 4 px off the bottom edge and looked like it had fallen out. And the quiet face floats as much as the loud one does, so it gets the same 1 px shadow — the control hints, the gesture-demo labels, the card subs, the modal body and prompt, the death summary lines. The scoreboard and the rack keep theirs off: both already sit on dark panels, and a shadow under 6 px type on a flat fill only smears it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
55c0f75c37 |
Give him a sidestep, and split the messages into two voices
He died at every chapter boss because he could not move, and there were two independent reasons for it. The movement block is 'rally'-only — a settled feel decision the six boss AI styles were written around — and a boss is fought from 'pretoss'. On top of that canAimServe() is true through a boss fight, so his one finger was already spent on the slingshot: there was no gesture left over that could have meant "move". So it is a SIDESTEP, not a walk. One committed step on a cooldown, carrying invulnerability that beats both damage channels — the chip damage in hurtPlayer and the instant-death contact test. The rule about walking stands untouched. The gesture is picked by WHICH WAY the drag leaves the deadzone, decided once and never revisited for that touch: down-ish is the pull the game already teaches, sideways is the step. No timing window, no invisible mode, so "one gesture means one thing" still holds. The finger-steer modes are untouched — stick.aimable is false in a horde, so the drag is the absolute steer it always was. The apron demo now alternates PULL DOWN and SWIPE TO DODGE, and Marvin has a page for it. The rest of the playtest list: - Bangers is bundled beside Silkscreen, and the register IS the category. The quiet face keeps the HUD, the rack, the control hints and RESUMING IN; the loud one takes SET, CHAPTER, EPIC!, PERFECT and every free-floating flash. Floating text gets one shared y and a drop shadow, so it survives crowd, clay and blood. - Modals queue instead of being dropped. That was why the trainer and the weapon had to be crammed into one card after a won duel; the heal gets its own popup with the x_cross tin over it — a sprite loaded since that merge and drawn by nothing. - weaponModal's first-sight gate moved inside the function. The barrels gated themselves and the tin ladder did not, so four weapons re-explained themselves every three games while a barrel weapon explained itself once. - Ten rack slots. Past five it grows a second row upward rather than shrinking ten tiles under the thumb target; slots 0-4 stay welded to the bottom edge. - The boss win holds for its own 3.6s and says EPIC!, with no GAME n NEXT line — the bar under the scoreboard already prints that permanently. That bar went from 6px to 7px, which is the smallest type in the game becoming words. - The turn punches the camera in on him and the ground shakes harder at each beat. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
99774ab4be |
Shape the cone by hand, and give the playground its knobs back
Oliver's tuned numbers become the defaults: beamY 0.95, beamLen 23, beamW0 0.08, beamW1 3.1, beamOriginLift 0.44, beamBorder 0.18. Three knobs earned their keep on the way there. beamLen is the DRAWN length now, not only the kill range. The drawn length was solved every frame so the far end landed on the middle of the ad strip, which made this a control that visibly did nothing. One meaning beats one clever behaviour -- and it pairs with beamY, since a lower plane meets the ads sooner. beamOriginLift replaces beamOriginY: the apex is a lift ABOVE the plane rather than an absolute height, so lowering beamY brings the near end down with it instead of leaving it hanging at chest height with the beam sloping away. beamBorder exposes the edge width, which was hardcoded. And the playground is back, deliberately NOT paused. The first version froze the world so the picture would hold still, and then nothing redrew, so every value changed did nothing -- a still frame you cannot change is worth nothing. The bodies are held still individually and spawning is off instead. Also: a boolean living in TUNE has to be tested with >= 0.5. The panel steps every key by 0.02, so one nudge set beamBand to 0.02 -- still truthy -- and the renderer switched variants under him mid-tune. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ac1d5fbccd |
Draw the laser like the ball's aim band, and let the two be compared
TUNE.beamBand flips between the two looks live from the T panel. 1, the new default, draws the laser the way the BALL's aim trajectory is drawn: a path down the lane whose width comes from each point's own projection scale, so it narrows into the distance like everything else on the court, with a shadow on the lawn to pin it there. 0 is the solid widening cone. The cone was a true projection, which is why it was not obviously wrong to me -- but it GROWS in world width, so on screen it opens out while the court recedes, and the eye reads that as a shape stuck to the screen rather than a plane lying in the world. The kill follows whichever look is on, in both fireBeam and drawBeamCuts. Otherwise the comparison would be a lie: a narrow band that deletes a cone-wide crowd tells you nothing about how the narrow band plays. Measured down one lane, the cone takes 9 of 9 and the band 5 of 9. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8f16d085f0 |
Stop bodies blinking out, and give the cone a core and edges
The blinking was drawZombie validating its frame count against the LAST frame only: a set with (say) z_h2 missing but z_h4 present kept N = 4, the walk cycle stepped onto a name that had never loaded, and drawStatic silently drew nothing. The body vanished for that frame of the cycle and came back on the next one, forever. One cached 404 -- this project's oldest trap -- is enough to cause it. It holds frame one now: a body that stops animating is a bug you can see, one that flickers in and out reads as the game breaking. SPRV bumped to flush whatever his phone has cached. And no ctx.filter anywhere. The cut line was briefly the sprite redrawn clipped to a band and flattened by a canvas filter, which put it exactly on the silhouette -- and on the iPhone the line did not draw at all. Unsupported or leaking past its restore, a rendering feature that fails silently on the target device is not worth a pixel of polish. It is a plain bar again, brighter, and it crosses the odd gap between the legs, which is the trade. The cone gets the reference's look: a horizontal gradient across the width with a near-white core down the centre line falling to blue at the edges, plus the depth falloff along its length, plus stroked outer edges. Canvas has no 2D gradient, so it is filled in eight slices each carrying its own across-gradient -- enough that the seams vanish, which is what the old nested wedges got wrong. Test mode: barrels gone (the rack already holds everything they would give you) and the crowd raised to 16 bodies at a spawn every 0.9s, so it does not run dry between kills. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
337def8526 |
Cut them with the laser plane instead of painting over them
The beam is a real plane now, holding TUNE.beamY (1.15 m) all the way out instead of sloping to the ground. That one change is what makes the rest possible: sloping, it lay flat on the grass across the whole far half, so there was nothing to cut exactly where the crowd stands. One flat translucent triangle, apex at the player's CHEST, opening out down the court with the alpha falling away. It was four nested wedges composited 'lighter' and that layering WAS the banding -- additive light blowing out to white where the layers overlapped. Flat alpha also means a body under the plane is tinted rather than brightened, which is what lets the cut read at all. Bodies are sliced by it: the part above the plane is redrawn clipped so it sits on top, the part below stays tinted underneath, and it happens while AIMING as well as on the shot. Measured, the plane takes a shambler at 64% of its height, a brute at 43% and the roller man at 36% -- bigger bodies visibly take it lower. The cut line follows the SPRITE rather than a rectangle. A fillRect spanned the body's bounding width, so it crossed the gaps between the legs and under a held-out arm and read as a bar lying behind him; the line is the sprite itself, redrawn clipped to a thin band and flattened to one colour by ctx.filter. It thins with distance on s squared, because this camera's scale only spans about 1.5x across the court and a linear falloff was invisible. Aiming a filter at a specific colour turned out to be a measurement, not a guess: brightness(0) invert(1) goes to WHITE, and sepia adds almost no chroma to white, so the first chain rendered a grey. Leaving a mid grey for sepia to tint lands on (32,154,255) against the laser icon's (29,153,232). The probe fills a rect and reads the pixel back. The pass is bounded to the eight nearest bodies above 14 px, because it draws each one twice more through a filter and runs every frame you are aiming. 0.27 ms/frame with 20 bodies on desktop, against a 16.7 ms budget. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6e88fd5bb9 |
Burn them to ashes, and give the burst back its bar
A burnt body does not burst. Fire kills by cooking, so there is nothing left to spray: a body that dies alight -- flamethrower or flame ball -- leaves dark grey ash instead of blood. Same decal, mound and particles, recoloured and smaller, with a puff of smoke and a dull thud instead of the wet splat and the headshot crack. The flame BALL has to mark itself, because that kill lands before the body ever catches. The flamethrower is a lighter, not a gun: no impact damage at all, it pierces, and it only IGNITES -- the burn does the killing, so a lit crowd walks a few steps and goes down together. 26 m of reach against a 22.8 m court, so it crosses to the far baseline. The guard matters: hp -= 0 fell straight through to killZombie, which made the cone delete the crowd on contact. Every burst weapon had no progress bar and the cause was the spend. Arming spent the tin at once, so the rack entry vanished -- and the bar draws on the slot holding G.gunId, which no longer existed. The tin is spent when the burst ENDS now, so icon, count and bar are all there while it runs, and every path that ends a burst routes through endBurst or it would hand out free ammo. Auto-fire movement again: the previous fix read moving as mx||mz||assist, and finger-steering zeroes mx/mz, so sliding left and right still counted as standing still. And the oval on his back was the mg racket -- its head projected almost exactly onto the sprite; offset by facing it reads as held out beside him. The laser's far edge is pinned in SCREEN space to the middle of the ad strip rather than running out to beamLen on the ground, which landed on their top line. beamLen goes back to being the kill range instead of a number tuned to one camera. Beacon thumps, rocket swooshes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
91053b4c35 |
Fire the laser as light, and stop him jogging on the spot
The auto-fire jog was one guard. gameplay skipped the pose update in mgSteer mode, so the pose stuck on 'idle' -- and render covered for it by faking a run cycle off G.idleT, which animated whether he was moving or not. That fake is what read as "this hectic run animation... odd all the time". The pose follows MOVEMENT in every mode now and the fake is deleted, so auto-fire, the horde and the melee racket all use the ordinary run animation and standing still is standing still. The comment that justified the fake -- two rackets when idle -- was solving a smaller problem than it caused; at sprite scale the overlap does not read. The laser, second pass, all of it "it must read as light he fired": - it draws AFTER the net and every body. It was drawn before the far-side pass, so the net and the crowd painted over it, which is exactly why it looked like it was lying on the floor and sitting under the net. - beamLen 26 -> 34: at 26 it stopped just past the far baseline and read as cut off in mid-court. It carries into the courtside ads now. - beamW0 0.35 -> 0.06, a POINT at the strings. At 0.35 it left the racket as a 0.7 m bar hanging in mid-air instead of something emitted from it. - it DELETES ordinary bodies, hp irrelevant. A plane of light that left a roller man standing made it a worse ball, not a weapon. A boss is the exception and takes dmgBeam 8 -- severe, survivable. - four nested wedges composited 'lighter' (violet bloom, blue, cyan, white core), each fading along its length. One quad with a gradient was a coloured shape. Rocket: speed 17 -> 38, and a real jet plume laid tight ON the flight line rather than scattered sparks, which looked like a firework. Beacon: a finer one-pixel thread and a much brighter ball, so the BALL is the bright thing and the trail only says where it has been. And the bug in the middle of that: firePellet stamps a whitelist of fields onto the round, and jet/trailR went onto the WEAPONS rows without being added to the list -- so the rocket flew with no plume and nothing errored. Any new pellet field has to be added in both places. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
46e268973d |
Triple-tap the scoreboard for a sandbox
A test mode for trying the new content without playing twenty games to reach it. Three taps on the scoreboard toggles it, three more turns it off, and a single tap is still the miss-log overlay. Same shape as the umpire triple-tap cheat it sits next to. It OVERRIDES the difficulty director rather than editing it, so the wave is the same whether you opened it in game 1 or game 40 and the run keeps running underneath: 8 bodies, a fixed 1.15 m/s shamble, a spawn every 1.6 s, and pickVariant pretending to be at game 20 so every body type can turn up. All four barrel weapons sit on the rack and are topped back up each frame, because committing a burst spends the whole tin and a sandbox that runs dry is not one. A barrel rolls out every 14 s so the pickup itself is testable too. The third tap must NOT toggle logShow: taps one and two already toggled it twice and it is back where it started, so toggling again left the miss-log overlay switched on behind you every single time. Also records the trap that cost a debugging round getting here: index.html loads main.js?v=N, and fetching 'main.js' does not bust that URL -- so main.js stayed stale through a reload while every other module was fresh, and the tap handler appeared to do nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
282f5145a9 |
Make the pink weapons throw themselves, and let the big bodies soak
Oliver's playtest of the barrel weapons. Four notes, all four real defects. "The bigger guys were way too easy to kill at the beginning." A bug, and an old one. The ball-vs-body chain handled boss, g/m, s, f and a/b, and everything NOT named in it fell through to "splat-quality kills outright" -- so the STEWARD has died to one serve since the day he shipped with stewardHp 5 and a comment calling it a brute's worth of hp, and the three new bodies inherited that the moment they existed. Pellets and melee always read hp; only the ball skipped it. One generic hpMax > 1 branch now, and measured after: shambler 2, steward 5, brute 5, ball boy 3, bouncer 5, roller man 7 -- each exactly its hp. "I have seen all of them by round 10." The new three move 3/8/15 -> 5/12/20. The original four keep their gates: that curve was already tuned through game 12. "The laser fired only when I tapped, that was confusing." The whole tap-per-round kind is gone. A barrel weapon is now either the normal ball gesture made spectacular or something that fires itself -- nothing waits for a tap. - laser is a serve, and the RAY is the weapon: no ball anywhere in it. The preview IS the wedge, it leaves the RACKET rather than lying on the grass, the release erases the plane along its own path behind a bright front, and the edges are hairlines instead of the heaviest lines on the court. - beacon and rocket are LOBBED bursts -- `arc` turns a flat pellet into a thrown one, solved onto the nearest body ahead, and landing is itself a hit so a rocket that misses still goes off where it lands. The beacon changes colour every ball and drags that colour behind it. - flamer got "way more pronounced": 30 rounds/s, a ~10 m cone, embers. "I got the laser almost every time." A flat 1-in-4 over four items does not feel uniform. BARREL_WEAPONS carries weight and a `from` gate now, never repeats twice running, and the beacon is what you meet first. Measured: laser 16-28%, rocket unlocked at game 12 so it is genuinely a late find. And the barrel keeps its pink halo -- that was always procedural, painted by drawBarrel, and never went near the chroma key. Three rings and a pool on the lawn now, because one flat disc read as a smudge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
25f915f236 |
Fill the rack yourself, and roll barrels out that fill it
The rack had five hardcoded slots where the index WAS the weapon, spelled out in
five places: SLOTS, slotStock, WDESC, drawRack's three parallel arrays,
deliverTin's w+1, and bare wsel === 1|2|3 / G.weapon === 4 tests across five
files. config.WEAPONS is the one table now, G.rack holds up to four {id, n} in
the order you GATHERED them, and slot 0 stays BALL. Everything reads through
slotId/slotStock/slotEntry, so nothing else knows how the rack is stored.
A fifth weapon is always allowed, per Oliver: it rides slot 0 as G.temp until
spent, then slot 0 goes back to the infinite ball. A barrel arms on the spot and
is spent quickly, so refusing it would mean a barrel you shot did nothing. And a
spent weapon LEAVES its slot -- with gathered slots, an exhausted weapon holding
one is a quarter of the rack you can never pick up into. (The old rack kept its
empties dimmed, which was right when a slot meant "BOMB goes here".)
Four new weapons, and they are all the ONE existing pellet loop. firePellet(w)
stamps the WEAPONS row onto the round -- r, dmg, hp, pierce, boom, ignite, col,
speed, life -- so the beacon cannon, flamethrower, laser and rocket needed no new
array and no new update; the break after the first body was the only thing making
a pellet single-target. All four go through the same isBoss branch, which is why
none can silently no-op on a boss the way three of five once did. Measured:
gun/beacon/laser/rocket reach past 16 m, the flamer is a ~6 m cone that ignites.
G.mgT stays the mode timer for all of them, so the eighteen !(G.mgT > 0) guards
across five files did not have to change. kind:'burst' auto-fires at its own rate
for len seconds; kind:'shot' holds the same braced stance and fires one per tap.
The barrels reuse the ball machine's shape: a rolling, shootable non-body. All
three of the registrations that fail silently are done -- blocksClear (loot must
not gate the hand-clear), the contact guard (furniture cannot eat you), and
hitR/GORE_MUL/zombieH. It scores nothing.
The pink had to become procedural. The chroma key deletes every pixel where red
and blue both beat green by 40, so a magenta drum and a magenta laser pistol both
came back as hollow outlines -- 19% and 15% solid against 50-56% for the icons in
other colours. The drum is grey steel with hazard chevrons and every bit of its
pink is the halo drawBarrel paints; the laser went electric blue. Both are solid
now (92% and 58%), and the rule is written down in HANDOFF.
tinTriples 3 -> 10 as asked, and the four dead TUNE ammo constants are gone --
nothing read them once the table landed, which is a panel value you can tune all
day for no effect.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
ae64daa0fb |
Fill the screen, and let the swish keep up with a late horde
The portrait canvas is adaptive now. A fixed 270x480 is 9:16 while a modern iPhone is taller, so width bound the fit and ~150 CSS px of screen went unused at the bottom. Height is derived from the device aspect, and TUNE.horizon rides with it — proj() places the court in absolute pixels, so leaving horizon fixed just grew a dead green band between the court and the rack instead of moving the court down. RES 3 to 4 for the extra pixels: an iPhone gives ~1125 device px across and we were rendering 810. That is the first knob to turn back down if the phone stutters. The swish cooldown scales with how swarmed you are — 1.00s at one body, 0.47 at four, 0.22 from fourteen. Oliver died on every game-20 horde because the wave grows with hordeGrow while the panic button stayed a flat second; he chose scaling the melee over shrinking the horde. The first version measured the crowd after meleeSwipe had already killed it, so it counted survivors and barely moved. Winning a duel while wounded lost the weapon popup entirely: showModal never clobbers an open modal, so the heal modal opened first and the tin's was dropped. One popup carries both now. And the mop boy stays off the court unless a stain actually passes the test mopRound uses to pick a target. He was walking on, finding nothing reachable, and walking straight off again. Tennis untouched: 50 hits / 0 whiffs. 45-game soak clean, all four chapters. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
49d391dc64 |
Make it readable: 2x sprites, a flatter camera, honest hit timing
The umpire is situational again. Each line is tied to the thing that justifies it — a new variant appearing, the court filling, one crossing your baseline, a game lost, a horde, a changeover — behind one shared cooldown, so several triggers at once still yield one remark. The random timer pool is gone, the interval went 26s to 55s, and ump_out_of_order, ump_let and ump_no_appeal are deleted. "Visually the ball is not yet at the player but somehow the player already is hit" was real and measurable. The hostile-ball hit test reused the parry's hypot(dx, dz*0.8), and weighting depth shrinks it: contact fired while the ball was still 0.68 m up-court and drawn up around his head. It has its own unweighted test now, and contact lands at 0.25 m with the ball on his chest. The parry keeps its weighting — a racket sweep really is wider than it is deep. On "everything is a tad too small": measured first, because higher-resolution sprites are only half the answer. At RES 3 the player rasterises into 170 device pixels from a 112px source — upscaled 1.5x — while a far shambler is downscaled to 78. So near bodies needed resolution and far bodies needed the camera. Camera: pushed back and lengthened (camZ 42->48, camF 580->760) to flatten the perspective so distant bodies grow relative to near ones rather than everything zooming, camY raised for a steeper look-down, horizon re-centred. Far shambler 26->30px, boss 39->45px, player unchanged, court in the same band. Sprites: the original sheets were still in the repo at full resolution, so nothing needed regenerating — IDLE_TARGET_H went to 224 and every sheet was re-sliced. The sheet-to-output mapping was never recorded, so it was recovered by matching sliced frames to existing assets by image signature: 58 came back from sheets, the 29 without a confident match were 2x-upscaled to stay on the shared scale, since a sprite off that scale renders at half height. Tennis untouched: 50 hits / 0 whiffs. 45-game soak clean, all four chapters. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8bc57e9358 |
Tighten the returns, give each chapter its own boss, and let the umpire talk
Five reports from Oliver's second playtest. The parry was too generous — at 6 m of arm with a 0.34 s window the shot flew into reach on its own, so any tap once it was vaguely close worked. 3.4 m / 0.2 s / 1.7 m reach now: a tap at 4.5 m is rejected and costs blood, 3.2 m and in returns. The boss telegraphs in 0.5 s against the machine's 0.9 and throws harder, because a boss should be worse than furniture. His three balls were decoration: fanned ±1.9 m across space, so only the middle one could ever reach you. They are a timed salvo now — three shots 0.42 s apart, each aimed at where you are at that moment. hurtGrace still caps a whole volley at one point of damage, which is what it is for. Every chapter was the same boss. Three designs cycling now, each with its own sheet, stride, throw pose, wrecked skin and wrecked-throw skin: the GROUNDSKEEPER, the LINE JUDGE, the CHAIR UMPIRE with his chair strapped on. The chapter card names them. Designs are data in BOSS_ART, so a fourth is a sheet plus a row. Healing announces itself with a first-aid tin and a red cross instead of a 1.4 s flash. And 14 new umpire lines: ten sarcastic asides on a jittered timer, four hooked to specific beats. He still has not acknowledged that the match became a massacre, so he keeps officiating it. Two traps found and written into the pipeline notes. Sprite loads now carry ?v=SPRV, because python http.server sends no cache headers and the browser caches 404s — a name added before its PNG exists stays permanently missing for that browser. And a scripted replace that silently didn't match left drawZombie on the old hardcoded boss names while everything else moved to BOSS_ART; the built-in fallback hid it perfectly, all four designs rendering as design 1 with no error. Assert your rewiring edits matched. Tennis untouched: 30 tapped points, 50 hits / 0 whiffs. 45-game soak clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
295470b73c |
Make the parry reachable, and stop a boss dying to one flame ball
Six reports from Oliver's first playtest of the boss build. Two mattered. A boss sprite turned up in the shambler crowd because z_B1.png and z_b1.png are the same file on a case-insensitive filesystem — writing the boss frames silently overwrote the 'b' shambler's first two frames, and `ls` only ever showed the original lowercase name. Restored from HEAD~1, boss frames renamed z_boss*, and the rule is now in the asset pipeline notes. The parry was unreachable, which killed the entire new mechanic: P.swingT is only set in keyPressed's `phase === 'rally'` branch, and splatter sits in pretoss almost all the time, so every tap aimed at an incoming shot tossed the serve ball instead of swinging. An incoming shot inside parryArm now owns the tap in every phase, with its own generous reach and window — this is a phone, one thumb, against something aimed at you. The three fail cases (too early, too late, no tap) are what keep it a timing skill. The swish no longer eats shots: it runs first in keyPressed and was swallowing the very ball the parry was about to return. One flame ball killed a 16 hp boss, and that was a bug rather than tuning. A flame ball is not consumed on contact (full pierce, so it can cut a lane) but a boss is not removed when hit, so the same ball re-wounded it every frame. Boss bodies now take a contact cooldown and flame spends itself on them. Chapter 1 costs 4 flame balls or 16 clean smashes, and bossHp goes 10 to 16. The boss had no wounded state while throwing — correct, the wound skins were standing poses only. New sheet gives all four damage states their own overarm pitch. The ball machine's four painted rects are replaced with a real sheet, idle and firing, keeping only the muzzle glow procedural because the telegraph has to read at 20px tall. And the player's blood no longer sits on him as a painted layer: live drip and the screen-edge wash, nothing else. Tennis untouched: 30 tapped points, 50 hits / 0 whiffs. 45-game soak clean, all four chapters firing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4a066e5cc6 |
Add chapter bosses and a second damage channel; demote the racket zombie
Steps 4 and 5 of the late-game depth plan. The ranged channel. A rogue ball machine gets wheeled onto the far court every four games: it never walks, never crosses your baseline and never scores, so it is a standing threat you choose when to deal with — the target-priority decision the game was missing. Its shot is a real arc solved like a serve, not a flat bolt, so it reads coming in and the swing window has something to meet. Three outcomes: a swing inside the volley window returns it as your own ball flying up-court (an extras clone, so "a parry is a weapon" cost nothing to build), the panic swish bats it away, and letting it reach you costs blood. The swish path is load-bearing, not politeness: in pretoss the player is rooted and cannot swing at all. Contact still kills outright — that stays a separate channel, and killPlayer() is now the single death path for both causes so the run summary and the best-run write cannot drift. Health is drawn on the man: procedural spatter at fixed anchors, a live trickle, and a red wash from the screen edges. No bar. Healing is winning a duel, which finally gives the duel a reason to be wanted. Chapter bosses every ten games, with their own sheet — a colossal zombie groundskeeper hauling a ball hopper, coming apart through arm-off, ribs-out and headless-still-throwing. Chapter 1 is 10 hp and 2.7 m, chapter 4 is 34 hp and 3.67 m. It outranks the set and duel beats at a clear, because a boss walking into a standing wave is unreadable. And it fixes the economy: a chapter-4 boss costs 12 bombs or 100 rounds or 34 side balls against a per-type cap of 9, so no single weapon can take one and a banked mixed arsenal is mandatory. The racket zombie is demoted to an occasional opponent, as asked — appearing every won game is exactly why he read as the boss. Five silent bugs found on the way, all in HANDOFF: a blast deleted a 34 hp boss outright (isBoss() exists to stop that class recurring), the machine's hp did nothing because it fell through to the one-shot fallback, the boss both scored and fired its card, the machine gated the hand-clear, and a boss died with a shambler-sized gore burst. Tennis untouched: 30 tapped points still 50 hits / 0 whiffs. 45-game soak clean with all four chapters firing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ffac9b5512 |
Make every weapon hurt the boss, drop his HP bar, un-cap the director
Steps 1-3 of the late-game depth plan, after Oliver's 50+ game playtest. "Sometimes the racket zombie doesn't seem harmable" was real, not a feeling. Three of the five rack slots did nothing at all to him and said nothing about it: TRIPLE side balls skipped him, GUN and horde pellets skipped him, fire couldn't ignite him. BOMB meanwhile deleted him outright ignoring his hp. All of it now routes through one wound() with a TUNE.dmg* table, so damage is proportional to the body and a future 20 hp boss needs no new code. Fractional damage banks on zb.chip so whole points only come off hp — the wound skins switch on hp thresholds. His contact radius goes to 0.7 m, because part of "unharmable" was just missing him. Melee still skips him and that's correct: he's on the far side of the net and melee can't span it. The HP bar is gone. It was the only floating UI on a body, it was on screen for nearly the whole run, and nobody knew what it meant — while the wound skins already say the same thing in the language every other multi-hit enemy uses. A one-per-run modal explains what he is instead. Skins and bleed anchors now read a fraction of hpMax, since his hp scales with the run. And the plateau: every difficulty dial bottomed out by about game 12, so games 12-50 were arithmetically identical. Body cap, spawn gap, shamble speed, variant mix, boss hp and horde size all keep climbing now, calibrated to track the old curve through game 12 rather than replace it. One deliberate exception: shamble speed was time-based and maxed during game 2, so the early game is genuinely gentler now — zSpeedPerGame if that reads flabby. HANDOFF carries the full spec for steps 4 and 5 (ranged attacks with player damage, then chapter bosses) including the decisions we settled, so the next session can pick it up cold. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4cf54603f3 |
Fly the modals in with a swoosh instead of cutting to them
m.in (0..1 over TUNE.modalIn, 0.34 s) is the entrance clock, ticked inside update()'s modal early-return — the one thing that animates while the world is frozen. Icon and title fly in from the right trailing two ghost copies as a speed streak, the body lines cascade in from the left one after another, everything on easeOutBack so it overshoots and settles rather than decelerating politely into place. The dim washes in over the first third. A descending noise sweep on arrival (swish is parameterised now — same six lines of Web Audio, opposite reading) and a low pock as it lands. Every x is rounded: a fractional x on a nearest-neighbour scaled image re-snaps its pixel columns, which is the boiling that once read as HUD flicker. Dismissal is gated on the entrance: while m.in < 1 dismissModal swallows the gesture but doesn't clear the modal, so a tap already in the air when a popup appears can't skip it — and swallowing rather than returning false is what keeps that tap from falling through into a serve. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
7dde5874ab |
Brighter stands, a serve hint that shows the gesture, and a boy who walks
- The crowd sat in the dark, worst of all in splatter mode where you spend the game. Both crowd frames get an identical gamma lift (means 30/14/39 -> 42/22/56) and the rot tint drops 0.5 -> 0.26. The curve MUST stay identical across the two frames or the clap swap flashes brightness; _sheet_crowd1.png keeps the pre-lift original. - The splatter hint led with the tap when pulling is the whole gesture. It now reads PULL BACK TO AIM - RELEASE TO FIRE, and after 1.5s of sitting in the serve stance untouched a ghost thumb slides back while chevrons grow up-court on a loop. Pull back, shoot forward is an inverse relationship that a line of text cannot carry on its own. - The mop boy has three sheets instead of two: the push cycle now plays ONLY while he is stopped and working, a new carry sheet walks him upright with the mop shouldered, and the run sheet stays for the tennis-mode ball fetch. He was mopping thin air the entire way to the stain. Sheet note: frames 2 and 3 of the carry sheet merged (a mop handle bridging the gap), so it is cut at the true column gaps plus the thinnest column inside the merged pair, then common-scaled — same technique as the brute stride sheet. 26-game soak clean. Serve returns 133/150 = 88.7% +/-2.6%, 30 human-tapped points = 50 hits / 0 whiffs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
21b9dd53fa |
Thumb-scale draw meter, looser aim magnet, a bomb worth the tin
- The draw meter is a fixed bar across the bottom, ~90% of the width and 10px tall, with the kill notch on it. It was a 26x4 stub floating on the apron by the player's feet, which is nothing under a thumb at arm's length. The serve hint is suppressed while pulling since they share the slot. pullFull 95 -> 130: at 95 every pull maxed out by accident, so the meter never meant anything. - The aim magnet took the nearest body ALONG the lane, so a close shambler a little off-axis stole shots clearly aimed past it. It now picks the best ALIGNED body, with TUNE.aimTie of slack in which a nearer one still wins — a straight-ahead lane still eats the nearest body first, which is the settled feel. aimTie: 1 restores the old behaviour exactly, for A/B. - Bomb radius 2.5 -> 4.2 (2 kills -> 5 in a scattered ten-wave), a 120-particle eruption, a ring of thrown stains and a full-size pile. The blinding white frame is now a brief warm bloom — it was hiding the very gore it announced. - Rack tiles 22 -> 30 tall, and the touch band is padded above and runs to the screen edge below. At 22 logical px they were ~30 CSS px on a phone, well under the ~44 a thumb expects; now ~62. 26-game soak clean with heavy bomb use, arrays stay bounded. Serve returns 136/150 = 90.7% +/-2.4%, 30 human-tapped points = 50 hits / 0 whiffs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
55ddf5932e |
Fix the serve deadlock, give the mop boy a job, align net and umpire
The freeze first: G.mgT only counted down inside `G.phase === 'rally'`, so anything that moved the phase mid-burst — a scheduled duel calling setupPoint — stranded it above zero forever. mgT > 0 is one of keyPressed's early returns, so no tap could serve again, and the yellow shape over the player's head was drawMgRacket's muzzle flash still being drawn. Fixed at the root (the clock ticks in every phase, only the pellets are gated on rally) and semantically: starting a duel cancels any burst, because the duel is tennis. The mop boy now does an actual round: walk to the biggest nearby stain, stand on it and push the mop while the cycle animates in place, scrub it out, move to the next, then leave. Gore fades ~3.5x faster while he is stopped and working than when walking past, so the stain dies under him instead of fading as he drifts by. boyMaxJobs caps the round so a kill streak can't employ him forever. Applause: the clip is 4.16s and always plays in full, but every cheer() passed a shorter beat, so the stand sat down while it was still audible. Floored at the clip length. New, much bolder clap frame — the re-roll came back brighter and bluer, which would have flashed the whole stand, so it is colour-matched to frame one per channel on mean and sd before quantising. Net and umpire were both wrong since forever: netW was the singles-stick width (10.06) against a court drawn to the doubles lines, so the net stopped 0.46m short each side; the chair's near legs stood 0.45m inside the sideline. Both now sit exactly on the doubles line. 26-game soak clean, no stalls. Serve returns 134/150 = 89.3% +/-2.5% with the wider net (the bar is inside one sigma), 30 human-tapped points = 50 hits / 0 whiffs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
32401366fe |
Drawn frames, not procedural fakes: brute lurch, boy stride, crowd applause
Every animation in this round gets real frames. The procedural squash-and-sway on the wounded brute read as fake on sight, and that gait is where the comedy lives. - Each brute wound stage now has its own 2-frame lurch (z_gw/z_gwb, z_gw2/z_gw2b, ...). The generator gave every B frame a dragged pole that only stage 1 actually carries, so it would have blinked in and out each step — stripped by keeping the largest blob, and where the pole touched the body, by cutting on column density (a torso is dense, a dragged pole is a thin diagonal). - Frames step off DISTANCE COVERED, never elapsed time — boy.dist for ball boys, z/bruteStride for brutes. A time-driven cycle moonwalks the moment speed and frame rate disagree, which is exactly what "he slides faster than the animation suggests" was. Change a speed now and the legs keep up on their own. - One boy, one slot. sendBoy no longer replaces a boy already on court: that was the appearing and disappearing, since every kill teleported him back to the wing mid-stride. New mess is appended to his round instead. He now walks for 10.5s a crossing, and there is a lot of mopping to do. - The applauding crowd is a real two-frame animation (seated / arms up), swapped per column on its own phase. The old "smooth wave" still shifted each column by a FRACTIONAL dy, and nearest-neighbour resampling at a fractional y re-snaps a column's pixel rows every frame — that boiling beside the HUD is what read as the scoreboard flickering. The board art is 99.9% opaque; nothing was ever showing through it. With the crowd calm the frame delta is now 0.0%. Benchmarks: 136/150 serve returns (90.7% ±2.4%, the 11/12 bar is inside one sigma), 30 human-tapped points = 50 hits / 0 whiffs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ff77922e85 |
Feedback round: fire actually spreads, brutes lurch, crowd stops flickering
- Fire never spread in play. The flame ball's full pierce wiped its own lane, so
the bodies that would have caught were already gone. It now ignites everything
it FLIES PAST (TUNE.burnBallR), with a wider ignition and spread radius. One
flame serve into a scattered 10-wave kills 9, nine burning at once.
- Wounded brutes slid along like decals: every wound stage is a single frame and
the brute now spends most of its life wounded. They get a procedural lumber
instead of eight more sprites — translate + squash, no rotation, so the pixels
stay on the grid, and the amplitudes scale with the projection or the whole
thing goes sub-pixel at range.
- The "scoreboard flicker" was the crowd: the cheer bounce was per-column
Math.random() every frame, i.e. TV static, showing through the scoreboard art's
transparent ornate edges. Now a smooth per-column sine on idleT.
- Ball boys: mopping is the default in splatter mode now, and the boy is half
again as tall — at 1.2m he read as a distant speck. The running fetch-boy
survives for tennis, where there is nothing on the grass but a ball.
- Changeover 7s -> 5s, with the mop crossing timed to fill it.
- Save-to-homescreen: a bloody tennis ball, wired up as apple-touch-icon +
favicon + webmanifest. The Dockerfile now ships the manifest and gives it its
own MIME type via an exact-match location (a bare `types {}` block would have
replaced nginx's whole map and broken .js and .png with it).
Benchmarks re-run: CPU returns 67/72 serves (93.1%, above the 11/12 bar — a
20/24 sample in between was flub noise), 30 human-tapped points = 50 hits /
0 whiffs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
a09a5d7559 |
Polish round: modal system, softened palette, pixel font, and the 14 plan items
The three foundations first, since everything else leans on them:
- G.modal is the only thing that stops the world. update() early-returns on it
so no zombie steps, no ball flight, no spawn timers run while a popup is on
screen — you can read it without being eaten. Dismissal swallows the gesture;
the pointerdown dismisser is registered only on pointer:fine, because on touch
pointerdown beats touchstart and a shared handler cleared the modal and then
served with the same thumb.
- UI in config.js is the accent palette. The arcade primaries were shouting over
the artwork; the gore reds deliberately stay loud.
- Every canvas font string now goes through F(). Bare `monospace` resolved to
Courier — a slab serif — which was the "generic serif" bug, not taste.
Then: weapons are only ever WON. deliverTin() has exactly one caller now
(resolveInterlude('P')); the old game schedule calls the DUEL instead, fired off
proceedAfterClear() where the court is guaranteed empty. Four popups (splatter
tutorial with a generated illustration, first duel, weapon received, horde),
wCap 9 with the tin amounts tunable, a 5-hp brute that comes apart over four
wound skins, fire that ignites the neighbours and chains outward, a three-stage
death where you become the biggest gore mound on the court, and a 7s changeover
with a ball boy who finally has a mop.
New art: assets/logo.png, x_howto, z_gw2-4, x_boymop1-4, plus assets/_key.py for
standalone art (chroma-key + crop, no frame splitting, no forced sprite height).
Benchmarks re-run and holding: CPU returns 23/24 serves, 30 human-tapped points
= 50 hits / 0 whiffs, 26 simulated games with no exceptions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
f3dcf0b16f |
Splatter: chevrons in perspective, and a smash that actually flies
The chevrons were stepped along the SCREEN line at a fixed size, so they read as a flat sticker over a 3D court. They now step through world space and take their size from their own projection scale — near ones roughly twice the far ones — and they fade IN toward the target so the eye is walked up the lane instead of being handed a uniform stripe. The arrowhead scales with the target's own depth too. (proj is a true projective map, so the screen direction stays constant along the line; only size and alpha needed to vary.) Hard shots now fly hard: serve pace is weighted onto quality (19 base + 22×q, so 21 -> 41 m/s) instead of 21 -> 28. Two things had to give way for it. Serve topspin was a fixed 1 + 0.35q, which meant a fast flat ball could not both clear the tape and land on a target, so the net-loft loop throttled every smash back to walking pace — measured 21.8 m/s whether you drew 60px or 95px. TUNE.serveSpin 1.3 lets it leave flat and DIVE, which is what a smash is. And the loft loop's margin was doHit's quality cushion, which taxed exactly the shots meant to hurt. It's now sized to the real error it exists to cover: the sim samples the net plane once a frame, so a diving ball can test below the tape when the closed form clears it. Margin = descent rate × the dt clamp. A flat drive pays almost nothing, a lob pays what it must. Measured, target 20m out: 24.7 / 32.2 / 41 m/s for weak / mid / full draw. At the net it's 9.6 / 11.2 / 11.6 — still the slow floaty lob, as it should be. Ball-vs-shambler contact gets a crude sweep (radius + half a frame's travel), because a 41 m/s ball steps 0.7m per frame at 60fps and 1.6m at main.js's dt clamp, both wider than a shambler. Verified the full smash connects at both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |