feat(engine): require N-key confirmation when abandoning an active game
Pressing N during an active game (move_count > 0, not won) now shows a "Press N again to start a new game" toast and only starts a new game if N is pressed a second time within 3 seconds. Starting a fresh game or pressing N after a win still acts immediately. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ use crate::auto_complete_plugin::AutoCompleteState;
|
|||||||
use crate::card_plugin::CardEntity;
|
use crate::card_plugin::CardEntity;
|
||||||
use crate::challenge_plugin::ChallengeAdvancedEvent;
|
use crate::challenge_plugin::ChallengeAdvancedEvent;
|
||||||
use crate::daily_challenge_plugin::{DailyChallengeCompletedEvent, DailyGoalAnnouncementEvent};
|
use crate::daily_challenge_plugin::{DailyChallengeCompletedEvent, DailyGoalAnnouncementEvent};
|
||||||
|
use crate::events::NewGameConfirmEvent;
|
||||||
use crate::events::{AchievementUnlockedEvent, GameWonEvent};
|
use crate::events::{AchievementUnlockedEvent, GameWonEvent};
|
||||||
use crate::game_plugin::GameMutation;
|
use crate::game_plugin::GameMutation;
|
||||||
use crate::layout::LayoutResource;
|
use crate::layout::LayoutResource;
|
||||||
@@ -91,6 +92,7 @@ impl Plugin for AnimationPlugin {
|
|||||||
.add_event::<TimeAttackEndedEvent>()
|
.add_event::<TimeAttackEndedEvent>()
|
||||||
.add_event::<ChallengeAdvancedEvent>()
|
.add_event::<ChallengeAdvancedEvent>()
|
||||||
.add_event::<SettingsChangedEvent>()
|
.add_event::<SettingsChangedEvent>()
|
||||||
|
.add_event::<NewGameConfirmEvent>()
|
||||||
.init_resource::<EffectiveSlideDuration>()
|
.init_resource::<EffectiveSlideDuration>()
|
||||||
.add_systems(Startup, init_slide_duration)
|
.add_systems(Startup, init_slide_duration)
|
||||||
.add_systems(
|
.add_systems(
|
||||||
@@ -108,6 +110,7 @@ impl Plugin for AnimationPlugin {
|
|||||||
handle_challenge_toast,
|
handle_challenge_toast,
|
||||||
handle_settings_toast,
|
handle_settings_toast,
|
||||||
handle_auto_complete_toast,
|
handle_auto_complete_toast,
|
||||||
|
handle_new_game_confirm_toast,
|
||||||
tick_toasts,
|
tick_toasts,
|
||||||
)
|
)
|
||||||
.after(GameMutation),
|
.after(GameMutation),
|
||||||
@@ -310,6 +313,15 @@ fn handle_auto_complete_toast(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_new_game_confirm_toast(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut events: EventReader<NewGameConfirmEvent>,
|
||||||
|
) {
|
||||||
|
for _ in events.read() {
|
||||||
|
spawn_toast(&mut commands, "Press N again to start a new game".to_string(), 3.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn tick_toasts(
|
fn tick_toasts(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
time: Res<Time>,
|
time: Res<Time>,
|
||||||
|
|||||||
@@ -68,3 +68,10 @@ pub struct AchievementUnlockedEvent(pub AchievementRecord);
|
|||||||
/// starting a new pull task if one is not already in flight.
|
/// starting a new pull task if one is not already in flight.
|
||||||
#[derive(Event, Debug, Clone, Copy, Default)]
|
#[derive(Event, Debug, Clone, Copy, Default)]
|
||||||
pub struct ManualSyncRequestEvent;
|
pub struct ManualSyncRequestEvent;
|
||||||
|
|
||||||
|
/// 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(Event, Debug, Clone, Copy, Default)]
|
||||||
|
pub struct NewGameConfirmEvent;
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ fn spawn_help_screen(commands: &mut Commands) {
|
|||||||
" Click stock Draw".to_string(),
|
" Click stock Draw".to_string(),
|
||||||
String::new(),
|
String::new(),
|
||||||
"-- New Game --".to_string(),
|
"-- New Game --".to_string(),
|
||||||
" N New Classic game".to_string(),
|
" N New Classic game (N twice if in progress)".to_string(),
|
||||||
" C Start today's daily challenge".to_string(),
|
" C Start today's daily challenge".to_string(),
|
||||||
" Z Start a Zen game (level 5+)".to_string(),
|
" Z Start a Zen game (level 5+)".to_string(),
|
||||||
" X Start the next Challenge (level 5+)".to_string(),
|
" X Start the next Challenge (level 5+)".to_string(),
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ use solitaire_core::rules::{can_place_on_foundation, can_place_on_tableau};
|
|||||||
use crate::card_plugin::{CardEntity, TABLEAU_FAN_FRAC};
|
use crate::card_plugin::{CardEntity, TABLEAU_FAN_FRAC};
|
||||||
use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL;
|
use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL;
|
||||||
use crate::events::{
|
use crate::events::{
|
||||||
DrawRequestEvent, MoveRejectedEvent, MoveRequestEvent, NewGameRequestEvent, StateChangedEvent,
|
DrawRequestEvent, MoveRejectedEvent, MoveRequestEvent, NewGameConfirmEvent, NewGameRequestEvent,
|
||||||
UndoRequestEvent,
|
StateChangedEvent, UndoRequestEvent,
|
||||||
};
|
};
|
||||||
use crate::game_plugin::GameMutation;
|
use crate::game_plugin::GameMutation;
|
||||||
use crate::progress_plugin::ProgressResource;
|
use crate::progress_plugin::ProgressResource;
|
||||||
@@ -47,32 +47,62 @@ pub struct InputPlugin;
|
|||||||
|
|
||||||
impl Plugin for InputPlugin {
|
impl Plugin for InputPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.add_systems(
|
app.add_event::<NewGameConfirmEvent>()
|
||||||
Update,
|
.add_systems(
|
||||||
(
|
Update,
|
||||||
handle_keyboard,
|
(
|
||||||
handle_stock_click,
|
handle_keyboard,
|
||||||
start_drag,
|
handle_stock_click,
|
||||||
follow_drag,
|
start_drag,
|
||||||
end_drag.before(GameMutation),
|
follow_drag,
|
||||||
)
|
end_drag.before(GameMutation),
|
||||||
.chain(),
|
)
|
||||||
);
|
.chain(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Seconds after the first N press during which a second N confirms new game.
|
||||||
|
const NEW_GAME_CONFIRM_WINDOW: f32 = 3.0;
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn handle_keyboard(
|
fn handle_keyboard(
|
||||||
keys: Res<ButtonInput<KeyCode>>,
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
progress: Option<Res<ProgressResource>>,
|
progress: Option<Res<ProgressResource>>,
|
||||||
|
game: Option<Res<crate::resources::GameStateResource>>,
|
||||||
|
time: Res<Time>,
|
||||||
|
mut confirm_countdown: Local<f32>,
|
||||||
mut undo: EventWriter<UndoRequestEvent>,
|
mut undo: EventWriter<UndoRequestEvent>,
|
||||||
mut new_game: EventWriter<NewGameRequestEvent>,
|
mut new_game: EventWriter<NewGameRequestEvent>,
|
||||||
|
mut confirm_event: EventWriter<NewGameConfirmEvent>,
|
||||||
mut draw: EventWriter<DrawRequestEvent>,
|
mut draw: EventWriter<DrawRequestEvent>,
|
||||||
) {
|
) {
|
||||||
|
// Tick down any active confirmation window.
|
||||||
|
if *confirm_countdown > 0.0 {
|
||||||
|
*confirm_countdown -= time.delta_secs();
|
||||||
|
if *confirm_countdown <= 0.0 {
|
||||||
|
*confirm_countdown = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if keys.just_pressed(KeyCode::KeyU) {
|
if keys.just_pressed(KeyCode::KeyU) {
|
||||||
undo.send(UndoRequestEvent);
|
undo.send(UndoRequestEvent);
|
||||||
}
|
}
|
||||||
if keys.just_pressed(KeyCode::KeyN) {
|
if keys.just_pressed(KeyCode::KeyN) {
|
||||||
new_game.send(NewGameRequestEvent::default());
|
let active_game = game.as_ref().is_some_and(|g| g.0.move_count > 0 && !g.0.is_won);
|
||||||
|
if !active_game {
|
||||||
|
// No active game — start immediately.
|
||||||
|
new_game.send(NewGameRequestEvent::default());
|
||||||
|
*confirm_countdown = 0.0;
|
||||||
|
} else if *confirm_countdown > 0.0 {
|
||||||
|
// Second press within the window — confirmed.
|
||||||
|
new_game.send(NewGameRequestEvent::default());
|
||||||
|
*confirm_countdown = 0.0;
|
||||||
|
} else {
|
||||||
|
// First press on an active game — require confirmation.
|
||||||
|
*confirm_countdown = NEW_GAME_CONFIRM_WINDOW;
|
||||||
|
confirm_event.send(NewGameConfirmEvent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if keys.just_pressed(KeyCode::KeyZ) {
|
if keys.just_pressed(KeyCode::KeyZ) {
|
||||||
// Zen / Challenge / Time Attack are gated to level >= CHALLENGE_UNLOCK_LEVEL.
|
// Zen / Challenge / Time Attack are gated to level >= CHALLENGE_UNLOCK_LEVEL.
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ pub use audio_plugin::{AudioPlugin, AudioState, SoundLibrary};
|
|||||||
pub use card_plugin::{CardEntity, CardLabel, CardPlugin};
|
pub use card_plugin::{CardEntity, CardLabel, CardPlugin};
|
||||||
pub use events::{
|
pub use events::{
|
||||||
AchievementUnlockedEvent, CardFlippedEvent, DrawRequestEvent, GameWonEvent, ManualSyncRequestEvent,
|
AchievementUnlockedEvent, CardFlippedEvent, DrawRequestEvent, GameWonEvent, ManualSyncRequestEvent,
|
||||||
MoveRejectedEvent, MoveRequestEvent, NewGameRequestEvent, StateChangedEvent, UndoRequestEvent,
|
MoveRejectedEvent, MoveRequestEvent, NewGameConfirmEvent, NewGameRequestEvent, StateChangedEvent,
|
||||||
|
UndoRequestEvent,
|
||||||
};
|
};
|
||||||
pub use game_plugin::{GameMutation, GamePlugin, GameStatePath};
|
pub use game_plugin::{GameMutation, GamePlugin, GameStatePath};
|
||||||
pub use help_plugin::{HelpPlugin, HelpScreen};
|
pub use help_plugin::{HelpPlugin, HelpScreen};
|
||||||
|
|||||||
Reference in New Issue
Block a user