Merge pull request 'chore: delete dead code approved from the PR #166 sweep' (#169) from chore/dead-code-removals into master
Build and Deploy / build-and-push (push) Successful in 11m15s
Web E2E / web-e2e (push) Successful in 9m32s
Android Release / build-apk (push) Successful in 5m18s
Test / test (push) Successful in 36m51s

This commit was merged in pull request #169.
This commit is contained in:
2026-07-09 23:46:47 +00:00
8 changed files with 15 additions and 219 deletions
+1 -3
View File
@@ -145,7 +145,6 @@ Shared API contract types imported by both the game client (`solitaire_data`) an
Owns: Owns:
- `SyncPayload`, `SyncResponse`, `ConflictReport` - `SyncPayload`, `SyncResponse`, `ConflictReport`
- `ChallengeGoal`, `LeaderboardEntry` - `ChallengeGoal`, `LeaderboardEntry`
- `ApiError` enum
- Merge logic (pure functions, no I/O) - Merge logic (pure functions, no I/O)
### `solitaire_data` ### `solitaire_data`
@@ -257,7 +256,7 @@ solitaire_sync::merge(local, remote)
Write merged result to disk Write merged result to disk
fires SyncCompleteEvent
Bevy main thread reads updated StatsResource Bevy main thread reads updated StatsResource
``` ```
@@ -377,7 +376,6 @@ struct StateChangedEvent;
struct CardFlippedEvent(u32); struct CardFlippedEvent(u32);
struct GameWonEvent { score: i32, time_seconds: u64 } struct GameWonEvent { score: i32, time_seconds: u64 }
struct AchievementUnlockedEvent(AchievementRecord); struct AchievementUnlockedEvent(AchievementRecord);
struct SyncCompleteEvent(Result<SyncResponse, String>);
``` ```
### Layout System ### Layout System
@@ -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 // Tests
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -304,21 +283,4 @@ mod tests {
.with_z_lift(12.0); .with_z_lift(12.0);
assert!((anim.z_lift - 12.0).abs() < 1e-6); 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:?}"
);
}
}
} }
+2 -87
View File
@@ -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 //! # Coexistence rules
//! //!
//! | Condition | Safe? | //! | Condition | Safe? |
@@ -58,24 +48,21 @@ pub mod interaction;
pub mod timing; pub mod timing;
pub mod tuning; pub mod tuning;
pub use animation::{CardAnimation, win_scatter_targets}; pub use animation::CardAnimation;
pub use chain::AnimationChain; pub use chain::AnimationChain;
pub use curves::{MotionCurve, sample_curve}; pub use curves::{MotionCurve, sample_curve};
pub use diagnostics::{FrameTimeDiagnostics, WINDOW_SIZE as DIAG_WINDOW_SIZE}; pub use diagnostics::{FrameTimeDiagnostics, WINDOW_SIZE as DIAG_WINDOW_SIZE};
pub use interaction::{BufferedInput, HoverState, InputBuffer}; pub use interaction::{BufferedInput, HoverState, InputBuffer};
pub use timing::{ pub use timing::{
DEAL_INTERVAL_SECS, MAX_DURATION_SECS, MIN_DURATION_SECS, WIN_CASCADE_INTERVAL_SECS, DEAL_INTERVAL_SECS, MAX_DURATION_SECS, MIN_DURATION_SECS, compute_duration, micro_vary,
cascade_delay, compute_duration, micro_vary,
}; };
pub use tuning::{AnimationTuning, InputPlatform}; pub use tuning::{AnimationTuning, InputPlatform};
use bevy::prelude::*; use bevy::prelude::*;
use bevy::window::RequestRedraw; use bevy::window::RequestRedraw;
use crate::card_plugin::CardEntity;
use crate::events::{DrawRequestEvent, GameWonEvent, MoveRequestEvent, UndoRequestEvent}; use crate::events::{DrawRequestEvent, GameWonEvent, MoveRequestEvent, UndoRequestEvent};
use crate::game_plugin::GameMutation; use crate::game_plugin::GameMutation;
use crate::layout::LayoutResource;
use crate::resources::DragState; use crate::resources::DragState;
use animation::advance_card_animations; 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<GameWonEvent>,
cards: Query<(Entity, &Transform), With<CardEntity>>,
layout: Option<Res<LayoutResource>>,
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 // Tests
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -433,19 +363,4 @@ mod tests {
let state = HoverState::default(); let state = HoverState::default();
assert!(state.entity.is_none()); 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"
);
}
}
}
} }
@@ -49,17 +49,6 @@ pub fn micro_vary(duration: f32, entity_index: u32) -> f32 {
duration * (1.0 + variation) 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). /// Recommended per-card interval for deal animations (Normal speed).
pub const DEAL_INTERVAL_SECS: f32 = 0.022; pub const DEAL_INTERVAL_SECS: f32 = 0.022;
@@ -137,22 +126,4 @@ mod tests {
"micro_vary should differ for different indices" "micro_vary should differ for different indices"
); );
} }
#[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}"
);
}
}
} }
-11
View File
@@ -5,7 +5,6 @@ use solitaire_core::KlondikePile;
use solitaire_core::game_state::GameMode; use solitaire_core::game_state::GameMode;
use solitaire_core::{Card, Suit}; use solitaire_core::{Card, Suit};
use solitaire_data::AchievementRecord; use solitaire_data::AchievementRecord;
use solitaire_sync::SyncResponse;
/// Request to move `count` cards from `from` to `to`. Fired by input systems, /// Request to move `count` cards from `from` to `to`. Fired by input systems,
/// consumed by `GamePlugin`. /// consumed by `GamePlugin`.
@@ -248,16 +247,6 @@ pub struct ToggleLeaderboardRequestEvent;
#[derive(Message, Debug, Clone, Copy, Default)] #[derive(Message, Debug, Clone, Copy, Default)]
pub struct ToggleHomeRequestEvent; 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<SyncResponse, String>);
/// Generic informational toast message. Any system can fire this to display /// Generic informational toast message. Any system can fire this to display
/// a short string to the player, e.g. "Locked — reach level 5". /// a short string to the player, e.g. "Locked — reach level 5".
#[derive(Message, Debug, Clone)] #[derive(Message, Debug, Clone)]
+3 -4
View File
@@ -83,9 +83,8 @@ pub use avatar_plugin::{AvatarFetchEvent, AvatarPlugin, AvatarResource};
pub use card_animation::{ pub use card_animation::{
AnimationChain, AnimationTuning, BufferedInput, CardAnimation, CardAnimationPlugin, AnimationChain, AnimationTuning, BufferedInput, CardAnimation, CardAnimationPlugin,
DEAL_INTERVAL_SECS, DIAG_WINDOW_SIZE, FrameTimeDiagnostics, HoverState, InputBuffer, DEAL_INTERVAL_SECS, DIAG_WINDOW_SIZE, FrameTimeDiagnostics, HoverState, InputBuffer,
InputPlatform, MAX_DURATION_SECS, MIN_DURATION_SECS, MotionCurve, WIN_CASCADE_INTERVAL_SECS, InputPlatform, MAX_DURATION_SECS, MIN_DURATION_SECS, MotionCurve, compute_duration, micro_vary,
WinCascadePlugin, cascade_delay, compute_duration, micro_vary, sample_curve, sample_curve,
win_scatter_targets,
}; };
pub use card_plugin::{ pub use card_plugin::{
CardEntity, CardImageSet, CardLabel, CardPlugin, HintHighlight, HintHighlightTimer, CardEntity, CardImageSet, CardLabel, CardPlugin, HintHighlight, HintHighlightTimer,
@@ -107,7 +106,7 @@ pub use events::{
HintVisualEvent, InfoToastEvent, ManualSyncRequestEvent, MoveRejectedEvent, MoveRequestEvent, HintVisualEvent, InfoToastEvent, ManualSyncRequestEvent, MoveRejectedEvent, MoveRequestEvent,
NewGameRequestEvent, PauseRequestEvent, StartChallengeRequestEvent, NewGameRequestEvent, PauseRequestEvent, StartChallengeRequestEvent,
StartDailyChallengeRequestEvent, StartDifficultyRequestEvent, StartPlayBySeedRequestEvent, StartDailyChallengeRequestEvent, StartDifficultyRequestEvent, StartPlayBySeedRequestEvent,
StartTimeAttackRequestEvent, StartZenRequestEvent, StateChangedEvent, SyncCompleteEvent, StartTimeAttackRequestEvent, StartZenRequestEvent, StateChangedEvent,
ToggleAchievementsRequestEvent, ToggleLeaderboardRequestEvent, ToggleProfileRequestEvent, ToggleAchievementsRequestEvent, ToggleLeaderboardRequestEvent, ToggleProfileRequestEvent,
ToggleSettingsRequestEvent, ToggleStatsRequestEvent, UndoRequestEvent, WinStreakMilestoneEvent, ToggleSettingsRequestEvent, ToggleStatsRequestEvent, UndoRequestEvent, WinStreakMilestoneEvent,
XpAwardedEvent, XpAwardedEvent,
+8 -19
View File
@@ -22,12 +22,11 @@ use solitaire_data::{
AchievementRecord, PlayerProgress, Replay, StatsSnapshot, SyncError, SyncProvider, AchievementRecord, PlayerProgress, Replay, StatsSnapshot, SyncError, SyncProvider,
save_achievements_to, save_progress_to, save_replay_history_to, save_stats_to, 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::achievement_plugin::{AchievementsResource, AchievementsStoragePath};
use crate::events::{ use crate::events::{
GameWonEvent, ManualSyncRequestEvent, SyncCompleteEvent, SyncConfigureRequestEvent, GameWonEvent, ManualSyncRequestEvent, SyncConfigureRequestEvent, WarningToastEvent,
WarningToastEvent,
}; };
use crate::game_plugin::RecordingReplay; use crate::game_plugin::RecordingReplay;
use crate::progress_plugin::{ProgressResource, ProgressStoragePath}; use crate::progress_plugin::{ProgressResource, ProgressStoragePath};
@@ -108,7 +107,6 @@ impl Plugin for SyncPlugin {
.init_resource::<PullTask>() .init_resource::<PullTask>()
.init_resource::<PendingReplayUpload>() .init_resource::<PendingReplayUpload>()
.add_message::<ManualSyncRequestEvent>() .add_message::<ManualSyncRequestEvent>()
.add_message::<SyncCompleteEvent>()
.add_message::<SyncConfigureRequestEvent>() .add_message::<SyncConfigureRequestEvent>()
.add_message::<WarningToastEvent>(); .add_message::<WarningToastEvent>();
@@ -198,7 +196,6 @@ fn poll_pull_result(
achievements_path: Res<AchievementsStoragePath>, achievements_path: Res<AchievementsStoragePath>,
mut progress: ResMut<ProgressResource>, mut progress: ResMut<ProgressResource>,
progress_path: Res<ProgressStoragePath>, progress_path: Res<ProgressStoragePath>,
mut complete_writer: MessageWriter<SyncCompleteEvent>,
mut configure_sync: MessageWriter<SyncConfigureRequestEvent>, mut configure_sync: MessageWriter<SyncConfigureRequestEvent>,
mut warning_toast: MessageWriter<WarningToastEvent>, mut warning_toast: MessageWriter<WarningToastEvent>,
) { ) {
@@ -213,7 +210,7 @@ fn poll_pull_result(
match result { match result {
Ok(remote) => { Ok(remote) => {
let local = build_payload(&stats.0, &achievements.0, &progress.0); 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. // Persist merged state atomically.
if let Some(p) = &stats_path.0 if let Some(p) = &stats_path.0
@@ -233,17 +230,10 @@ fn poll_pull_result(
} }
// Update in-world resources. // Update in-world resources.
let now = Utc::now(); stats.0 = merged.stats;
stats.0 = merged.stats.clone(); achievements.0 = merged.achievements;
achievements.0 = merged.achievements.clone(); progress.0 = merged.progress;
progress.0 = merged.progress.clone(); status.0 = SyncStatus::LastSynced(Utc::now());
status.0 = SyncStatus::LastSynced(now);
complete_writer.write(SyncCompleteEvent(Ok(SyncResponse {
merged,
server_time: now,
conflicts,
})));
} }
Err(SyncError::UnsupportedPlatform) => { Err(SyncError::UnsupportedPlatform) => {
// No backend configured — not an error, just leave status as Idle. // No backend configured — not an error, just leave status as Idle.
@@ -266,8 +256,7 @@ fn poll_pull_result(
if matches!(e, SyncError::Auth(_)) { if matches!(e, SyncError::Auth(_)) {
configure_sync.write(SyncConfigureRequestEvent); configure_sync.write(SyncConfigureRequestEvent);
} }
status.0 = SyncStatus::Error(msg.clone()); status.0 = SyncStatus::Error(msg);
complete_writer.write(SyncCompleteEvent(Err(msg)));
} }
} }
} }
+1 -28
View File
@@ -13,7 +13,7 @@ pub mod stats;
pub mod theme_store; pub mod theme_store;
pub use achievements::AchievementRecord; pub use achievements::AchievementRecord;
pub use merge::{merge, merge_at}; pub use merge::merge;
pub use progress::{PlayerProgress, level_for_xp}; pub use progress::{PlayerProgress, level_for_xp};
pub use stats::StatsSnapshot; pub use stats::StatsSnapshot;
pub use theme_store::{ThemeCatalogEntry, ThemeCatalogResponse}; pub use theme_store::{ThemeCatalogEntry, ThemeCatalogResponse};
@@ -98,30 +98,3 @@ pub struct LeaderboardEntry {
/// When this entry was last recorded. /// When this entry was last recorded.
pub recorded_at: DateTime<Utc>, pub recorded_at: DateTime<Utc>,
} }
// ---------------------------------------------------------------------------
// 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,
}