Skip to content

feat(butterfly): a jollier chase, and a catch that is a catch - #42

Merged
JOhnsonKC201 merged 8 commits into
mainfrom
feat/jolly-butterfly
Sep 4, 2026
Merged

JOhnsonKC201 merged 8 commits into
mainfrom
feat/jolly-butterfly

Conversation

@JOhnsonKC201

Copy link
Copy Markdown
Owner

Supersedes #41, which GitHub closed when #40's branch was deleted on merge (a closed PR cannot be retargeted). Same 8 commits, rebased onto main after #40 landed; tests green on the rebased tree.

What the cat does now

stalk -> swats that rise on the haunches -> butt-wiggle -> leap -> catch: a fan of pink sparkles and a heart at the paws, one pleased trill, the bug pinned between the reaching paws -> land, sit, peer down at it wide-eyed with a blush while its wings barely move (550 ms) -> it slips out and climbs away -> the cat does a quick double bounce after it with happy eyes and an excited tail. A whiff: land, look up after it, no sparkle, no sound.

Before this the catch was six white pixels and a heart, no sound, no face, and the cat snapped straight back to idle; a leap that missed looked exactly the same; the swat was two arms on a static body; the tail never changed.

Commits

  1. fix(butterfly): a Focus Guard busy signal sends a flying butterfly home. The mid-visit eviction read config.workMode while the spawn gate read workModeOn(), so a meeting blocked new visits but left one already flying.

  2. test(butterfly): pins the 08-26 quiet fix. Nothing guarded bfSwatCool = t + 2600 + rand*1400, BF_SWAT_MS = 600 or the 900 ms whoosh floor; the only whoosh test drives the scroll leaf with the butterfly off. A full visit with the bug parked in swat range now asserts one whoosh per swat, swats >= 2600 ms apart, whooshes >= 900 ms apart, and the constants by name.

  3. feat(butterfly): the catch is a victory beat and a whiff looks like one.

  4. feat(butterfly): hold the catch between the paws before it escapes (bfMode = 'held').

  5. feat(butterfly): rise into the swat, notice the bug, and wag while playing.

  6. fix(butterfly): the button outranks work mode, the way a treat does. Found live: with work mode on, "Send a butterfly" summoned a bug that left on the next frame, a dead click since July. A summoned visit is exempt from that one eviction for its duration; unsolicited visits are gated exactly as before (test covers both).

  7. fix(butterfly): aim the leap at the paws, and keep the catch a game. Measured with the harness over 30 natural visits: 316 pounces, 0 catches. The pounce aimed the feet at the bug; the catch happens at the paws, a paw-reach higher, so the cat flew through the bug feet-first. The feet now aim one paw-reach under the bug and meet it at the leap's apex. Since the bug is frozen during a hunt that would connect every time and end every visit at the first pounce, so: no pounce in the first 7 s of a visit (notice, stalk, a swat or two first) and a 30% jink where the bug flits aside as the cat coils. After: 41 pounces / 30 visits, 30 catches, 73% connect, mean visit 15 s. Seeded test pins the shape.

  8. fix(butterfly): a shorter warm-up (7 s -> 4 s; median first pounce 5.1 s, catch 5.9 s), and no leap at a bug the cat cannot reach. Measuring exposed a second miss the jink did not explain: the hunt target is clamped to the play area, the bug flies on screen-margin clamps of its own, so near an edge it hovered where the cat may not go and the leap went straight up under nothing (a cat resting in a corner met this most visits). The glider's zone now stays inside the cat's reach and bugPounceable() keeps both triggers from committing to a leap that cannot land. 60 visits: 89 pounces, 60 catches, 67% connect, 0 non-jink whiffs.

Found on the way

  • The dodge reflex had no mode guard. A bug climbing away after a catch passed under the head and was startled straight back into a dodge, so the old catch that "ended the visit" never actually did. Guarded on out and held.
  • renderRearBat always accepted a strength for the whoosh; no caller set it. It now scales with mood (calm 0.73, playful 0.85, zoomies 1.0).
  • wagBoost was zeroed for cats every frame, which is what kept the cat's tail locked out of the excitement the dog's already had.

Sound budget, worst case per 22-30 s visit

