//! Keyboard + mouse input for the game board. //! //! All systems exit immediately when `PausedResource(true)` — no moves, //! draws, undos, or drags are processed while the pause overlay is showing. //! //! Keyboard: //! - `U` → `UndoRequestEvent` //! - `N` → `NewGameRequestEvent { seed: None }` (cancels Time Attack if active) //! - `D` / `Space` → `DrawRequestEvent` //! - `Esc` → handled by `PausePlugin` (overlay toggle + paused flag) //! //! Mouse: //! - Left-click on the stock pile (face-down top) → `DrawRequestEvent` //! - Left-press-drag-release on a face-up card → `MoveRequestEvent` between //! the origin pile and whatever pile the cursor is over at release. //! On rejection, the drag cards snap back to their origin via a //! `StateChangedEvent` re-sync. use std::collections::HashMap; use bevy::ecs::system::SystemParam; use bevy::input::touch::{TouchInput, TouchPhase, Touches}; use bevy::input::ButtonInput; use bevy::math::{Vec2, Vec3}; use bevy::prelude::*; use bevy::window::{MonitorSelection, PrimaryWindow, WindowMode}; use solitaire_core::card::{Card, Suit}; use solitaire_core::game_state::GameState; use solitaire_core::pile::PileType; use solitaire_core::rules::{can_place_on_foundation, can_place_on_tableau}; use crate::card_animation::tuning::AnimationTuning; use crate::card_plugin::{CardEntity, HintHighlight, HintHighlightTimer, TABLEAU_FAN_FRAC}; use crate::feedback_anim_plugin::ShakeAnim; use solitaire_core::game_state::DrawMode; use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL; use crate::events::{ DrawRequestEvent, ForfeitEvent, HintVisualEvent, InfoToastEvent, MoveRejectedEvent, MoveRequestEvent, NewGameConfirmEvent, NewGameRequestEvent, StateChangedEvent, UndoRequestEvent, }; use crate::game_plugin::GameMutation; use crate::pause_plugin::PausedResource; use crate::progress_plugin::ProgressResource; use crate::layout::{Layout, LayoutResource}; use crate::resources::{DragState, GameStateResource, HintCycleIndex}; use crate::time_attack_plugin::TimeAttackResource; /// Z-depth used for cards while being dragged — above all resting cards. const DRAG_Z: f32 = 500.0; /// Shared countdown timers for the double-press confirmation flows. /// /// Using a resource (instead of `Local`) lets the three keyboard sub-systems /// share the same countdown state without needing to pass values between them. #[derive(Resource, Debug, Default)] struct KeyboardConfirmState { /// Seconds remaining in the new-game confirmation window (> 0 while open). new_game_countdown: f32, /// True while we are waiting for the second N press to confirm a new game. new_game_pending: bool, /// Seconds remaining in the forfeit confirmation window (> 0 while open). forfeit_countdown: f32, } /// Registers keyboard, mouse, and touch input systems. /// /// Mouse drag pipeline (ordered, left-to-right): /// `start_drag` → `follow_drag` → `end_drag` /// /// Touch drag pipeline (ordered, interleaved with mouse): /// `touch_start_drag` → `touch_follow_drag` → `touch_end_drag` /// /// Both pipelines share [`DragState`]. Only one can be active at a time — /// the second checks `drag.is_idle()` before proceeding, and mouse drags /// check `drag.active_touch_id.is_none()`. /// /// All drag systems run before [`GameMutation`] so move events are consumed /// in the same frame they are emitted. pub struct InputPlugin; impl Plugin for InputPlugin { fn build(&self, app: &mut App) { app.init_resource::() .init_resource::() .add_message::() .add_message::() .add_message::() .add_message::() .add_systems( Update, ( handle_keyboard_core, handle_keyboard_hint, handle_keyboard_forfeit, handle_stock_click, handle_touch_stock_tap, handle_double_click, // Mouse drag pipeline. start_drag, follow_drag, end_drag.before(GameMutation), // Touch drag pipeline (parallel path through DragState). touch_start_drag, touch_follow_drag, touch_end_drag.before(GameMutation), ) .chain(), ) .add_systems(Update, handle_fullscreen) .add_systems(Update, reset_hint_cycle_on_state_change); } } /// Seconds after the first N press during which a second N confirms new game. const NEW_GAME_CONFIRM_WINDOW: f32 = 3.0; /// Seconds after the first G press during which a second G confirms forfeit. const FORFEIT_CONFIRM_WINDOW: f32 = 3.0; /// Bundles the event writers needed by the core keyboard handler. /// /// Keeping these in a [`SystemParam`] avoids hitting Bevy's 16-parameter limit. #[derive(SystemParam)] struct CoreKeyboardMessages<'w> { undo: MessageWriter<'w, UndoRequestEvent>, new_game: MessageWriter<'w, NewGameRequestEvent>, confirm_event: MessageWriter<'w, NewGameConfirmEvent>, info_toast: MessageWriter<'w, InfoToastEvent>, draw: MessageWriter<'w, DrawRequestEvent>, } /// Handles the core keyboard shortcuts: U (undo), N (new game + confirmation /// window), Z (zen mode), D / Space (draw), and ticks down the new-game /// confirmation countdown each frame. /// /// Also resets `forfeit_countdown` whenever U, D, Z, or N are pressed so that /// an in-flight forfeit confirmation is cancelled by any other action. fn handle_keyboard_core( keys: Res>, paused: Option>, progress: Option>, game: Option>, time: Res