From 93660c2217bd68b24789c6eb3a1c6aa616721ebd Mon Sep 17 00:00:00 2001 From: funman300 Date: Wed, 6 May 2026 06:57:14 +0000 Subject: [PATCH] feat(engine): N keypress now opens the real Confirm/Cancel modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously a first N press during an active game showed a "Press N again" toast and started a 3-second countdown — a UI-first violation since the only continuation was another keystroke. The HUD New Game button already routed through `ConfirmNewGameScreen` with real Cancel / New game buttons; this change makes keyboard N do the same. - handle_keyboard_core fires NewGameRequestEvent::default() directly; handle_new_game's existing active-game check spawns the modal. - Shift+N keeps the keyboard power-user bypass (confirmed: true). - N is suppressed while the confirm modal or restore prompt is open so those modals' own input handlers can process N (cancel / start-new-game) without us re-firing the same frame they close. - KeyboardConfirmState, NEW_GAME_CONFIRM_WINDOW, NewGameConfirmEvent, and the "Press N again" toast handler are removed. Co-Authored-By: Claude Opus 4.7 (1M context) --- solitaire_engine/src/animation_plugin.rs | 13 +-- solitaire_engine/src/events.rs | 7 -- solitaire_engine/src/feedback_anim_plugin.rs | 4 +- solitaire_engine/src/input_plugin.rs | 101 ++++++------------- solitaire_engine/src/lib.rs | 2 +- 5 files changed, 36 insertions(+), 91 deletions(-) diff --git a/solitaire_engine/src/animation_plugin.rs b/solitaire_engine/src/animation_plugin.rs index 747caf6..205e79a 100644 --- a/solitaire_engine/src/animation_plugin.rs +++ b/solitaire_engine/src/animation_plugin.rs @@ -21,7 +21,7 @@ use crate::card_animation::{sample_curve, CardAnimation, MotionCurve}; use crate::card_plugin::CardEntity; use crate::challenge_plugin::ChallengeAdvancedEvent; use crate::daily_challenge_plugin::{DailyChallengeCompletedEvent, DailyGoalAnnouncementEvent}; -use crate::events::{InfoToastEvent, NewGameConfirmEvent, XpAwardedEvent}; +use crate::events::{InfoToastEvent, XpAwardedEvent}; use crate::events::{AchievementUnlockedEvent, GameWonEvent}; use crate::game_plugin::GameMutation; use crate::layout::LayoutResource; @@ -161,7 +161,6 @@ impl Plugin for AnimationPlugin { .add_message::() .add_message::() .add_message::() - .add_message::() .add_message::() .add_message::() .init_resource::() @@ -183,7 +182,6 @@ impl Plugin for AnimationPlugin { handle_challenge_toast, handle_settings_toast, handle_auto_complete_toast, - handle_new_game_confirm_toast, handle_xp_awarded_toast, tick_toasts, (enqueue_toasts, drive_toast_display).chain(), @@ -459,15 +457,6 @@ fn handle_auto_complete_toast( } } -fn handle_new_game_confirm_toast( - mut commands: Commands, - mut events: MessageReader, -) { - for _ in events.read() { - spawn_toast(&mut commands, "Press N again to start a new game".to_string(), 3.0); - } -} - /// Reads every incoming `InfoToastEvent` and appends its text to `ToastQueue`. /// /// This is the first half of the two-system toast queue (Task #67). The queue diff --git a/solitaire_engine/src/events.rs b/solitaire_engine/src/events.rs index 9f351e8..5f2a1e5 100644 --- a/solitaire_engine/src/events.rs +++ b/solitaire_engine/src/events.rs @@ -207,13 +207,6 @@ pub struct ToggleLeaderboardRequestEvent; #[derive(Message, Debug, Clone)] pub struct SyncCompleteEvent(pub Result); -/// Fired by `InputPlugin` when N is pressed while a game is in progress -/// but confirmation has not yet been received. The animation plugin shows -/// a "Press N again to confirm" toast. A second N press within the -/// confirmation window sends `NewGameRequestEvent`. -#[derive(Message, Debug, Clone, Copy, Default)] -pub struct NewGameConfirmEvent; - /// Generic informational toast message. Any system can fire this to display /// a short string to the player, e.g. "Locked — reach level 5". #[derive(Message, Debug, Clone)] diff --git a/solitaire_engine/src/feedback_anim_plugin.rs b/solitaire_engine/src/feedback_anim_plugin.rs index b549230..cffc3b1 100644 --- a/solitaire_engine/src/feedback_anim_plugin.rs +++ b/solitaire_engine/src/feedback_anim_plugin.rs @@ -21,8 +21,8 @@ //! //! # Task #69 — Animated card deal on new game start //! -//! When `NewGameRequestEvent` fires (on a fresh game, `move_count == 0`) or -//! `NewGameConfirmEvent` fires, `start_deal_anim` reads `LayoutResource` and +//! When `NewGameRequestEvent` fires (on a fresh game, `move_count == 0`), +//! `start_deal_anim` reads `LayoutResource` and //! inserts a `CardAnim` on every card entity, sliding each card from the stock //! pile's position to its current (final) position with a per-card stagger //! derived from the current `AnimSpeed` setting plus a deterministic ±10 % diff --git a/solitaire_engine/src/input_plugin.rs b/solitaire_engine/src/input_plugin.rs index a07cea4..56a2f18 100644 --- a/solitaire_engine/src/input_plugin.rs +++ b/solitaire_engine/src/input_plugin.rs @@ -40,10 +40,10 @@ use solitaire_core::game_state::DrawMode; use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL; use crate::events::{ DrawRequestEvent, ForfeitRequestEvent, HintVisualEvent, InfoToastEvent, MoveRejectedEvent, - MoveRequestEvent, NewGameConfirmEvent, NewGameRequestEvent, StartZenRequestEvent, - StateChangedEvent, UndoRequestEvent, + MoveRequestEvent, NewGameRequestEvent, StartZenRequestEvent, StateChangedEvent, + UndoRequestEvent, }; -use crate::game_plugin::GameMutation; +use crate::game_plugin::{ConfirmNewGameScreen, GameMutation, RestorePromptScreen}; use crate::pause_plugin::PausedResource; use crate::progress_plugin::ProgressResource; use crate::layout::{Layout, LayoutResource}; @@ -64,22 +64,6 @@ const DRAG_Z: f32 = 500.0; #[derive(Resource, Debug, Clone, Default)] pub struct HintSolverConfig(pub solitaire_core::solver::SolverConfig); -/// Shared countdown state for the new-game double-press confirmation -/// flow. -/// -/// 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, -} - /// Registers keyboard, mouse, and touch input systems. /// /// Mouse drag pipeline (ordered, left-to-right): @@ -100,8 +84,6 @@ impl Plugin for InputPlugin { fn build(&self, app: &mut App) { app.init_resource::() .init_resource::() - .init_resource::() - .add_message::() .add_message::() .add_message::() .add_message::() @@ -131,9 +113,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; - /// Bundles the event writers needed by the core keyboard handler. /// /// Keeping these in a [`SystemParam`] avoids hitting Bevy's 16-parameter limit. @@ -141,43 +120,39 @@ const NEW_GAME_CONFIRM_WINDOW: f32 = 3.0; 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. +/// Handles the core keyboard shortcuts: U (undo), N (new game), Z (zen mode), +/// D / Space (draw). +/// +/// `N` fires `NewGameRequestEvent` straight through; the existing +/// `handle_new_game` flow shows the `ConfirmNewGameScreen` modal when +/// the current game is in progress, so a single press surfaces a real +/// Confirm / Cancel UI instead of a "press N again" toast. `Shift+N` +/// keeps the keyboard power-user bypass by setting `confirmed: true`. +/// +/// While the confirm modal or the restore prompt is already open, the +/// system skips the N branch so those modals' own input handlers can +/// process N (cancel / start-new-game) without us re-firing a request +/// the same frame. #[allow(clippy::too_many_arguments)] fn handle_keyboard_core( keys: Res>, paused: Option>, progress: Option>, - game: Option>, - time: Res