The debug report was only reachable in DEBUG mode, which is the one mode
a player hitting a real bug won't be running. Move it to a "🐛 Report a
bug" item in the game menu: a note field, a download, and a copy-as-text,
on any server.
That knowingly shows the reporter their opponents' hands and the shop
deck order. Credentials are still required, so a report only ever reaches
someone seated at that table. The buy-any-card panel stays DEBUG-only —
it changes the game; a report only reads it.
A report dumps everything needed to understand a game that went wrong:
the full state (decks card by card, shop row and tier deck order,
discards, pending choices, banked Mana/Trumpets/apples), the round's
battles, and the whole event log. Available as JSON, which replays, or
text, which reads — and the text ends with a paste-ready repro test.
Battles now record the randomness they consume alongside what they
started from, so a result can be replayed long after the round cleared
those banks: same lineups, same dice, same events, down to the log text.
Results predating the recording say so rather than quietly re-rolling.
Three ways in: the 🐛 panel's download/copy buttons, GET
/api/debug/report (DEBUG-only, since a report holds both players' hands
and the shop deck order), and `mise run report`, which reads the
database directly so a live game can be dumped without DEBUG.
`noMagnify` only suppressed rendering — each card went on tracking hover
underneath it. During a drag the cursor outruns the lifted card (its
transform lands a frame later), so the rows it passes take mouseenter,
and React re-inserts those rows as the list reorders, so the matching
mouseleave often never arrives. Every card left holding stale hover then
popped its preview the moment the drag state cleared, at stale
coordinates, and stayed there until that exact card was hovered and left
again.
noMagnify now means the magnifier is off, not hidden: the handlers come
off and any tracked hover is dropped. Nothing can be left half-armed
regardless of which boundary events the browser does or doesn't deliver.
The drag also cleared its state 180ms after release — its own glide — while
the neighbours it passed keep gliding for anim.ts's MOVE_MS of 240ms. A
row sliding out from under a stationary cursor in that 60ms tail could
re-arm the same stale hover, so the settle window now covers the whole
tray coming to rest.
Reordering the battle line meant finding a small ⠿ handle beside each
card. Now the card itself is the drag surface, with the gesture split by
input type so it can't collide with the two things a press already means:
* A mouse lifts the card once the cursor travels 5px. Shorter presses
stay clicks.
* A finger has to hold still for 220ms first. Arrange is the one screen
tall enough to scroll, and a finger swiping a card almost always
means scrolling — so the card deliberately does NOT set
`touch-action: none`. The hold is what distinguishes a drag, and once
it lands the scroll is cancelled by preventing `touchmove`, which
works precisely because a still finger hasn't started one.
That also fixes the phone's tap-to-magnify fighting the drag: only a
press that never became a drag counts as a tap, so hauling a card up the
list no longer pops the expanded card view open under your finger. A
picked-up card swells and casts a shadow, which on touch is the only
confirmation the hold registered.
The up/down arrows stay as the precise alternative and grow to 46px
(40px on phones) now that they aren't sharing space with the handle.
The play area grew and shrank as cards entered and left play. Two slots
were sized independently of the cards that fill them: the pet zone
reserved 150px for a 162px battle card, and the empty-deck placeholder
kept its desktop 84x116 on phones where the face-down card is 56x78. So
each faint/reveal moved the arena 12px per side on desktop, and a deck
running dry moved it 38px per side on a phone.
Every arena slot now derives from one set of --unit-*/--deck-* metrics on
.battle, and holds that size whether or not a card occupies it. Measured
across a battle's states (no pets, both revealed, one fainted, fans of
apples, decks dry), the arena height spread goes from 24px desktop /
76px phone to 0.
Also hide the arena's horizontal scrollbar: a wide apple fan made it
appear and vanish mid-battle, and with a content-driven height that alone
jumped the arena by the scrollbar's thickness. Touch, trackpad and
shift+wheel still scroll it.
- Force the topbar to a single row and let long player names truncate with an
ellipsis, so the three-dot menu no longer wraps to a second line.
- Drop the card's outer corner radius from 14px to 9px so it matches the
scene/panel boxes inside (7-8px) instead of dwarfing them.
Treat the app as a game surface: no tap-flash, and a long-press on a card or
button no longer starts a text selection or pops an image callout. Form inputs
and the room code opt back into selection so typing and copying still work.
The horizontal-scroll trays clip vertical overflow, so a selected card's lift,
gold outline, and check badge were cut off at the top. Add row headroom and
tuck the check badge inside the card corner. Also make the in-tray cards taller
with a larger rule panel and bigger ability text so descriptions aren't cramped
(widths stay tight so all four shop cards still fit without scrolling).
- A tap on a touch device synthesized mouseenter/mousemove and popped the
hover magnifier on top of the tap-to-magnify overlay — a double image. Gate
the hover preview on real hover capability ((hover: hover)) so touch taps
only open the tap overlay.
- In that overlay the card is large, but its art, power badge, and text were
still the tiny in-tray sizes; restore full-size art/power/damage/name/effect
so the magnified card reads like a real card.
A phone has no hover to preview a card, so tapping now opens a magnified,
fully-readable view with the actions that fit it instead of acting blind:
- Shop card: tap to see it big, then Buy (or read it even when unaffordable).
- Your deck card: tap to see it big, then Sell it, or Select to enter a
bulk-selection mode where taps toggle pets (no magnify) and the action bar
offers Sell / Triple / Cancel.
Desktop keeps click-to-buy and click-to-select unchanged; the new behavior is
gated on a max-width:600px media query via a small useMediaQuery hook.
- End-of-battle result is now a centered dialog (Victory/Defeat/Draw) with
'Next round' and 'Not yet'; 'Not yet' dismisses it to rewatch the battle,
and a toolbar button remains to advance. A modal is always fully on screen,
fixing the next-round button being unreachable below the arena on phones.
- Also make the play area genuinely scrollable on phones (stretch the body
so table-main's overflow can engage) as a safety net.
- Collapse the 5-button playback speed picker into one button that cycles
through the options, keeping the battle controls on a single row.
- Topbar: show the round as a die face (1-6 pips) on all screens, move the
room code and Leave into a '...' dropdown menu, and drop the wordmark on
phones so the rail stays one row (die + players + menu).
- On phones, strip battle cards to art + power/damage + name (no suit, tier,
or ability text); the magnified view still shows the full card.
Lock the app to the viewport on screens <=600px wide so the page itself
never scrolls: the topbar becomes a slim rail and the play area fills the
rest. Cards, trays, and type shrink so a shop round or a battle fits on
one screen; the shop and deck rows scroll sideways when a full six-pet
deck outgrows the width, keeping each tray's height fixed. Arrange is the
one screen allowed to scroll past the viewport, since a battle line can
run long.
Only buying costs gold; selling and trading (Triple) are free. Pass is
now a final action, legal only at or under the pet limit: it forfeits
remaining gold and ends that player's shopping for the round, with the
shop closing once everyone has passed. That makes the separate cleanup
phase unreachable (you sell down in-shop before passing), so it is
removed. The client asks for confirmation before passing, and the bot
knows buys are the only coin sink, when passing is legal, and that it
must sell down before it can pass.