feat(engine): add Menu dropdown for Stats/Achievements/Profile/Settings/Leaderboard
CI / Test & Lint (push) Failing after 20s
CI / Release Build (push) Has been skipped

Continues the UI-first pass. The five informational overlays were
each behind a single-key shortcut (S/A/P/O/L) with no visible UI
affordance. Add a "Menu ▾" button to the action bar that toggles a
popover with one row per overlay. Each row dispatches the same code
path the keyboard accelerator uses by writing a new
`Toggle*RequestEvent`:

- Stats        → ToggleStatsRequestEvent
- Achievements → ToggleAchievementsRequestEvent
- Profile      → ToggleProfileRequestEvent
- Settings     → ToggleSettingsRequestEvent
- Leaderboard  → ToggleLeaderboardRequestEvent

Each plugin's existing toggle handler now reads either its key or
the matching request event so the spawn / despawn / fetch logic stays
in the owning plugin (the popover never duplicates that behaviour).

Action bar order is now (left → right):
  Menu ▾   Undo   Pause   Help   Modes ▾   New Game

Menu sits on the far left because it's a navigation aggregator;
New Game stays on the far right as the most consequential action.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
funman300
2026-04-29 23:55:43 +00:00
parent 1d9fb1884a
commit 6240156fee
8 changed files with 224 additions and 18 deletions
+9 -3
View File
@@ -17,7 +17,9 @@ use solitaire_data::{
save_progress_to, save_progress_to,
}; };
use crate::events::{AchievementUnlockedEvent, GameWonEvent, XpAwardedEvent}; use crate::events::{
AchievementUnlockedEvent, GameWonEvent, ToggleAchievementsRequestEvent, XpAwardedEvent,
};
use crate::game_plugin::GameMutation; use crate::game_plugin::GameMutation;
use crate::progress_plugin::{LevelUpEvent, ProgressResource, ProgressStoragePath, ProgressUpdate}; use crate::progress_plugin::{LevelUpEvent, ProgressResource, ProgressStoragePath, ProgressUpdate};
use crate::resources::GameStateResource; use crate::resources::GameStateResource;
@@ -73,6 +75,7 @@ impl Plugin for AchievementPlugin {
.add_message::<AchievementUnlockedEvent>() .add_message::<AchievementUnlockedEvent>()
.add_message::<GameWonEvent>() .add_message::<GameWonEvent>()
.add_message::<XpAwardedEvent>() .add_message::<XpAwardedEvent>()
.add_message::<ToggleAchievementsRequestEvent>()
// Run after GameMutation (so GameWonEvent is available), after // Run after GameMutation (so GameWonEvent is available), after
// StatsUpdate (so stats reflect this win), and after ProgressUpdate // StatsUpdate (so stats reflect this win), and after ProgressUpdate
// (so daily_challenge_streak is up to date for daily_devotee). // (so daily_challenge_streak is up to date for daily_devotee).
@@ -197,14 +200,17 @@ pub fn display_name_for(id: &str) -> String {
.unwrap_or_else(|| id.to_string()) .unwrap_or_else(|| id.to_string())
} }
/// Toggle the achievements overlay with the `A` key. /// Toggle the achievements overlay — `A` keyboard accelerator or
/// `ToggleAchievementsRequestEvent` from the HUD Menu popover.
fn toggle_achievements_screen( fn toggle_achievements_screen(
mut commands: Commands, mut commands: Commands,
keys: Res<ButtonInput<KeyCode>>, keys: Res<ButtonInput<KeyCode>>,
mut requests: MessageReader<ToggleAchievementsRequestEvent>,
achievements: Res<AchievementsResource>, achievements: Res<AchievementsResource>,
screens: Query<Entity, With<AchievementsScreen>>, screens: Query<Entity, With<AchievementsScreen>>,
) { ) {
if !keys.just_pressed(KeyCode::KeyA) { let button_clicked = requests.read().count() > 0;
if !keys.just_pressed(KeyCode::KeyA) && !button_clicked {
return; return;
} }
if let Ok(entity) = screens.single() { if let Ok(entity) = screens.single() {
+25
View File
@@ -129,6 +129,31 @@ pub struct StartTimeAttackRequestEvent;
#[derive(Message, Debug, Clone, Copy, Default)] #[derive(Message, Debug, Clone, Copy, Default)]
pub struct StartDailyChallengeRequestEvent; pub struct StartDailyChallengeRequestEvent;
/// Request to toggle the Stats overlay. Fired by the HUD Menu-popover
/// "Stats" row alongside the existing `S` accelerator.
#[derive(Message, Debug, Clone, Copy, Default)]
pub struct ToggleStatsRequestEvent;
/// Request to toggle the Achievements overlay. Fired by the HUD
/// Menu-popover "Achievements" row alongside the existing `A` accelerator.
#[derive(Message, Debug, Clone, Copy, Default)]
pub struct ToggleAchievementsRequestEvent;
/// Request to toggle the Profile overlay. Fired by the HUD Menu-popover
/// "Profile" row alongside the existing `P` accelerator.
#[derive(Message, Debug, Clone, Copy, Default)]
pub struct ToggleProfileRequestEvent;
/// Request to toggle the Settings overlay. Fired by the HUD Menu-popover
/// "Settings" row alongside the existing `O` accelerator.
#[derive(Message, Debug, Clone, Copy, Default)]
pub struct ToggleSettingsRequestEvent;
/// Request to toggle the Leaderboard overlay. Fired by the HUD
/// Menu-popover "Leaderboard" row alongside the existing `L` accelerator.
#[derive(Message, Debug, Clone, Copy, Default)]
pub struct ToggleLeaderboardRequestEvent;
/// Fired by `SyncPlugin` after a pull task resolves and the merged result has /// Fired by `SyncPlugin` after a pull task resolves and the merged result has
/// been persisted to disk. `Ok(SyncResponse)` carries the merged payload plus /// been persisted to disk. `Ok(SyncResponse)` carries the merged payload plus
/// any `ConflictReport`s the merge produced. `Err(String)` carries a /// any `ConflictReport`s the merge produced. `Err(String)` carries a
+156 -1
View File
@@ -18,7 +18,9 @@ use crate::progress_plugin::ProgressResource;
use crate::events::{ use crate::events::{
HelpRequestEvent, InfoToastEvent, NewGameRequestEvent, PauseRequestEvent, HelpRequestEvent, InfoToastEvent, NewGameRequestEvent, PauseRequestEvent,
StartChallengeRequestEvent, StartDailyChallengeRequestEvent, StartTimeAttackRequestEvent, StartChallengeRequestEvent, StartDailyChallengeRequestEvent, StartTimeAttackRequestEvent,
StartZenRequestEvent, UndoRequestEvent, StartZenRequestEvent, ToggleAchievementsRequestEvent, ToggleLeaderboardRequestEvent,
ToggleProfileRequestEvent, ToggleSettingsRequestEvent, ToggleStatsRequestEvent,
UndoRequestEvent,
}; };
use crate::font_plugin::FontResource; use crate::font_plugin::FontResource;
use crate::game_plugin::GameMutation; use crate::game_plugin::GameMutation;
@@ -143,6 +145,27 @@ pub enum ModeOption {
TimeAttack, TimeAttack,
} }
/// Marker on the "Menu" action button. Click toggles the [`MenuPopover`]
/// which exposes the Stats / Achievements / Profile / Settings /
/// Leaderboard overlays without needing the S/A/P/O/L hotkeys.
#[derive(Component, Debug)]
pub struct MenuButton;
/// Marker on the dropdown panel that opens below the [`MenuButton`].
#[derive(Component, Debug)]
pub struct MenuPopover;
/// One row inside the [`MenuPopover`]. The variant selects which
/// `Toggle*RequestEvent` the click handler fires.
#[derive(Component, Debug, Clone, Copy)]
pub enum MenuOption {
Stats,
Achievements,
Profile,
Settings,
Leaderboard,
}
/// HUD Z-layer — above cards (which start at z=0) but below overlay screens. /// HUD Z-layer — above cards (which start at z=0) but below overlay screens.
const Z_HUD: i32 = 50; const Z_HUD: i32 = 50;
@@ -170,6 +193,11 @@ impl Plugin for HudPlugin {
.add_message::<StartChallengeRequestEvent>() .add_message::<StartChallengeRequestEvent>()
.add_message::<StartTimeAttackRequestEvent>() .add_message::<StartTimeAttackRequestEvent>()
.add_message::<StartDailyChallengeRequestEvent>() .add_message::<StartDailyChallengeRequestEvent>()
.add_message::<ToggleStatsRequestEvent>()
.add_message::<ToggleAchievementsRequestEvent>()
.add_message::<ToggleProfileRequestEvent>()
.add_message::<ToggleSettingsRequestEvent>()
.add_message::<ToggleLeaderboardRequestEvent>()
.add_systems(Startup, (spawn_hud, spawn_action_buttons)) .add_systems(Startup, (spawn_hud, spawn_action_buttons))
.add_systems(Update, update_hud.after(GameMutation)) .add_systems(Update, update_hud.after(GameMutation))
.add_systems(Update, announce_auto_complete.after(GameMutation)) .add_systems(Update, announce_auto_complete.after(GameMutation))
@@ -183,6 +211,8 @@ impl Plugin for HudPlugin {
handle_help_button, handle_help_button,
handle_modes_button, handle_modes_button,
handle_mode_option_click, handle_mode_option_click,
handle_menu_button,
handle_menu_option_click,
paint_action_buttons, paint_action_buttons,
), ),
); );
@@ -292,6 +322,7 @@ fn spawn_action_buttons(font_res: Option<Res<FontResource>>, mut commands: Comma
ZIndex(Z_HUD), ZIndex(Z_HUD),
)) ))
.with_children(|row| { .with_children(|row| {
spawn_action_button(row, MenuButton, "Menu \u{25BE}", &font);
spawn_action_button(row, UndoButton, "Undo", &font); spawn_action_button(row, UndoButton, "Undo", &font);
spawn_action_button(row, PauseButton, "Pause", &font); spawn_action_button(row, PauseButton, "Pause", &font);
spawn_action_button(row, HelpButton, "Help", &font); spawn_action_button(row, HelpButton, "Help", &font);
@@ -520,6 +551,130 @@ fn handle_mode_option_click(
} }
} }
/// Toggles the [`MenuPopover`]: spawns it on first click, despawns it on
/// second click. The popover lists the five overlays previously only
/// reachable via the S / A / P / O / L hotkeys.
fn handle_menu_button(
interaction_query: Query<&Interaction, (With<MenuButton>, Changed<Interaction>)>,
popovers: Query<Entity, With<MenuPopover>>,
font_res: Option<Res<FontResource>>,
mut commands: Commands,
) {
let pressed = interaction_query
.iter()
.any(|i| *i == Interaction::Pressed);
if !pressed {
return;
}
if let Ok(entity) = popovers.single() {
commands.entity(entity).despawn();
} else {
spawn_menu_popover(&mut commands, font_res.as_deref());
}
}
/// Spawns the menu popover anchored just below the action bar, with one
/// row per overlay. Each row dispatches its corresponding
/// `Toggle*RequestEvent` so the existing toggle handler runs (and the
/// HUD never duplicates spawn / despawn / fetch logic).
fn spawn_menu_popover(commands: &mut Commands, font_res: Option<&FontResource>) {
let font = TextFont {
font: font_res.map(|f| f.0.clone()).unwrap_or_default(),
font_size: 15.0,
..default()
};
let rows: [(MenuOption, &'static str); 5] = [
(MenuOption::Stats, "Stats"),
(MenuOption::Achievements, "Achievements"),
(MenuOption::Profile, "Profile"),
(MenuOption::Settings, "Settings"),
(MenuOption::Leaderboard, "Leaderboard"),
];
commands
.spawn((
MenuPopover,
Node {
position_type: PositionType::Absolute,
right: Val::Px(12.0),
top: Val::Px(50.0),
flex_direction: FlexDirection::Column,
row_gap: Val::Px(4.0),
padding: UiRect::all(Val::Px(8.0)),
border_radius: BorderRadius::all(Val::Px(6.0)),
..default()
},
BackgroundColor(Color::srgba(0.10, 0.12, 0.15, 0.96)),
ZIndex(Z_HUD + 5),
))
.with_children(|panel| {
for (option, label) in rows {
panel
.spawn((
option,
ActionButton,
Button,
Node {
padding: UiRect::axes(Val::Px(12.0), Val::Px(6.0)),
justify_content: JustifyContent::FlexStart,
align_items: AlignItems::Center,
min_width: Val::Px(150.0),
border_radius: BorderRadius::all(Val::Px(4.0)),
..default()
},
BackgroundColor(ACTION_BTN_IDLE),
))
.with_children(|b| {
b.spawn((Text::new(label), font.clone(), TextColor(Color::WHITE)));
});
}
});
}
/// Dispatches the click on a menu row to the matching toggle event,
/// then despawns the popover.
#[allow(clippy::too_many_arguments)]
fn handle_menu_option_click(
interaction_query: Query<(&Interaction, &MenuOption), Changed<Interaction>>,
popovers: Query<Entity, With<MenuPopover>>,
mut stats: MessageWriter<ToggleStatsRequestEvent>,
mut achievements: MessageWriter<ToggleAchievementsRequestEvent>,
mut profile: MessageWriter<ToggleProfileRequestEvent>,
mut settings: MessageWriter<ToggleSettingsRequestEvent>,
mut leaderboard: MessageWriter<ToggleLeaderboardRequestEvent>,
mut commands: Commands,
) {
let mut clicked_any = false;
for (interaction, option) in &interaction_query {
if *interaction != Interaction::Pressed {
continue;
}
clicked_any = true;
match option {
MenuOption::Stats => {
stats.write(ToggleStatsRequestEvent);
}
MenuOption::Achievements => {
achievements.write(ToggleAchievementsRequestEvent);
}
MenuOption::Profile => {
profile.write(ToggleProfileRequestEvent);
}
MenuOption::Settings => {
settings.write(ToggleSettingsRequestEvent);
}
MenuOption::Leaderboard => {
leaderboard.write(ToggleLeaderboardRequestEvent);
}
}
}
if clicked_any
&& let Ok(entity) = popovers.single() {
commands.entity(entity).despawn();
}
}
/// Visual feedback for every action button — paints idle / hover / pressed /// Visual feedback for every action button — paints idle / hover / pressed
/// states by mutating `BackgroundColor` whenever the interaction state /// states by mutating `BackgroundColor` whenever the interaction state
/// changes. One query covers all action buttons via the shared /// changes. One query covers all action buttons via the shared
+9 -4
View File
@@ -14,7 +14,7 @@ use bevy::tasks::{futures_lite::future, AsyncComputeTaskPool, Task};
use solitaire_data::settings::SyncBackend; use solitaire_data::settings::SyncBackend;
use solitaire_sync::LeaderboardEntry; use solitaire_sync::LeaderboardEntry;
use crate::events::InfoToastEvent; use crate::events::{InfoToastEvent, ToggleLeaderboardRequestEvent};
use crate::settings_plugin::SettingsResource; use crate::settings_plugin::SettingsResource;
use crate::sync_plugin::SyncProviderResource; use crate::sync_plugin::SyncProviderResource;
@@ -73,6 +73,7 @@ impl Plugin for LeaderboardPlugin {
.init_resource::<ClosedThisFrame>() .init_resource::<ClosedThisFrame>()
.init_resource::<OptInTask>() .init_resource::<OptInTask>()
.init_resource::<OptOutTask>() .init_resource::<OptOutTask>()
.add_message::<ToggleLeaderboardRequestEvent>()
.add_systems( .add_systems(
Update, Update,
( (
@@ -99,18 +100,22 @@ fn reset_closed_flag(mut flag: ResMut<ClosedThisFrame>) {
flag.0 = false; flag.0 = false;
} }
/// `L` key — open or close the leaderboard panel. /// `L` keyboard accelerator or `ToggleLeaderboardRequestEvent` from the
/// On open, starts a new fetch if no data is cached or a fetch is not in flight. /// HUD Menu popover — open or close the leaderboard panel. On open,
/// starts a new fetch if no data is cached or a fetch is not in flight.
#[allow(clippy::too_many_arguments)]
fn toggle_leaderboard_screen( fn toggle_leaderboard_screen(
mut commands: Commands, mut commands: Commands,
keys: Res<ButtonInput<KeyCode>>, keys: Res<ButtonInput<KeyCode>>,
mut requests: MessageReader<ToggleLeaderboardRequestEvent>,
screens: Query<Entity, With<LeaderboardScreen>>, screens: Query<Entity, With<LeaderboardScreen>>,
data: Res<LeaderboardResource>, data: Res<LeaderboardResource>,
provider: Option<Res<SyncProviderResource>>, provider: Option<Res<SyncProviderResource>>,
mut task_res: ResMut<LeaderboardFetchTask>, mut task_res: ResMut<LeaderboardFetchTask>,
mut closed_flag: ResMut<ClosedThisFrame>, mut closed_flag: ResMut<ClosedThisFrame>,
) { ) {
if !keys.just_pressed(KeyCode::KeyL) { let button_clicked = requests.read().count() > 0;
if !keys.just_pressed(KeyCode::KeyL) && !button_clicked {
return; return;
} }
if let Ok(entity) = screens.single() { if let Ok(entity) = screens.single() {
+5 -3
View File
@@ -71,14 +71,16 @@ pub use events::{
ManualSyncRequestEvent, MoveRejectedEvent, MoveRequestEvent, NewGameConfirmEvent, ManualSyncRequestEvent, MoveRejectedEvent, MoveRequestEvent, NewGameConfirmEvent,
NewGameRequestEvent, PauseRequestEvent, StartChallengeRequestEvent, NewGameRequestEvent, PauseRequestEvent, StartChallengeRequestEvent,
StartDailyChallengeRequestEvent, StartTimeAttackRequestEvent, StartZenRequestEvent, StartDailyChallengeRequestEvent, StartTimeAttackRequestEvent, StartZenRequestEvent,
StateChangedEvent, SyncCompleteEvent, UndoRequestEvent, XpAwardedEvent, StateChangedEvent, SyncCompleteEvent, ToggleAchievementsRequestEvent,
ToggleLeaderboardRequestEvent, ToggleProfileRequestEvent, ToggleSettingsRequestEvent,
ToggleStatsRequestEvent, UndoRequestEvent, XpAwardedEvent,
}; };
pub use game_plugin::{ConfirmNewGameScreen, GameMutation, GameOverScreen, GamePlugin, GameStatePath}; pub use game_plugin::{ConfirmNewGameScreen, GameMutation, GameOverScreen, GamePlugin, GameStatePath};
pub use help_plugin::{HelpPlugin, HelpScreen}; pub use help_plugin::{HelpPlugin, HelpScreen};
pub use home_plugin::{HomePlugin, HomeScreen}; pub use home_plugin::{HomePlugin, HomeScreen};
pub use hud_plugin::{ pub use hud_plugin::{
ActionButton, HelpButton, HudAutoComplete, HudPlugin, ModeOption, ModesButton, ModesPopover, ActionButton, HelpButton, HudAutoComplete, HudPlugin, MenuButton, MenuOption, MenuPopover,
NewGameButton, PauseButton, UndoButton, ModeOption, ModesButton, ModesPopover, NewGameButton, PauseButton, UndoButton,
}; };
pub use leaderboard_plugin::{LeaderboardPlugin, LeaderboardResource, LeaderboardScreen}; pub use leaderboard_plugin::{LeaderboardPlugin, LeaderboardResource, LeaderboardScreen};
pub use input_plugin::InputPlugin; pub use input_plugin::InputPlugin;
+6 -2
View File
@@ -10,6 +10,7 @@ use solitaire_core::achievement::achievement_by_id;
use solitaire_data::SyncBackend; use solitaire_data::SyncBackend;
use crate::achievement_plugin::AchievementsResource; use crate::achievement_plugin::AchievementsResource;
use crate::events::ToggleProfileRequestEvent;
use crate::progress_plugin::ProgressResource; use crate::progress_plugin::ProgressResource;
use crate::resources::{SyncStatus, SyncStatusResource}; use crate::resources::{SyncStatus, SyncStatusResource};
use crate::settings_plugin::SettingsResource; use crate::settings_plugin::SettingsResource;
@@ -24,7 +25,8 @@ pub struct ProfilePlugin;
impl Plugin for ProfilePlugin { impl Plugin for ProfilePlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_systems(Update, toggle_profile_screen); app.add_message::<ToggleProfileRequestEvent>()
.add_systems(Update, toggle_profile_screen);
} }
} }
@@ -32,6 +34,7 @@ impl Plugin for ProfilePlugin {
fn toggle_profile_screen( fn toggle_profile_screen(
mut commands: Commands, mut commands: Commands,
keys: Res<ButtonInput<KeyCode>>, keys: Res<ButtonInput<KeyCode>>,
mut requests: MessageReader<ToggleProfileRequestEvent>,
settings: Option<Res<SettingsResource>>, settings: Option<Res<SettingsResource>>,
sync_status: Option<Res<SyncStatusResource>>, sync_status: Option<Res<SyncStatusResource>>,
progress: Option<Res<ProgressResource>>, progress: Option<Res<ProgressResource>>,
@@ -39,7 +42,8 @@ fn toggle_profile_screen(
stats: Option<Res<StatsResource>>, stats: Option<Res<StatsResource>>,
screens: Query<Entity, With<ProfileScreen>>, screens: Query<Entity, With<ProfileScreen>>,
) { ) {
if !keys.just_pressed(KeyCode::KeyP) { let button_clicked = requests.read().count() > 0;
if !keys.just_pressed(KeyCode::KeyP) && !button_clicked {
return; return;
} }
if let Ok(entity) = screens.single() { if let Ok(entity) = screens.single() {
+7 -3
View File
@@ -16,7 +16,7 @@ use bevy::prelude::*;
use solitaire_core::game_state::DrawMode; use solitaire_core::game_state::DrawMode;
use solitaire_data::{load_settings_from, save_settings_to, settings_file_path, settings::Theme, AnimSpeed, Settings}; use solitaire_data::{load_settings_from, save_settings_to, settings_file_path, settings::Theme, AnimSpeed, Settings};
use crate::events::ManualSyncRequestEvent; use crate::events::{ManualSyncRequestEvent, ToggleSettingsRequestEvent};
use crate::progress_plugin::ProgressResource; use crate::progress_plugin::ProgressResource;
use crate::resources::{SettingsScrollPos, SyncStatus, SyncStatusResource}; use crate::resources::{SettingsScrollPos, SyncStatus, SyncStatusResource};
@@ -146,6 +146,7 @@ impl Plugin for SettingsPlugin {
.init_resource::<SettingsScrollPos>() .init_resource::<SettingsScrollPos>()
.add_message::<SettingsChangedEvent>() .add_message::<SettingsChangedEvent>()
.add_message::<ManualSyncRequestEvent>() .add_message::<ManualSyncRequestEvent>()
.add_message::<ToggleSettingsRequestEvent>()
.add_message::<bevy::input::mouse::MouseWheel>() .add_message::<bevy::input::mouse::MouseWheel>()
.add_systems(Update, (handle_volume_keys, toggle_settings_screen, scroll_settings_panel)); .add_systems(Update, (handle_volume_keys, toggle_settings_screen, scroll_settings_panel));
@@ -206,12 +207,15 @@ fn handle_volume_keys(
changed.write(SettingsChangedEvent(settings.0.clone())); changed.write(SettingsChangedEvent(settings.0.clone()));
} }
/// Opens or closes the Settings panel when `O` is pressed. /// Opens or closes the Settings panel `O` keyboard accelerator or
/// `ToggleSettingsRequestEvent` from the HUD Menu popover.
fn toggle_settings_screen( fn toggle_settings_screen(
keys: Res<ButtonInput<KeyCode>>, keys: Res<ButtonInput<KeyCode>>,
mut requests: MessageReader<ToggleSettingsRequestEvent>,
mut screen: ResMut<SettingsScreen>, mut screen: ResMut<SettingsScreen>,
) { ) {
if keys.just_pressed(KeyCode::KeyO) { let button_clicked = requests.read().count() > 0;
if keys.just_pressed(KeyCode::KeyO) || button_clicked {
screen.0 = !screen.0; screen.0 = !screen.0;
} }
} }
+7 -2
View File
@@ -17,7 +17,9 @@ use solitaire_data::{
use crate::auto_complete_plugin::AutoCompleteState; use crate::auto_complete_plugin::AutoCompleteState;
use crate::challenge_plugin::challenge_progress_label; use crate::challenge_plugin::challenge_progress_label;
use crate::events::{ForfeitEvent, GameWonEvent, InfoToastEvent, NewGameRequestEvent}; use crate::events::{
ForfeitEvent, GameWonEvent, InfoToastEvent, NewGameRequestEvent, ToggleStatsRequestEvent,
};
use crate::game_plugin::GameMutation; use crate::game_plugin::GameMutation;
use crate::progress_plugin::ProgressResource; use crate::progress_plugin::ProgressResource;
use crate::resources::GameStateResource; use crate::resources::GameStateResource;
@@ -81,6 +83,7 @@ impl Plugin for StatsPlugin {
.add_message::<NewGameRequestEvent>() .add_message::<NewGameRequestEvent>()
.add_message::<ForfeitEvent>() .add_message::<ForfeitEvent>()
.add_message::<InfoToastEvent>() .add_message::<InfoToastEvent>()
.add_message::<ToggleStatsRequestEvent>()
// record_abandoned must read `move_count` BEFORE handle_new_game // record_abandoned must read `move_count` BEFORE handle_new_game
// clobbers it with a fresh game. These are NOT in StatsUpdate because // clobbers it with a fresh game. These are NOT in StatsUpdate because
// StatsUpdate (as a set) is ordered after GameMutation by external // StatsUpdate (as a set) is ordered after GameMutation by external
@@ -181,12 +184,14 @@ fn handle_forfeit(
fn toggle_stats_screen( fn toggle_stats_screen(
mut commands: Commands, mut commands: Commands,
keys: Res<ButtonInput<KeyCode>>, keys: Res<ButtonInput<KeyCode>>,
mut requests: MessageReader<ToggleStatsRequestEvent>,
stats: Res<StatsResource>, stats: Res<StatsResource>,
progress: Option<Res<ProgressResource>>, progress: Option<Res<ProgressResource>>,
time_attack: Option<Res<TimeAttackResource>>, time_attack: Option<Res<TimeAttackResource>>,
screens: Query<Entity, With<StatsScreen>>, screens: Query<Entity, With<StatsScreen>>,
) { ) {
if !keys.just_pressed(KeyCode::KeyS) { let button_clicked = requests.read().count() > 0;
if !keys.just_pressed(KeyCode::KeyS) && !button_clicked {
return; return;
} }
if let Ok(entity) = screens.single() { if let Ok(entity) = screens.single() {