Commit Graph

267 Commits

Author SHA1 Message Date
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 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 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
funman300 7cda2a9f1a fix(engine): resolve all clippy warnings introduced by PNG asset pipeline
CI / Test & Lint (push) Failing after 1m34s
CI / Release Build (push) Has been skipped
- Collapse nested-if patterns into let-chains across 13 plugins (42 instances)
- Add #[allow(clippy::too_many_arguments)] to 5 Bevy systems in card_plugin
  and input_plugin where ECS parameter count exceeds the lint threshold
- Gate Theme import in table_plugin under #[cfg(test)] — only used by
  test-only colour helpers; removing the unconditional import silences the
  unused-import lint without breaking the test suite
- Wrap ButtonInput<MouseButton> in Option<> in update_input_platform so that
  tests using MinimalPlugins (no InputPlugin) no longer panic on startup

All 789 tests pass; cargo clippy --workspace -- -D warnings is clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 03:35:41 +00:00
funman300 08202f9351 docs(engine): update card_plugin module comment for PNG-based rendering
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 01:24:45 +00:00
funman300 e22fcadb22 feat(engine,assetgen): generate 52 individual card face PNGs
Replace the single shared face.png placeholder with 52 individual card
face images (120×168 px each), generated by the updated gen_art tool:

- solitaire_assetgen: add ab_glyph dep; rewrite gen_art to render each
  card with FiraMono rank characters, programmatic suit symbols (heart,
  spade, diamond, club drawn via circles/triangles), and standard pip
  layout for numbered cards (A–10) plus large face letter for J/Q/K.
- CardImageSet: replace single `face` handle with `faces: [[Handle; 13]; 4]`
  indexed by [suit][rank].
- card_sprite(): select the per-card face image by suit/rank indices.
- spawn/update_card_entity: suppress Text2d overlay when PNG faces are
  loaded (rank/suit baked into image); keep overlay in solid-colour
  fallback for tests.
- gen_sfx.rs: rename `gen` variable to `make` (reserved keyword in 2024).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 01:20:31 +00:00
funman300 18ac5adef5 feat(engine): art pass — PNG assets, custom font, and keyring v4 upgrade
Art pass (Phase 4):
- Generate placeholder PNG assets: face.png, back_0–4.png, bg_0–4.png via
  solitaire_assetgen gen_art binary (16×16 RGBA, embedded via include_bytes!)
- Add FiraMono-Medium font (assets/fonts/main.ttf) embedded at compile time
- Add FontPlugin: loads font at startup, exposes FontResource; gracefully
  falls back to default handle when Assets<Font> absent (MinimalPlugins tests)
- Wire CardImageSet into card_plugin: face/back PNGs replace solid-colour
  sprites when available; tests continue using colour fallback via MinimalPlugins
- Wire BackgroundImageSet into table_plugin: bg PNGs replace solid-colour
  background; empty set inserted when Assets<Image> absent in tests
- Fix hint highlight system (input_plugin): tint sprite.color directly instead
  of replacing the whole Sprite (which would discard the image handle)
- Export FontPlugin, FontResource, CardImageSet from solitaire_engine::lib
- Register FontPlugin in solitaire_app before other plugins

Dependency upgrades (latest releases):
- keyring "2" → keyring "4" + keyring-core "1" (v4 split architecture into
  separate core library crate)
- auth_tokens.rs: Entry::new now returns Result; delete_password →
  delete_credential; NoDefaultStore error variant handled
- solitaire_app: add keyring::use_native_store(true) at startup for Linux
  Secret Service / macOS Keychain / Windows Credential Store selection

ARCHITECTURE.md: fix Edition 2025→2021, update asset pipeline section,
add FontPlugin/CardImageSet/BackgroundImageSet to plugin and resource tables,
update Section 14 to reflect actual include_bytes!() rendering approach,
add Decision Log entries for embedded PNG and font decisions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 00:30:55 +00:00
funman300 41d75b50de feat/fix/perf(engine,data,assetgen): ambient audio, sync bug fixes, hot-path cleanup
**ambient_loop.wav (task 5)**
- solitaire_assetgen: add ambient_loop() synthesizer — 5 s seamless loop,
  55 Hz drone with 2nd/3rd harmonics, 0.2 Hz LFO breath, 16-bit mono 44100 Hz
- audio_plugin: load ambient_loop.wav via include_bytes!() replacing the
  card_flip.wav placeholder; decouple start_ambient_loop() from SoundLibrary

**sync bug fixes (task 11)**
- sync_plugin: LocalOnlyProvider returning UnsupportedPlatform now sets
  SyncStatus::Idle instead of displaying a misleading "Sync not configured" error
