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>
This commit is contained in:
funman300
2026-04-29 03:35:41 +00:00
parent 2b04718f33
commit 7cda2a9f1a
17 changed files with 89 additions and 124 deletions
+6 -10
View File
@@ -176,21 +176,17 @@ fn evaluate_on_win(
unlocks.write(AchievementUnlockedEvent(record.clone())); unlocks.write(AchievementUnlockedEvent(record.clone()));
} }
if achievements_changed { if achievements_changed
if let Some(target) = &path.0 { && let Some(target) = &path.0
if let Err(e) = save_achievements_to(target, &achievements.0) { && let Err(e) = save_achievements_to(target, &achievements.0) {
warn!("failed to save achievements: {e}"); warn!("failed to save achievements: {e}");
} }
}
}
if progress_changed { if progress_changed
if let Some(target) = &progress_path.0 { && let Some(target) = &progress_path.0
if let Err(e) = save_progress_to(target, &progress.0) { && let Err(e) = save_progress_to(target, &progress.0) {
warn!("failed to save progress after reward: {e}"); warn!("failed to save progress after reward: {e}");
} }
}
}
} }
/// Convenience: resolve an achievement ID to its human-readable name. /// Convenience: resolve an achievement ID to its human-readable name.
+2 -3
View File
@@ -472,13 +472,12 @@ fn drive_toast_display(
} }
// If no active toast and the queue has messages, show the next one. // If no active toast and the queue has messages, show the next one.
if active.entity.is_none() { if active.entity.is_none()
if let Some(message) = queue.0.pop_front() { && let Some(message) = queue.0.pop_front() {
let entity = spawn_queued_toast(&mut commands, message); let entity = spawn_queued_toast(&mut commands, message);
active.entity = Some(entity); active.entity = Some(entity);
active.timer = QUEUED_TOAST_SECS; active.timer = QUEUED_TOAST_SECS;
} }
}
} }
/// Spawns a centered top-of-screen `ToastEntity` for the queued toast system. /// Spawns a centered top-of-screen `ToastEntity` for the queued toast system.
@@ -148,7 +148,7 @@ impl Default for AnimationTuning {
/// running under `MinimalPlugins` (which does not register the touch subsystem). /// running under `MinimalPlugins` (which does not register the touch subsystem).
pub(crate) fn update_input_platform( pub(crate) fn update_input_platform(
touches: Option<Res<Touches>>, touches: Option<Res<Touches>>,
mouse_buttons: Res<ButtonInput<MouseButton>>, mouse_buttons: Option<Res<ButtonInput<MouseButton>>>,
mut tuning: ResMut<AnimationTuning>, mut tuning: ResMut<AnimationTuning>,
) { ) {
let touch_active = touches.as_ref().is_some_and(|t| { let touch_active = touches.as_ref().is_some_and(|t| {
@@ -157,8 +157,9 @@ pub(crate) fn update_input_platform(
|| t.iter_just_released().next().is_some() || t.iter_just_released().next().is_some()
}); });
let mouse_active = mouse_buttons.get_just_pressed().next().is_some() let mouse_active = mouse_buttons.as_ref().is_some_and(|mb| {
|| mouse_buttons.get_pressed().next().is_some(); mb.get_just_pressed().next().is_some() || mb.get_pressed().next().is_some()
});
if touch_active && tuning.platform != InputPlatform::Touch { if touch_active && tuning.platform != InputPlatform::Touch {
*tuning = AnimationTuning::mobile(); *tuning = AnimationTuning::mobile();
+3
View File
@@ -394,6 +394,7 @@ fn sync_cards_startup(
} }
} }
#[allow(clippy::too_many_arguments)]
fn sync_cards_on_change( fn sync_cards_on_change(
mut events: MessageReader<StateChangedEvent>, mut events: MessageReader<StateChangedEvent>,
commands: Commands, commands: Commands,
@@ -416,6 +417,7 @@ fn sync_cards_on_change(
} }
} }
#[allow(clippy::too_many_arguments)]
fn sync_cards( fn sync_cards(
mut commands: Commands, mut commands: Commands,
game: &GameState, game: &GameState,
@@ -542,6 +544,7 @@ fn face_colour(card: &Card, color_blind: bool) -> Color {
} }
} }
#[allow(clippy::too_many_arguments)]
fn spawn_card_entity( fn spawn_card_entity(
commands: &mut Commands, commands: &mut Commands,
card: &Card, card: &Card,
+2 -3
View File
@@ -54,11 +54,10 @@ fn advance_on_challenge_win(
} }
let prev = progress.0.challenge_index; let prev = progress.0.challenge_index;
progress.0.challenge_index = prev.saturating_add(1); progress.0.challenge_index = prev.saturating_add(1);
if let Some(target) = &path.0 { if let Some(target) = &path.0
if let Err(e) = save_progress_to(target, &progress.0) { && let Err(e) = save_progress_to(target, &progress.0) {
warn!("failed to save progress after challenge advance: {e}"); warn!("failed to save progress after challenge advance: {e}");
} }
}
// Human-readable level is 1-based (index 0 → "Challenge 1"). // Human-readable level is 1-based (index 0 → "Challenge 1").
let level_number = prev.saturating_add(1); let level_number = prev.saturating_add(1);
toast.write(InfoToastEvent(format!("Challenge {level_number} complete!"))); toast.write(InfoToastEvent(format!("Challenge {level_number} complete!")));
@@ -161,27 +161,24 @@ fn handle_daily_completion(
continue; continue;
} }
// Enforce server-supplied goal constraints when present. // Enforce server-supplied goal constraints when present.
if let Some(target) = daily.target_score { if let Some(target) = daily.target_score
if ev.score < target { && ev.score < target {
continue; // score goal not met continue; // score goal not met
} }
} if let Some(max_secs) = daily.max_time_secs
if let Some(max_secs) = daily.max_time_secs { && ev.time_seconds > max_secs {
if ev.time_seconds > max_secs {
continue; // time limit exceeded continue; // time limit exceeded
} }
}
if !progress.0.record_daily_completion(daily.date) { if !progress.0.record_daily_completion(daily.date) {
// Already counted today — no-op. // Already counted today — no-op.
continue; continue;
} }
progress.0.add_xp(DAILY_BONUS_XP); progress.0.add_xp(DAILY_BONUS_XP);
xp_awarded.write(XpAwardedEvent { amount: DAILY_BONUS_XP }); xp_awarded.write(XpAwardedEvent { amount: DAILY_BONUS_XP });
if let Some(target) = &path.0 { if let Some(target) = &path.0
if let Err(e) = save_progress_to(target, &progress.0) { && let Err(e) = save_progress_to(target, &progress.0) {
warn!("failed to save progress after daily completion: {e}"); warn!("failed to save progress after daily completion: {e}");
} }
}
completed.write(DailyChallengeCompletedEvent { completed.write(DailyChallengeCompletedEvent {
date: daily.date, date: daily.date,
streak: progress.0.daily_challenge_streak, streak: progress.0.daily_challenge_streak,
+10 -15
View File
@@ -194,11 +194,10 @@ fn handle_new_game(
let mode = ev.mode.unwrap_or(game.0.mode); let mode = ev.mode.unwrap_or(game.0.mode);
game.0 = GameState::new_with_mode(seed, draw_mode, mode); game.0 = GameState::new_with_mode(seed, draw_mode, mode);
// Delete any previously saved in-progress state — this is a fresh game. // Delete any previously saved in-progress state — this is a fresh game.
if let Some(p) = path.as_ref().and_then(|r| r.0.as_deref()) { if let Some(p) = path.as_ref().and_then(|r| r.0.as_deref())
if let Err(e) = delete_game_state_at(p) { && let Err(e) = delete_game_state_at(p) {
warn!("game_state: failed to delete saved game: {e}"); warn!("game_state: failed to delete saved game: {e}");
} }
}
changed.write(StateChangedEvent); changed.write(StateChangedEvent);
} }
} }
@@ -380,14 +379,13 @@ fn handle_move(
match game.0.move_cards(ev.from.clone(), ev.to.clone(), ev.count) { match game.0.move_cards(ev.from.clone(), ev.to.clone(), ev.count) {
Ok(()) => { Ok(()) => {
// Fire flip event if the candidate card is now face-up. // Fire flip event if the candidate card is now face-up.
if let Some(fid) = flip_candidate_id { if let Some(fid) = flip_candidate_id
if game.0.piles.get(&ev.from) && game.0.piles.get(&ev.from)
.and_then(|p| p.cards.last()) .and_then(|p| p.cards.last())
.is_some_and(|c| c.id == fid && c.face_up) .is_some_and(|c| c.id == fid && c.face_up)
{ {
flipped.write(crate::events::CardFlippedEvent(fid)); flipped.write(crate::events::CardFlippedEvent(fid));
} }
}
changed.write(StateChangedEvent); changed.write(StateChangedEvent);
if !was_won && game.0.is_won { if !was_won && game.0.is_won {
won.write(GameWonEvent { won.write(GameWonEvent {
@@ -395,13 +393,12 @@ fn handle_move(
time_seconds: game.0.elapsed_seconds, time_seconds: game.0.elapsed_seconds,
}); });
// Delete the saved state — a won game should not be resumed. // Delete the saved state — a won game should not be resumed.
if let Some(p) = path.as_ref().and_then(|r| r.0.as_deref()) { if let Some(p) = path.as_ref().and_then(|r| r.0.as_deref())
if let Err(e) = delete_game_state_at(p) { && let Err(e) = delete_game_state_at(p) {
warn!("game_state: failed to delete on win: {e}"); warn!("game_state: failed to delete on win: {e}");
} }
} }
} }
}
Err(e) => warn!("move rejected {:?} -> {:?} x{}: {e}", ev.from, ev.to, ev.count), Err(e) => warn!("move rejected {:?} -> {:?} x{}: {e}", ev.from, ev.to, ev.count),
} }
} }
@@ -468,12 +465,11 @@ pub fn has_legal_moves(game: &GameState) -> bool {
// Check foundations. // Check foundations.
for &suit in &suits { for &suit in &suits {
let dest = PileType::Foundation(suit); let dest = PileType::Foundation(suit);
if let Some(dest_pile) = game.piles.get(&dest) { if let Some(dest_pile) = game.piles.get(&dest)
if can_place_on_foundation(card, dest_pile, suit) { && can_place_on_foundation(card, dest_pile, suit) {
return true; return true;
} }
} }
}
// Check tableau piles. // Check tableau piles.
for i in 0..7_usize { for i in 0..7_usize {
@@ -481,13 +477,12 @@ pub fn has_legal_moves(game: &GameState) -> bool {
if dest == *from { if dest == *from {
continue; continue;
} }
if let Some(dest_pile) = game.piles.get(&dest) { if let Some(dest_pile) = game.piles.get(&dest)
if can_place_on_tableau(card, dest_pile) { && can_place_on_tableau(card, dest_pile) {
return true; return true;
} }
} }
} }
}
false false
} }
+2 -3
View File
@@ -437,15 +437,14 @@ fn update_hud(
// Reflects the AutoCompleteState resource; update whenever it changes or game changes. // Reflects the AutoCompleteState resource; update whenever it changes or game changes.
let ac_active = auto_complete.as_ref().is_some_and(|ac| ac.active); let ac_active = auto_complete.as_ref().is_some_and(|ac| ac.active);
let ac_changed = auto_complete.as_ref().is_some_and(|ac| ac.is_changed()); let ac_changed = auto_complete.as_ref().is_some_and(|ac| ac.is_changed());
if ac_changed || game.is_changed() { if (ac_changed || game.is_changed())
if let Ok(mut t) = auto_q.single_mut() { && let Ok(mut t) = auto_q.single_mut() {
**t = if ac_active { **t = if ac_active {
"AUTO".to_string() "AUTO".to_string()
} else { } else {
String::new() String::new()
}; };
} }
}
} }
/// Updates the `HudSelection` text node to show which pile is Tab-selected. /// Updates the `HudSelection` text node to show which pile is Tab-selected.
+19 -25
View File
@@ -136,6 +136,7 @@ struct CoreKeyboardMessages<'w> {
/// ///
/// Also resets `forfeit_countdown` whenever U, D, Z, or N are pressed so that /// Also resets `forfeit_countdown` whenever U, D, Z, or N are pressed so that
/// an in-flight forfeit confirmation is cancelled by any other action. /// an in-flight forfeit confirmation is cancelled by any other action.
#[allow(clippy::too_many_arguments)]
fn handle_keyboard_core( fn handle_keyboard_core(
keys: Res<ButtonInput<KeyCode>>, keys: Res<ButtonInput<KeyCode>>,
paused: Option<Res<PausedResource>>, paused: Option<Res<PausedResource>>,
@@ -174,8 +175,8 @@ fn handle_keyboard_core(
confirm.forfeit_countdown = 0.0; confirm.forfeit_countdown = 0.0;
// If a Time Attack session is running, cancel it and start a Classic game. // If a Time Attack session is running, cancel it and start a Classic game.
if let Some(ref mut session) = time_attack { if let Some(ref mut session) = time_attack
if session.active { && session.active {
session.active = false; session.active = false;
session.remaining_secs = 0.0; session.remaining_secs = 0.0;
ev.info_toast.write(InfoToastEvent("Time Attack ended".to_string())); ev.info_toast.write(InfoToastEvent("Time Attack ended".to_string()));
@@ -186,7 +187,6 @@ fn handle_keyboard_core(
confirm.new_game_countdown = 0.0; confirm.new_game_countdown = 0.0;
return; return;
} }
}
let active_game = game.as_ref().is_some_and(|g| g.0.move_count > 0 && !g.0.is_won); let active_game = game.as_ref().is_some_and(|g| g.0.move_count > 0 && !g.0.is_won);
let shift_held = keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight); let shift_held = keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight);
@@ -244,6 +244,7 @@ fn handle_keyboard_core(
/// ///
/// The hint index wraps around once all hints have been cycled through. When no /// The hint index wraps around once all hints have been cycled through. When no
/// moves are available a "No hints available" toast is shown instead. /// moves are available a "No hints available" toast is shown instead.
#[allow(clippy::too_many_arguments)]
fn handle_keyboard_hint( fn handle_keyboard_hint(
keys: Res<ButtonInput<KeyCode>>, keys: Res<ButtonInput<KeyCode>>,
paused: Option<Res<PausedResource>>, paused: Option<Res<PausedResource>>,
@@ -273,7 +274,7 @@ fn handle_keyboard_hint(
return; return;
} }
let Some(ref layout_res) = layout else { return }; let Some(_layout_res) = layout else { return };
let hints = all_hints(&g.0); let hints = all_hints(&g.0);
if hints.is_empty() { if hints.is_empty() {
@@ -661,8 +662,8 @@ fn end_drag(
// the placement is illegal, fire MoveRejectedEvent so AudioPlugin can // the placement is illegal, fire MoveRejectedEvent so AudioPlugin can
// play card_invalid.wav. // play card_invalid.wav.
let mut fired = false; let mut fired = false;
if let Some(target) = target { if let Some(target) = target
if target != origin { && target != origin {
let bottom_card_id = drag.cards[0]; let bottom_card_id = drag.cards[0];
if let Some(bottom_card) = card_by_id(&game.0, bottom_card_id) { if let Some(bottom_card) = card_by_id(&game.0, bottom_card_id) {
let ok = match &target { let ok = match &target {
@@ -708,7 +709,6 @@ fn end_drag(
} }
} }
} }
}
drag.clear(); drag.clear();
@@ -892,8 +892,8 @@ fn touch_end_drag(
world.and_then(|w| find_drop_target(w, &game.0, &layout.0, &origin)); world.and_then(|w| find_drop_target(w, &game.0, &layout.0, &origin));
let mut fired = false; let mut fired = false;
if let Some(target) = target { if let Some(target) = target
if target != origin { && target != origin {
let bottom_card_id = drag.cards[0]; let bottom_card_id = drag.cards[0];
if let Some(bottom_card) = card_by_id(&game.0, bottom_card_id) { if let Some(bottom_card) = card_by_id(&game.0, bottom_card_id) {
let ok = match &target { let ok = match &target {
@@ -924,7 +924,6 @@ fn touch_end_drag(
} }
} }
} }
}
drag.clear(); drag.clear();
changed.write(StateChangedEvent); changed.write(StateChangedEvent);
@@ -1132,21 +1131,19 @@ pub fn best_destination(card: &Card, game: &GameState) -> Option<PileType> {
// Try all four foundations first. // Try all four foundations first.
for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] { for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
let dest = PileType::Foundation(suit); let dest = PileType::Foundation(suit);
if let Some(pile) = game.piles.get(&dest) { if let Some(pile) = game.piles.get(&dest)
if can_place_on_foundation(card, pile, suit) { && can_place_on_foundation(card, pile, suit) {
return Some(dest); return Some(dest);
} }
} }
}
// Then try all seven tableau piles. // Then try all seven tableau piles.
for i in 0..7_usize { for i in 0..7_usize {
let dest = PileType::Tableau(i); let dest = PileType::Tableau(i);
if let Some(pile) = game.piles.get(&dest) { if let Some(pile) = game.piles.get(&dest)
if can_place_on_tableau(card, pile) { && can_place_on_tableau(card, pile) {
return Some(dest); return Some(dest);
} }
} }
}
None None
} }
@@ -1167,12 +1164,11 @@ pub fn best_tableau_destination_for_stack(
if dest == *from { if dest == *from {
continue; continue;
} }
if let Some(pile) = game.piles.get(&dest) { if let Some(pile) = game.piles.get(&dest)
if can_place_on_tableau(bottom_card, pile) { && can_place_on_tableau(bottom_card, pile) {
return Some((dest, stack_count)); return Some((dest, stack_count));
} }
} }
}
None None
} }
@@ -1309,8 +1305,8 @@ pub fn all_hints(game: &GameState) -> Vec<(PileType, PileType, usize)> {
let Some(card) = from_pile.cards.last().filter(|c| c.face_up) else { continue }; let Some(card) = from_pile.cards.last().filter(|c| c.face_up) else { continue };
for &suit in &suits { for &suit in &suits {
let dest = PileType::Foundation(suit); let dest = PileType::Foundation(suit);
if let Some(dest_pile) = game.piles.get(&dest) { if let Some(dest_pile) = game.piles.get(&dest)
if can_place_on_foundation(card, dest_pile, suit) { && can_place_on_foundation(card, dest_pile, suit) {
hints.push((from.clone(), dest, 1)); hints.push((from.clone(), dest, 1));
// Each source card can go to at most one foundation suit; // Each source card can go to at most one foundation suit;
// no need to check the remaining three for this card. // no need to check the remaining three for this card.
@@ -1318,7 +1314,6 @@ pub fn all_hints(game: &GameState) -> Vec<(PileType, PileType, usize)> {
} }
} }
} }
}
// Pass 2 — tableau moves (deduplicated by source pile so we don't // Pass 2 — tableau moves (deduplicated by source pile so we don't
// repeat the same source card multiple times for different destinations). // repeat the same source card multiple times for different destinations).
@@ -1338,8 +1333,8 @@ pub fn all_hints(game: &GameState) -> Vec<(PileType, PileType, usize)> {
if dest == *from { if dest == *from {
continue; continue;
} }
if let Some(dest_pile) = game.piles.get(&dest) { if let Some(dest_pile) = game.piles.get(&dest)
if can_place_on_tableau(card, dest_pile) { && can_place_on_tableau(card, dest_pile) {
hints.push((from.clone(), dest, 1)); hints.push((from.clone(), dest, 1));
// One tableau destination per source card is enough for the // One tableau destination per source card is enough for the
// hint list — the player can see where else a card can go // hint list — the player can see where else a card can go
@@ -1348,7 +1343,6 @@ pub fn all_hints(game: &GameState) -> Vec<(PileType, PileType, usize)> {
} }
} }
} }
}
// Pass 3 — suggest drawing from the stock when no other hint was found. // Pass 3 — suggest drawing from the stock when no other hint was found.
if hints.is_empty() { if hints.is_empty() {
+2 -3
View File
@@ -123,15 +123,14 @@ fn toggle_leaderboard_screen(
spawn_leaderboard_screen(&mut commands, data.0.as_deref()); spawn_leaderboard_screen(&mut commands, data.0.as_deref());
// Start a background fetch if not already in flight. // Start a background fetch if not already in flight.
if task_res.0.is_none() { if task_res.0.is_none()
if let Some(p) = provider { && let Some(p) = provider {
let provider = p.0.clone(); let provider = p.0.clone();
let task = AsyncComputeTaskPool::get().spawn(async move { let task = AsyncComputeTaskPool::get().spawn(async move {
provider.fetch_leaderboard().await.map_err(|e| e.to_string()) provider.fetch_leaderboard().await.map_err(|e| e.to_string())
}); });
task_res.0 = Some(task); task_res.0 = Some(task);
} }
}
} }
/// Poll the background fetch task; store results when complete. /// Poll the background fetch task; store results when complete.
+8 -13
View File
@@ -103,13 +103,12 @@ fn toggle_pause(
// If a drag is in progress, cancel it instead of opening the pause overlay. // If a drag is in progress, cancel it instead of opening the pause overlay.
// Clearing DragState and emitting StateChangedEvent snaps the dragged cards // Clearing DragState and emitting StateChangedEvent snaps the dragged cards
// back to their resting positions exactly as a rejected drop does. // back to their resting positions exactly as a rejected drop does.
if let Some(ref mut d) = drag { if let Some(ref mut d) = drag
if !d.is_idle() { && !d.is_idle() {
d.clear(); d.clear();
changed.write(StateChangedEvent); changed.write(StateChangedEvent);
return; return;
} }
}
if let Ok(entity) = screens.single() { if let Ok(entity) = screens.single() {
commands.entity(entity).despawn(); commands.entity(entity).despawn();
paused.0 = false; paused.0 = false;
@@ -122,14 +121,12 @@ fn toggle_pause(
paused.0 = true; paused.0 = true;
// Persist the current game state whenever the player opens the pause // Persist the current game state whenever the player opens the pause
// overlay so an OS-level kill still leaves a resumable save. // overlay so an OS-level kill still leaves a resumable save.
if let (Some(g), Some(p)) = (game, path) { if let (Some(g), Some(p)) = (game, path)
if let Some(disk_path) = p.0.as_deref() { && let Some(disk_path) = p.0.as_deref()
if let Err(e) = save_game_state_to(disk_path, &g.0) { && let Err(e) = save_game_state_to(disk_path, &g.0) {
warn!("game_state: failed to save on pause: {e}"); warn!("game_state: failed to save on pause: {e}");
} }
} }
}
}
} }
/// Handles the draw-mode toggle button on the pause overlay. /// Handles the draw-mode toggle button on the pause overlay.
@@ -155,13 +152,11 @@ fn handle_pause_draw_toggle(
DrawMode::DrawOne => DrawMode::DrawThree, DrawMode::DrawOne => DrawMode::DrawThree,
DrawMode::DrawThree => DrawMode::DrawOne, DrawMode::DrawThree => DrawMode::DrawOne,
}; };
if let Some(p) = &path { if let Some(p) = &path
if let Some(target) = &p.0 { && let Some(target) = &p.0
if let Err(e) = solitaire_data::save_settings_to(target, &settings.0) { && let Err(e) = solitaire_data::save_settings_to(target, &settings.0) {
warn!("failed to save settings after draw-mode toggle: {e}"); warn!("failed to save settings after draw-mode toggle: {e}");
} }
}
}
changed.write(SettingsChangedEvent(settings.0.clone())); changed.write(SettingsChangedEvent(settings.0.clone()));
} }
} }
+2 -3
View File
@@ -101,12 +101,11 @@ fn award_xp_on_win(
total_xp: progress.0.total_xp, total_xp: progress.0.total_xp,
}); });
} }
if let Some(target) = &path.0 { if let Some(target) = &path.0
if let Err(e) = save_progress_to(target, &progress.0) { && let Err(e) = save_progress_to(target, &progress.0) {
warn!("failed to save progress: {e}"); warn!("failed to save progress: {e}");
} }
} }
}
} }
#[cfg(test)] #[cfg(test)]
+7 -11
View File
@@ -234,9 +234,9 @@ fn handle_selection_keys(
// 2. Tableau stack move — count = full face-up run length from the source. // 2. Tableau stack move — count = full face-up run length from the source.
let activate = let activate =
keys.just_pressed(KeyCode::Enter) || keys.just_pressed(KeyCode::Space); keys.just_pressed(KeyCode::Enter) || keys.just_pressed(KeyCode::Space);
if activate { if activate
if let Some(ref pile) = selection.selected_pile.clone() { && let Some(ref pile) = selection.selected_pile.clone()
if let Some(card) = game && let Some(card) = game
.0 .0
.piles .piles
.get(pile) .get(pile)
@@ -266,8 +266,8 @@ fn handle_selection_keys(
let start = p.cards.len().saturating_sub(run_len); let start = p.cards.len().saturating_sub(run_len);
p.cards.get(start) p.cards.get(start)
}); });
if let Some(bottom) = bottom_card { if let Some(bottom) = bottom_card
if let Some((dest, count)) = && let Some((dest, count)) =
best_tableau_destination_for_stack(bottom, pile, &game.0, run_len) best_tableau_destination_for_stack(bottom, pile, &game.0, run_len)
{ {
moves.write(MoveRequestEvent { moves.write(MoveRequestEvent {
@@ -278,7 +278,6 @@ fn handle_selection_keys(
selection.selected_pile = None; selection.selected_pile = None;
return; return;
} }
}
// --- Fallback: single-card move to any destination --- // --- Fallback: single-card move to any destination ---
// Covers non-tableau sources (Waste, Foundation) that have no // Covers non-tableau sources (Waste, Foundation) that have no
@@ -292,8 +291,6 @@ fn handle_selection_keys(
selection.selected_pile = None; selection.selected_pile = None;
} }
} }
}
}
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -330,12 +327,11 @@ fn try_foundation_dest(
use solitaire_core::rules::can_place_on_foundation; use solitaire_core::rules::can_place_on_foundation;
for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] { for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
let dest = PileType::Foundation(suit); let dest = PileType::Foundation(suit);
if let Some(pile) = game.piles.get(&dest) { if let Some(pile) = game.piles.get(&dest)
if can_place_on_foundation(card, pile, suit) { && can_place_on_foundation(card, pile, suit) {
return Some(dest); return Some(dest);
} }
} }
}
None None
} }
+2 -3
View File
@@ -326,8 +326,8 @@ fn spawn_stats_screen(
} }
// Time Attack section // Time Attack section
if let Some(ta) = time_attack { if let Some(ta) = time_attack
if ta.active { && ta.active {
let mins = (ta.remaining_secs / 60.0).floor() as u64; let mins = (ta.remaining_secs / 60.0).floor() as u64;
let secs = (ta.remaining_secs % 60.0).floor() as u64; let secs = (ta.remaining_secs % 60.0).floor() as u64;
root.spawn(( root.spawn((
@@ -336,7 +336,6 @@ fn spawn_stats_screen(
TextColor(Color::srgb(1.0, 0.6, 0.2)), TextColor(Color::srgb(1.0, 0.6, 0.2)),
)); ));
} }
}
// Dismiss hint // Dismiss hint
root.spawn(( root.spawn((
+6 -9
View File
@@ -176,21 +176,18 @@ fn poll_pull_result(
let (merged, _conflicts) = merge(&local, &remote); let (merged, _conflicts) = merge(&local, &remote);
// Persist merged state atomically. // Persist merged state atomically.
if let Some(p) = &stats_path.0 { if let Some(p) = &stats_path.0
if let Err(e) = save_stats_to(p, &merged.stats) { && let Err(e) = save_stats_to(p, &merged.stats) {
warn!("sync: failed to persist stats: {e}"); warn!("sync: failed to persist stats: {e}");
} }
} if let Some(p) = &achievements_path.0
if let Some(p) = &achievements_path.0 { && let Err(e) = save_achievements_to(p, &merged.achievements) {
if let Err(e) = save_achievements_to(p, &merged.achievements) {
warn!("sync: failed to persist achievements: {e}"); warn!("sync: failed to persist achievements: {e}");
} }
} if let Some(p) = &progress_path.0
if let Some(p) = &progress_path.0 { && let Err(e) = save_progress_to(p, &merged.progress) {
if let Err(e) = save_progress_to(p, &merged.progress) {
warn!("sync: failed to persist progress: {e}"); warn!("sync: failed to persist progress: {e}");
} }
}
// Update in-world resources. // Update in-world resources.
stats.0 = merged.stats; stats.0 = merged.stats;
+2 -1
View File
@@ -8,13 +8,14 @@ use bevy::prelude::*;
use bevy::window::WindowResized; use bevy::window::WindowResized;
use solitaire_core::card::Suit; use solitaire_core::card::Suit;
use solitaire_core::pile::PileType; use solitaire_core::pile::PileType;
use solitaire_data::settings::Theme;
use crate::events::HintVisualEvent; use crate::events::HintVisualEvent;
use crate::layout::{compute_layout, Layout, LayoutResource}; use crate::layout::{compute_layout, Layout, LayoutResource};
#[cfg(test)] #[cfg(test)]
use crate::layout::TABLE_COLOUR; use crate::layout::TABLE_COLOUR;
use crate::settings_plugin::{SettingsChangedEvent, SettingsResource}; use crate::settings_plugin::{SettingsChangedEvent, SettingsResource};
#[cfg(test)]
use solitaire_data::Theme;
/// Holds pre-loaded [`Handle<Image>`]s for the 5 selectable table backgrounds. /// Holds pre-loaded [`Handle<Image>`]s for the 5 selectable table backgrounds.
/// ///
+6 -10
View File
@@ -49,13 +49,11 @@ fn roll_weekly_goals_on_startup(
path: Res<ProgressStoragePath>, path: Res<ProgressStoragePath>,
) { ) {
let week_key = current_iso_week_key(Local::now().date_naive()); let week_key = current_iso_week_key(Local::now().date_naive());
if progress.0.roll_weekly_goals_if_new_week(&week_key) { if progress.0.roll_weekly_goals_if_new_week(&week_key)
if let Some(target) = &path.0 { && let Some(target) = &path.0
if let Err(e) = save_progress_to(target, &progress.0) { && let Err(e) = save_progress_to(target, &progress.0) {
warn!("failed to save progress after weekly reset on startup: {e}"); warn!("failed to save progress after weekly reset on startup: {e}");
} }
}
}
} }
fn evaluate_weekly_goals( fn evaluate_weekly_goals(
@@ -114,13 +112,11 @@ fn evaluate_weekly_goals(
} }
} }
if any_change { if any_change
if let Some(target) = &path.0 { && let Some(target) = &path.0
if let Err(e) = save_progress_to(target, &progress.0) { && let Err(e) = save_progress_to(target, &progress.0) {
warn!("failed to save progress after weekly goal update: {e}"); warn!("failed to save progress after weekly goal update: {e}");
} }
}
}
} }
/// Resolve a goal id to its description (used for toasts). /// Resolve a goal id to its description (used for toasts).