Sound Gate Max
whoosh (playSwipe) one per swat window, windows >= 2600 ms apart, 900 ms floor, all unchanged 12
trill (playChirp) on a catch config.soundOn, and the catch is unrepeatable (held guard) 1
notice, hold, whiff, bounce, sparks, tail silent 0

Nothing periodic was added. Sound off is fully silent.

Invariants kept

  • No new beat writes pos: a stay-put pet (roamOn: false) is asserted at exactly 0 px drift through a catch, the hold and the bounce.
  • No new Math.random(): the sparkle fan is derived from its index, so the seeded stream tests/self-play.test.js reads (dog ball and cat butterfly on the same frame) is untouched.
  • bfMode names, the 5.5 speed cap, the grace window, spawn gates, startBflyVisit(t), the single bfOn = false exit, FORCED_STATE === 'rearup', batSpriteFor geometry: unchanged.
  • previews/cat-poses.png sha256 f0e06b1c... identical before and after every commit (no composer changes). site/cat-live.js untouched.

Tests

tests/butterfly-play.test.js (new, 8 tests): metronome regression + constant pins, catch (one trill with Sound on, none off, pink sparks, happy eyes that end), whiff (nothing fires, cat looks up), zero drift through catch/hold/release plus held cannot arm a swat or a dodge and the bounce amplitude curve, notice "!" for exactly its window, cat tail arity + 0..1 excitement + wagBoost decay with the dog path unchanged, Focus Guard eviction. Suite: 226 tests, 0 failures. Lint clean. npm run test:boot passes.

Seeing it

  • npx electron . --shot --bfly --state=rearup --at=1021 vs --at=817: stroke peak vs trough, body a few px higher at the peak.
  • npx electron . --shot --joy --at=100 (and 500, 900, 1300): the bounce loops every 2 s under the flag because --at counts from page load while the renderer clock started earlier; one of those is mid-air with ^^ eyes and blush.
  • Running app: Settings > Send a butterfly, cursor still, wait for a pounce.

The mid-visit eviction checked config.workMode, while the spawn gate a few
lines above already used workModeOn(). So when Focus Guard flagged a meeting
it blocked NEW visits but left a butterfly that was already here flying
through the whole call. Same predicate on both sides now, plus a test that
flips the focus signal mid-visit.
The 08-26 fix rested the paw 2.6-4.0s between swats and put a 900ms floor
under the whoosh, but nothing guarded either number: the only whoosh test
drives the scroll leaf with the butterfly switched off. Drive a whole visit
with the bug parked in swat range and count: one whoosh per swat, swats at
least 2600ms apart, whooshes at least 900ms apart, and the constants pinned
by name so a nudge cannot slip through the bounds.
The catch was six white pixels and a heart, no sound, no face, and the cat
snapped straight back to idle; a leap that missed looked exactly the same.
Now a catch throws a fan of pink sparkles at the paws, one pleased trill
(Sound on only), and opens a short victory window the idle branch turns into
a quick double bounce with happy eyes and a blush. A whiff gets none of that:
the cat lands and looks up after the bug instead.

All of it is render-only, so a pet set to stay put still does not move a
pixel, and the sparkle fan is derived rather than rolled, so the seeded PRNG
stream the self-play parity test reads is untouched. The whoosh cadence from
the 08-26 quiet fix is not touched either.

Both pounce triggers stand down under --shot, so the beat was unreachable in
a QA capture: --joy pins it open (documented next to --bfly and rearup).
A catch used to eject the bug the same frame the paws reached it, which read
as a hit, not a catch. Now it is held: pinned between the reaching paws for
the rest of the leap, then cupped in front of the chest for half a second with
its wings barely moving while the cat peers down at it wide-eyed, and only
then does it slip out and climb away with the victory bounce behind it.

'held' is a new bfMode. Every eviction still forces 'out' (visit end, mouse
use, toggles, species swap), so the one place that clears bfOn is unchanged.
A held bug sits 50px under the head, inside the swat's reach and the dodge
radius, so both are guarded against it.

Found on the way: the dodge trigger had no mode guard at all. A bug leaving
after a catch passed under the head and was startled straight back into a
dodge, so the old catch that "ended the visit" never actually did. It is
guarded on 'out' now too.
…aying

