fix(engine): resolve all clippy warnings introduced by PNG asset pipeline
- 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:
@@ -176,22 +176,18 @@ 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.
|
||||||
/// Used by the toast renderer in `animation_plugin`.
|
/// Used by the toast renderer in `animation_plugin`.
|
||||||
|
|||||||
@@ -472,14 +472,13 @@ 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.
|
||||||
fn spawn_queued_toast(commands: &mut Commands, message: String) -> Entity {
|
fn spawn_queued_toast(commands: &mut Commands, message: String) -> Entity {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -437,8 +437,8 @@ 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 {
|
||||||
@@ -446,7 +446,6 @@ fn update_hud(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Updates the `HudSelection` text node to show which pile is Tab-selected.
|
/// Updates the `HudSelection` text node to show which pile is Tab-selected.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -123,8 +123,8 @@ 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())
|
||||||
@@ -132,7 +132,6 @@ fn toggle_leaderboard_screen(
|
|||||||
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.
|
||||||
fn poll_leaderboard_fetch(
|
fn poll_leaderboard_fetch(
|
||||||
|
|||||||
@@ -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,15 +121,13 @@ 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()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,13 +101,12 @@ 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)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -293,8 +292,6 @@ fn handle_selection_keys(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Private helpers
|
// Private helpers
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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((
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -49,14 +49,12 @@ 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(
|
||||||
mut wins: MessageReader<GameWonEvent>,
|
mut wins: MessageReader<GameWonEvent>,
|
||||||
@@ -114,14 +112,12 @@ 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).
|
||||||
pub fn weekly_goal_description(id: &str) -> String {
|
pub fn weekly_goal_description(id: &str) -> String {
|
||||||
|
|||||||
Reference in New Issue
Block a user