//! 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::achievement_plugin::display_name_for; 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