The swat moved two arms on a static body; the cat now rises on its haunches
with the reaching paw (render-only, a few pixels, scaled by mood) and the
whoosh takes the strength knob renderRearBat always accepted but nobody set.
The cadence from the 08-26 quiet fix is untouched and the test from two
commits ago proves it.

The cat's tail takes an excitement argument, mirroring the mood argument the
dog's tail already had: a quicker sway added on top of the resting wag while
a butterfly is in play and for a few seconds after a catch. It is additive on
purpose; changing the base rate with a decaying value jumps phase at large t
and reads as a twitch. wagBoost now decays for cats instead of being zeroed
every frame, which is what kept them locked out of it.

Spotting the bug (bfMode in to wander) gets a small perk and the "!" glyph
over the head for half a second, silent, drawn above whichever body is up.

The --joy preview beat loops the way --state=done does, because a shot's
--at counts from page load while the renderer clock started earlier, so no
single --at lands on a known phase of a one-shot beat. The notice opens on
the first frame, before the capture timer starts, so it is pinned by a test
rather than a shot. Changelog and the features tour updated.
Work mode (and a Focus Guard busy signal) exist to stop unsolicited visits
and to keep the pet from wandering off; they were also sending home the
butterfly you had just asked for with "Send a butterfly", on the very next
frame, which made the button a dead click for anyone who works with the
mode on. The treat and the ball already outrank the park for the same
reason (see giveTrekOn). A summoned visit is now exempt from that one
eviction for its duration; the cat plays from its parked corner, and the
next autonomous visit is gated exactly as before.

Found live: the user's settings have work mode on, and the butterfly left
the instant it arrived.
The pounce aimed the cat's FEET at the butterfly. The catch happens at the
paws, POUNCE_REACH (a paw-length plus the leap's apex) above the feet, so
the cat flew through the bug feet-first and the paws closed on air. Measured
over 30 natural visits with the harness: 316 pounces, 0 catches. The whole
catch-and-release beat was unreachable in practice.

The feet now aim one paw-reach under the bug and meet it at the top of the
leap. The bug is frozen for the whole hunt (the flight integrator is paused
while hunting), so a correctly aimed leap always connects and a visit would
end at the first pounce a couple of seconds after arrival, before the stalk
and the swats. Two things keep it a game: no pounce in the first 7 seconds
of a visit, and a 30 percent chance the bug flits aside as the cat coils, so
the cat, already committed, whiffs and watches it go.

After: 41 pounces in 30 visits, 30 catches, 73 percent of pounces connect,
mean visit 15 s. A seeded test pins the shape (most visits end in a catch,
some pounces still whiff, none before the embargo). The random roll on the
dive path stays first in its condition so the per-frame Math.random() count
is unchanged.
…t reach

BF_POUNCE_AFTER_MS 7000 -> 4000: the bug is noticed at about 1.5 s and the
first swat lands around 2-3 s, so 4 s leaves room for one before the first
pounce can come. Median first pounce is now 5.1 s after arrival and the
catch 5.9 s (was 7.3 s / 8.1 s).

Measuring that exposed a second miss the jink did not explain: in every
non-jink whiff the cat stood at x = 1812, the right edge of its play zone,
leaping straight up under a bug at x 1846-1880. The hunt target is clamped
to the play area, but the bug flies on screen-margin clamps of its own, so
it hovers past an edge the cat may not cross. A cat resting in a corner met
this on most visits. Two changes: the glider's wander zone now also stays
inside the cat's reach, and bugPounceable() keeps both pounce triggers from
committing to a leap whose clamped target cannot put the paws on the bug
(it swats instead). Both checks sit after the random roll, so the per-frame
Math.random() count is unchanged.

Over 60 natural visits: 89 pounces, 60 catches, 67 percent connect, 0
whiffs that are not jinks (was 10 of 110). The work-mode test now treats a
catch inside its window as the play working rather than an eviction.
@vercel

vercel Bot commented Sep 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
pixelcat Ready Ready Preview Sep 4, 2026 5:23am UTC

@JOhnsonKC201
JOhnsonKC201 merged commit 6d4f9c8 into main Sep 4, 2026
5 checks passed
@JOhnsonKC201
JOhnsonKC201 deleted the feat/jolly-butterfly branch September 4, 2026 05:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant