//! `CardAnimation` component and the system that drives it. //! //! # Design //! //! `CardAnimation` is a **drop-in upgrade** for the existing linear `CardAnim`. //! It targets `Transform` (the current sprite-based architecture). Swapping to //! Bevy UI requires only changing the four write lines in `advance_card_animations` //! to write `Style.left` / `Style.top` via a `Style` component query instead. //! //! # Z-lift //! //! During motion, `translation.z` follows a parabolic arc: //! //! ```text //! z(t) = lerp(start_z, end_z, t) + z_lift × sin(t × π) //! ``` //! //! The sine term is 0 at `t = 0` and `t = 1` and peaks at `t = 0.5`, so the //! card "floats up" in the middle of its travel and lands at its correct rest z. //! //! # Coexistence with `CardAnim` //! //! `CardAnimation` and the legacy `CardAnim` can coexist in the same world but //! **must never be on the same entity** — both write to `Transform`. When //! migrating, replace `CardAnim` insertions with `CardAnimation` insertions and //! register `CardAnimationPlugin` alongside `AnimationPlugin`. use std::f32::consts::PI; use bevy::prelude::*; use bevy::window::RequestRedraw; use super::curves::{MotionCurve, sample_curve}; use super::timing::compute_duration; use crate::pause_plugin::PausedResource; // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- /// Curve-based card animation. /// /// Drives `Transform` XY translation via a [`MotionCurve`], with optional /// z-lift and scale interpolation. Removes itself when the animation completes. #[derive(Component, Debug, Clone)] pub struct CardAnimation { /// 2-D start position (world space). pub start: Vec2, /// 2-D destination (world space). pub end: Vec2, /// Seconds elapsed since the delay expired. pub elapsed: f32, /// Total animation duration in seconds (excluding delay). pub duration: f32, /// Easing curve applied to the interpolation factor. pub curve: MotionCurve, /// Seconds to wait before starting movement. pub delay: f32, /// Z coordinate at animation start (used for parabolic lift calculation). pub start_z: f32, /// Z coordinate at animation end — the card's resting z after completion. pub end_z: f32, /// Extra Z added at the midpoint of motion (`z(0.5) = base_z + z_lift`). /// Set to 0.0 to disable the depth arc. pub z_lift: f32, /// Transform scale at `t = 0`. pub scale_start: f32, /// Transform scale at `t = 1`. pub scale_end: f32, } impl CardAnimation { /// Convenience constructor: slide from `start` to `end` with auto-computed /// duration based on pixel distance. No z-lift or scale change. pub fn slide(start: Vec2, start_z: f32, end: Vec2, end_z: f32, curve: MotionCurve) -> Self { Self { start, end, elapsed: 0.0, duration: compute_duration(start.distance(end)), curve, delay: 0.0, start_z, end_z, z_lift: 0.0, scale_start: 1.0, scale_end: 1.0, } } /// Sets the pre-animation delay in seconds. #[must_use] pub fn with_delay(mut self, secs: f32) -> Self { self.delay = secs; self } /// Overrides the auto-computed duration. #[must_use] pub fn with_duration(mut self, secs: f32) -> Self { self.duration = secs; self } /// Enables the parabolic z-lift arc with the given peak offset. #[must_use] pub fn with_z_lift(mut self, lift: f32) -> Self { self.z_lift = lift; self } /// Interpolates `Transform.scale` from `start` to `end` over the animation. #[must_use] pub fn with_scale(mut self, start: f32, end: f32) -> Self { self.scale_start = start; self.scale_end = end; self } /// Returns the current interpolated XY position without advancing time. pub fn current_xy(&self) -> Vec2 { if self.duration <= 0.0 { return self.end; } let t = (self.elapsed / self.duration).clamp(0.0, 1.0); let s = sample_curve(self.curve, t); self.start.lerp(self.end, s) } } // --------------------------------------------------------------------------- // System // --------------------------------------------------------------------------- /// Advances all [`CardAnimation`] components each frame. /// /// Skipped while the game is paused. On completion the component is removed /// and `Transform` is snapped to the exact destination to prevent floating-point /// drift. pub(crate) fn advance_card_animations( mut commands: Commands, time: Res