- sync_client: extract_pull_body / extract_push_body now return SyncError::Auth
  only for HTTP 401/403; all other non-2xx statuses return SyncError::Network
- sync_plugin: push_on_exit now logs a warn! on failure instead of silently
  discarding the result

**hot-path performance (task 12)**
- card_plugin: card_positions() now returns &Card references (lifetime-bound to
  GameState) instead of owned Card clones — eliminates 52 Card clones per
  sync_cards() call (runs every animation frame)
- input_plugin: card_position() takes &PileType instead of PileType, eliminating
  PileType copies at every drag hit-test call site
- animation_plugin: eliminate intermediate AnimSpeed clone in handle_win_cascade()

**docs (tasks 11, 13)**
- docs/sync_test_runbook.md: manual test runbook for cross-machine sync
- docs/android_investigation.md: cargo-mobile2 port investigation and effort estimate

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:51:58 +00:00
funman300 4bd562671e chore(data,engine,docs): remove Google Play Games Services sync backend
Deletes the solitaire_gpgs crate and all GPGS references from settings,
sync client, profile plugin, CLAUDE.md, and ARCHITECTURE.md. The
self-hosted server covers all sync needs without the Android-only backend.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:22:25 +00:00
funman300 8221ebc803 fix(engine): replace EventReader with MessageReader for TouchInput events
EventReader was removed in Bevy 0.18 in favour of MessageReader.
Three touch drag/tap handlers used the old type, causing compile errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:00:53 +00:00
funman300 800dfb50ce chore(pkg): add Arch Linux PKGBUILDs for game client and sync server
- pkg/solitaire-quest/PKGBUILD: builds solitaire_app binary, depends on
  alsa-lib, libxkbcommon, systemd-libs (Bevy Linux requirements); check()
  runs only non-Bevy crates (solitaire_core, solitaire_sync) since Bevy
  integration tests require a GPU/display unavailable in chroot
- pkg/solitaire-quest-server/PKGBUILD: builds solitaire_server binary,
  installs systemd service unit and hardened environment file template
- pkg/solitaire-quest-server/solitaire-quest-server.service: systemd unit
  with ProtectSystem=strict, NoNewPrivileges, dedicated service user
- pkg/solitaire-quest-server/server.env: documented env template installed
  to /etc/solitaire-quest-server/server.env (mode 0640, listed in backup=)
- LICENSE: add MIT license
- Cargo.toml: add license = "MIT" to [workspace.package]
- All member crates: add license.workspace = true

Both PKGBUILDs follow the Arch Rust package guidelines:
  prepare() uses --locked + cargo fetch
  build() uses --frozen --release -p <crate>
  RUSTUP_TOOLCHAIN=stable and CARGO_TARGET_DIR=target set in each stage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 22:44:44 +00:00
funman300 735d8766a2 docs(engine): add missing doc comments on layout, ProgressPlugin; fix audio format in ARCHITECTURE.md
- Add field-level doc comments to Layout::card_size and Layout::pile_positions
- Add struct-level doc comment to ProgressPlugin
- Fix ARCHITECTURE.md Section 14: .ogg → .wav throughout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 22:37:07 +00:00
funman300 2407686e13 fix(engine,gpgs,core,server): export CardFaceRevealedEvent, explicit gpgs stub, enum/constant docs
- engine/lib.rs: re-export CardFaceRevealedEvent so external crates can consume flip-midpoint audio events
- gpgs/stub.rs: add explicit impls for all six defaulted SyncProvider methods; future trait changes now cause a compile error in the stub rather than silently picking up wrong defaults
- core/game_state.rs: add /// doc comments to DrawMode and GameMode variants
- server/auth.rs: replace terse BCRYPT_COST comment with full /// doc comment matching ARCHITECTURE.md §19
- server/leaderboard.rs: add /// doc comment to DISPLAY_NAME_MAX; fix misplaced comment that was prepended to the opt_in handler instead of the constant

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 22:30:22 +00:00
funman300 1ec2593137 fix(engine): resolve input coordination bugs in selection/pause/keyboard
- SelectionPlugin: add clear_selection_on_state_change system so undo/move/reject
  never leave a stale selection pointing at the wrong card
- SelectionPlugin: expose SelectionKeySet system set for cross-plugin ordering
- PausePlugin: skip Escape→pause when a card is keyboard-selected; toggle_pause
  now runs before SelectionKeySet so it reads SelectionState before it is cleared
- InputPlugin: guard Space→DrawRequestEvent when SelectionState has an active pile
  so Space executes a card move instead of also drawing from stock
