feat(engine): add Menu dropdown for Stats/Achievements/Profile/Settings/Leaderboard
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:
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user