diff --git a/solitaire_app/src/main.rs b/solitaire_app/src/main.rs index 239b62c..a71d87c 100644 --- a/solitaire_app/src/main.rs +++ b/solitaire_app/src/main.rs @@ -1,5 +1,5 @@ use bevy::prelude::*; -use solitaire_engine::{CardPlugin, GamePlugin, InputPlugin, TablePlugin}; +use solitaire_engine::{AnimationPlugin, CardPlugin, GamePlugin, InputPlugin, TablePlugin}; fn main() { App::new() @@ -17,5 +17,6 @@ fn main() { .add_plugins(TablePlugin) .add_plugins(CardPlugin) .add_plugins(InputPlugin) + .add_plugins(AnimationPlugin) .run(); } diff --git a/solitaire_engine/src/animation_plugin.rs b/solitaire_engine/src/animation_plugin.rs new file mode 100644 index 0000000..a7c699b --- /dev/null +++ b/solitaire_engine/src/animation_plugin.rs @@ -0,0 +1,293 @@ +//! Smooth animations: card slide (linear lerp), win cascade, achievement toast. +//! +//! `CardAnim` is the only animation component used by other plugins — import +//! it directly when adding animations outside this file. + +use bevy::prelude::*; + +use crate::card_plugin::CardEntity; +use crate::events::{AchievementUnlockedEvent, GameWonEvent}; +use crate::game_plugin::GameMutation; +use crate::layout::LayoutResource; + +/// Duration of a card slide (move) animation in seconds. +pub const SLIDE_SECS: f32 = 0.15; + +const WIN_TOAST_SECS: f32 = 4.0; +const ACHIEVEMENT_TOAST_SECS: f32 = 3.0; +const CASCADE_STAGGER: f32 = 0.05; +const CASCADE_DURATION: f32 = 0.5; + +/// Linear-lerp slide animation. +/// +/// After `delay` seconds the card moves from `start` to `target` over +/// `duration` seconds. The component removes itself when the slide completes. +#[derive(Component, Debug, Clone)] +pub struct CardAnim { + pub start: Vec3, + pub target: Vec3, + pub elapsed: f32, + pub duration: f32, + /// Additional wait before the slide begins. + pub delay: f32, +} + +/// Marker on a toast overlay UI node. +#[derive(Component, Debug)] +pub struct ToastOverlay; + +/// Auto-dismiss countdown (seconds remaining). Attached to toast entities. +#[derive(Component, Debug)] +pub struct ToastTimer(pub f32); + +pub struct AnimationPlugin; + +impl Plugin for AnimationPlugin { + fn build(&self, app: &mut App) { + // Register the events this plugin consumes so tests that don't include + // GamePlugin can still run AnimationPlugin in isolation. Double-registration + // is idempotent in Bevy. + app.add_event::() + .add_event::() + .add_systems( + Update, + ( + advance_card_anims, + handle_win_cascade, + handle_achievement_toast, + tick_toasts, + ) + .after(GameMutation), + ); + } +} + +fn advance_card_anims( + mut commands: Commands, + time: Res