Compare commits

...

41 Commits

Author SHA1 Message Date
funman300 de52c8a7b7 docs: update SESSION_HANDOFF for completed phase-4 polish tracks
CI / Test & Lint (push) Failing after 26s
CI / Release Build (push) Has been skipped
Reflects that Track B (window polish) and Track G (modal + score
animations) are now landed, and brings the resume prompt and
release-readiness scope in line with the post-commit state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 20:05:36 +00:00
funman300 dcfa976dad feat(engine): score change feedback — pulse and floating delta
Score readouts now react to mutations: ScorePulse drives a triangular
1.0 → 1.1 → 1.0 scale on the HUD score over MOTION_SCORE_PULSE_SECS,
and jumps of at least SCORE_FLOATER_THRESHOLD points spawn a floating
"+N" that drifts up 40px and fades over 2× the pulse duration before
despawning. Detection runs after GameMutation so the visuals trail the
state update by exactly one frame.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 20:05:00 +00:00
funman300 71999e1062 feat(engine): modal open animation — fade + scale with ease-out
Modals now animate in via the new ModalEntering component: scrim alpha
ramps from 0 to its full value while the card scales from 0.96 to 1.0
over MOTION_MODAL_SECS using an ease-out curve. AnimSpeed::Instant
collapses the duration to zero so reduced-motion users see the modal
snap into place on the first frame.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 20:04:51 +00:00
funman300 5f5aba8dff feat(app): window polish — WM_CLASS, centered window, crash log hook
Sets Window::name so X11/Wayland taskbars group the game correctly,
centers the window on the primary monitor on startup, and installs a
panic hook that appends a timestamped crash record to crash.log under
the platform data dir (gracefully no-ops when the directory is
unavailable).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 20:04:43 +00:00
funman300 9bfca929cb chore(workspace): satisfy clippy --all-targets in test code
Five test-only lints surfaced by --all-targets were blocking CI under
-D warnings: a useless vec! in a leaderboard sort test, a
field_reassign_with_default in tuning tests, and three
assertions_on_constants in card_plugin sanity tests. The constant
assertions are now wrapped in const blocks so they run at compile time;
the runtime-formatted values were dropped from their messages because
const-block assert messages must be string literals.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 19:54:40 +00:00
funman300 534870a68a docs: expand SESSION_HANDOFF resume prompt with release-readiness scope
CI / Test & Lint (push) Failing after 15s
CI / Release Build (push) Has been skipped
Replaces the 3-line resume prompt with a senior-Rust-developer framing
plus seven concrete next-direction tracks (A–G) covering the Home modal
decision, window/release polish, sound, sync, achievements, release
backlog, and UI/UX professional polish. Each track names specific
files/tokens so the next session can act on a chosen direction without
re-discovery.
2026-04-30 05:00:41 +00:00
funman300 0066ca6205 docs: mark UX overhaul Phase 3 complete in SESSION_HANDOFF
CI / Test & Lint (push) Failing after 30s
CI / Release Build (push) Has been skipped
All 10 steps landed; commit list, smoke-test checklist, and open
follow-ups updated for the next session.
2026-04-30 04:48:33 +00:00
funman300 54e024c1b0 chore(engine): final literal-to-token sweep
Migrates the last remaining colour, spacing, font-size, and z-index
literals in animation_plugin (toasts), hud_plugin (action bar +
Modes/Menu popovers), and win_summary_plugin (full win modal restyle)
onto the ui_theme token system established in step 1. Win summary now
uses SCRIM/BG_ELEVATED/ACCENT_PRIMARY/STATE_* with a yellow Play Again
button. Sprite-tinted gameplay art (cards, felt, drop-zone hints,
pile markers) and sub-rung pixel sizes (1px borders, fixed cell
widths) are intentionally left untouched.

cargo build / clippy --workspace -- -D warnings / test --workspace
all green (819 passed, 0 failed, 8 ignored).
2026-04-30 04:47:20 +00:00
funman300 3a01318fbd feat(engine): upgrade animations — curves, scoped settle, deal jitter, cascade rotation
Slide animations now interpolate through MotionCurve::SmoothSnap via
sample_curve() at the call site (no struct field added). Slide and
cascade durations route through ui_theme::scaled_duration with
MOTION_SLIDE_SECS / MOTION_CASCADE_STAGGER_SECS / MOTION_CASCADE_SLIDE_SECS.

Settle bounce in feedback_anim_plugin scoped to MoveRequestEvent and
DrawRequestEvent receivers — only the top `count` cards of the
destination pile (or top of waste) bounce; undo and other state
changes no longer trigger a global all-tops settle.

Deal stagger gains a deterministic ±10% jitter via DefaultHasher on
card_id (no rand dep). Per-card stagger = base * (1.0 + jitter).

Win cascade switched from CardAnim to CardAnimation with
MotionCurve::Expressive and a deterministic ±15° per-card Z-rotation
via Fibonacci hash. Win screen shake routes through
MOTION_WIN_SHAKE_SECS / MOTION_WIN_SHAKE_AMPLITUDE; ScreenShakeResource
gained a `total` field so decay computes correctly under Fast / Instant.

cargo build / clippy --workspace -- -D warnings / test --workspace
all green (819 passed, 0 failed, 8 ignored).
2026-04-30 04:38:59 +00:00
funman300 79d391724e chore(data): derive Copy on AnimSpeed
AnimSpeed is a fieldless enum; adding Copy lets `scaled_duration`
and other helpers take it by value through `&AnimSpeed` deref
without requiring a `.clone()` at every call site. Prerequisite
for the upcoming animation token-routing work.
2026-04-30 04:26:57 +00:00
funman300 ba019c0ba7 feat(engine): convert SettingsPanel to modal scaffold + Done button
Replace the bespoke side-panel with the ui_modal scaffold. Layout
collapses into four sections: Audio (SFX / Music volume), Gameplay
(Draw Mode / Anim Speed), Cosmetic (Theme / Color-blind / Card Back
/ Background), and Sync (status + manual Sync Now).

Body lives in a scrollable child of the modal card with
max_height: Vh(60.0) so tall content stays reachable on short
windows. Done is a primary button outside the scroll so it's always
one click away regardless of scroll offset.

All colours, spacing, typography, and z-index from ui_theme tokens.
Two file-local sub-rung sizes (SWATCH_PX = 40, ICON_BUTTON_PX = 28)
remain as documented literals — they're smaller than SPACE_2 (8 px)
which is the smallest rung.

Existing systems (handle_settings_buttons, update_*_text,
scroll_settings_panel, persistence) untouched; the SettingsPanel /
SettingsPanelScrollable / SettingsScrollNode markers and every
button marker carry over so all existing tests and click handlers
keep working.

