Commit Graph

89 Commits

Author SHA1 Message Date
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 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 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 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 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 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 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 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
root de840fb006 feat(engine,server): XP toast on win + display_name max-length validation
ProgressPlugin now fires XpAwardedEvent on every win. AnimationPlugin
shows a "+N XP" toast so players see XP feedback immediately after
winning. Server leaderboard opt-in endpoint also now validates that
display_name is at most 32 characters.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 03:02:59 +00:00
root 11cb53ab29 feat(engine): InfoToastEvent — show locked-mode messages on-screen
Replaces silent info!() log calls with on-screen toasts when the player
presses Z/X/T without reaching the required unlock level. Any system
can now fire InfoToastEvent(message) to surface a brief text overlay
without depending on a specific plugin.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 02:57:34 +00:00
root 4a33cbdc22 feat(engine): require N-key confirmation when abandoning an active game
Pressing N during an active game (move_count > 0, not won) now shows a
"Press N again to start a new game" toast and only starts a new game if
N is pressed a second time within 3 seconds. Starting a fresh game or
pressing N after a win still acts immediately.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 02:53:28 +00:00
root fff8c66bf7 feat(engine): in-game HUD — score, move count, elapsed time, mode badge
Adds HudPlugin with a persistent top-left overlay that shows score,
move count, and elapsed time during every game. A mode badge highlights
DAILY, CHALLENGE, ZEN, or TIME ATTACK when the game is not in Classic
mode. HUD updates whenever GameStateResource changes (moves and per-second
time ticks) without a separate polling system.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 01:51:50 +00:00
root d56abcd7a9 feat(engine): leaderboard screen (press L to toggle)
Press L to open a leaderboard overlay. On open, an async fetch is
dispatched against the active SyncProvider. Results cache in
LeaderboardResource; the panel rebuilds live when data arrives.
Closing during a fetch is safe (ClosedThisFrame flag prevents
re-spawning the panel in the same frame as the user's despawn command).
Format: ranked table with player name, best score, and fastest win time.
Non-authenticated / LocalOnly providers return an empty list gracefully.

- solitaire_data: add fetch_leaderboard() to SyncProvider trait
  (default → Ok([])) and implement in SolitaireServerClient
- solitaire_engine: new LeaderboardPlugin with 5 unit tests
- solitaire_app: register LeaderboardPlugin

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 00:38:55 +00:00
root a7b781cd36 feat(engine): achievements screen (press A to toggle)
Adds a full-screen overlay listing all achievements with unlock status.
Unlocked achievements show in gold with a check mark; locked ones are
greyed out. Secret achievements that are still locked are hidden. Header
shows unlocked/total count. Press A again to dismiss.

Two new unit tests: spawns on first A press, dismisses on second.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 00:27:21 +00:00
root f7850c0075 feat(engine): auto-complete — cards auto-deal to foundations
When is_auto_completable flips true (stock/waste empty, all cards
face-up), AutoCompletePlugin fires MoveRequestEvent every 120 ms,
driving cards to the foundation one at a time without player input.
An "Auto-completing…" toast announces the sequence.

- solitaire_core: add next_auto_complete_move() to GameState with
  3 new unit tests
- solitaire_engine: new AutoCompletePlugin with detect + drive systems
  and 4 unit tests; animation_plugin shows one-shot toast on activation
- solitaire_app: register AutoCompletePlugin

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 00:24:21 +00:00
root 00f0383867 feat(engine): in-progress game state persistence
Save game_state.json on app exit and on pause open so players can
resume interrupted sessions. Delete the file on win, loss, or new-game
start. Restore the saved game on launch if it exists and isn't won.

- solitaire_core: add pile_map_serde module so HashMap<PileType,Pile>
  round-trips through JSON (serialized as Vec of pairs)
- solitaire_data: add game_state_file_path, load_game_state_from,
  save_game_state_to, delete_game_state_at with 8 new unit tests
- solitaire_engine/GamePlugin: restore saved game on startup, expose
  GameStatePath resource, save on AppExit, delete on new-game and win
- solitaire_engine/PausePlugin: save on pause open (guards against
  OS-level kills while the overlay is showing)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 00:17:47 +00:00
root 20db4b312a feat(engine): ManualSyncRequestEvent + Sync Now button in settings
- Added ManualSyncRequestEvent to events.rs (exported from lib.rs).
- SyncPlugin now handles ManualSyncRequestEvent: if no pull is in
  flight, spawns a new AsyncComputeTaskPool task and sets status to
  Syncing. Ignores duplicate requests while a pull is active.
- Settings panel "Sync" section now shows the status text alongside a
  "Sync Now" button that fires ManualSyncRequestEvent.
- Cleaned up stale doc comment in input_plugin.rs (Esc pause note).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 00:03:24 +00:00
root 34ba4dc6ed feat(workspace): full server + sync implementation, all tests green
- solitaire_server: Axum auth, sync push/pull, leaderboard, daily
  challenge, account deletion, JWT middleware, rate limiting via
  tower_governor, SQLite migrations, health endpoint
- solitaire_server: expose build_test_router (no rate limiting) so
  integration tests work without a peer IP in oneshot requests
- solitaire_sync: SyncPayload, merge logic, shared API types
- solitaire_data: SyncProvider trait, LocalOnlyProvider,
  SolitaireServerClient, auth_tokens keyring integration, blanket
  Box<dyn SyncProvider> impl
- solitaire_data/settings: derive Default on SyncBackend (clippy fix)
- .sqlx/: offline query cache so server compiles without a live DB
- sqlx: removed non-existent "offline" feature flag
- keyring v2: fixed Entry::new() returning Result<Entry>
- sqlx 0.8: all SQLite TEXT columns wrapped in Option<T>
- Integration tests: max_connections(1) on in-memory pool so all
  connections share the same schema

All 191 tests pass; cargo clippy -D warnings clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 23:32:56 +00:00
funman300 13b428b81c feat(engine): first-run onboarding banner
OnboardingPlugin spawns a centered welcome banner at PostStartup
when Settings.first_run_complete is false. Any key or mouse
press dismisses it, sets the flag, and persists settings.json
so returning players never see it again.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 23:14:10 -07:00
funman300 9d0f9478b2 feat(data,engine): persistent Settings + SFX volume hotkeys
- solitaire_data::Settings { sfx_volume, first_run_complete } with
  atomic JSON persistence and clamping sanitizer.
- SettingsPlugin (engine): [ / ] adjust SFX volume by 0.1, clamped;
  persists on change; emits SettingsChangedEvent. No-op at rails.
- AudioPlugin applies sfx_volume to kira's main track at startup
  and on every change so live tweaks take effect without restart.
- Brief "SFX: N%" toast on each change. Help cheat sheet updated.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 23:08:20 -07:00
funman300 b720588687 feat(engine): MoveRejectedEvent + PausePlugin (Esc)
- New MoveRejectedEvent fires from end_drag when the cursor is over
  a real pile but the placement is illegal. AudioPlugin plays
  card_invalid.wav on it.
- New PausePlugin + PausedResource: Esc toggles a full-window
  overlay and the flag. tick_elapsed_time and advance_time_attack
  skip work while paused. Help cheat sheet updated.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 22:56:35 -07:00
funman300 adacdf533c feat(engine,assetgen): synthesized SFX + kira AudioPlugin
- New solitaire_assetgen crate with gen_sfx binary: synthesizes
  five 44.1kHz mono 16-bit PCM WAVs (flip/place/deal/invalid/fanfare)
  from an LCG noise source + sine/square synths. Output committed
  under assets/audio/.
- AudioPlugin (engine): embeds the WAVs via include_bytes!, decodes
  once with kira::StaticSoundData, plays on Draw / Move / NewGame /
  GameWon events. card_invalid is loaded but unused — wiring it
  needs a MoveRejectedEvent.
- AudioManager kept on the main thread (NonSend) since cpal is !Send
  on some platforms; degrades gracefully if no audio device present.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 22:48:58 -07:00
funman300 7dfbff45d1 feat(engine): add HelpPlugin (H/?) and Challenge cleared toast
- HelpPlugin: full-window cheat sheet listing every keybinding,
  toggled with H or ?. Three unit tests cover open/close/slash.
- AnimationPlugin: ChallengeAdvancedEvent now surfaces as a
  3-second "Challenge N cleared!" toast.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 22:39:08 -07:00
funman300 193410200e feat(engine,core): add Time Attack mode + unlocks panel
- Core: GameMode::TimeAttack variant (no scoring/undo changes — session marker only)
- Engine: TimeAttackPlugin with TimeAttackResource, TimeAttackEndedEvent,
  T hotkey (gated to level >= 5), auto-deal on win, summary toast
- Engine: Stats overlay (S) gains an Unlocks subsection (card backs /
  backgrounds, sorted/deduped) and a live Time Attack panel while active

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 17:27:53 -07:00
funman300 788ac9f65a feat(engine,core,data): add Challenge mode with seed list and level-5 gate
Phase 6 part 4b (partial):
- GameMode::Challenge variant in solitaire_core. undo() returns
  RuleViolation when mode is Challenge so the player commits to each
  decision.
- solitaire_data::challenge defines a stable CHALLENGE_SEEDS list with
  challenge_seed_for(index) wrapping modulo length.
- PlayerProgress.challenge_index (serde-default for older saves) tracks
  how far the player has progressed.
- ChallengePlugin advances the cursor on Challenge-mode wins, persists,
  and emits ChallengeAdvancedEvent. Pressing X starts a Challenge-mode
  game with the current seed; gated to level >= CHALLENGE_UNLOCK_LEVEL (5).
- InputPlugin's Z key (Zen mode) is now also gated to level >= 5.

Time Attack and unlock UI still deferred.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 17:18:32 -07:00
funman300 b730902d76 feat(engine): add weekly goals with ISO-week rollover and +75 XP bonus
Phase 6 part 2b:
- solitaire_data::weekly defines WeeklyGoalKind, WeeklyGoalDef,
  WeeklyGoalContext, current_iso_week_key, and three starter goals
  (5 wins, 3 no-undo wins, 3 fast wins).
- PlayerProgress gains weekly_goal_week_iso, roll_weekly_goals_if_new_week,
  and record_weekly_progress (returns true exactly once per goal completion).
- WeeklyGoalsPlugin evaluates GameWonEvent against WEEKLY_GOALS, rolls the
  week if needed, increments matching counters, awards WEEKLY_GOAL_XP for
  newly-completed goals, persists progress, and fires
  WeeklyGoalCompletedEvent.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 19:25:18 -07:00
funman300 622b35a3bf feat(engine): add daily challenge, level-up toast, and daily_devotee achievement
Phase 6 part 2 (partial):
- daily_seed_for(date) and PlayerProgress::record_daily_completion in
  solitaire_data, with streak logic that increments on consecutive days,
  resets on a skipped day, and is idempotent on same-day re-completions.
- DailyChallengePlugin tracks today's seed, awards +100 XP and updates
  the streak when the player wins a game whose seed matches. Pressing C
  starts a new game with the daily seed.
- LevelUpEvent toast in AnimationPlugin announces level changes.
- AchievementContext gains daily_challenge_streak; daily_devotee
  achievement unlocks at streak >= 7. AchievementPlugin reads
  ProgressResource and runs after ProgressUpdate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 19:17:59 -07:00
funman300 ef043c14d4 feat(engine): add ProgressPlugin awarding XP on wins with level-up events
On GameWonEvent, computes xp_for_win(time, used_undo) from
solitaire_data, calls PlayerProgress::add_xp, and emits LevelUpEvent
when the level changes. Persists atomically through the configurable
storage path; ProgressPlugin::headless() disables I/O for tests.

Introduces ProgressUpdate system set so future systems can run after
progress mutations.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 19:11:22 -07:00
funman300 1f6994a084 feat(engine): add AchievementPlugin with persistent unlock tracking
On GameWonEvent, build an AchievementContext from StatsResource + GameState
+ wall-clock hour, evaluate ALL_ACHIEVEMENTS, flip newly-satisfied records
to unlocked, persist atomically, and emit AchievementUnlockedEvent for
each new unlock. AnimationPlugin's toast resolves the event's ID to the
achievement's display name via achievement_plugin::display_name_for.

Introduces StatsUpdate system set so AchievementPlugin can reliably run
after StatsResource reflects the win. AchievementPlugin::headless() used
in tests to avoid touching ~/.local/share/solitaire_quest/achievements.json.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 12:53:31 -07:00
funman300 2ce11f8f4d feat(engine): add StatsPlugin with persistent stats and toggleable overlay
StatsPlugin loads stats on startup, persists them on every GameWonEvent
and abandoned NewGameRequestEvent (>=1 move, not won), and provides a
full-window overlay toggled with `S` showing games played/won, win rate,
streak, best score, fastest win, and average win time.

The storage path is configurable via StatsPlugin::storage_path: the
default ctor uses dirs::data_dir(); StatsPlugin::headless() disables
I/O entirely so tests don't read or overwrite the user's real
stats.json. record_abandoned runs before GameMutation so it reads
move_count before handle_new_game clobbers it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 12:43:49 -07:00
Solitaire Quest 4d2379c426 feat(engine): add AnimationPlugin with slide, cascade, and toast (Phase 3F)
- CardAnim component lerps cards from old to new position on every move
- card_plugin now adds CardAnim instead of teleporting cards on state change
- Snap-back on invalid drag reuses the same mechanism (StateChangedEvent)
- Win cascade flies all 52 cards off-screen with staggered delay on GameWonEvent
- Achievement toast scaffold wired to AchievementUnlockedEvent (Phase 5 content)
- Fix input_plugin test: click Queen's visible strip, not geometric centre

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 21:32:42 -07:00
funman300 900de7f376 feat(engine): add InputPlugin with keyboard and stock-click
Keyboard: U=undo, N=new game, D=draw, Escape=pause placeholder (logged
only until the pause screen lands). Mouse: left-click on the stock pile
fires DrawRequestEvent. Cursor coordinates are converted via the active
Camera2d's viewport_to_world_2d so the hit-test works under arbitrary
camera setups.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 16:26:40 -07:00
funman300 0a87f0f8f2 feat(engine): add CardPlugin with procedural card rendering
Each card is a parent Sprite (white for face-up, blue for face-down) with
a Text2d child showing rank+suit (e.g. "AH", "10C", "KS"). Hearts and
diamonds render red; clubs and spades black. Face-down labels are hidden.
Tableau cards fan downward; other piles stack at the same position with a
small z-offset.

Sync runs in PostStartup for the initial deal and in Update after every
StateChangedEvent. To avoid a one-frame lag, downstream plugins run after
the new GameMutation system set.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 16:22:49 -07:00
funman300 d92b4a8648 feat(engine): add layout, LayoutResource, and TablePlugin
compute_layout is a pure function that maps window size to card size and
the 13 pile positions, with clamping at the 800x600 minimum and seven
tableau columns horizontally aligned with stock/waste (cols 0,1) and the
four foundations (cols 3,4,5,6). TablePlugin spawns a 2D camera, a felt
background sprite, and 13 translucent pile-marker sprites, and
repositions them on WindowResized. Plugin registers WindowResized
explicitly so it works under MinimalPlugins in tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 16:18:24 -07:00
funman300 c393eab17d feat(engine): add resources, events, and GamePlugin event routing
Introduces the plumbing layer for Phase 3: GameStateResource wraps
solitaire_core::GameState, DragState tracks in-progress drags, and
SyncStatusResource holds runtime sync status. GamePlugin routes
Draw/Move/Undo/NewGame request events into GameState and emits
StateChangedEvent and GameWonEvent for downstream systems.

Also adds the Phase 3 implementation plan under docs/superpowers/plans/.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 16:15:38 -07:00
Solitaire Quest 684f07746d feat(workspace): initialize all seven crates with stubs and blank Bevy window
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 11:00:42 -07:00