- window: enforce 800×600 minimum via WindowResizeConstraints
- game_state: add precondition doc to next_auto_complete_move (waste exclusion)
- card_plugin: 12 unit tests for constants, face_colour, label_visibility, label_for
- pause_plugin: add paused_resource_default and draw_mode_label exhaustiveness tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 22:13:10 +00:00
funman300 ffc79447d4 fix+refactor+docs: P0–P3 todo list items
P0 fixes:
- Register WinSummaryPlugin, SelectionPlugin, CardAnimationPlugin in main.rs
  (all three were exported but never wired — features silently did nothing)
- game_state::draw(): increment move_count on waste→stock recycle, not just
  on normal draws; add move_count_increments_on_recycle regression test

P1 fixes:
- solitaire_server/Cargo.toml: remove duplicate dev-dependencies
  (solitaire_sync, uuid, chrono, jsonwebtoken were in both sections)

P2 — input_plugin refactor:
- Split 198-line handle_keyboard() into three focused systems under 110 lines each:
  handle_keyboard_core (U/N/Z/D/Space), handle_keyboard_hint (H), handle_keyboard_forfeit (G)
- Introduce KeyboardConfirmState resource to share countdown timers across systems
- Add three new unit tests: all_hints_suggests_draw_*, all_hints_is_empty_when_truly_stuck,
  new_game_confirm_window_is_positive

P2 — achievement predicate tests (solitaire_core):
- Add 10 direct unit tests for speed_demon, lightning, no_undo, high_scorer,
  on_a_roll, comeback predicates (previously only covered via check_achievements())
- 141 core tests now passing

P2 — server tests:
- solitaire_server/src/sync.rs: 4 unit tests for merge logic (no DB required)
- solitaire_server/src/leaderboard.rs: 2 unit tests for entry shape and sort order

P3 — documentation:
- Add struct-level ///  to 12 Plugin structs (ChallengePlugin, CursorPlugin,
  AnimationPlugin, HelpPlugin, PausePlugin, AudioPlugin, DailyChallengePlugin,
  HudPlugin, LeaderboardPlugin, OnboardingPlugin, TimeAttackPlugin, WeeklyGoalsPlugin)
- Add field-level /// to Card, Pile, Deck, GameState, AchievementContext, AchievementDef
- Add /// to WeeklyGoalKind, WeeklyGoalDef, WeeklyGoalContext, StatsExt::update_on_win

card_animation module (new files from previous session):
- chain.rs, diagnostics.rs, tuning.rs, updated interaction.rs/animation.rs/mod.rs/lib.rs
- Remove unused HOVER_SCALE_DEFAULT / DRAG_LIFT_SCALE_DEFAULT / HOVER_LERP_SPEED_DEFAULT constants
- Add handle_touch_stock_tap so touch users can draw from the stock pile

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 22:02:52 +00:00
funman300 71c0c273a1 chore(deps): migrate kira 0.9 → 0.12
- Import paths simplified: manager/tween modules re-exported from kira root
  (AudioManager, AudioManagerSettings, DefaultBackend, Tween all via kira::*)
- Volume::Amplitude removed; replaced with Value<Decibels> using a new
  amplitude_to_decibels() helper (20*log10 conversion, clamps to SILENCE)
- output_destination field removed from StaticSoundSettings; sounds routed
  to sub-tracks by calling TrackHandle::play() directly instead of
  AudioManager::play()
- set_volume() now accepts f32 (Decibels) not f64
- start_ambient_loop signature updated to take &mut Option<TrackHandle>

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 13:54:01 -07:00
funman300 21d0c289b5 chore(deps): migrate to Bevy 0.18
- BorderRadius is no longer a Component; moved into Node.border_radius
  field at all 15 spawn sites across 6 plugin files
- Events<T> renamed to Messages<T> in test code (12 files)
- KeyboardEvents SystemParam renamed to KeyboardMessages to match the
  MessageWriter rename done in the 0.17 hop
- WindowResolution::from((f32,f32)) removed; use (u32,u32) tuple in main.rs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 13:48:41 -07:00
funman300 648cd44387 chore(deps): migrate to Bevy 0.17
- Event/EventReader/EventWriter renamed to Message/MessageReader/MessageWriter
- add_event → add_message for all 67 call sites
- ScrollPosition changed to tuple struct ScrollPosition(Vec2)
- CursorIcon import moved from bevy::winit::cursor to bevy::window
- WindowResolution::from((f32,f32)) removed — use (u32,u32) tuple
- World::send_event → World::write_message in test code

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 13:04:44 -07:00
funman300 c8553dc8c5 chore(deps): migrate to Bevy 0.16, axum 0.8, and other package updates
- Bump bevy 0.15 → 0.16; fixes all breaking API changes:
  ChildBuilder → ChildSpawnerCommands, Parent → ChildOf,
  despawn_descendants → despawn_related::<Children>(),
  despawn_recursive → despawn (now recursive by default),
  EventWriter::send → write, Query::{get_single,get_single_mut}
  → {single,single_mut}, ChildOf::get → parent()
