diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 81a4dcd..de61390 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -256,16 +256,35 @@ Done ### Bevy Plugins -| Plugin | Responsibility | -|---|---| -| `CardPlugin` | Card entity spawning, sprite management, drag-and-drop | -| `TablePlugin` | Pile markers, background, layout calculation | -| `AnimationPlugin` | Slide, flip, win cascade, toast animations | -| `AudioPlugin` | Sound effect and music playback via bevy_kira_audio | -| `UIPlugin` | All Bevy UI screens: Home, Stats, Achievements, Settings, Profile | -| `AchievementPlugin` | Listens for game events, evaluates unlock conditions, fires toasts | -| `SyncPlugin` | Manages sync lifecycle (pull on start, push on exit, status display) | -| `GamePlugin` | Core game state resource, input routing, win detection | +| Plugin | Key | Responsibility | +|---|---|---| +| `CardPlugin` | — | Card entity spawning, sprite management, drag-and-drop | +| `TablePlugin` | — | Pile markers, background, layout calculation | +| `AnimationPlugin` | — | Slide, flip, win cascade, toast animations | +| `FeedbackAnimPlugin` | — | Shake, settle, and deal-stagger animations | +| `AutoCompletePlugin` | Enter | Executes auto-complete when the HUD badge is lit | +| `AudioPlugin` | — | Sound effect and music playback via bevy_kira_audio | +| `InputPlugin` | — | Keyboard and mouse input routing | +| `CursorPlugin` | — | Custom cursor sprite during drag | +| `SelectionPlugin` | — | Keyboard-driven card selection | +| `GamePlugin` | N | Core game state resource, new-game flow, win/game-over overlays | +| `HudPlugin` | — | Score, move counter, timer, auto-complete badge | +| `StatsPlugin` | S | Stats overlay and persistence | +| `ProgressPlugin` | — | XP/level system, persistence | +| `AchievementPlugin` | A | Unlock evaluation, toast events, persistence | +| `DailyChallengePlugin` | — | Daily challenge resource and completion tracking | +| `WeeklyGoalsPlugin` | — | Weekly goal progress and completion events | +| `ChallengePlugin` | — | Challenge mode progression (seeded hard deals) | +| `TimeAttackPlugin` | — | 10-minute time-attack mode timer | +| `HomePlugin` | M | Main-menu overlay with keyboard shortcut reference | +| `ProfilePlugin` | P | Player profile overlay: level, XP, achievements, sync status | +| `SettingsPlugin` | O | Settings panel: audio, draw mode, theme, sync, cosmetics | +| `LeaderboardPlugin` | L | Leaderboard overlay | +| `HelpPlugin` | H | Help / controls overlay | +| `PausePlugin` | Esc | Pause and resume | +| `OnboardingPlugin` | — | First-run welcome screen | +| `SyncPlugin` | — | Async sync lifecycle (pull on start, push on exit, status display) | +| `WinSummaryPlugin` | — | Win cascade overlay and screen-shake effect | ### Key Bevy Resources @@ -588,6 +607,9 @@ pub enum PileType { pub enum DrawMode { DrawOne, DrawThree } +/// Active game mode. Classic is the default; others unlock at level 5. +pub enum GameMode { Classic, Zen, Challenge, TimeAttack } + pub enum MoveError { InvalidSource, InvalidDestination, @@ -600,13 +622,16 @@ pub enum MoveError { pub struct GameState { pub piles: HashMap>, pub draw_mode: DrawMode, + pub mode: GameMode, pub score: i32, pub move_count: u32, + pub undo_count: u32, // number of undos used in this game + pub recycle_count: u32, // number of stock recycles pub elapsed_seconds: u64, pub seed: u64, pub is_won: bool, pub is_auto_completable: bool, - undo_stack: Vec, // private, max 64 + undo_stack: VecDeque, // private, max 64 (VecDeque for O(1) pop_front) } ``` diff --git a/solitaire_app/src/main.rs b/solitaire_app/src/main.rs index 43e629a..32a7524 100644 --- a/solitaire_app/src/main.rs +++ b/solitaire_app/src/main.rs @@ -3,9 +3,9 @@ use solitaire_data::{load_settings_from, provider_for_backend, settings_file_pat use solitaire_engine::{ AchievementPlugin, AnimationPlugin, AudioPlugin, AutoCompletePlugin, CardPlugin, ChallengePlugin, CursorPlugin, DailyChallengePlugin, FeedbackAnimPlugin, GamePlugin, - HelpPlugin, HudPlugin, InputPlugin, LeaderboardPlugin, OnboardingPlugin, PausePlugin, - ProgressPlugin, SettingsPlugin, StatsPlugin, SyncPlugin, TablePlugin, TimeAttackPlugin, - WeeklyGoalsPlugin, + HelpPlugin, HomePlugin, HudPlugin, InputPlugin, LeaderboardPlugin, OnboardingPlugin, + PausePlugin, ProfilePlugin, ProgressPlugin, SettingsPlugin, StatsPlugin, SyncPlugin, + TablePlugin, TimeAttackPlugin, WeeklyGoalsPlugin, }; fn main() { @@ -44,6 +44,8 @@ fn main() { .add_plugins(TimeAttackPlugin) .add_plugins(HudPlugin) .add_plugins(HelpPlugin) + .add_plugins(HomePlugin) + .add_plugins(ProfilePlugin) .add_plugins(PausePlugin) .add_plugins(SettingsPlugin::default()) .add_plugins(AudioPlugin) diff --git a/solitaire_engine/src/animation_plugin.rs b/solitaire_engine/src/animation_plugin.rs index f931dc6..c2ef5ff 100644 --- a/solitaire_engine/src/animation_plugin.rs +++ b/solitaire_engine/src/animation_plugin.rs @@ -24,6 +24,7 @@ use crate::events::{InfoToastEvent, NewGameConfirmEvent, XpAwardedEvent}; use crate::events::{AchievementUnlockedEvent, GameWonEvent}; use crate::game_plugin::GameMutation; use crate::layout::LayoutResource; +use crate::pause_plugin::PausedResource; use crate::progress_plugin::LevelUpEvent; use crate::settings_plugin::{SettingsChangedEvent, SettingsResource}; use crate::time_attack_plugin::TimeAttackEndedEvent; @@ -60,8 +61,41 @@ const WEEKLY_TOAST_SECS: f32 = 3.0; const TIME_ATTACK_TOAST_SECS: f32 = 5.0; const CHALLENGE_TOAST_SECS: f32 = 3.0; const VOLUME_TOAST_SECS: f32 = 1.4; -const CASCADE_STAGGER: f32 = 0.05; -const CASCADE_DURATION: f32 = 0.5; + +/// Per-card stagger interval for the win cascade at Normal speed (seconds). +const CASCADE_STAGGER_NORMAL: f32 = 0.05; +/// Duration of each card's cascade slide at Normal speed (seconds). +const CASCADE_DURATION_NORMAL: f32 = 0.5; + +/// Returns the per-card stagger delay for the win cascade at the given `AnimSpeed`. +/// +/// | `AnimSpeed` | Returned value | +/// |-------------|----------------| +/// | `Normal` | 0.05 s | +/// | `Fast` | 0.025 s | +/// | `Instant` | 0.0 s | +pub fn cascade_step_secs(speed: AnimSpeed) -> f32 { + match speed { + AnimSpeed::Normal => CASCADE_STAGGER_NORMAL, + AnimSpeed::Fast => CASCADE_STAGGER_NORMAL / 2.0, + AnimSpeed::Instant => 0.0, + } +} + +/// Returns the slide duration for each card in the win cascade at the given `AnimSpeed`. +/// +/// | `AnimSpeed` | Returned value | +/// |-------------|----------------| +/// | `Normal` | 0.5 s | +/// | `Fast` | 0.25 s | +/// | `Instant` | 0.0 s | +pub fn cascade_duration_secs(speed: AnimSpeed) -> f32 { + match speed { + AnimSpeed::Normal => CASCADE_DURATION_NORMAL, + AnimSpeed::Fast => CASCADE_DURATION_NORMAL / 2.0, + AnimSpeed::Instant => 0.0, + } +} /// Linear-lerp slide animation. /// @@ -181,11 +215,19 @@ fn sync_slide_duration( } } +/// Advances all in-flight `CardAnim` slide animations. +/// +/// Skipped while the game is paused so cards do not move while the pause +/// overlay is open. fn advance_card_anims( mut commands: Commands, time: Res