diff --git a/solitaire_engine/src/challenge_plugin.rs b/solitaire_engine/src/challenge_plugin.rs index dae83bd..d2acd60 100644 --- a/solitaire_engine/src/challenge_plugin.rs +++ b/solitaire_engine/src/challenge_plugin.rs @@ -90,6 +90,7 @@ fn handle_start_challenge_request( new_game.write(NewGameRequestEvent { seed: Some(seed), mode: Some(GameMode::Challenge), + confirmed: false, }); } diff --git a/solitaire_engine/src/daily_challenge_plugin.rs b/solitaire_engine/src/daily_challenge_plugin.rs index b8768f2..1cde67f 100644 --- a/solitaire_engine/src/daily_challenge_plugin.rs +++ b/solitaire_engine/src/daily_challenge_plugin.rs @@ -197,6 +197,7 @@ fn handle_start_daily_request( new_game.write(NewGameRequestEvent { seed: Some(daily.seed), mode: None, + confirmed: false, }); let desc = daily .goal_description diff --git a/solitaire_engine/src/events.rs b/solitaire_engine/src/events.rs index 9e93fcf..bb2f290 100644 --- a/solitaire_engine/src/events.rs +++ b/solitaire_engine/src/events.rs @@ -29,6 +29,13 @@ pub struct UndoRequestEvent; pub struct NewGameRequestEvent { pub seed: Option, pub mode: Option, + /// `true` when this request originated from the user confirming the + /// abandon-current-game modal (Y / Enter on `ConfirmNewGameScreen`). + /// `handle_new_game` skips spawning the dialog when this is set, + /// otherwise it would respawn the modal in the frame after the player + /// presses Y (the despawn-on-Y has flushed by then) and the new game + /// would never actually start. + pub confirmed: bool, } /// Fired by `GamePlugin` after any successful state mutation. Rendering and diff --git a/solitaire_engine/src/game_plugin.rs b/solitaire_engine/src/game_plugin.rs index 4054d50..706e44a 100644 --- a/solitaire_engine/src/game_plugin.rs +++ b/solitaire_engine/src/game_plugin.rs @@ -164,9 +164,12 @@ fn handle_new_game( // If an active game is in progress, intercept and show a confirm dialog. // A game is "active" when moves have been made and it is not yet won. let needs_confirm = game.0.move_count > 0 && !game.0.is_won; - // Skip confirmation if a ConfirmNewGameScreen already exists (prevents duplicates). + // Skip confirmation if a ConfirmNewGameScreen already exists (prevents + // duplicates) or if the event itself was already confirmed by the + // player pressing Y on the modal — without the `confirmed` check the + // modal would be respawned the frame after the despawn flushes. let confirm_already_open = !confirm_screens.is_empty(); - if needs_confirm && !confirm_already_open { + if needs_confirm && !confirm_already_open && !ev.confirmed { // Despawn any stale game-over overlay before showing confirm dialog. for entity in &game_over_screens { commands.entity(entity).despawn(); @@ -300,12 +303,15 @@ fn handle_confirm_input( if confirmed { commands.entity(entity).despawn(); - // Re-send with move_count already 0 would bypass the dialog next time. - // We fire the event — handle_new_game will skip the dialog because - // the screen is despawned before the next read. + // Set `confirmed: true` so handle_new_game skips the dialog spawn + // and goes straight to the start-game branch. Without this flag the + // modal would respawn the frame after the despawn flushes (because + // confirm_screens is empty by then) and the new game would never + // actually start. new_game.write(NewGameRequestEvent { seed: original.0.seed, mode: original.0.mode, + confirmed: true, }); } else if cancelled { commands.entity(entity).despawn(); @@ -790,7 +796,7 @@ mod tests { .map(|c| c.id) .collect(); - app.world_mut().write_message(NewGameRequestEvent { seed: Some(999), mode: None }); + app.world_mut().write_message(NewGameRequestEvent { seed: Some(999), mode: None, confirmed: false }); app.update(); let after: Vec = app @@ -908,7 +914,7 @@ mod tests { let mut app = test_app(1); app.insert_resource(GameStatePath(Some(path.clone()))); - app.world_mut().write_message(NewGameRequestEvent { seed: Some(2), mode: None }); + app.world_mut().write_message(NewGameRequestEvent { seed: Some(2), mode: None, confirmed: false }); app.update(); assert!(!path.exists(), "saved file should be deleted after new game"); @@ -1120,7 +1126,7 @@ mod tests { // Simulate an active game with moves made. app.world_mut().resource_mut::().0.move_count = 5; app.world_mut() - .write_message(NewGameRequestEvent { seed: None, mode: None }); + .write_message(NewGameRequestEvent { seed: None, mode: None, confirmed: false }); app.update(); let count = app @@ -1141,7 +1147,7 @@ mod tests { "test assumes a fresh game with no moves" ); app.world_mut() - .write_message(NewGameRequestEvent { seed: None, mode: None }); + .write_message(NewGameRequestEvent { seed: None, mode: None, confirmed: false }); app.update(); let count = app diff --git a/solitaire_engine/src/input_plugin.rs b/solitaire_engine/src/input_plugin.rs index f9b4d92..e7d7961 100644 --- a/solitaire_engine/src/input_plugin.rs +++ b/solitaire_engine/src/input_plugin.rs @@ -183,6 +183,7 @@ fn handle_keyboard_core( ev.new_game.write(NewGameRequestEvent { seed: None, mode: Some(solitaire_core::game_state::GameMode::Classic), + confirmed: false, }); confirm.new_game_countdown = 0.0; return; @@ -218,6 +219,7 @@ fn handle_keyboard_core( ev.new_game.write(NewGameRequestEvent { seed: None, mode: Some(solitaire_core::game_state::GameMode::Zen), + confirmed: false, }); } else { ev.info_toast.write(InfoToastEvent(format!( diff --git a/solitaire_engine/src/stats_plugin.rs b/solitaire_engine/src/stats_plugin.rs index d3a54c1..5c198e3 100644 --- a/solitaire_engine/src/stats_plugin.rs +++ b/solitaire_engine/src/stats_plugin.rs @@ -554,7 +554,7 @@ mod tests { .move_count = 3; app.world_mut() - .write_message(NewGameRequestEvent { seed: Some(999), mode: None }); + .write_message(NewGameRequestEvent { seed: Some(999), mode: None, confirmed: false }); app.update(); let stats = &app.world().resource::().0; @@ -567,7 +567,7 @@ mod tests { fn new_game_without_moves_does_not_record_abandoned() { let mut app = headless_app(); app.world_mut() - .write_message(NewGameRequestEvent { seed: Some(42), mode: None }); + .write_message(NewGameRequestEvent { seed: Some(42), mode: None, confirmed: false }); app.update(); let stats = &app.world().resource::().0; diff --git a/solitaire_engine/src/time_attack_plugin.rs b/solitaire_engine/src/time_attack_plugin.rs index 762c99f..d0ea6b8 100644 --- a/solitaire_engine/src/time_attack_plugin.rs +++ b/solitaire_engine/src/time_attack_plugin.rs @@ -74,6 +74,7 @@ fn handle_start_time_attack_request( new_game.write(NewGameRequestEvent { seed: None, mode: Some(GameMode::TimeAttack), + confirmed: false, }); } @@ -112,6 +113,7 @@ fn auto_deal_on_time_attack_win( new_game.write(NewGameRequestEvent { seed: None, mode: Some(GameMode::TimeAttack), + confirmed: false, }); } } diff --git a/solitaire_engine/src/win_summary_plugin.rs b/solitaire_engine/src/win_summary_plugin.rs index 9a343b8..8b616bf 100644 --- a/solitaire_engine/src/win_summary_plugin.rs +++ b/solitaire_engine/src/win_summary_plugin.rs @@ -742,6 +742,7 @@ mod tests { app.world_mut().write_message(NewGameRequestEvent { seed: None, mode: Some(GameMode::Zen), + confirmed: false, }); app.update();