- Bump axum 0.7 → 0.8; remove axum::async_trait from FromRequestParts
- Bump tower_governor 0.4 → 0.8; fix GovernorLayer::new() API
- Bump jsonwebtoken 9 → 10 with rust_crypto feature only
- Bump thiserror 1 → 2, dirs 5 → 6, bcrypt 0.15 → 0.19,
  reqwest 0.12 → 0.13 (rustls feature rename)
- Regenerate .sqlx offline cache for sqlx compile-time query checks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 12:31:12 -07:00
funman300 eedddb979e feat(engine): add curve-based card animation module
Introduces solitaire_engine::card_animation — a drop-in upgrade over the
existing linear CardAnim. Supports MotionCurve easing, parabolic z-lift,
scale interpolation, delay, retargeting mid-flight, and per-card timing
variation. Coexists with the legacy AnimationPlugin during migration.

Also adds .claude/ to .gitignore so Claude Code local tooling is never
committed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 18:06:58 +00:00
funman300 59a023ed5e chore(workspace): fix all clippy warnings in test code
Resolves 15 violations found by `cargo clippy --workspace --tests -D warnings`:
- Remove unused imports (Card, Rank) in cursor_plugin tests
- Replace absurd i32::MAX comparison with a meaningful >= 0 check
- Use range .contains() instead of manual >= && <= (manual_range_contains)
- Move impl FromRequestParts before test module in middleware.rs (items_after_test_module)
- Move _VEC3_REFERENCED const before test module in input_plugin.rs
- Convert runtime assert on constant to const { assert!(...) }
- Use .contains() instead of .iter().any() for slice membership
- Replace .get(...).is_none() with !.contains_key(...) in HashMap checks
- Collapse Default::default() + field assignment into struct literal initializers
  across solitaire_sync, solitaire_data, and solitaire_engine test helpers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 18:02:27 +00:00
