//! 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::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_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; /// Registers keyboard and mouse input systems. /// /// Drag systems run in a fixed order each frame: /// `start_drag` → `follow_drag` → `end_drag`, with `follow_drag` after the /// card-position sync so it overrides resting positions for cards being /// dragged. `end_drag` runs before `GameMutation` so the `MoveRequestEvent` /// it fires is consumed the same frame. pub struct InputPlugin; impl Plugin for InputPlugin { fn build(&self, app: &mut App) { app.init_resource::() .add_message::() .add_message::() .add_message::() .add_message::() .add_systems( Update, ( handle_keyboard, handle_stock_click, handle_double_click, start_drag, follow_drag, 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 all event writers used by `handle_keyboard` so the system stays /// within Bevy's 16-parameter limit. #[derive(SystemParam)] struct KeyboardMessages<'w> { undo: MessageWriter<'w, UndoRequestEvent>, new_game: MessageWriter<'w, NewGameRequestEvent>, confirm_event: MessageWriter<'w, NewGameConfirmEvent>, info_toast: MessageWriter<'w, InfoToastEvent>, draw: MessageWriter<'w, DrawRequestEvent>, forfeit: MessageWriter<'w, ForfeitEvent>, hint_visual: MessageWriter<'w, HintVisualEvent>, } #[allow(clippy::too_many_arguments)] fn handle_keyboard( keys: Res>, paused: Option>, progress: Option>, game: Option>, time: Res