Commit Graph

19 Commits

Author SHA1 Message Date
funman300 4b9d008be2 refactor(workspace): sweep low-risk clippy::pedantic findings
Conservative cleanup pass — applied only the high-signal pedantic
lints whose fixes either remove genuine waste or read more naturally,
skipping anything stylistic that would bloat the diff.

- map_unwrap_or: 29 .map(...).unwrap_or(...) sites collapsed to
  .map_or / .is_some_and / .map_or_else equivalents
- uninlined_format_args: 7 production format!/write!/println! sites
  rewritten to the inline-argument style; assert! sites in test code
  intentionally untouched
- match_same_arms: 2 redundant arms collapsed where the bodies were
  identical and the merger didn't obscure intent

Public API is unchanged. No dependencies added or removed. The
pedantic warning count dropped from 840 to 807 (-33). Out-of-scope
findings — needless_pass_by_value on Bevy Res params, false-positive
explicit_iter_loop on Bevy Query iterators, items_after_statements
inside test mods, and the "ask before changing" merge logic in
solitaire_sync — were intentionally deferred.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 02:46:32 +00:00
funman300 74482252d1 feat(engine): tooltips on Achievements screen rows
Each achievement row now carries a Tooltip whose text is derived from
the row's unlock state and the AchievementDef's reward, surfacing
information the row layout doesn't already show.

Four-state policy:
- Unlocked + reward → "Reward: <reward>." (e.g. "Reward: Card Back #1.")
- Unlocked + no reward → "Earned!"
- Locked, non-secret → "How to unlock: <description>." plus
  " Reward: <reward>." when one exists
- Locked, secret → no tooltip; the existing row-spawn skip preserves
  the achievement's discovery surprise

The row spawn loop tags each row with a new AchievementRow marker so
tests can locate them; the helper tooltip_for_row keeps the policy in
one place.

Six tests pin the policy: one full-flow test for unlocked + reward
mention, one secret-row negative test that asserts no tooltip
contains the verbatim secret condition or the secret reward, plus
four direct unit tests on tooltip_for_row covering all four states.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 02:18:04 +00:00
funman300 2e080d02ce test(engine): integration coverage for draw_three_master and zen_winner
Closes the audit gap: the two achievements that previously had only
unit-level condition tests now also have full-flow tests that fire a
GameWonEvent and assert the unlock state through the same plugin
ordering production uses (update_stats_on_win runs before
evaluate_on_win, so the freshly bumped stat is visible to the
condition closure).

Four tests, headless under MinimalPlugins:
- draw_three_master_fires_on_tenth_draw_three_win — pre-seed 9 wins,
  fire a Draw3 win, assert unlock
- draw_three_master_does_not_fire_at_nine_wins — pre-seed 8, fire a
  Draw3 win bumping to 9, assert still locked
- zen_winner_fires_on_zen_mode_win — Zen-mode win unlocks the badge
- zen_winner_does_not_fire_for_classic_win — Classic win in same
  fixture leaves it locked

After this commit every advertised achievement has an integration
test that exercises the production unlock path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 22:28:32 +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 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 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 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 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 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
root fe986ef4a1 fix(engine): fire XpAwardedEvent for achievement BonusXp rewards
The Reward::BonusXp path in evaluate_on_win was adding XP directly
without sending XpAwardedEvent, so players saw no "+25 XP" toast when
the no_undo achievement first unlocked. Adds the missing event send
and a regression test verifying the event fires on unlock.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 03:44:29 +00:00
root 46dd9cdfab feat(engine): achievements screen shows reward and unlock date per entry
Each achievement row now displays:
- "Reward: Card Back #N / Background #N / +N XP / Badge" (green, all entries)
- "Unlocked YYYY-MM-DD" (dim, unlocked entries only)

Helps players understand what they earn and when they earned it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 02:38:16 +00:00
root f579b96d76 feat(engine): wire AnimSpeed to animation, new achievements, leaderboard opt-in, daily goal display
- AnimSpeed setting now drives card slide duration (Normal=0.15s, Fast=0.07s, Instant=snap);
  EffectiveSlideDuration resource updated on SettingsChangedEvent; AnimSpeed row added to Settings panel
- GameState.recycle_count tracks waste recycles; perfectionist/comeback/zen_winner achievements added
  with full unit tests
- SyncProvider gains opt_in_leaderboard(); SolitaireServerClient implements POST /api/leaderboard/opt-in;
  Opt In button added to leaderboard panel
- DailyChallengeResource stores goal_description/target_score/max_time_secs from server;
  pressing C shows goal description as toast (DailyGoalAnnouncementEvent)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 01:38:25 +00:00
root 6728a4311f feat(engine): grant achievement rewards + gate cosmetic selectors
- Add Reward enum to solitaire_core with CardBack/Background/BonusXp/Badge variants
- Wire rewards into ALL_ACHIEVEMENTS per architecture spec
- evaluate_on_win now applies rewards on first unlock: pushes cosmetic
  indices into PlayerProgress, awards BonusXp (with level-up detection),
  and marks reward_granted = true so rewards are never double-granted
- Add selected_card_back / selected_background fields to Settings
- Settings panel grows Card Back and Background cycle rows, shown only
  when the player has unlocked more than the default (index 0)
- cycle_unlocked() cycles only through earned options

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 01:00:18 +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 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 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 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