diff --git a/solitaire_engine/src/events.rs b/solitaire_engine/src/events.rs index 42e313c..65b5f20 100644 --- a/solitaire_engine/src/events.rs +++ b/solitaire_engine/src/events.rs @@ -189,6 +189,15 @@ pub struct XpAwardedEvent { #[derive(Message, Debug, Clone, Copy, Default)] pub struct ForfeitEvent; +/// Request to open the forfeit-confirm modal. Fired by the `G` accelerator +/// and by the Pause modal's "Forfeit" button so the same modal opens +/// either way. Consumed by `PausePlugin`, which spawns +/// `ForfeitConfirmScreen` after checking that a game is in progress and +/// no forfeit modal is already showing. Confirmation inside that modal +/// then fires `ForfeitEvent` for `StatsPlugin` to consume. +#[derive(Message, Debug, Clone, Copy, Default)] +pub struct ForfeitRequestEvent; + /// Fired when the player requests a hint (H key). Carries the source card ID /// and destination pile for visual highlighting. /// diff --git a/solitaire_engine/src/input_plugin.rs b/solitaire_engine/src/input_plugin.rs index 4ac5d07..bba8625 100644 --- a/solitaire_engine/src/input_plugin.rs +++ b/solitaire_engine/src/input_plugin.rs @@ -35,7 +35,7 @@ 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, + DrawRequestEvent, ForfeitRequestEvent, HintVisualEvent, InfoToastEvent, MoveRejectedEvent, MoveRequestEvent, NewGameConfirmEvent, NewGameRequestEvent, StartZenRequestEvent, StateChangedEvent, UndoRequestEvent, }; @@ -50,18 +50,20 @@ 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. +/// Shared countdown state for the new-game double-press confirmation +/// flow. /// -/// Using a resource (instead of `Local`) lets the three keyboard sub-systems -/// share the same countdown state without needing to pass values between them. +/// Using a resource (instead of `Local`) lets the keyboard sub-systems +/// share the same countdown state without needing to pass values +/// between them. Forfeit no longer has a keyboard countdown — `G` now +/// fires `ForfeitRequestEvent` and `PausePlugin` shows a real +/// `ForfeitConfirmScreen` modal. #[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. @@ -87,7 +89,7 @@ impl Plugin for InputPlugin { .add_message::() .add_message::() .add_message::() - .add_message::() + .add_message::() .add_message::() .add_systems( Update, @@ -117,9 +119,6 @@ impl Plugin for InputPlugin { /// 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. @@ -135,9 +134,6 @@ struct CoreKeyboardMessages<'w> { /// 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. #[allow(clippy::too_many_arguments)] fn handle_keyboard_core( keys: Res>, @@ -168,15 +164,10 @@ fn handle_keyboard_core( } if keys.just_pressed(KeyCode::KeyU) { - // Cancel any pending forfeit when the player takes another action. - confirm.forfeit_countdown = 0.0; ev.undo.write(UndoRequestEvent); } if keys.just_pressed(KeyCode::KeyN) { - // Cancel any pending forfeit when the player takes another action. - confirm.forfeit_countdown = 0.0; - // If a Time Attack session is running, cancel it and start a Classic game. if let Some(ref mut session) = time_attack && session.active { @@ -214,8 +205,6 @@ fn handle_keyboard_core( let zen_clicked = zen_requests.read().count() > 0; if keys.just_pressed(KeyCode::KeyZ) || zen_clicked { - // Cancel any pending forfeit when the player takes another action. - confirm.forfeit_countdown = 0.0; // Zen / Challenge / Time Attack are gated to level >= CHALLENGE_UNLOCK_LEVEL. // X is gated separately by ChallengePlugin. Either Z or the HUD // Modes-popover "Zen" row reaches this branch. @@ -238,16 +227,13 @@ fn handle_keyboard_core( let space_draws = keys.just_pressed(KeyCode::Space) && selection.as_ref().is_none_or(|s| s.selected_pile.is_none()); if keys.just_pressed(KeyCode::KeyD) || space_draws { - // Cancel any pending forfeit when the player takes another action. - confirm.forfeit_countdown = 0.0; ev.draw.write(DrawRequestEvent); } // Esc is handled by `PausePlugin` (overlay toggle + paused flag). } /// Handles the H key: cycles through all available hints, highlighting the -/// source card yellow for 2 s and showing a descriptive toast. Resets the -/// forfeit countdown on each press. +/// source card yellow for 2 s and showing a descriptive toast. /// /// The hint index wraps around once all hints have been cycled through. When no /// moves are available a "No hints available" toast is shown instead. @@ -257,7 +243,6 @@ fn handle_keyboard_hint( paused: Option>, game: Option>, layout: Option>, - mut confirm: ResMut, mut hint_cycle: ResMut, mut commands: Commands, mut card_entities: Query<(Entity, &CardEntity, &mut Sprite)>, @@ -271,9 +256,6 @@ fn handle_keyboard_hint( return; } - // H cancels any in-flight forfeit confirmation. - confirm.forfeit_countdown = 0.0; - let Some(ref g) = game else { return }; if g.0.is_won { @@ -353,52 +335,30 @@ fn handle_keyboard_hint( info_toast.write(InfoToastEvent(msg)); } -/// Handles the G key: forfeit the current game with a 3-second double-confirm -/// window to prevent accidental forfeits. +/// Handles the G key: fires `ForfeitRequestEvent` so `PausePlugin` +/// can spawn the `ForfeitConfirmScreen` modal. /// -/// First press shows a toast and starts the countdown. -/// Second press **within the window** sends [`ForfeitEvent`]. -/// Pressing any other key between presses cancels the countdown -/// (handled by [`handle_keyboard_core`]). +/// Replaces a prior double-press toast countdown with a real +/// Cancel / Yes-forfeit modal — the same code path the Pause modal's +/// Forfeit button takes. Bails when no game is in progress so the +/// hotkey is a no-op on the home screen / game-over screen. fn handle_keyboard_forfeit( keys: Res>, paused: Option>, - time: Res