funman300 8cd28cfb29 feat(engine): right-click highlight timer and visual hint glow (#5, #6)
Task #5: Add RightClickHighlightTimer(1.5 s) so destination highlights
auto-despawn after 1.5 s. Existing clear-on-state-change and
clear-on-pause logic still fires early when a move is made or the game
is paused. Three unit tests cover timer countdown behaviour.

Task #6: Add HintVisualEvent emitted on H key. Source card gets
HintHighlight + HintHighlightTimer(2 s) for a yellow glow. Destination
PileMarker gets HintPileHighlight with a gold tint (Color::srgb(1.0,
0.85, 0.1)) that restores the original colour when the 2 s timer
expires. Five unit tests cover timer expiry and colour invariants.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 17:36:23 +00:00
funman300 03227f8c77 feat(engine): playability improvements — rounds 7–9 (#40–#64)
Round 7 — Input & feedback
- H key cycles hints; F1 opens help (conflict resolved)
- N key cancels active Time Attack session
- Hint text distinguishes "draw from stock" vs "recycle waste"
- Forfeit (G) clears AutoCompleteState so chime does not bleed into new deal
- Zen mode timer clears immediately on Z press
- HUD shows recycle count in both draw modes
- Settings scroll position persisted across open/close

Round 8 — Polish & clarity
- Undo unavailable fires "Nothing to undo" toast
- Streak-break toast on forfeit/abandon when streak > 1
- F11 fullscreen toggle with toast; documented in help and home screens
- H-after-win, new-game countdown expiry, Tab-no-cards toasts
- Win cascade duration/stagger scales with animation speed setting
- Draw-Three cycle counter HUD ("Cycle: N/3")
- Forfeit requires G×2 confirmation within 3 s (mirrors N key)

Round 9 — Game feel & information
- Escape dismisses game-over/stuck overlay (PausePlugin skips Escape when overlay visible)
- Shake animation on rejected drag before snap-back
- Forfeit countdown cancels when any other key is pressed (U/H/D/Z/Space)
- Tab wrap-around fires "Back to first card" toast
- HUD selection indicator shows active Tab-selected pile in yellow
- Challenge time-limit HUD turns orange < 60s, red < 30s
- Win summary shows XP breakdown (+50 base, +25 no-undo, +N speed)
- Game-over overlay: "No more moves available" with clear N/Escape/G instructions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 02:35:15 +00:00
funman300 d387ee68d7 feat(engine): stats improvements, toast queue, keyboard selection (#65, #66, #67, #68)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 20:07:49 +00:00
funman300 1c6094dc93 feat(engine): auto-complete badge, confirm dialog, game-over overlay (#56, #57, #58)
Task #56 — HudAutoComplete badge: shows "AUTO" in green when AutoCompleteState.active
is true; announce_auto_complete system fires InfoToastEvent on leading edge.
Task #57 — ConfirmNewGameScreen: intercepts NewGameRequestEvent when move_count > 0 and
is_won=false, shows modal with Y/N keyboard handling.
Task #58 — GameOverScreen: spawns when check_no_moves detects no legal moves; handle_game_over_input
responds to N (new game) and U (undo + despawn). Fix animation system ordering
(chain enqueue_toasts → drive_toast_display) to eliminate flaky test.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 20:07:47 +00:00
funman300 f32e53dd0b feat(engine): shake/settle/deal animations (#54, #55, #69)
Add FeedbackAnimPlugin with three card feedback animations:
- #54 ShakeAnim: horizontal shake on MoveRejectedEvent targeting
  destination pile cards; 0.3 s damped sine wave
- #55 SettleAnim: Y-scale bounce on valid placement (StateChangedEvent);
  1.0 → 0.92 → 1.0 over 0.15 s for all top-of-pile cards
- #69 Deal animation: slides each card from stock position to its deal
  position on NewGameRequestEvent (move_count == 0), using existing
  CardAnim with 0.04 s per-card stagger

Pure-function helpers shake_offset, settle_scale, and deal_stagger_delay
are public and covered by 6 unit tests. Fix pre-existing compile/clippy
errors: stubbed handle_confirm_input/handle_game_over_input, removed dead
CycleCardBack/CycleBackground variants, annotated ambient_handle field,
and fixed draw_mode.clone() in pause_plugin.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 19:55:24 +00:00
funman300 ddd7502a06 feat(engine): playability improvements — input intelligence, audio, HUD, onboarding (#27–#30, #37, #39–#40, #44, #48–#49)
Task #27: Double-click auto-move — best_destination() finds optimal target
(foundation over tableau); handle_double_click() fires MoveRequestEvent.

Task #28: Hint system — find_hint() returns first legal from/to/count triple;
H key tints the source stack HintHighlight (yellow pulse via tick_hint_highlight).

Task #29: No-moves detection — has_legal_moves() checks stock/waste/all face-up
cards; check_no_moves system fires InfoToastEvent("No moves available") once per
stalemate (debounced so it fires only once until the state changes).

Task #30: Forfeit — G key fires ForfeitEvent; StatsPlugin records abandoned game,
persists stats, starts a new deal.

Task #37: Mute-all (M) and mute-music (Shift+M) toggles; MuteState resource
applied in apply_volume_on_change.

Task #39: Daily challenge HUD constraint label (time limit / target score).

Task #40: Undo-count HUD label; amber colour when undos > 0.

Task #44: Win-streak and level line on pause screen.

Task #48: Undo sound routes UndoRequestEvent → lib.flip audio channel.

Task #49: Onboarding banner rich-text key highlights — D and H rendered as
orange KeyHighlightSpan children so they stand out from body text.

Also registers CursorPlugin in solitaire_app (tasks #31/#32 wire-up).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 19:11:47 +00:00
funman300 c3ee7c45a7 feat(engine): card visual improvements — flip animation, foundation/tableau placeholders, drag shadow
Task #34: CardFlipAnim component + start_flip_anim/tick_flip_anim systems animate revealed
cards by squashing scale.x to 0 then expanding back to 1 (2×0.08 s). Skipped at Instant speed.

Task #35: spawn_pile_markers now adds a Text2d child (S/H/D/C, 45% alpha) on Foundation
markers so the suit is visible while the pile is empty.

Task #43: Tableau pile markers get a "K" Text2d child (35% alpha) indicating only Kings land
on empty columns.

Task #38: update_drag_shadow system maintains a single ShadowEntity while dragging — a
card_w+8 × card_h+8 dark semi-transparent sprite at z−1 behind the top dragged card.

Also fixed pre-existing clippy/compiler errors in hud_plugin, pause_plugin, stats_plugin,
cursor_plugin, and settings_plugin (missing imports, too-many-arguments, doc formatting).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 19:03:59 +00:00
funman300 4d132afdc2 test(engine): add unit tests for format_reward variants in achievement_plugin
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 05:22:22 +00:00