//! Card interaction visuals: hover scale, drag lift, and input buffering. //! //! # Hover //! //! [`HoverState`] tracks the entity currently under the cursor. A system //! smoothly lerps `Transform.scale` toward `HOVER_SCALE` on the hovered card //! and back to 1.0 when the cursor leaves. Scale is only written when no //! [`CardAnimation`] is active on the entity (the animation takes priority). //! //! # Drag visual //! //! While [`DragState`] is non-idle, the dragged card entities receive a subtle //! scale boost (`DRAG_LIFT_SCALE`) and their z-order is pushed up. The exact //! translation is still controlled by the existing [`crate::input_plugin`] — //! this system only applies the _visual_ enhancement without touching XY. //! //! # Input buffer //! //! [`InputBuffer`] stores move/draw/undo actions that arrived while cards are //! still animating. Call [`InputBuffer::push`] from any system that wants //! buffering. The drain system fires the oldest buffered action as soon as all //! [`CardAnimation`] components have cleared, giving a responsive feel on //! fast repeated clicks. //! //! # Visual priority //! //! Dragged cards always have the highest z. The existing [`crate::input_plugin`] //! sets drag z; this module applies scale on top. The ordering constraint //! `.after(crate::game_plugin::GameMutation)` ensures all game-state changes //! settle before visual updates run. use std::collections::VecDeque; use bevy::prelude::*; use bevy::window::PrimaryWindow; use super::animation::CardAnimation; use crate::card_plugin::CardEntity; use crate::events::{DrawRequestEvent, MoveRequestEvent, UndoRequestEvent}; use crate::layout::LayoutResource; use crate::resources::DragState; /// Type alias to reduce complexity in hover/drag query signatures. type CardTransformQuery<'w, 's> = Query<'w, 's, (Entity, &'static mut Transform), (With, Without)>; // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- /// Scale applied to the card currently under the cursor (1.0 = no change). const HOVER_SCALE: f32 = 1.04; /// Additional scale applied to dragged cards while in flight. const DRAG_LIFT_SCALE: f32 = 1.08; /// Lerp speed for hover scale interpolation (higher = snappier). const HOVER_LERP_SPEED: f32 = 14.0; /// Lerp speed for drag scale interpolation. const DRAG_LERP_SPEED: f32 = 20.0; /// Maximum number of buffered inputs retained. const INPUT_BUFFER_CAPACITY: usize = 4; // --------------------------------------------------------------------------- // Resources // --------------------------------------------------------------------------- /// Tracks the entity currently under the cursor and the interpolated hover scale. #[derive(Resource, Debug, Default)] pub struct HoverState { /// Entity currently hovered (`None` when cursor is off all cards or dragging). pub entity: Option, /// Current interpolated scale applied to the hovered card. pub scale: f32, } /// Describes a user action that arrived while cards were still animating. #[derive(Debug, Clone)] pub enum BufferedInput { Move { from: crate::events::MoveRequestEvent }, Draw, Undo, } /// FIFO queue of inputs deferred until ongoing animations complete. /// /// Populate via [`InputBuffer::push`] and consume via the drain system. /// Capped at [`INPUT_BUFFER_CAPACITY`] — further pushes when full are silently /// dropped to prevent stale action pileup. #[derive(Resource, Debug, Default)] pub struct InputBuffer { pub(crate) queue: VecDeque, } impl InputBuffer { /// Enqueues an input if the buffer is not full. pub fn push(&mut self, input: BufferedInput) { if self.queue.len() < INPUT_BUFFER_CAPACITY { self.queue.push_back(input); } } /// Returns `true` when no inputs are pending. pub fn is_empty(&self) -> bool { self.queue.is_empty() } /// Returns how many inputs are queued. pub fn len(&self) -> usize { self.queue.len() } } // --------------------------------------------------------------------------- // Systems // --------------------------------------------------------------------------- /// Detects which card is under the cursor and updates [`HoverState`]. /// /// Clears hover when [`DragState`] is active (dragging takes visual priority). /// Picks the topmost card (highest `translation.z`) when multiple cards overlap. pub(crate) fn detect_hover( windows: Query<&Window, With>, cameras: Query<(&Camera, &GlobalTransform)>, drag: Option>, layout: Option>, cards: Query<(Entity, &Transform), With>, mut hover: ResMut, ) { let is_dragging = drag.as_ref().is_some_and(|d| !d.is_idle()); if is_dragging { hover.entity = None; return; } let Some(layout) = layout else { return }; let Some(cursor_world) = cursor_world(&windows, &cameras) else { hover.entity = None; return; }; let half_w = layout.0.card_size.x * 0.5; let half_h = layout.0.card_size.y * 0.5; let mut best: Option<(Entity, f32)> = None; for (entity, transform) in &cards { let pos = transform.translation.truncate(); if (cursor_world.x - pos.x).abs() < half_w && (cursor_world.y - pos.y).abs() < half_h { let z = transform.translation.z; if best.is_none_or(|(_, bz)| z > bz) { best = Some((entity, z)); } } } hover.entity = best.map(|(e, _)| e); } /// Applies the hover scale to the currently hovered card via smooth lerp. /// /// Only runs on cards that have **no active [`CardAnimation`]** — animated /// cards control their own scale. When hover changes entities, the previous /// entity's scale is snapped back to 1.0 to avoid leaving a permanently /// enlarged card. pub(crate) fn apply_hover_scale( time: Res