From b26200f948bc9b05ba886f7666e59e62bcf3b380 Mon Sep 17 00:00:00 2001 From: funman300 Date: Thu, 9 Jul 2026 13:28:09 -0700 Subject: [PATCH 1/2] chore: delete dead code approved from the PR #166 sweep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three items the multi-agent sweep flagged, now removed with user approval (§8 for the solitaire_sync changes): - WinCascadePlugin: never registered; handle_win_cascade in AnimationPlugin is the live win cascade and builds its own targets. Its now-orphaned helpers (win_scatter_targets, cascade_delay, WIN_CASCADE_INTERVAL_SECS) had no callers outside their own tests and go with it. - SyncCompleteEvent: written by the pull-completion system, zero readers — UI reads SyncStatusResource instead. - solitaire_sync::ApiError: unused by client and server; the merge_at crate-root re-export goes too (merge::merge_at stays for the merge module's own use). ARCHITECTURE.md updated to match. Co-Authored-By: Claude Fable 5 --- ARCHITECTURE.md | 4 +- .../src/card_animation/animation.rs | 37 -------- solitaire_engine/src/card_animation/mod.rs | 89 +------------------ solitaire_engine/src/card_animation/timing.rs | 28 ------ solitaire_engine/src/events.rs | 11 --- solitaire_engine/src/lib.rs | 7 +- solitaire_engine/src/sync_plugin.rs | 27 ++---- solitaire_sync/src/lib.rs | 29 +----- 8 files changed, 15 insertions(+), 217 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ee2dfca..489322b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -145,7 +145,6 @@ Shared API contract types imported by both the game client (`solitaire_data`) an Owns: - `SyncPayload`, `SyncResponse`, `ConflictReport` - `ChallengeGoal`, `LeaderboardEntry` -- `ApiError` enum - Merge logic (pure functions, no I/O) ### `solitaire_data` @@ -257,7 +256,7 @@ solitaire_sync::merge(local, remote) │ ▼ Write merged result to disk - │ fires SyncCompleteEvent + │ ▼ Bevy main thread reads updated StatsResource ``` @@ -377,7 +376,6 @@ struct StateChangedEvent; struct CardFlippedEvent(u32); struct GameWonEvent { score: i32, time_seconds: u64 } struct AchievementUnlockedEvent(AchievementRecord); -struct SyncCompleteEvent(Result); ``` ### Layout System diff --git a/solitaire_engine/src/card_animation/animation.rs b/solitaire_engine/src/card_animation/animation.rs index 34fb46c..0b403d3 100644 --- a/solitaire_engine/src/card_animation/animation.rs +++ b/solitaire_engine/src/card_animation/animation.rs @@ -201,27 +201,6 @@ pub(crate) fn advance_card_animations( } } -// --------------------------------------------------------------------------- -// Win cascade -// --------------------------------------------------------------------------- - -/// Win-cascade scatter targets — 8 points beyond the window edges. -/// -/// Scaled by `radius` (pass `layout.card_size.x * 8.0` for a good result). -pub fn win_scatter_targets(radius: f32) -> [Vec2; 8] { - let r = radius; - [ - Vec2::new(r, r), - Vec2::new(-r, r), - Vec2::new(r, -r), - Vec2::new(-r, -r), - Vec2::new(0.0, r), - Vec2::new(0.0, -r), - Vec2::new(r, 0.0), - Vec2::new(-r, 0.0), - ] -} - // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- @@ -305,20 +284,4 @@ mod tests { assert!((anim.z_lift - 12.0).abs() < 1e-6); } - #[test] - fn win_scatter_has_eight_targets() { - let targets = win_scatter_targets(800.0); - assert_eq!(targets.len(), 8); - } - - #[test] - fn win_scatter_targets_are_off_center() { - for t in win_scatter_targets(400.0) { - let dist = t.length(); - assert!( - dist > 100.0, - "scatter target should be well off-center: {t:?}" - ); - } - } } diff --git a/solitaire_engine/src/card_animation/mod.rs b/solitaire_engine/src/card_animation/mod.rs index e740c96..6b9db8d 100644 --- a/solitaire_engine/src/card_animation/mod.rs +++ b/solitaire_engine/src/card_animation/mod.rs @@ -31,16 +31,6 @@ //! )); //! ``` //! -//! # Win cascade with `Expressive` curve -//! -//! The existing `AnimationPlugin` drives the win cascade with `CardAnim` -//! (linear). To use the curve-based cascade instead, disable -//! `handle_win_cascade` in `AnimationPlugin` and register `WinCascadePlugin` -//! (declared below) which uses `CardAnimation` + `MotionCurve::Expressive`. -//! -//! They **must not both be active** — both write to `Transform` on the same -//! 52 entities and will race. -//! //! # Coexistence rules //! //! | Condition | Safe? | @@ -58,24 +48,21 @@ pub mod interaction; pub mod timing; pub mod tuning; -pub use animation::{CardAnimation, win_scatter_targets}; +pub use animation::CardAnimation; pub use chain::AnimationChain; pub use curves::{MotionCurve, sample_curve}; pub use diagnostics::{FrameTimeDiagnostics, WINDOW_SIZE as DIAG_WINDOW_SIZE}; pub use interaction::{BufferedInput, HoverState, InputBuffer}; pub use timing::{ - DEAL_INTERVAL_SECS, MAX_DURATION_SECS, MIN_DURATION_SECS, WIN_CASCADE_INTERVAL_SECS, - cascade_delay, compute_duration, micro_vary, + DEAL_INTERVAL_SECS, MAX_DURATION_SECS, MIN_DURATION_SECS, compute_duration, micro_vary, }; pub use tuning::{AnimationTuning, InputPlatform}; use bevy::prelude::*; use bevy::window::RequestRedraw; -use crate::card_plugin::CardEntity; use crate::events::{DrawRequestEvent, GameWonEvent, MoveRequestEvent, UndoRequestEvent}; use crate::game_plugin::GameMutation; -use crate::layout::LayoutResource; use crate::resources::DragState; use animation::advance_card_animations; @@ -144,63 +131,6 @@ impl Plugin for CardAnimationPlugin { } } -// --------------------------------------------------------------------------- -// Optional: win cascade with Expressive curve -// --------------------------------------------------------------------------- - -/// Optional plugin that replaces the linear win cascade in `AnimationPlugin` -/// with an `Expressive`-curve cascade. -/// -/// **Do not register this alongside `AnimationPlugin`'s win cascade** — they -/// will race on the same card entities. To use this plugin, prevent -/// `AnimationPlugin` from handling `GameWonEvent` (or remove it and manage -/// win toasts manually). -pub struct WinCascadePlugin; - -impl Plugin for WinCascadePlugin { - fn build(&self, app: &mut App) { - app.add_systems(Update, trigger_expressive_win_cascade.after(GameMutation)); - } -} - -/// Inserts `CardAnimation` (Expressive curve) on every card when `GameWonEvent` fires. -/// -/// Cards scatter to 8 off-screen positions with per-card stagger. The z-lift -/// creates a "burst" effect as cards fly outward. -fn trigger_expressive_win_cascade( - mut events: MessageReader, - cards: Query<(Entity, &Transform), With>, - layout: Option>, - mut commands: Commands, -) { - if events.read().next().is_none() { - return; - } - - let radius = layout.as_ref().map_or(800.0, |l| l.0.card_size.x * 8.0); - - let targets = win_scatter_targets(radius); - - for (index, (entity, transform)) in cards.iter().enumerate() { - let start_xy = transform.translation.truncate(); - let start_z = transform.translation.z; - let target = targets[index % targets.len()]; - - commands.entity(entity).insert( - CardAnimation::slide( - start_xy, - start_z, - target, - start_z + 60.0, - MotionCurve::Expressive, - ) - .with_delay(cascade_delay(index, WIN_CASCADE_INTERVAL_SECS)) - .with_duration(0.65) - .with_z_lift(25.0), - ); - } -} - // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- @@ -433,19 +363,4 @@ mod tests { let state = HoverState::default(); assert!(state.entity.is_none()); } - - #[test] - fn win_scatter_produces_eight_distinct_points() { - let targets = win_scatter_targets(600.0); - assert_eq!(targets.len(), 8); - // All must be different. - for i in 0..8 { - for j in (i + 1)..8 { - assert_ne!( - targets[i], targets[j], - "scatter targets {i} and {j} must be distinct" - ); - } - } - } } diff --git a/solitaire_engine/src/card_animation/timing.rs b/solitaire_engine/src/card_animation/timing.rs index 189ada2..14b9f3c 100644 --- a/solitaire_engine/src/card_animation/timing.rs +++ b/solitaire_engine/src/card_animation/timing.rs @@ -49,17 +49,6 @@ pub fn micro_vary(duration: f32, entity_index: u32) -> f32 { duration * (1.0 + variation) } -/// Returns the pre-animation delay for card at `index` in a staggered cascade. -/// -/// `delay = index × interval_secs`. -#[inline] -pub fn cascade_delay(index: usize, interval_secs: f32) -> f32 { - index as f32 * interval_secs -} - -/// Recommended per-card interval for the win cascade (Normal speed). -pub const WIN_CASCADE_INTERVAL_SECS: f32 = 0.018; - /// Recommended per-card interval for deal animations (Normal speed). pub const DEAL_INTERVAL_SECS: f32 = 0.022; @@ -138,21 +127,4 @@ mod tests { ); } - #[test] - fn cascade_delay_zero_index_is_zero() { - assert_eq!(cascade_delay(0, 0.018), 0.0); - } - - #[test] - fn cascade_delay_scales_linearly() { - let interval = 0.018; - for i in 0..52usize { - let expected = i as f32 * interval; - let actual = cascade_delay(i, interval); - assert!( - (actual - expected).abs() < 1e-6, - "cascade_delay({i}) = {actual}, expected {expected}" - ); - } - } } diff --git a/solitaire_engine/src/events.rs b/solitaire_engine/src/events.rs index c908fc7..1fe61b8 100644 --- a/solitaire_engine/src/events.rs +++ b/solitaire_engine/src/events.rs @@ -5,7 +5,6 @@ use solitaire_core::KlondikePile; use solitaire_core::game_state::GameMode; use solitaire_core::{Card, Suit}; use solitaire_data::AchievementRecord; -use solitaire_sync::SyncResponse; /// Request to move `count` cards from `from` to `to`. Fired by input systems, /// consumed by `GamePlugin`. @@ -248,16 +247,6 @@ pub struct ToggleLeaderboardRequestEvent; #[derive(Message, Debug, Clone, Copy, Default)] pub struct ToggleHomeRequestEvent; -/// Fired by `SyncPlugin` after a pull task resolves and the merged result has -/// been persisted to disk. `Ok(SyncResponse)` carries the merged payload plus -/// any `ConflictReport`s the merge produced. `Err(String)` carries a -/// human-readable failure message (network, auth, serialization, etc.). -/// -/// UI systems listen for this to refresh views without polling -/// `SyncStatusResource`. See [ARCHITECTURE.md §4](../../ARCHITECTURE.md). -#[derive(Message, Debug, Clone)] -pub struct SyncCompleteEvent(pub Result); - /// Generic informational toast message. Any system can fire this to display /// a short string to the player, e.g. "Locked — reach level 5". #[derive(Message, Debug, Clone)] diff --git a/solitaire_engine/src/lib.rs b/solitaire_engine/src/lib.rs index 56b20c4..be576ee 100644 --- a/solitaire_engine/src/lib.rs +++ b/solitaire_engine/src/lib.rs @@ -83,9 +83,8 @@ pub use avatar_plugin::{AvatarFetchEvent, AvatarPlugin, AvatarResource}; pub use card_animation::{ AnimationChain, AnimationTuning, BufferedInput, CardAnimation, CardAnimationPlugin, DEAL_INTERVAL_SECS, DIAG_WINDOW_SIZE, FrameTimeDiagnostics, HoverState, InputBuffer, - InputPlatform, MAX_DURATION_SECS, MIN_DURATION_SECS, MotionCurve, WIN_CASCADE_INTERVAL_SECS, - WinCascadePlugin, cascade_delay, compute_duration, micro_vary, sample_curve, - win_scatter_targets, + InputPlatform, MAX_DURATION_SECS, MIN_DURATION_SECS, MotionCurve, compute_duration, + micro_vary, sample_curve, }; pub use card_plugin::{ CardEntity, CardImageSet, CardLabel, CardPlugin, HintHighlight, HintHighlightTimer, @@ -107,7 +106,7 @@ pub use events::{ HintVisualEvent, InfoToastEvent, ManualSyncRequestEvent, MoveRejectedEvent, MoveRequestEvent, NewGameRequestEvent, PauseRequestEvent, StartChallengeRequestEvent, StartDailyChallengeRequestEvent, StartDifficultyRequestEvent, StartPlayBySeedRequestEvent, - StartTimeAttackRequestEvent, StartZenRequestEvent, StateChangedEvent, SyncCompleteEvent, + StartTimeAttackRequestEvent, StartZenRequestEvent, StateChangedEvent, ToggleAchievementsRequestEvent, ToggleLeaderboardRequestEvent, ToggleProfileRequestEvent, ToggleSettingsRequestEvent, ToggleStatsRequestEvent, UndoRequestEvent, WinStreakMilestoneEvent, XpAwardedEvent, diff --git a/solitaire_engine/src/sync_plugin.rs b/solitaire_engine/src/sync_plugin.rs index 05dca1e..55d03af 100644 --- a/solitaire_engine/src/sync_plugin.rs +++ b/solitaire_engine/src/sync_plugin.rs @@ -22,12 +22,11 @@ use solitaire_data::{ AchievementRecord, PlayerProgress, Replay, StatsSnapshot, SyncError, SyncProvider, save_achievements_to, save_progress_to, save_replay_history_to, save_stats_to, }; -use solitaire_sync::{SyncPayload, SyncResponse, merge}; +use solitaire_sync::{SyncPayload, merge}; use crate::achievement_plugin::{AchievementsResource, AchievementsStoragePath}; use crate::events::{ - GameWonEvent, ManualSyncRequestEvent, SyncCompleteEvent, SyncConfigureRequestEvent, - WarningToastEvent, + GameWonEvent, ManualSyncRequestEvent, SyncConfigureRequestEvent, WarningToastEvent, }; use crate::game_plugin::RecordingReplay; use crate::progress_plugin::{ProgressResource, ProgressStoragePath}; @@ -108,7 +107,6 @@ impl Plugin for SyncPlugin { .init_resource::() .init_resource::() .add_message::() - .add_message::() .add_message::() .add_message::(); @@ -198,7 +196,6 @@ fn poll_pull_result( achievements_path: Res, mut progress: ResMut, progress_path: Res, - mut complete_writer: MessageWriter, mut configure_sync: MessageWriter, mut warning_toast: MessageWriter, ) { @@ -213,7 +210,7 @@ fn poll_pull_result( match result { Ok(remote) => { let local = build_payload(&stats.0, &achievements.0, &progress.0); - let (merged, conflicts) = merge(&local, &remote); + let (merged, _conflicts) = merge(&local, &remote); // Persist merged state atomically. if let Some(p) = &stats_path.0 @@ -233,17 +230,10 @@ fn poll_pull_result( } // Update in-world resources. - let now = Utc::now(); - stats.0 = merged.stats.clone(); - achievements.0 = merged.achievements.clone(); - progress.0 = merged.progress.clone(); - status.0 = SyncStatus::LastSynced(now); - - complete_writer.write(SyncCompleteEvent(Ok(SyncResponse { - merged, - server_time: now, - conflicts, - }))); + stats.0 = merged.stats; + achievements.0 = merged.achievements; + progress.0 = merged.progress; + status.0 = SyncStatus::LastSynced(Utc::now()); } Err(SyncError::UnsupportedPlatform) => { // No backend configured — not an error, just leave status as Idle. @@ -266,8 +256,7 @@ fn poll_pull_result( if matches!(e, SyncError::Auth(_)) { configure_sync.write(SyncConfigureRequestEvent); } - status.0 = SyncStatus::Error(msg.clone()); - complete_writer.write(SyncCompleteEvent(Err(msg))); + status.0 = SyncStatus::Error(msg); } } } diff --git a/solitaire_sync/src/lib.rs b/solitaire_sync/src/lib.rs index 77f3161..935ef96 100644 --- a/solitaire_sync/src/lib.rs +++ b/solitaire_sync/src/lib.rs @@ -13,7 +13,7 @@ pub mod stats; pub mod theme_store; pub use achievements::AchievementRecord; -pub use merge::{merge, merge_at}; +pub use merge::merge; pub use progress::{PlayerProgress, level_for_xp}; pub use stats::StatsSnapshot; pub use theme_store::{ThemeCatalogEntry, ThemeCatalogResponse}; @@ -98,30 +98,3 @@ pub struct LeaderboardEntry { /// When this entry was last recorded. pub recorded_at: DateTime, } - -// --------------------------------------------------------------------------- -// Error types -// --------------------------------------------------------------------------- - -/// Errors returned by the sync server in `application/json` error bodies. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)] -pub enum ApiError { - /// The request could not be authenticated (missing or invalid JWT). - #[error("unauthorized")] - Unauthorized, - /// The supplied credentials were incorrect. - #[error("invalid credentials")] - InvalidCredentials, - /// A username that was requested for registration is already taken. - #[error("username already taken")] - UsernameTaken, - /// The request payload was too large (> 1 MB). - #[error("payload too large")] - PayloadTooLarge, - /// The request body could not be parsed. - #[error("bad request: {0}")] - BadRequest(String), - /// An unexpected server-side error occurred. - #[error("internal server error")] - Internal, -} From 8d80f95bd09e800ff03dbd7f7879ccb055d80400 Mon Sep 17 00:00:00 2001 From: funman300 Date: Thu, 9 Jul 2026 15:02:15 -0700 Subject: [PATCH 2/2] chore(engine): apply rustfmt after dead-code removals Co-Authored-By: Claude Fable 5 --- solitaire_engine/src/card_animation/animation.rs | 1 - solitaire_engine/src/card_animation/timing.rs | 1 - solitaire_engine/src/lib.rs | 4 ++-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/solitaire_engine/src/card_animation/animation.rs b/solitaire_engine/src/card_animation/animation.rs index 0b403d3..e8cc2db 100644 --- a/solitaire_engine/src/card_animation/animation.rs +++ b/solitaire_engine/src/card_animation/animation.rs @@ -283,5 +283,4 @@ mod tests { .with_z_lift(12.0); assert!((anim.z_lift - 12.0).abs() < 1e-6); } - } diff --git a/solitaire_engine/src/card_animation/timing.rs b/solitaire_engine/src/card_animation/timing.rs index 14b9f3c..4c1ae36 100644 --- a/solitaire_engine/src/card_animation/timing.rs +++ b/solitaire_engine/src/card_animation/timing.rs @@ -126,5 +126,4 @@ mod tests { "micro_vary should differ for different indices" ); } - } diff --git a/solitaire_engine/src/lib.rs b/solitaire_engine/src/lib.rs index be576ee..2555d30 100644 --- a/solitaire_engine/src/lib.rs +++ b/solitaire_engine/src/lib.rs @@ -83,8 +83,8 @@ pub use avatar_plugin::{AvatarFetchEvent, AvatarPlugin, AvatarResource}; pub use card_animation::{ AnimationChain, AnimationTuning, BufferedInput, CardAnimation, CardAnimationPlugin, DEAL_INTERVAL_SECS, DIAG_WINDOW_SIZE, FrameTimeDiagnostics, HoverState, InputBuffer, - InputPlatform, MAX_DURATION_SECS, MIN_DURATION_SECS, MotionCurve, compute_duration, - micro_vary, sample_curve, + InputPlatform, MAX_DURATION_SECS, MIN_DURATION_SECS, MotionCurve, compute_duration, micro_vary, + sample_curve, }; pub use card_plugin::{ CardEntity, CardImageSet, CardLabel, CardPlugin, HintHighlight, HintHighlightTimer,