From 08b006ff305c5e7900ef6b072ce3964a4456df7b Mon Sep 17 00:00:00 2001 From: funman300 Date: Wed, 6 May 2026 15:20:39 +0000 Subject: [PATCH] fix(engine): Esc on a modal no longer also opens Pause underneath MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A single Esc press while the Confirm New Game / Restore / Home / Onboarding / Settings modals were open would both close the modal (via its own input handler) and spawn the Pause overlay on top in the same frame, dumping the player on a screen they didn't ask for. toggle_pause now skips when any non-Pause `ModalScrim` is in the world. The HUD-button path is gated too — clicking Pause while another modal is up is almost always an accident. The four modal queries are bundled into a `PauseModalQueries` SystemParam to stay under Bevy's 16-parameter cap. Co-Authored-By: Claude Opus 4.7 (1M context) --- solitaire_engine/src/pause_plugin.rs | 35 ++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/solitaire_engine/src/pause_plugin.rs b/solitaire_engine/src/pause_plugin.rs index 594940b..ffb66a4 100644 --- a/solitaire_engine/src/pause_plugin.rs +++ b/solitaire_engine/src/pause_plugin.rs @@ -36,8 +36,9 @@ use crate::settings_plugin::{SettingsChangedEvent, SettingsResource, SettingsSto use crate::stats_plugin::StatsResource; use crate::ui_modal::{ spawn_modal, spawn_modal_actions, spawn_modal_body_text, spawn_modal_button, - spawn_modal_header, ButtonVariant, + spawn_modal_header, ButtonVariant, ModalScrim, }; +use bevy::ecs::system::SystemParam; use crate::ui_theme::{ self, TEXT_PRIMARY, TEXT_SECONDARY, TYPE_BODY_LG, TYPE_CAPTION, VAL_SPACE_3, }; @@ -126,15 +127,24 @@ impl Plugin for PausePlugin { } } +/// Bundles the modal-related queries `toggle_pause` reads each tick. +/// Pulled into a [`SystemParam`] so the system stays under Bevy's 16- +/// parameter cap after the cross-modal Esc guard query was added. +#[derive(SystemParam)] +struct PauseModalQueries<'w, 's> { + pause_screens: Query<'w, 's, Entity, With>, + forfeit_screens: Query<'w, 's, Entity, With>, + game_over_screens: Query<'w, 's, Entity, With>, + other_modal_scrims: Query<'w, 's, Entity, (With, Without)>, +} + #[allow(clippy::too_many_arguments)] fn toggle_pause( mut commands: Commands, keys: Res>, mut requests: MessageReader, mut paused: ResMut, - screens: Query>, - forfeit_screens: Query>, - game_over_screens: Query>, + modal_queries: PauseModalQueries<'_, '_>, game: Option>, path: Option>, progress: Option>, @@ -145,6 +155,13 @@ fn toggle_pause( mut changed: MessageWriter, selection: Option>, ) { + let PauseModalQueries { + pause_screens: screens, + forfeit_screens, + game_over_screens, + other_modal_scrims, + } = modal_queries; + // Either Esc or a click on the HUD "Pause" button (which fires // PauseRequestEvent) opens or closes the overlay. Drain the queue so a // burst of clicks doesn't queue future toggles. @@ -157,6 +174,16 @@ fn toggle_pause( if !forfeit_screens.is_empty() { return; } + // Any other modal (Confirm New Game, Restore, Home, Onboarding, + // Settings, etc.) owns its own dismissal — pause must not stack + // on top of it. Without this guard a single Esc both closes the + // open modal AND spawns the pause overlay underneath, leaving the + // player on a screen they didn't ask for. The HUD-button path + // (`button_clicked`) is gated too; clicking Pause while another + // modal is up is almost always an accident. + if !other_modal_scrims.is_empty() { + return; + } // If a card is currently selected, let SelectionPlugin handle this Escape // (it will clear the selection). Pause must not also open in the same frame. if selection.is_some_and(|s| s.selected_pile.is_some()) {