cargo build / cargo clippy --workspace -- -D warnings / cargo test
-p solitaire_engine all green (444 passed, 0 failed).
2026-04-30 04:13:20 +00:00
funman300 18d7c121a3 feat(engine): convert OnboardingPlugin to 3-slide modal flow
Replace the single-screen first-run banner with a 3-slide flow built
on the ui_modal scaffold:

  1. Welcome
  2. How to play (drag-and-drop / double-click / right-click hints)
  3. Keyboard shortcuts (8 rows mirroring help_plugin's canonical list)

Navigation: primary Next button (advances; final slide reads
"Start playing" and writes first_run_complete), secondary Back button
(slide >0), tertiary Skip on slide 0. Arrow / Enter / Esc keep
working as accelerators.

OnboardingSlideIndex resource persists across despawn/respawn so the
rebuild system always knows which slide to show next.

All colours, spacing, typography come from ui_theme tokens; no
literals in the new code.

cargo build / cargo clippy --workspace -- -D warnings / cargo test
--workspace all green (813 passed, 0 failed, 8 ignored).
2026-04-30 03:24:32 +00:00
funman300 cb93bd9265 fix(engine): pin modals via GlobalZIndex and surface forfeit-no-op toast
CI / Test & Lint (push) Failing after 28s
CI / Release Build (push) Has been skipped
Two fixes the smoke test surfaced:

1. The forfeit-confirm modal at `Z_PAUSE_DIALOG` (225) was invisible
   behind the pause card at `Z_PAUSE` (220). In Bevy 0.18, root-level
   UI nodes don't reliably sort across stacking contexts via plain
   `ZIndex` alone, so `spawn_modal` now adds `GlobalZIndex(z_panel)`
   alongside the existing `ZIndex(z_panel)`. Every overlay built on
   `ui_modal` (pause, forfeit-confirm, confirm-new-game, help, home,
   leaderboard, profile, achievements, stats, game-over) inherits the
   fix.

2. `handle_forfeit_request` no longer silently drops the request when
   `move_count == 0` — pressing G or clicking the pause modal's
   Forfeit button on a freshly-dealt game now opens the confirm modal,
   and the only short-circuit is "game is already won", which now
   fires an `InfoToastEvent` ("No game to forfeit") so the player
   gets feedback. The `move_count > 0` half of the gate was the
   reason a fresh-deal G press appeared to do nothing.

The G-key gate in `handle_keyboard_forfeit` is simplified to just
"not paused"; the rest of the forfeit-eligibility check moves into
`handle_forfeit_request` so it can surface the toast.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 02:35:52 +00:00
funman300 6723416a55 feat(engine): convert PauseScreen to modal + add ForfeitConfirmScreen
CI / Test & Lint (push) Failing after 25s
CI / Release Build (push) Has been skipped
Pause overlay drops its bespoke full-screen layout and is rebuilt on
the standard `ui_modal` scaffold: uniform scrim, centred card, real
Resume (Primary, Esc) and Forfeit (Tertiary, G) action buttons. The
Draw Mode row stays inline in the body so the existing toggle still
fires `SettingsChangedEvent`.

The G-key double-press toast countdown is replaced with a real
modal: `G` (or clicking Forfeit on Pause) fires the new
`ForfeitRequestEvent`, which `PausePlugin` answers by spawning
`ForfeitConfirmScreen` at `Z_PAUSE_DIALOG` (above pause). The modal
exposes Cancel + "Yes, forfeit" buttons plus Y/Enter/N/Esc
accelerators; confirmation despawns both modals, clears
`PausedResource`, and fires `ForfeitEvent` for `StatsPlugin`.

`toggle_pause` now early-returns when a forfeit modal is visible (and
runs `.before(handle_forfeit_keyboard)`) so an Esc that closes the
forfeit modal doesn't also re-open pause in the same frame.

The legacy `forfeit_countdown` field, `FORFEIT_CONFIRM_WINDOW`
constant, and the six pure-function countdown tests are removed; new
tests cover the modal-spawn / confirm / cancel paths and the active-
game predicate that still gates the G hotkey.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 02:15:47 +00:00
funman300 afb08799e8 docs: add SESSION_HANDOFF.md mid-overhaul checkpoint
CI / Test & Lint (push) Failing after 23s
CI / Release Build (push) Has been skipped
Pause point in the UX overhaul Phase 3. Documents:
- The 11 commits landed this session (foundation + HUD + 8 overlay
  conversions).
- Test status (797 / 797 pass, clippy clean).
- The 5 steps still pending (Pause + Forfeit, Settings, Onboarding,
  animation upgrades, final literal sweep) and what each touches.
- A smoke-test checklist for the user to run on the current state.
- A copy-pasteable kickoff prompt for the next session.
- An open question about whether to keep, repurpose, or drop the
  Home overlay (now that the action bar + Help cover its surface).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 01:46:35 +00:00
funman300 3b619b8950 feat(engine): convert HomeScreen to modal scaffold + Done button
Phase 3 step 5f of the UX overhaul. Closes the per-overlay
conversion phase: every read-only overlay (Help, Stats, Achievements,
Profile, Leaderboard, and now Home) sits inside the same ui_modal
scaffold, picks colours from ui_theme, and dismisses via a real
"Done" primary button alongside its keyboard accelerator.

Home modal:
- Header: "Solitaire Quest"
- Mode badge: "Current mode: <mode>" in ACCENT_PRIMARY (yellow)
- Two sections (Game Controls / Screens), each rendering keyboard
  shortcuts as kbd-chip rows — the same pattern Help uses, so the
  two reference screens read consistently. Section titles use
  STATE_INFO.
- "L" leaderboard row added so the screens list is now complete.
- Actions: primary Done button with the M hotkey chip.
- handle_home_close_button is the click counterpart to M.

Home overlap with Help is intentional during the overhaul — both
exist as hotkey references for now. A future commit can repurpose
Home as a true mode launcher (the proposal called for this) or
remove it entirely if Help is sufficient. Either path is easier with
both screens already in the consistent shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 01:44:33 +00:00
funman300 37681cf33e feat(engine): convert LeaderboardScreen to modal scaffold + Done button
Phase 3 step 5e of the UX overhaul. Wraps the leaderboard list inside
the standard ui_modal scaffold; converts the Opt In / Opt Out buttons
to use spawn_modal_button (so they pick up the shared hover / press
paint system); replaces "Press L to close" prose with a primary Done
button.

Changes:
- spawn_leaderboard_screen now goes through spawn_modal(LeaderboardScreen,
  Z_MODAL_PANEL, ...). The bespoke 0.82-alpha scrim and hand-rolled
  card surface are gone — same visual contract as every other overlay.
- Opt In becomes a Secondary modal button; Opt Out becomes Tertiary.
  Both fire the same fetch tasks they did before.
- Header / data cells switch to ui_theme tokens. The top-3 podium
  effect now uses ACCENT_PRIMARY (yellow) for #1 and TEXT_PRIMARY for
  #2/#3 instead of metallic-coloured srgb literals; #4+ use
  TEXT_SECONDARY.
- Header-cell and data-cell helpers now take a `&TextFont` so all
  three sizes (HEADLINE / BODY_LG / BODY / CAPTION) come from the
  shared scale instead of inline 13px / 15px sizes.
- "Fetching\u{2026}" loading state uses STATE_INFO; empty-state copy
  uses TEXT_SECONDARY.
- handle_leaderboard_close_button is the click counterpart to L; it
  also sets ClosedThisFrame so update_leaderboard_panel doesn't
  immediately respawn the modal when a fetch completes in the same
  frame.

The sort-by-score code is replaced with `sort_by_key(Reverse(...))`
to satisfy clippy's unnecessary_sort_by lint that surfaced once the
file was otherwise warning-free.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 01:40:59 +00:00
funman300 99064ce808 feat(engine): convert ProfileScreen to modal scaffold + Done button
Phase 3 step 5d of the UX overhaul. Wraps the profile sections (Sync,
Progression, Achievements, Statistics Summary) in the standard modal
scaffold; replaces every inline colour with a ui_theme token; adds an
explicit "Sync" section header so the four sections all read in the
same shape; replaces the "Press P to close" prose hint with a primary
Done button.

The previous bare full-screen scrim + inline-text approach was on the
audit's "feels like a debug panel" list — same fix as Stats /
Achievements / Help.

Section headers now use STATE_INFO at TYPE_BODY_LG, body lines use
TEXT_PRIMARY at TYPE_BODY, secondary lines (sync status, "no
achievements yet") use TEXT_SECONDARY. The achievement-count line
adopts ACCENT_PRIMARY (yellow) and unlocked-achievement entries use
STATE_SUCCESS (green) — same colour vocabulary the Achievements
overlay uses.

The unused `spawn_spacer` helper now takes a `Val` so callers can
pass spacing-token constants directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 01:36:33 +00:00
funman300 de4dba6f98 feat(engine): convert AchievementsScreen to modal scaffold + Done button
Phase 3 step 5c of the UX overhaul. Wraps the achievements list in
the standard ui_modal scaffold, recolours every line via tokens, and
replaces the "Press A to close" caption with a primary Done button.

The achievements list itself keeps its previous shape (unlocked
first then alphabetical, secret achievements hidden until unlocked,
each row showing name + description + reward + unlock date). The
visual changes:

- Headline now comes from spawn_modal_header (TYPE_HEADLINE,
  TEXT_PRIMARY) — was bespoke 26px white.
- Unlocked names use ACCENT_PRIMARY (yellow); descriptions in
  TEXT_PRIMARY at TYPE_BODY.
- Locked names and descriptions use TEXT_DISABLED so they read as
  "future content" without disappearing.
- Reward lines use STATE_SUCCESS (green) at TYPE_CAPTION.
- Unlock dates use TEXT_SECONDARY at TYPE_CAPTION.
- A subtle BORDER_SUBTLE separator follows each row instead of one
  big separator under the header — easier to scan a long list.
- The "✓" / "○" status glyphs stay; their colours come from the
  per-state tokens.

handle_achievements_close_button is the click counterpart to the A
key. font_res threaded through toggle_achievements_screen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 01:32:45 +00:00
funman300 75fc3aa3d6 feat(engine): convert StatsScreen to modal scaffold + Done button
Phase 3 step 5b of the UX overhaul. Wraps the existing 8-cell stats
grid + progression / weekly-goals / time-attack sections inside the
standard modal scaffold. The cell layout (the audit's pick for
"best layout in the codebase") is preserved.

Changes:
- spawn_stats_screen now calls spawn_modal(StatsScreen, ...) and
  populates the card with the same content as before, retoned to
  ui_theme: stat values are TYPE_HEADLINE in ACCENT_PRIMARY (yellow
  numbers pop against the midnight-purple card), labels are TYPE_BODY
  in TEXT_SECONDARY.
- Stat cells lose their 6%-alpha-white fill (clashed with the new
  card surface) and gain a BORDER_SUBTLE outline at RADIUS_SM
  instead — same visual purpose, fits the new palette.
- Section headers ("Progression", "Weekly Goals") use STATE_INFO and
  TEXT_SECONDARY respectively at TYPE_BODY_LG.
- Time Attack callout uses STATE_WARNING.
- "Press S to close" prose hint replaced by a primary "Done" button
  carrying its "S" hotkey chip.

A new handle_stats_close_button system mirrors the keyboard `S`
toggle for clicks. font_res threaded through toggle_stats_screen so
the modal scaffold can pick up FiraMono.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 01:12:55 +00:00
funman300 deb034c5fb feat(engine): convert HelpScreen to real-button modal with kbd-chip rows
Phase 3 step 5a of the UX overhaul. Replaces the old monospace
text-dump (a flat vertical column of "  D            Draw from
stock"-style lines) with a proper modal layout: section titles,
two-column rows where each shortcut renders inside a small
border-outlined chip alongside its description.

Modal contents:
- Header: "Controls"
- Body: three sections (Gameplay / New Game / Overlays), each with a
  section title in TEXT_SECONDARY plus a row per shortcut.
- Each row: a 64 px-min-width chip (caption font, border, radius-sm)
  carrying the key name, then the description in TEXT_PRIMARY at
  TYPE_BODY.
- Actions: a primary "Close" button (hotkey hint "F1").

CONTROL_SECTIONS is a static const-data table of `ControlRow`
records grouped into `ControlSection`s — easier to maintain than the
prior `Vec<String>` of free-form text and easier to extend.

handle_help_close_button is the click counterpart to F1; it
despawns the modal when the player clicks Close.

The audit identified the prior layout as the worst of the
"feels like a 2010 monospace debug dump" overlays. This
restructure is the largest visual upgrade so far in the overhaul.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 01:06:00 +00:00
funman300 242b5fef21 feat(engine): convert GameOverScreen to real-button modal
Phase 3 step 4b of the UX overhaul. Same shape as the Confirm modal
conversion (3f922ed): replace plain "Press N for new game" /
"Press G to forfeit" text hints with real Button entities, hover
and press feedback included.

The audit flagged the Game Over overlay as the second instance of
the "feels like a debug panel" problem. Players had to know the
hotkeys to escape the screen — there was no clickable affordance.

Modal contents:
- Header: "No more moves available"
- Body:   "Final score: {N}" (TYPE_BODY_LG, TEXT_PRIMARY)
- Actions:
    Undo (Secondary, hotkey "U")        — left
    New Game (Primary yellow, hotkey "N") — right

The G/forfeit hint is dropped from the modal because:
1. Forfeit is handled globally by `input_plugin::handle_forfeit`
   (which works whether the modal is up or not).
2. The proposal calls for replacing the toast-countdown forfeit
   flow with its own modal in step 4c (next commit).

A new `handle_game_over_button_input` system mirrors the keyboard
handler for clicks. Existing N/Esc and U accelerators continue to
work via the original `handle_game_over_input`.

The `game_over_screen_text_content` test is updated to assert the
new button-label / hotkey-chip strings instead of the prior prose
hints. All 797 tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 01:01:39 +00:00
funman300 3f922ede28 feat(engine): convert ConfirmNewGameScreen to real-button modal
CI / Test & Lint (push) Failing after 21s
CI / Release Build (push) Has been skipped
Phase 3 step 4a of the UX overhaul. Closes the player's #2 smoke-test
complaint head-on: the abandon-current-game prompt previously rendered
"Yes (Y)" and "No (N)" as plain `Text` entities — not real `Button`s.
Clicks did nothing, hover/press feedback was absent, and the only path
through the modal was the keyboard.

Replace the bespoke 60-line spawn function with a 30-line call to the
ui_modal primitive:

- spawn_modal(ConfirmNewGameScreen, Z_MODAL_PANEL, ...) — uniform
  scrim + centred card with header / body / actions slots.
- Header: "Abandon current game?" (TYPE_HEADLINE, TEXT_PRIMARY).
- Body: "Your progress will be lost." (TYPE_BODY_LG, TEXT_SECONDARY).
- Actions row:
    Cancel (Secondary variant, hotkey "Esc") — left
    Yes, abandon (Primary yellow CTA, hotkey "Y") — right

The ConfirmNewGameScreen marker rides on the scrim entity per
ui_modal's contract; OriginalNewGameRequest is attached to the same
entity after spawn so handle_confirm_input / handle_confirm_button_input
can read it.

A new handle_confirm_button_input system mirrors the keyboard handler
for clicks: it queries `Changed<Interaction>` on `ConfirmYesButton` /
`ConfirmNoButton` and dispatches the same despawn + new-game-fire
logic. Keyboard accelerators (Y/Enter, N/Esc) still work; both paths
reach the same code through the existing `confirmed: true` flag on
NewGameRequestEvent (62cd1cf).

UiModalPlugin's paint_modal_buttons system (8da62bd) handles
hover/press recolouring automatically; no per-modal paint logic
needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 00:51:28 +00:00
funman300 8da62bd05f feat(engine): add ui_modal primitive (scaffold + button variants)
Phase 3 step 3 of the UX overhaul. Adds a reusable modal helper that
the next 6 commits use to convert each overlay screen. The audit found
11 overlays using 3 different visual styles with scrim alpha drift
between 0.60 and 0.92; this primitive collapses all of that into one
consistent shape.

API surface:
- spawn_modal(commands, plugin_marker, z, build_card)  — full-screen
  scrim (uniform SCRIM token) + centred card (BG_ELEVATED, RADIUS_LG,
  BORDER_STRONG outline, max-width 720, min-width 360, padding
  SPACE_5).  Returns the scrim entity for one-call despawn.
- spawn_modal_header(parent, title, font_res)          — TYPE_HEADLINE
  + TEXT_PRIMARY, the canonical overlay heading.
- spawn_modal_body_text(parent, text, color, font_res) — TYPE_BODY_LG
  paragraph; pass TEXT_PRIMARY or TEXT_SECONDARY.
- spawn_modal_actions(parent, build_buttons)           — flex-row
  justify-end with margin-top.
- spawn_modal_button(parent, marker, label, hotkey,
                     variant, font_res)                — real Button
  entity with optional TYPE_CAPTION hotkey-hint chip.

ButtonVariant enum drives colour:
  Primary    idle ACCENT_PRIMARY      hover ACCENT_PRIMARY_HOVER
             pressed ACCENT_SECONDARY (yellow → pink press flash)
  Secondary  idle BG_ELEVATED_HI      hover BG_ELEVATED_TOP
             pressed BG_ELEVATED
  Tertiary   idle BG_ELEVATED         hover BG_ELEVATED_HI
             pressed BG_ELEVATED_PRESSED

A new BG_ELEVATED_TOP token plus ACCENT_PRIMARY_HOVER cover the new
hover/press combinations cleanly.

UiModalPlugin registers paint_modal_buttons so every ModalButton gets
hover and press feedback automatically — overlay plugins don't add
their own paint systems. Plugin registered in solitaire_app.

A self-test asserts each variant's idle / hover / pressed colours are
all distinct; another verifies the plugin builds under MinimalPlugins.

This commit is purely additive — no overlay calls the new helpers
yet. The next commits convert each overlay to use them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 00:43:14 +00:00
funman300 73cad7e205 feat(engine): restructure HUD into 4-tier layout, adopt design tokens
Phase 3 step 2 of the UX overhaul. Closes the player's #1 complaint
("HUD too cluttered") by regrouping the 10 readouts that previously
sat in a single dense horizontal row.

HUD structure (top → bottom):
- Tier 1 (always on)        Score · Moves · Timer
                            Score uses TYPE_HEADLINE so it's the
                            visual protagonist; Moves/Timer use
                            TYPE_BODY_LG with TEXT_SECONDARY tone.
- Tier 2 (mode context)     Mode · Daily-challenge constraint ·
                            Draw-cycle indicator. Each cell is
                            empty when not relevant — the row
                            collapses visually if all are empty.
- Tier 3 (penalty / bonus)  Undos · Recycles · Auto-complete badge.
                            Both penalty counters now share
                            STATE_WARNING — the audit found Undos
                            were amber but Recycles were white,
                            making the "you took a penalty" signal
                            inconsistent.
- Tier 4 (selection chip)   keyboard-driven pile selector.

Action bar polish:
- Each button gains a TYPE_CAPTION hotkey-hint chip (Undo · U,
  Pause · Esc, Help · F1, New Game · N). Menu and Modes get no
  chip because each row in their popovers carries its own hotkey.
- Buttons recoloured to BG_ELEVATED / BG_ELEVATED_HI /
  BG_ELEVATED_PRESSED — the bright-blue palette stood out from
  the rest of the (still-to-come) midnight purple chrome.
- Buttons gain a BORDER_SUBTLE outline so the boundary reads even
  when hovered over the felt.

Other migrations in this commit:
- Popover panels (Menu, Modes) now use BG_ELEVATED instead of an
  ad-hoc dark grey.
- challenge_time_color now returns STATE_DANGER / STATE_WARNING /
  STATE_INFO tokens instead of literal hexes; tests updated.
- The Undos in-place colour toggle uses TEXT_PRIMARY / STATE_WARNING.

The four `ui_theme` self-tests plus all existing 791 tests stay
green (795 total).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 00:30:42 +00:00
funman300 e14852c093 feat(engine): add ui_theme.rs design-token module
Phase 3 step 1 of the UX overhaul. Centralises every UI design token —
colours, typography, spacing, border-radius, z-index, and motion
durations — so subsequent overhaul commits read from one source of
truth instead of scattering hex codes and magic numbers across plugin
files.

The audit (2026-04-30) found:
- 40+ hardcoded Color::srgb literals across UI surfaces.
- 12 distinct font sizes (14/15/16/17/18/22/26/28/30/32/40/48 px)
  with no scale.
- 8+ z-index magic numbers across overlay plugins (200, 210, 220,
  230, 250, 300, 400) with no documented hierarchy.
- Motion durations only partially honouring AnimSpeed — slide and
  cascade did, but toast / shake / settle / deal were hardcoded.

ui_theme.rs collapses these into:
- Midnight Purple base (BG_BASE / BG_ELEVATED / BG_ELEVATED_HI /
  BG_ELEVATED_PRESSED) + Balatro-yellow ACCENT_PRIMARY + warm
  magenta ACCENT_SECONDARY + state colours (success/warning/danger/
  info) + text tiers (primary/secondary/disabled) + a uniform SCRIM.
- 5-rung typography scale (display 40 / headline 26 / body-lg 18 /
  body 14 / caption 11).
- 4-multiple spacing scale (4/8/12/16/24/32/48), with VAL_SPACE_*
  Val::Px convenience constants.
- 3 border-radius rungs (sm 4 / md 8 / lg 16).
- Documented monotonically-increasing z-index hierarchy enforced
  by a unit test.
- All MOTION_* duration constants funnelled through scaled_duration()
  so AnimSpeed (Normal/Fast/Instant) applies to every animation,
  not just slide and cascade.

This commit is purely additive — no call sites change yet.
Subsequent commits in the overhaul migrate plugins to the tokens
one region at a time (HUD restructure, modal primitive, then per-
overlay conversions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 00:20:19 +00:00
funman300 6240156fee feat(engine): add Menu dropdown for Stats/Achievements/Profile/Settings/Leaderboard
CI / Test & Lint (push) Failing after 20s
CI / Release Build (push) Has been skipped
Continues the UI-first pass. The five informational overlays were
each behind a single-key shortcut (S/A/P/O/L) with no visible UI
affordance. Add a "Menu ▾" button to the action bar that toggles a
popover with one row per overlay. Each row dispatches the same code
path the keyboard accelerator uses by writing a new
`Toggle*RequestEvent`:

- Stats        → ToggleStatsRequestEvent
- Achievements → ToggleAchievementsRequestEvent
- Profile      → ToggleProfileRequestEvent
- Settings     → ToggleSettingsRequestEvent
- Leaderboard  → ToggleLeaderboardRequestEvent

Each plugin's existing toggle handler now reads either its key or
the matching request event so the spawn / despawn / fetch logic stays
in the owning plugin (the popover never duplicates that behaviour).

Action bar order is now (left → right):
  Menu ▾   Undo   Pause   Help   Modes ▾   New Game

Menu sits on the far left because it's a navigation aggregator;
New Game stays on the far right as the most consequential action.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 23:55:43 +00:00
funman300 1d9fb1884a feat(engine): add Modes dropdown with Classic/Daily/Zen/Challenge/Time Attack
Continues the UI-first pass. The five game modes were each behind a
keyboard shortcut (N/Z/X/T/C) with no visible UI affordance, three of
them additionally gated by an unlock level the player has to discover
themselves.

Add a "Modes ▾" button to the action bar that toggles a popover panel
beneath. Each row dispatches the same code path the keyboard
accelerator uses by writing a new `Start*RequestEvent` (or
`NewGameRequestEvent` for Classic):

- Classic        → NewGameRequestEvent::default()
- Daily Challenge → StartDailyChallengeRequestEvent
- Zen            → StartZenRequestEvent
- Challenge      → StartChallengeRequestEvent
- Time Attack    → StartTimeAttackRequestEvent

The existing keyboard handlers in input_plugin (Z), challenge_plugin
(X), time_attack_plugin (T), and daily_challenge_plugin (C) now read
either their key or the matching request event, so level gates,
TimeAttackResource setup, daily seed lookup, and toast feedback for
locked modes all stay in their owning plugins — the popover never
duplicates that logic.

The popover only lists modes available to the player: Classic always
shows, Daily Challenge shows when DailyChallengeResource is loaded,
and Zen/Challenge/Time Attack show once the player reaches level 5
(the existing CHALLENGE_UNLOCK_LEVEL).

Click handler despawns the popover after dispatch; clicking the
Modes button again toggles it shut.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 23:49:40 +00:00
funman300 97f38085e3 feat(engine): add Undo, Pause, Help UI buttons in HUD action bar
Continues the UI-first pass started by the New Game button. Per the
design principle in CLAUDE.md / ARCHITECTURE.md §1, every player action
must be reachable from a visible UI control with the keyboard shortcut
as an optional accelerator. Refactor the single New Game button into a
flex-row "action bar" anchored top-right with four buttons: Undo,
Pause, Help, New Game (left → right; New Game rightmost as the most
consequential action).

Plumbing:
- New `PauseRequestEvent` and `HelpRequestEvent` in events.rs.
- pause_plugin::toggle_pause reads either Esc or PauseRequestEvent so
  the button and the keyboard accelerator drive the same code path
  (with the existing drag / game-over / selection guards).
- help_plugin::toggle_help_screen reads either F1 or HelpRequestEvent;
  also fix the stale module-doc claim that H toggles help (it's F1 —
  H is bound to hint cycle in input_plugin).
- hud_plugin now spawns four ActionButton-marked buttons via a
  ChildSpawnerCommands helper, with one click handler per button
  firing its respective request event. A single
  paint_action_buttons system covers hover/pressed colour for all of
  them via the shared ActionButton marker. The click handlers
  defensively re-register their request events so the plugin works in
  isolation under MinimalPlugins (tests). add_message is idempotent.
- ARCHITECTURE.md HudPlugin row updated to call out the action bar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 23:38:54 +00:00
funman300 62cd1cf924 fix(engine): start new game when player confirms abandon-current-game modal
CI / Test & Lint (push) Failing after 19s
CI / Release Build (push) Has been skipped
Reported during 2026-04-29 smoke test: pressing Y on the
ConfirmNewGameScreen modal closed nothing and didn't start a new game.

Trace:
  Frame N: handle_confirm_input despawns the modal entity (deferred),
           writes NewGameRequestEvent.
  End of N: command flush — modal gone.
  Frame N+1: handle_new_game reads the event. needs_confirm is still
             true (game state unchanged). confirm_already_open is now
             false (modal flushed). Condition matches → spawn_confirm_
             dialog runs again, the modal reappears, and the new game
             never starts.

Add a `confirmed: bool` field to NewGameRequestEvent. handle_confirm_
input writes it as true on Y/Enter so handle_new_game's dialog-spawn
guard short-circuits and the existing despawn-and-start branch runs.
All other writers (button click, N hotkey, mode hotkeys, daily/
challenge/time-attack auto-deal, tests) stay at `confirmed: false`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 23:28:48 +00:00
funman300 b10e1a5a87 fix(engine): resize cards along with the rest of the layout
CI / Test & Lint (push) Failing after 24s
CI / Release Build (push) Has been skipped
The first resize-jitter fix (366fd6d) only snapped card transforms,
not the Sprite::custom_size. Cards stayed at the old size after a
window resize until the next StateChangedEvent (move, draw, undo,
new-game) refreshed them via sync_cards_on_change. Reported during
smoke testing: "the placeholder grey boxes change size but the cards
do not until I make an update to the window".

Replace the manual transform-only loop in snap_cards_on_window_resize
with a call to sync_cards(slide_secs = 0.0). update_card_entity
unconditionally inserts a fresh Sprite via card_sprite() with the
current layout.card_size, so cards now visibly resize. With
slide_secs=0 it also takes the snap branch (no CardAnim slide), so
the underlying jitter fix from 366fd6d is preserved.

apply_stock_empty_indicator is still called separately because
sync_cards doesn't touch the stock-empty "↺" label.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:52:16 +00:00
funman300 366fd6d127 fix(engine): snap cards directly on window resize
CI / Test & Lint (push) Failing after 19s
CI / Release Build (push) Has been skipped
on_window_resized was firing StateChangedEvent on every WindowResized
event. That ran sync_cards_on_change → update_card_entity, which
inserts a CardAnim slide tween for every card whose target moves >1
unit. During a corner drag the resize fires every frame, retargeting
the slide each time from the cards' current mid-tween positions, so
cards never reach steady state — the visible "snap back and forth"
jitter reported during the 2026-04-29 smoke test.

Replace the StateChangedEvent emit with a direct snap path:

- Add LayoutSystem::UpdateOnResize SystemSet in layout.rs so cross-
  plugin ordering is explicit (Bevy's automatic conflict-based order
  only forces non-parallel execution, not a particular order).
- table_plugin::on_window_resized: drop the StateChangedEvent emit;
  mark the system in_set(LayoutSystem::UpdateOnResize). It already
  snaps backgrounds and pile markers directly, so this aligns cards
  with the same instant-snap policy.
- card_plugin: new snap_cards_on_window_resize system listens for
  WindowResized, runs .after(LayoutSystem::UpdateOnResize), writes
  fresh transforms via the existing card_positions() helper, and
  removes any in-flight CardAnim. It also reapplies the stock-empty
  indicator so the "↺" label's font_size (derived from
  layout.card_size.x) still rescales on resize.

Other StateChangedEvent listeners — start_settle_anim,
detect_auto_complete, clear_selection_on_state_change, check_no_moves,
reset_hint_cycle_on_state_change, clear_right_click_highlights — no
longer fire spuriously on resize. They should not fire on a layout
change anyway; that was a pre-existing minor bug masked by the
jitter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:44:08 +00:00
funman300 7a77c66f6d fix(engine): restore card to origin slot after rejected drop
When a drag was rejected, ShakeAnim was inserted on each dragged card
with origin_x = transform.translation.x — the drop-location X, not the
origin pile slot's X. tick_shake_anim restores translation.x to
origin_x at the end of the 0.3s shake, which fights the sync_cards
slide that StateChangedEvent triggers and pins the card at the drop
location. The visible symptom (reported during the 2026-04-29 smoke
test) was "the card returns to the slot beside the pile".

Compute the target X using the existing card_position() helper
against the origin pile and the card's stack_index, then save that as
ShakeAnim::origin_x. The shake now ends with the card at its correct
resting slot. Apply the same fix to both the mouse path (end_drag)
and the touch path (touch_end_drag), and update the existing Task #57
test to reflect the new contract (origin_x = origin slot X, not
drop-location X).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:29:20 +00:00
funman300 adece12cf1 feat(engine): add New Game UI button in HUD
CI / Test & Lint (push) Failing after 21s
CI / Release Build (push) Has been skipped
Per the UI-first design principle (CLAUDE.md, ARCHITECTURE.md §1),
every player action must be reachable from a visible UI control with
the keyboard shortcut as an optional accelerator. Add a top-right
"New Game" button that fires NewGameRequestEvent on click; the
existing ConfirmNewGameScreen modal in GamePlugin handles the abandon-
current-game confirmation flow when a game is already in progress.

- NewGameButton marker component, BackgroundColor-styled with idle /
  hover / pressed states.
- spawn_new_game_button startup system anchors the button at the top
  right of the window using absolute positioning.
- handle_new_game_button reads Changed<Interaction> on Pressed and
  writes NewGameRequestEvent::default(); paint_new_game_button
  applies the colour for the current state.

The N key still works as an accelerator. The legacy
NewGameConfirmEvent toast / countdown machinery in InputPlugin is
left in place for now — the button gives players a discoverable
path that bypasses the toast/modal collision reported during the
2026-04-29 smoke test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:06:12 +00:00
funman300 2cfbc32715 docs: add UI-first design principle
Every player-triggered action (new game, undo, draw, pause, open any
overlay, switch mode, etc.) must be reachable from a visible UI
control. Keyboard shortcuts are optional accelerators only — never
the sole entry point. New gameplay features ship with the UI control
alongside the system that backs it.

- ARCHITECTURE.md §1 (Design Principles): add UI-first bullet.
- ARCHITECTURE.md §5 plugin table: rename "Key" column to
  "Shortcut" and add a note that the column lists optional
  accelerators, not primary entry points.
- CLAUDE.md (Bevy Conventions): add a matching hard rule.

Surfaced during smoke testing: the N+N "press again to confirm"
toast collides with the ConfirmNewGameScreen modal because the
keyboard flow is the only entry point. Adding a visible New Game
button (next commit) makes the modal the single source of truth for
the confirm flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 21:59:38 +00:00
funman300 56b37fc653 fix(app): point AssetPlugin at workspace assets dir
CI / Test & Lint (push) Failing after 30s
CI / Release Build (push) Has been skipped
Bevy resolves AssetPlugin::file_path relative to the binary's
CARGO_MANIFEST_DIR (solitaire_app/), but the assets/ directory lives at
the workspace root. After the switch to AssetServer in fbe984c, every
card face, back, background, and font load failed with "Path not found:
.../solitaire_app/assets/..." and the renderer fell back to Text2d
rank+suit placeholders.

Override file_path to "../assets" so cargo run -p solitaire_app from
anywhere finds the real artwork at <workspace>/assets/. Shipping a
release binary will need to either set the override differently or copy
assets/ next to the binary; that is left for whoever ships first.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 21:40:13 +00:00
funman300 3ffde038c5 docs: switch asset pipeline notes to AssetServer model
CI / Test & Lint (push) Failing after 23s
CI / Release Build (push) Has been skipped
Card faces, card backs, board backgrounds, and the UI font are loaded
via Bevy's AssetServer at startup (see commit fbe984c). The CLAUDE.md
hard rule still claimed cards/backgrounds were rendered procedurally
with no AssetServer, and ARCHITECTURE.md §14 / §20 still described
PNGs and TTFs as embedded via include_bytes!(). Update both docs:

- CLAUDE.md hard rule lists which assets ship in assets/ and notes the
  Option<Res<AssetServer>> fallback used under MinimalPlugins (tests).
- ARCHITECTURE.md §2/§3/§5/§14 rewritten to describe the AssetServer
  loaders for CardImageSet, BackgroundImageSet, and FontResource, and
  the Text2d / solid-colour fallbacks.
- ARCHITECTURE.md §20 decision log replaces the two reversed
  embed-via-include_bytes!() entries with a single entry covering the
  switch to AssetServer plus a note that audio remains embedded.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 21:14:48 +00:00
funman300 ece2a55ffb chore(engine): re-export BackgroundImageSet from engine lib
The resource is defined in table_plugin and used by the rest of the
engine, but it was the only one of the prominent table_plugin types not
re-exported from lib.rs. Add it next to PileMarker / TableBackground so
downstream binaries can reference it without reaching into the module
path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 21:14:34 +00:00
funman300 abda354562 feat(engine): emit SyncCompleteEvent on pull resolve
ARCHITECTURE.md §5 lists SyncCompleteEvent(Result<SyncResponse, String>) as
a cross-system event, but it was never declared or fired. Add the message
to events.rs, register it in SyncPlugin, and emit it from poll_pull_result
on both the success path (carrying the merged payload + conflicts as
SyncResponse) and the failure path (carrying the user-facing error
message). UI/persistence systems can now react to sync completion without
polling SyncStatusResource.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 21:14:21 +00:00
funman300 fbe984cf64 feat(engine): switch asset loading to AssetServer with xCards artwork
CI / Test & Lint (push) Failing after 19s
CI / Release Build (push) Has been skipped
Replace compile-time include_bytes!() embedding for card faces, backgrounds,
and font with runtime AssetServer::load() calls. Swap in 52 xCards @2x PNGs
(LGPL-3.0) as card face assets and xCards bicycle_blue as back_0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 20:06:02 +00:00
funman300 efec6f22d5 fix(engine): resolve StatsUpdate system-set scheduling cycle
CI / Test & Lint (push) Failing after 16s
CI / Release Build (push) Has been skipped
update_stats_on_new_game and handle_forfeit ran .before(GameMutation)
while being inside StatsUpdate.  win_summary_plugin constrains
cache_win_data.before(StatsUpdate), which forces the entire StatsUpdate
set to run after GameMutation — creating an unsolvable cycle that panicked
Bevy 0.18's schedule solver at startup.

Only update_stats_on_win (post-GameMutation) belongs in StatsUpdate.
The pre-GameMutation systems still run before GameMutation but outside
the set, so external .before(StatsUpdate)/.after(StatsUpdate) constraints
remain consistent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 04:05:26 +00:00
139 changed files with 6039 additions and 2149 deletions
+19 -14
View File
@@ -51,6 +51,7 @@ Solitaire Quest is a cross-platform Klondike Solitaire game written in Rust, tar
- **No panics in game logic.** Every state transition returns `Result<_, MoveError>`. Panics are only acceptable in startup/configuration code.
- **One language, one repo.** The game client, sync client, shared types, and sync server are all Rust crates in a single Cargo workspace.
- **Plugin-based Bevy architecture.** Each major feature is a Bevy `Plugin`. Systems are small and single-purpose. Cross-system communication uses Bevy `Event`s.
- **UI-first interaction.** Every player-triggered action — new game, undo, draw, pause, open stats / settings / help / profile / leaderboard, etc. — must be reachable from a visible UI control. Keyboard shortcuts exist only as optional accelerators for power users; they are never the sole entry point. A player using only mouse or touch must be able to perform every action. New gameplay features ship with the UI control alongside the system that backs it.
---
@@ -67,11 +68,11 @@ solitaire_quest/
├── Dockerfile # Multi-stage server build
├── docker-compose.yml # Server + Caddy reverse proxy
├── assets/ # Assets embedded at compile time via include_bytes!()
├── assets/ # Loaded at runtime via AssetServer (audio is embedded via include_bytes!())
│ ├── cards/
│ │ ├── faces/{rank}_{suit}.png # 52 individual card faces (120×168, generated by solitaire_assetgen)
│ │ └── backs/back_0.png back_4.png # placeholder patterns
│ ├── backgrounds/bg_0.png bg_4.png # placeholder textures
│ │ ├── faces/{RANK}{SUIT}.png # 52 card faces — xCards @2x artwork (LGPL-3.0)
│ │ └── backs/back_0.png back_4.png # back_0 = xCards bicycle_blue; back_14 are generated patterns
│ ├── backgrounds/bg_0.png bg_4.png # generated textures
│ ├── fonts/main.ttf # FiraMono-Medium (170K, OFL)
│ └── audio/
│ ├── card_deal.wav
@@ -144,7 +145,7 @@ Owns:
- All Bevy UI screens (Home, Stats, Achievements, Settings, Profile)
- Audio playback systems
- Sync status display
- Card, background, and font asset loading (embedded via `include_bytes!()` — no `AssetServer` dependency)
- Card, background, and font asset loading via Bevy `AssetServer` (audio is the lone exception — embedded via `include_bytes!()` in `audio_plugin.rs`)
### `solitaire_server`
**Dependencies:** `solitaire_sync`, `axum`, `sqlx`, `jsonwebtoken`, `bcrypt`, `tower-governor`, `tracing`, `tokio`, `dotenvy`.
@@ -235,11 +236,13 @@ Done
### Bevy Plugins
| Plugin | Key | Responsibility |
The "Shortcut" column lists optional keyboard accelerators. Every action in this table must also be reachable from a visible UI control (button, menu item, on-screen affordance) per the UI-first design principle in §1; the shortcut is a power-user convenience, not the sole entry point.
| Plugin | Shortcut | Responsibility |
|---|---|---|
| `CardPlugin` | — | Card entity spawning, sprite management, drag-and-drop |
| `TablePlugin` | — | Pile markers, background, layout calculation |
| `FontPlugin` | — | Embeds FiraMono-Medium font at compile time; exposes `FontResource` handle |
| `FontPlugin` | — | Loads FiraMono-Medium via `AssetServer` at startup; exposes `FontResource` handle |
| `AnimationPlugin` | — | Slide, flip, win cascade, toast animations |
| `FeedbackAnimPlugin` | — | Shake, settle, and deal-stagger animations |
| `AutoCompletePlugin` | Enter | Executes auto-complete when the HUD badge is lit |
@@ -248,7 +251,7 @@ Done
| `CursorPlugin` | — | Custom cursor sprite during drag |
| `SelectionPlugin` | — | Keyboard-driven card selection |
| `GamePlugin` | N | Core game state resource, new-game flow, win/game-over overlays |
| `HudPlugin` | — | Score, move counter, timer, auto-complete badge |
| `HudPlugin` | — | Score, move counter, timer, auto-complete badge, and the top-right action button bar (Undo / Pause / Help / New Game). Each button fires the same request event the corresponding hotkey does. |
| `StatsPlugin` | S | Stats overlay and persistence |
| `ProgressPlugin` | — | XP/level system, persistence |
| `AchievementPlugin` | A | Unlock evaluation, toast events, persistence |
@@ -296,7 +299,7 @@ struct CardImageSet {
backs: [Handle<Image>; 5], // indexed by selected_card_back setting
}
// Project-wide font handle (FiraMono-Medium embedded at compile time)
// Project-wide font handle (FiraMono-Medium loaded via AssetServer at startup)
struct FontResource(Handle<Font>);
// Pre-loaded background PNG handles
@@ -772,11 +775,13 @@ Audio systems listen for Bevy events and never block the game thread.
### Rendering approach
Cards are Bevy `Sprite` entities with `Handle<Image>` from `CardImageSet`. Face-up cards use one of 52 individual face PNGs selected by `faces[suit][rank]` — rank and suit are baked into each image and no `Text2d` overlay is spawned. Face-down cards use `backs/back_N.png` indexed by `settings.selected_card_back`. `Text2d` labels are only used as a fallback when `CardImageSet` is absent (e.g. tests with `MinimalPlugins`). `CardImageSet` is populated at startup from `include_bytes!()` — no `AssetServer`.
Cards are Bevy `Sprite` entities with `Handle<Image>` from `CardImageSet`. Face-up cards use one of 52 individual face PNGs selected by `faces[suit][rank]` — rank and suit are baked into each image and no `Text2d` overlay is spawned. Face-down cards use `backs/back_N.png` indexed by `settings.selected_card_back`. `Text2d` labels are only used as a fallback when `CardImageSet` is absent (e.g. tests with `MinimalPlugins`). `CardImageSet` is populated at startup by `card_plugin::load_card_images` via `AssetServer::load()`.
Backgrounds are Bevy `Sprite` entities with `Handle<Image>` from `BackgroundImageSet`. `BackgroundImageSet` is populated at startup from `include_bytes!()`.
Backgrounds are Bevy `Sprite` entities with `Handle<Image>` from `BackgroundImageSet`. `BackgroundImageSet` is populated at startup by `table_plugin::load_background_images` via `AssetServer::load()`.
The font `FiraMono-Medium` is embedded via `include_bytes!()` at startup by `FontPlugin` and exposed as `FontResource` for use by all UI and text systems.
The font `FiraMono-Medium` is loaded via `AssetServer::load("fonts/main.ttf")` at startup by `FontPlugin` and exposed as `FontResource` for use by all UI and text systems.
All three loaders take `Option<Res<AssetServer>>` so they degrade cleanly under `MinimalPlugins` in tests: when the server is absent, `CardImageSet`/`BackgroundImageSet` are inserted with empty handle slots and the plugins fall back to `Text2d` rank+suit overlays and solid-colour board backgrounds. The `assets/` directory must ship alongside the binary.
The `assets/` directory layout:
@@ -1004,5 +1009,5 @@ Using `axum::test` and an in-memory SQLite database:
| `SyncProvider` trait, not `SyncBackend` match arms | `SyncPlugin` stays backend-agnostic and testable; new backends can be added without touching the plugin | 2026-04-20 |
| Dropped WebDAV backend | Redundant once the self-hosted server exists; removing it reduces surface area and simplifies settings UI | 2026-04-20 |
| Dropped GPGS backend | Redundant with the self-hosted server; adds JNI complexity for no user-visible benefit on the target platforms | 2026-04-28 |
| PNG assets embedded via `include_bytes!()` | Using `Image::from_buffer()` in startup systems rather than `AssetServer::load()` keeps the binary self-contained and eliminates runtime file-not-found errors | 2026-04-29 |
| FiraMono-Medium font embedded via `include_bytes!()` | Exposed through `FontResource`; avoids runtime font loading errors on headless systems and ensures consistent text rendering across all platforms | 2026-04-29 |
| Card, background, and font assets loaded via `AssetServer` | Reverses the earlier embed-via-`include_bytes!()` decision: PNGs and TTFs are loaded at runtime so artwork can be swapped (e.g. xCards @2x faces, alternate card backs, themed backgrounds) without a recompile, and binary size stays small. Loaders take `Option<Res<AssetServer>>` and fall back gracefully under `MinimalPlugins`. The `assets/` directory must ship alongside the binary. | 2026-04-29 |
| Audio assets remain embedded via `include_bytes!()` | Audio files are small, change rarely, and the embedded path eliminates a class of runtime-load errors during gameplay; the asset-pipeline reversal does not extend to audio | 2026-04-29 |
+4 -1
View File
@@ -47,7 +47,9 @@ cargo clippy -p solitaire_core -- -D warnings
- `solitaire_core` and `solitaire_sync` must never gain Bevy or network dependencies.
- No `unwrap()` or `panic!()` in game logic. All state transitions return `Result<_, MoveError>`.
- Audio assets are embedded at compile time using `include_bytes!()` in `audio_plugin.rs`. Cards and backgrounds are rendered procedurally (colored `Sprite` entities + text) — no image files are used and no `AssetServer` is needed.
- Audio assets are embedded at compile time using `include_bytes!()` in `audio_plugin.rs`.
- Card faces (52 PNGs in `assets/cards/faces/`), card backs (`assets/cards/backs/back_N.png`), board backgrounds (`assets/backgrounds/bg_N.png`), and the UI font (`assets/fonts/main.ttf`) are loaded at runtime via `AssetServer::load()` and stored as `Handle<Image>`/`Handle<Font>` in the `CardImageSet`, `BackgroundImageSet`, and `FontResource` resources. The `assets/` directory must ship alongside the binary.
- Asset-loading systems take `Option<Res<AssetServer>>` so they degrade cleanly under `MinimalPlugins` (tests). When `CardImageSet` is absent, `card_plugin` falls back to a `Text2d` rank+suit overlay; when `BackgroundImageSet` is absent, the board falls back to a solid colour.
- Atomic file writes only: write to `filename.json.tmp`, then `rename()`.
- Passwords and tokens are stored in the OS keychain via the `keyring` crate — never in plaintext files or logs.
- Sync runs on `AsyncComputeTaskPool` — never block the Bevy main thread.
@@ -75,6 +77,7 @@ cargo clippy -p solitaire_core -- -D warnings
- Resources own shared state. Events communicate between systems. Components own per-entity data.
- All UI screens are built with Bevy UI (`bevy::ui`). Never mix UI layout and game logic in the same system.
- Layout is recomputed on `WindowResized` — never assume a fixed window size.
- **UI-first.** Every player-triggered action (new game, undo, draw, pause, open stats / settings / help / profile / leaderboard, switch mode, etc.) must be reachable from a visible UI control. Keyboard shortcuts are optional accelerators — never the sole entry point. New gameplay features ship with the UI control alongside the system that backs it; do not merge a feature that is keyboard-only.
---
+284
View File
@@ -0,0 +1,284 @@
# Solitaire Quest — UX Overhaul Session Handoff
**Last updated:** 2026-04-30 — Phase 3 complete + Phase 4 in progress (Track B landed on disk, Track G subset in flight via background agent).
## ⚠️ In-progress work at pause time
Smoke-test passed; Phase 4 was started. Pushed HEAD is `534870a`. The working tree has **uncommitted** work that is NOT pushed:
### Track B — window polish (on disk, ready to commit)
- **File:** `solitaire_app/src/main.rs` (+44 lines)
- **What landed:**
- X11/Wayland WM_CLASS via `Window::name = Some("solitaire-quest".into())`
- Default position `WindowPosition::Centered(MonitorSelection::Primary)`
- `install_crash_log_hook()` wraps the default panic hook to also append a `crash.log` next to `settings.json`. Uses `std::time::SystemTime` (no new chrono dep). Falls through silently if the data dir is unavailable.
- **Skipped this round (deferred):**
- App icon hookup — no artwork asset exists yet; add the loader path when art lands.
- Persisted window geometry — needs a `Settings` schema migration.
- F11 fullscreen toggle — already wired in `input_plugin.rs:114`, no change needed.
- **Build status:** `cargo build -p solitaire_app` clean; `cargo clippy -p solitaire_app -- -D warnings` clean.
- **Suggested commit subject:** `feat(app): window polish — class name, centered position, crash-log hook`
### Track G subset — modal open animation + score-change feedback (in flight)
- A **background agent** (`general-purpose`, no worktree) was launched against this turn's tree to:
- Extend `spawn_modal` in `solitaire_engine/src/ui_modal.rs` with a `ModalEntering` component + `advance_modal_enter` system that animates scrim alpha 0 → `SCRIM` and card scale 0.96 → 1.0 over `MOTION_MODAL_SECS`. Respects `AnimSpeed::Instant` via `scaled_duration`. Animate-OUT path is intentionally out of scope.
- In `solitaire_engine/src/hud_plugin.rs`, add a `ScorePulse` 1.0→1.1→1.0 readout pulse over `MOTION_SCORE_PULSE_SECS` and a floating "+N" Text2d (only for ≥ +50 jumps) that drifts up ~40 px and fades over `MOTION_SCORE_PULSE_SECS * 2`.
- Tests for both behaviours.
- **State at pause:** the agent had partial edits in `solitaire_engine/src/ui_modal.rs` (visible via `git status`) — at least one unused-import warning was already surfacing. It had not reported back when this snapshot was taken.
- **Resume options for the next session:**
1. **Wait for the notification.** The agent runs in background; if Claude Code is still alive, the completion notification will fire.
2. **Inspect and finish manually.** `git diff solitaire_engine/src/ui_modal.rs solitaire_engine/src/hud_plugin.rs` to see what landed; finish or revert and restart with a tighter prompt.
3. **Discard and restart.** `git restore solitaire_engine/src/ui_modal.rs solitaire_engine/src/hud_plugin.rs` then relaunch the agent with the prompt below.
### Next-session workflow at pause
1. Verify the workspace builds cleanly with **all** in-flight changes: `cargo build --workspace && cargo clippy --workspace -- -D warnings && cargo test --workspace`. The Track B `main.rs` change is independent — even if Track G is reverted, B compiles on its own.
2. If Track B is clean and Track G is incomplete or broken: commit Track B first using the subject above, then deal with Track G.
3. If both are clean: commit each as a separate landing — one feature per commit per project convention.
4. Use:
```
git -c user.name=funman300 -c user.email=root@vscode.infinity commit -m "<subject>"
```
5. Push with `git push origin master` (requires interactive credentials on `git.aleshym.co`).
### Original Track G subset prompt (for relaunch if needed)
The agent's full brief is preserved here verbatim — paste into a fresh agent if the current one is unrecoverable:
```
Two UI/UX polish items from track G. Tree clean at HEAD `534870a`.
Sub-agents CANNOT git commit — stage your work; orchestrator commits.
G1. Modal open animation: extend spawn_modal in ui_modal.rs with a
ModalEntering component + advance_modal_enter system that animates
scrim alpha 0 → SCRIM and card scale 0.96 → 1.0 over MOTION_MODAL_SECS.
Use scaled_duration for AnimSpeed respect; ease-out curve t*(2-t).
Register the system in UiModalPlugin::build. Animate-OUT is OUT of
scope. Add ≥2 tests covering ModalEntering presence on spawn and
removal after duration elapses.
G2. Score-change feedback in hud_plugin.rs: ScorePulse component that
scales the score Text 1.0→1.1→1.0 over MOTION_SCORE_PULSE_SECS using
triangular curve. Plus a floating "+N" Text2d (only for ≥ +50 jumps)
in ACCENT_PRIMARY that drifts up 40 px and fades over
MOTION_SCORE_PULSE_SECS * 2. Add ≥2 tests for floater spawn on +50
and despawn after lifetime, plus ≥1 test that +5 does NOT spawn.
Hard requirements: workspace build + clippy --workspace -- -D warnings
+ test --workspace all green. Touch ONLY ui_modal.rs, hud_plugin.rs,
optionally ui_theme.rs for new tokens (don't think you'll need any).
DO NOT touch solitaire_app/src/main.rs (parallel work).
```
---
## Where we are (Phase 3)
Phase 3 of the UX overhaul brief is **done**. The whole engine has been migrated to the `ui_theme` design-token system + `ui_modal` scaffold. Animation system upgraded. Final literal sweep landed. The work spans 17 commits this session, from the foundation (`e14852c`) through to the final sweep (`54e024c`).
### Design direction (already saved as project memory)
- **Tone:** Balatro — chunky readable type, theatrical hierarchy, satisfying micro-interactions.
- **Palette:** Midnight Purple base (`BG_BASE` `#1A0F2E` → `BG_ELEVATED` `#2D1B69` → `BG_ELEVATED_HI` `#3A2580` → `BG_ELEVATED_TOP` `#482F97`) + Balatro yellow primary accent (`ACCENT_PRIMARY` `#FFD23F`) + warm magenta secondary (`ACCENT_SECONDARY` `#FF6B9D`).
- See [memory/project_ux_overhaul_2026-04.md](.claude/projects/-home-manage-Rusty-Solitare/memory/project_ux_overhaul_2026-04.md) for the full direction.
### Top complaints from the original smoke test — all closed
1. **HUD too cluttered.** ✅ Closed by `73cad7e` — readouts now sit in a 4-tier vertical stack with progressive disclosure of penalty/bonus tiers.
2. **Y/N keyboard prompts feel like debug panels.** ✅ Closed across Confirm, GameOver, Pause, Forfeit, and Settings modals — every prompt now has real Primary/Secondary/Tertiary buttons with hover/press feedback.
## Foundation (done)
- **`solitaire_engine/src/ui_theme.rs`** — every design token: colours, 5-rung typography scale, 4-multiple spacing scale, three radius rungs, monotonically-ordered z-index hierarchy, motion durations with `scaled_duration(speed)` helper.
- **`solitaire_engine/src/ui_modal.rs`** — `spawn_modal` scaffold + `spawn_modal_header` / `spawn_modal_body_text` / `spawn_modal_actions` / `spawn_modal_button` helpers + `ButtonVariant` enum (Primary / Secondary / Tertiary) + `paint_modal_buttons` system. `UiModalPlugin` registered in `solitaire_app/src/main.rs`.
## Commits this session (Phase 3, latest first)
```
54e024c chore(engine): final literal-to-token sweep
3a01318 feat(engine): upgrade animations — curves, scoped settle, deal jitter, cascade rotation
79d3917 chore(data): derive Copy on AnimSpeed
ba019c0 feat(engine): convert SettingsPanel to modal scaffold + Done button
18d7c12 feat(engine): convert OnboardingPlugin to 3-slide modal flow
cb93bd9 fix(engine): pin modals via GlobalZIndex and surface forfeit-no-op toast
6723416 feat(engine): convert PauseScreen to modal + add ForfeitConfirmScreen
afb0879 docs: add SESSION_HANDOFF.md mid-overhaul checkpoint
3b619b8 feat(engine): convert HomeScreen to modal scaffold + Done button
37681cf feat(engine): convert LeaderboardScreen to modal scaffold + Done button
99064ce feat(engine): convert ProfileScreen to modal scaffold + Done button
de4dba6 feat(engine): convert AchievementsScreen to modal scaffold + Done button
75fc3aa feat(engine): convert StatsScreen to modal scaffold + Done button
deb034c feat(engine): convert HelpScreen to real-button modal with kbd-chip rows
242b5fe feat(engine): convert GameOverScreen to real-button modal
3f922ed feat(engine): convert ConfirmNewGameScreen to real-button modal
8da62bd feat(engine): add ui_modal primitive (scaffold + button variants)
73cad7e feat(engine): restructure HUD into 4-tier layout, adopt design tokens
e14852c feat(engine): add ui_theme.rs design-token module
```
**Test status:** `cargo build --workspace` clean, `cargo clippy --workspace -- -D warnings` clean, **819 tests pass / 0 failed / 8 ignored**.
## Smoke-test checklist
The whole overhaul is on disk. Worth running through once end-to-end:
1. **Run the game.** `cargo run -p solitaire_app --features bevy/dynamic_linking`.
2. **HUD layout** reads as 4 stacked tiers (Score / Mode / Penalty / Selection) with the new midnight-purple palette.
3. **Open every overlay** — `S` (Stats), `A` (Achievements), `P` (Profile), `O` (Settings), `L` (Leaderboard), `M` (Home), `F1` (Help). Each is a centred card on a uniform scrim with a yellow `Done` / `Close` primary button. Hover/press states on every button.
4. **Settings.** Four sections (Audio / Gameplay / Cosmetic / Sync). Body scrolls within the modal on small windows; `Done` button stays fixed at the bottom regardless of scroll. Card-back / Background pickers tint the selected swatch with `STATE_SUCCESS`.
5. **Confirm flow.** Click `New Game` while a game is in progress — the abandon-current-game modal has real Cancel/Confirm buttons. `Y/Enter` and the yellow primary button start a new game; `N/Esc` and the secondary button cancel.
6. **Pause + Forfeit.** Press `Esc` — pause modal shows real Resume / Forfeit buttons. Forfeit button opens a Cancel/Forfeit confirmation modal stacked above the pause modal (z-index ordered correctly via `GlobalZIndex`).
7. **First-run onboarding.** Delete `settings.json` (or set `first_run_complete = false`) — three-slide flow shows: Welcome → How to play → Keyboard shortcuts. Navigate with `Next` / `Back` buttons or `` / `` accelerators. `Esc` skips on slide 0.
8. **Animations.**
- Slide a card to a pile — motion curves through `SmoothSnap` (slight overshoot + settle), not linear lerp.
- Drop a card on a valid destination — only the moved cards bounce; the rest of the table stays still.
- Start a new game — deal stagger is no longer mechanically uniform; cards land with subtle ±10% timing variation.
- Win a game — cascade now uses `Expressive` curve with per-card ±15° Z-rotation, screen shake driven by the new `MOTION_WIN_SHAKE_*` tokens.
9. **Resize the window** — cards still snap, no "snap-back-and-forth" jitter.
10. **Win modal** — restyled with the design tokens: midnight-purple card, yellow `Play Again` button.
## Open follow-ups (not blockers)
- **Home / Help redundancy.** Home is still a kbd-reference modal that mostly duplicates Help. Three options: (1) keep as-is, (2) convert into a true mode launcher (Classic / Daily / Zen / Challenge / Time Attack cards, locked options visibly disabled below level 5), (3) drop entirely now that the action bar covers everything Home does. Worth asking the user which direction they want.
- **Forfeit countdown toast** is now superseded by the Forfeit modal (`6723416`). Confirm the toast path is no longer reachable when smoke-testing.
- **Sub-rung pixel sizes** (1 px borders, 64/80/110/150/160 px fixed widths, 28/36/50 px specific spacings) were intentionally left as literals during the step-10 sweep — they're below the smallest `SPACE_*` rung. If the design system grows a "fine" spacing tier in the future, those become candidates for migration.
## Resume prompt for the next session
```
You are a senior Rust + Bevy developer working toward a public release
of Solitaire Quest. Working directory: /home/manage/Rusty_Solitare.
Branch: master. Apply that lens to every decision: prefer shipping
quality (polish, packaging, defaults, credits, crash safety) over
greenfield features. If something is half-done, the question is
"finish for v1 or cut for v1?" not "what else can we add?".
State: HEAD=0066ca6. Phase 3 of the UX overhaul is shipped. cargo
build / clippy --workspace -- -D warnings / test --workspace all
green — 819 tests pass / 0 fail / 8 ignored.
READ FIRST (in order, before doing anything):
1. SESSION_HANDOFF.md — full state, smoke-test checklist, follow-ups
2. CLAUDE.md — hard rules (UI-first, no panics, etc.)
3. ARCHITECTURE.md §1, §15, §17 — design principles, platform
targets, deployment guide
4. ~/.claude/projects/-home-manage-Rusty-Solitare/memory/MEMORY.md
— saved feedback / project context
GATING SIGNAL — ASK FIRST, DON'T ASSUME:
Before proposing new work, ask: "Did the smoke-test (items 1-10 in
SESSION_HANDOFF.md) pass, or did anything regress?" If a regression
exists, fix it before opening any new thread.
LIKELY NEXT DIRECTIONS — surface for the user to choose, don't pick
unilaterally. All framed through "what does v1 release need?":
A. Home modal decision (open in SESSION_HANDOFF.md).
- keep as kbd-reference (duplicates Help — release-blocking
confusion?)
- repurpose as mode launcher (Classic / Daily / Zen / Challenge /
Time Attack cards, locked options below level 5)
- drop (action bar already covers every action)
B. Window + release polish — `solitaire_app/src/main.rs:34-48`
currently sets only title + resolution + min size. For public
release the window needs:
- app icon (taskbar / dock / alt-tab) — Bevy `Window::window_icon`
or platform `set_window_icon`; ship a .png/.ico asset.
- window class / app id (`Window::name`) so X11/Wayland and
Windows group taskbar entries correctly.
- persist size + position across launches (Settings already
saves to JSON; add `window_geometry` field).
- F11 (or a Settings toggle) wired to real fullscreen mode.
- centered default position on first launch (Bevy supports
`WindowPosition::Centered`).
- present_mode + vsync verification — make sure Linux/macOS
don't ship at uncapped 4000 fps.
- panic hook (`std::panic::set_hook`) that writes a crash
report next to the save files instead of silently exiting.
- macOS Info.plist / Windows .ico bundling — ARCHITECTURE.md
§17 currently only covers server deploy.
C. Sound-design audit. The scoped settle bounce (3a01318) means
audio_plugin.rs trigger sites may fire less often than before;
verify card_place / card_flip / card_invalid still feel right.
D. Sync flow end-to-end on a real second machine. Server
scaffolding exists but the register → push → pull → restore-on-
other-device round trip hasn't been exercised against the new
Settings sync section.
E. Achievement unlock completeness. ARCHITECTURE.md §11 lists 18.
The three hidden ones (speed_and_skill, comeback, zen_winner)
are most likely to be untested. For release, every advertised
achievement needs to actually fire.
F. Release-readiness backlog:
- README / store-page copy / screenshots
- LICENSE + third-party credits (xCards art, FiraMono, Bevy)
- SemVer + a v0.1.0 git tag
- itch.io / Steam packaging per platform (ARCHITECTURE.md §15)
- App signing — macOS notarization, Windows Authenticode,
Linux AppImage
- Telemetry / crash reporting — opt-in, off by default; or
confirm we ship without and rely on player reports
G. UI/UX professional polish — Phase 3 shipped the design system;
v1 wants the difference between "consistent" and "feels
intentional":
- Microcopy pass: every button label, empty state, error
message, and onboarding line reviewed for voice + clarity.
Pick one verb per concept ("Done" vs "Close" vs "OK") and
apply it everywhere.
- Empty / loading / error states: Leaderboard before any
scores, Stats before any games, Sync UI before login.
Today these are likely blank panels.
- Modal open/close animation: `MOTION_MODAL_SECS` token exists
in `ui_theme.rs:255` but isn't wired up — modals
appear/disappear instantly. Add scale-from-0.96 + scrim fade
per the token's doc comment.
- Tooltips on HUD readouts and settings labels. Bevy has no
built-in tooltip; build a small one. Hover a number to learn
what it counts.
- Accessibility: verify the AAA-contrast claim on
`ACCENT_PRIMARY` over `BG_BASE` (ui_theme.rs:65). Confirm
`AnimSpeed::Instant` disables every new animation (slide
curve, scoped settle, deal jitter, cascade rotation). Add
focus rings on `Button` entities for keyboard navigation.
- Typography choice: FiraMono is one weight, monospace for
everything. Consider shipping a second proportional face for
body + headings, keep mono for numerics (HUD score, timer).
Or commit to mono and lean into the "calm coder" feel — pick
deliberately and document the decision.
- Onboarding artwork: the 3 slides are text + buttons. For
release, stylised illustrations (or simple animated card
props on each slide) elevate the first-launch feel.
- Score-change feedback: floating "+N" numbers when score
jumps; pulse on the readout when value crosses a milestone.
`MOTION_SCORE_PULSE_SECS` is already a token.
- Splash / loading screen: today the window goes straight to
gameplay. A 1-2 second branded splash signals "real game"
vs "rust prototype".
- Hit-target audit: every interactive element ≥ 32 px on
desktop. Settings has 28 px icon buttons (`ICON_BUTTON_PX`
in settings_plugin.rs); revisit.
- Win-moment design: the cascade is good; consider a score-
breakdown reveal, streak callout, "share your time"
affordance for v1.
WORKFLOW NOTES:
- Commits use:
git -c user.name=funman300 -c user.email=root@vscode.infinity commit -m "..."
- Sub-agents can Edit/Write but CANNOT `git commit`. Brief them to
stage + verify only; orchestrator commits on their behalf.
See memory/feedback_agent_commit_limit.md.
- Remote push needs interactive credentials on git.aleshym.co; the
user runs `git push origin master` themselves.
- Every commit must pass build / clippy / test. Pause-and-verify
is the user's preferred cadence — one feature per commit.
OPEN AT THE START: ask (1) did smoke-test pass, (2) which of AG to
pursue first. Do not assume.
```
Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Some files were not shown because too many files have changed in this diff Show More