Compare commits
3 Commits
62cd1cf924
...
6240156fee
| Author | SHA1 | Date | |
|---|---|---|---|
| 6240156fee | |||
| 1d9fb1884a | |||
| 97f38085e3 |
+1
-1
@@ -251,7 +251,7 @@ The "Shortcut" column lists optional keyboard accelerators. Every action in this
|
|||||||
| `CursorPlugin` | — | Custom cursor sprite during drag |
|
| `CursorPlugin` | — | Custom cursor sprite during drag |
|
||||||
| `SelectionPlugin` | — | Keyboard-driven card selection |
|
| `SelectionPlugin` | — | Keyboard-driven card selection |
|
||||||
| `GamePlugin` | N | Core game state resource, new-game flow, win/game-over overlays |
|
| `GamePlugin` | N | Core game state resource, new-game flow, win/game-over overlays |
|
||||||
| `HudPlugin` | — | Score, move counter, timer, auto-complete badge |
|
| `HudPlugin` | — | Score, move counter, timer, auto-complete badge, and the top-right action button bar (Undo / Pause / Help / New Game). Each button fires the same request event the corresponding hotkey does. |
|
||||||
| `StatsPlugin` | S | Stats overlay and persistence |
|
| `StatsPlugin` | S | Stats overlay and persistence |
|
||||||
| `ProgressPlugin` | — | XP/level system, persistence |
|
| `ProgressPlugin` | — | XP/level system, persistence |
|
||||||
| `AchievementPlugin` | A | Unlock evaluation, toast events, persistence |
|
| `AchievementPlugin` | A | Unlock evaluation, toast events, persistence |
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ use bevy::prelude::*;
|
|||||||
use solitaire_core::game_state::GameMode;
|
use solitaire_core::game_state::GameMode;
|
||||||
use solitaire_data::{challenge_count, challenge_seed_for, save_progress_to};
|
use solitaire_data::{challenge_count, challenge_seed_for, save_progress_to};
|
||||||
|
|
||||||
use crate::events::{GameWonEvent, InfoToastEvent, NewGameRequestEvent};
|
use crate::events::{
|
||||||
|
GameWonEvent, InfoToastEvent, NewGameRequestEvent, StartChallengeRequestEvent,
|
||||||
|
};
|
||||||
use crate::game_plugin::GameMutation;
|
use crate::game_plugin::GameMutation;
|
||||||
use crate::progress_plugin::{ProgressResource, ProgressStoragePath, ProgressUpdate};
|
use crate::progress_plugin::{ProgressResource, ProgressStoragePath, ProgressUpdate};
|
||||||
use crate::resources::GameStateResource;
|
use crate::resources::GameStateResource;
|
||||||
@@ -33,6 +35,7 @@ impl Plugin for ChallengePlugin {
|
|||||||
app.add_message::<ChallengeAdvancedEvent>()
|
app.add_message::<ChallengeAdvancedEvent>()
|
||||||
.add_message::<GameWonEvent>()
|
.add_message::<GameWonEvent>()
|
||||||
.add_message::<NewGameRequestEvent>()
|
.add_message::<NewGameRequestEvent>()
|
||||||
|
.add_message::<StartChallengeRequestEvent>()
|
||||||
.add_message::<InfoToastEvent>()
|
.add_message::<InfoToastEvent>()
|
||||||
// Run after ProgressUpdate so we don't fight ProgressPlugin's add_xp.
|
// Run after ProgressUpdate so we don't fight ProgressPlugin's add_xp.
|
||||||
.add_systems(Update, advance_on_challenge_win.after(ProgressUpdate))
|
.add_systems(Update, advance_on_challenge_win.after(ProgressUpdate))
|
||||||
@@ -70,11 +73,14 @@ fn advance_on_challenge_win(
|
|||||||
|
|
||||||
fn handle_start_challenge_request(
|
fn handle_start_challenge_request(
|
||||||
keys: Res<ButtonInput<KeyCode>>,
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
|
mut requests: MessageReader<StartChallengeRequestEvent>,
|
||||||
progress: Res<ProgressResource>,
|
progress: Res<ProgressResource>,
|
||||||
mut new_game: MessageWriter<NewGameRequestEvent>,
|
mut new_game: MessageWriter<NewGameRequestEvent>,
|
||||||
mut info_toast: MessageWriter<InfoToastEvent>,
|
mut info_toast: MessageWriter<InfoToastEvent>,
|
||||||
) {
|
) {
|
||||||
if !keys.just_pressed(KeyCode::KeyX) {
|
// Either X or the HUD Modes-popover "Challenge" row triggers this.
|
||||||
|
let button_clicked = requests.read().count() > 0;
|
||||||
|
if !keys.just_pressed(KeyCode::KeyX) && !button_clicked {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if progress.0.level < CHALLENGE_UNLOCK_LEVEL {
|
if progress.0.level < CHALLENGE_UNLOCK_LEVEL {
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ use chrono::{Local, NaiveDate};
|
|||||||
use solitaire_data::{daily_seed_for, save_progress_to};
|
use solitaire_data::{daily_seed_for, save_progress_to};
|
||||||
use solitaire_sync::ChallengeGoal;
|
use solitaire_sync::ChallengeGoal;
|
||||||
|
|
||||||
use crate::events::{GameWonEvent, InfoToastEvent, NewGameRequestEvent, XpAwardedEvent};
|
use crate::events::{
|
||||||
|
GameWonEvent, InfoToastEvent, NewGameRequestEvent, StartDailyChallengeRequestEvent,
|
||||||
|
XpAwardedEvent,
|
||||||
|
};
|
||||||
use crate::game_plugin::GameMutation;
|
use crate::game_plugin::GameMutation;
|
||||||
use crate::progress_plugin::{ProgressResource, ProgressStoragePath, ProgressUpdate};
|
use crate::progress_plugin::{ProgressResource, ProgressStoragePath, ProgressUpdate};
|
||||||
use crate::resources::GameStateResource;
|
use crate::resources::GameStateResource;
|
||||||
@@ -83,6 +86,7 @@ impl Plugin for DailyChallengePlugin {
|
|||||||
.add_message::<DailyGoalAnnouncementEvent>()
|
.add_message::<DailyGoalAnnouncementEvent>()
|
||||||
.add_message::<GameWonEvent>()
|
.add_message::<GameWonEvent>()
|
||||||
.add_message::<NewGameRequestEvent>()
|
.add_message::<NewGameRequestEvent>()
|
||||||
|
.add_message::<StartDailyChallengeRequestEvent>()
|
||||||
.add_message::<XpAwardedEvent>()
|
.add_message::<XpAwardedEvent>()
|
||||||
.add_systems(Startup, fetch_server_challenge)
|
.add_systems(Startup, fetch_server_challenge)
|
||||||
.add_systems(Update, poll_server_challenge)
|
.add_systems(Update, poll_server_challenge)
|
||||||
@@ -189,11 +193,16 @@ fn handle_daily_completion(
|
|||||||
|
|
||||||
fn handle_start_daily_request(
|
fn handle_start_daily_request(
|
||||||
keys: Res<ButtonInput<KeyCode>>,
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
|
mut requests: MessageReader<StartDailyChallengeRequestEvent>,
|
||||||
daily: Res<DailyChallengeResource>,
|
daily: Res<DailyChallengeResource>,
|
||||||
mut new_game: MessageWriter<NewGameRequestEvent>,
|
mut new_game: MessageWriter<NewGameRequestEvent>,
|
||||||
mut announce: MessageWriter<DailyGoalAnnouncementEvent>,
|
mut announce: MessageWriter<DailyGoalAnnouncementEvent>,
|
||||||
) {
|
) {
|
||||||
if keys.just_pressed(KeyCode::KeyC) {
|
// Either C or the HUD Modes-popover "Daily Challenge" row triggers this.
|
||||||
|
let button_clicked = requests.read().count() > 0;
|
||||||
|
if !keys.just_pressed(KeyCode::KeyC) && !button_clicked {
|
||||||
|
return;
|
||||||
|
}
|
||||||
new_game.write(NewGameRequestEvent {
|
new_game.write(NewGameRequestEvent {
|
||||||
seed: Some(daily.seed),
|
seed: Some(daily.seed),
|
||||||
mode: None,
|
mode: None,
|
||||||
@@ -205,7 +214,6 @@ fn handle_start_daily_request(
|
|||||||
.unwrap_or_else(|| "Daily Challenge".to_string());
|
.unwrap_or_else(|| "Daily Challenge".to_string());
|
||||||
announce.write(DailyGoalAnnouncementEvent(desc));
|
announce.write(DailyGoalAnnouncementEvent(desc));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|||||||
@@ -86,6 +86,74 @@ pub struct AchievementUnlockedEvent(pub AchievementRecord);
|
|||||||
#[derive(Message, Debug, Clone, Copy, Default)]
|
#[derive(Message, Debug, Clone, Copy, Default)]
|
||||||
pub struct ManualSyncRequestEvent;
|
pub struct ManualSyncRequestEvent;
|
||||||
|
|
||||||
|
/// Request to toggle the pause overlay. Fired by the HUD "Pause" button so
|
||||||
|
/// the same toggle path runs whether the player presses `Esc` or clicks.
|
||||||
|
/// Consumed by `pause_plugin::toggle_pause`, which honours the same drag /
|
||||||
|
/// game-over / selection guards either way.
|
||||||
|
#[derive(Message, Debug, Clone, Copy, Default)]
|
||||||
|
pub struct PauseRequestEvent;
|
||||||
|
|
||||||
|
/// Request to toggle the help / controls overlay. Fired by the HUD "Help"
|
||||||
|
/// button alongside the existing `F1` accelerator so the overlay is
|
||||||
|
/// reachable without a keyboard. Consumed by `help_plugin::toggle_help_screen`.
|
||||||
|
#[derive(Message, Debug, Clone, Copy, Default)]
|
||||||
|
pub struct HelpRequestEvent;
|
||||||
|
|
||||||
|
/// Request to start a Zen-mode game. Fired by the HUD Modes-popover "Zen"
|
||||||
|
/// row alongside the existing `Z` accelerator. The handler in
|
||||||
|
/// `input_plugin` enforces the level gate (Zen unlocks at level 5) and
|
||||||
|
/// shows an informational toast when locked.
|
||||||
|
#[derive(Message, Debug, Clone, Copy, Default)]
|
||||||
|
pub struct StartZenRequestEvent;
|
||||||
|
|
||||||
|
/// Request to start the next Challenge-mode game. Fired by the HUD
|
||||||
|
/// Modes-popover "Challenge" row alongside the existing `X` accelerator.
|
||||||
|
/// The handler in `challenge_plugin` enforces the level gate, picks the
|
||||||
|
/// next seed from `progress.challenge_index`, and writes the
|
||||||
|
/// corresponding `NewGameRequestEvent`.
|
||||||
|
#[derive(Message, Debug, Clone, Copy, Default)]
|
||||||
|
pub struct StartChallengeRequestEvent;
|
||||||
|
|
||||||
|
/// Request to start a Time Attack session. Fired by the HUD
|
||||||
|
/// Modes-popover "Time Attack" row alongside the existing `T`
|
||||||
|
/// accelerator. The handler in `time_attack_plugin` enforces the level
|
||||||
|
/// gate, initialises `TimeAttackResource`, and writes the corresponding
|
||||||
|
/// `NewGameRequestEvent`.
|
||||||
|
#[derive(Message, Debug, Clone, Copy, Default)]
|
||||||
|
pub struct StartTimeAttackRequestEvent;
|
||||||
|
|
||||||
|
/// Request to start today's Daily Challenge. Fired by the HUD
|
||||||
|
/// Modes-popover "Daily Challenge" row alongside the existing `C`
|
||||||
|
/// accelerator. The handler in `daily_challenge_plugin` reads
|
||||||
|
/// `DailyChallengeResource::seed` and writes a `NewGameRequestEvent`.
|
||||||
|
#[derive(Message, Debug, Clone, Copy, Default)]
|
||||||
|
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
|
||||||
|
|||||||
@@ -1,30 +1,39 @@
|
|||||||
//! Toggleable on-screen help / cheat sheet showing keyboard bindings.
|
//! Toggleable on-screen help / cheat sheet showing keyboard bindings.
|
||||||
//!
|
//!
|
||||||
//! Press **F1** to toggle. Listed shortcuts are grouped by intent —
|
//! Reachable from the HUD "Help" button (per the UI-first principle); `F1`
|
||||||
|
//! is an optional accelerator. Listed shortcuts are grouped by intent —
|
||||||
//! gameplay, modes, and overlays.
|
//! gameplay, modes, and overlays.
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use crate::events::HelpRequestEvent;
|
||||||
|
|
||||||
/// Marker on the help overlay root node.
|
/// Marker on the help overlay root node.
|
||||||
#[derive(Component, Debug)]
|
#[derive(Component, Debug)]
|
||||||
pub struct HelpScreen;
|
pub struct HelpScreen;
|
||||||
|
|
||||||
/// Spawns and despawns the help/controls overlay shown when the player presses H (or the help button).
|
/// Spawns and despawns the help / controls overlay shown when the player
|
||||||
/// All hotkeys and gesture guides live here.
|
/// clicks the "Help" HUD button or presses `F1`. All hotkeys and gesture
|
||||||
|
/// guides live here.
|
||||||
pub struct HelpPlugin;
|
pub struct HelpPlugin;
|
||||||
|
|
||||||
impl Plugin for HelpPlugin {
|
impl Plugin for HelpPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.add_systems(Update, toggle_help_screen);
|
app.add_message::<HelpRequestEvent>()
|
||||||
|
.add_systems(Update, toggle_help_screen);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_help_screen(
|
fn toggle_help_screen(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
keys: Res<ButtonInput<KeyCode>>,
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
|
mut requests: MessageReader<HelpRequestEvent>,
|
||||||
screens: Query<Entity, With<HelpScreen>>,
|
screens: Query<Entity, With<HelpScreen>>,
|
||||||
) {
|
) {
|
||||||
if !keys.just_pressed(KeyCode::F1) {
|
// Either F1 or a click on the HUD "Help" button (which fires
|
||||||
|
// HelpRequestEvent) toggles the overlay.
|
||||||
|
let button_clicked = requests.read().count() > 0;
|
||||||
|
if !keys.just_pressed(KeyCode::F1) && !button_clicked {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Ok(entity) = screens.single() {
|
if let Ok(entity) = screens.single() {
|
||||||
|
|||||||
@@ -12,8 +12,16 @@ use solitaire_core::game_state::{DrawMode, GameMode};
|
|||||||
use solitaire_core::pile::PileType;
|
use solitaire_core::pile::PileType;
|
||||||
|
|
||||||
use crate::auto_complete_plugin::AutoCompleteState;
|
use crate::auto_complete_plugin::AutoCompleteState;
|
||||||
|
use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL;
|
||||||
use crate::daily_challenge_plugin::DailyChallengeResource;
|
use crate::daily_challenge_plugin::DailyChallengeResource;
|
||||||
use crate::events::{InfoToastEvent, NewGameRequestEvent};
|
use crate::progress_plugin::ProgressResource;
|
||||||
|
use crate::events::{
|
||||||
|
HelpRequestEvent, InfoToastEvent, NewGameRequestEvent, PauseRequestEvent,
|
||||||
|
StartChallengeRequestEvent, StartDailyChallengeRequestEvent, StartTimeAttackRequestEvent,
|
||||||
|
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;
|
||||||
use crate::resources::GameStateResource;
|
use crate::resources::GameStateResource;
|
||||||
@@ -84,30 +92,130 @@ pub struct HudDrawCycle;
|
|||||||
#[derive(Component, Debug)]
|
#[derive(Component, Debug)]
|
||||||
pub struct HudSelection;
|
pub struct HudSelection;
|
||||||
|
|
||||||
/// Marker on the New Game action button anchored top-right of the play area.
|
/// Marker shared by every clickable HUD action button so a single
|
||||||
/// Click fires [`NewGameRequestEvent`]; the existing `ConfirmNewGameScreen`
|
/// `paint_action_buttons` system can recolour them on hover/press without
|
||||||
/// modal then handles confirmation when a game is in progress.
|
/// each button needing its own paint handler.
|
||||||
|
#[derive(Component, Debug)]
|
||||||
|
pub struct ActionButton;
|
||||||
|
|
||||||
|
/// Marker on the "New Game" action button anchored top-right of the play
|
||||||
|
/// area. Click fires [`NewGameRequestEvent`]; the existing
|
||||||
|
/// `ConfirmNewGameScreen` modal handles confirmation when a game is in
|
||||||
|
/// progress.
|
||||||
#[derive(Component, Debug)]
|
#[derive(Component, Debug)]
|
||||||
pub struct NewGameButton;
|
pub struct NewGameButton;
|
||||||
|
|
||||||
|
/// Marker on the "Undo" action button. Click fires [`UndoRequestEvent`],
|
||||||
|
/// mirroring the `U` keyboard accelerator.
|
||||||
|
#[derive(Component, Debug)]
|
||||||
|
pub struct UndoButton;
|
||||||
|
|
||||||
|
/// Marker on the "Pause" action button. Click fires [`PauseRequestEvent`],
|
||||||
|
/// mirroring the `Esc` keyboard accelerator. The pause overlay's own resume
|
||||||
|
/// affordance dismisses it from the paused state.
|
||||||
|
#[derive(Component, Debug)]
|
||||||
|
pub struct PauseButton;
|
||||||
|
|
||||||
|
/// Marker on the "Help" action button. Click fires [`HelpRequestEvent`],
|
||||||
|
/// mirroring the `F1` keyboard accelerator.
|
||||||
|
#[derive(Component, Debug)]
|
||||||
|
pub struct HelpButton;
|
||||||
|
|
||||||
|
/// Marker on the "Modes" action button. Click toggles the [`ModesPopover`]
|
||||||
|
/// (a small dropdown panel) below the action bar. Each popover row starts
|
||||||
|
/// the corresponding game mode.
|
||||||
|
#[derive(Component, Debug)]
|
||||||
|
pub struct ModesButton;
|
||||||
|
|
||||||
|
/// Marker on the dropdown panel that opens below the [`ModesButton`].
|
||||||
|
/// Spawned on first click, despawned on second click or on mode select.
|
||||||
|
#[derive(Component, Debug)]
|
||||||
|
pub struct ModesPopover;
|
||||||
|
|
||||||
|
/// One row inside the [`ModesPopover`]. The variant carries which event
|
||||||
|
/// the click handler should fire — Classic uses `NewGameRequestEvent`
|
||||||
|
/// directly, the others go through their `Start*RequestEvent` so the
|
||||||
|
/// existing keyboard handler's level gate / resource setup runs.
|
||||||
|
#[derive(Component, Debug, Clone, Copy)]
|
||||||
|
pub enum ModeOption {
|
||||||
|
Classic,
|
||||||
|
DailyChallenge,
|
||||||
|
Zen,
|
||||||
|
Challenge,
|
||||||
|
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;
|
||||||
|
|
||||||
/// Idle / hover / pressed colours for the New Game action button.
|
/// Idle / hover / pressed colours shared by every action button.
|
||||||
const NEW_GAME_BTN_IDLE: Color = Color::srgb(0.20, 0.55, 0.85);
|
const ACTION_BTN_IDLE: Color = Color::srgb(0.20, 0.55, 0.85);
|
||||||
const NEW_GAME_BTN_HOVER: Color = Color::srgb(0.28, 0.65, 0.95);
|
const ACTION_BTN_HOVER: Color = Color::srgb(0.28, 0.65, 0.95);
|
||||||
const NEW_GAME_BTN_PRESSED: Color = Color::srgb(0.15, 0.45, 0.75);
|
const ACTION_BTN_PRESSED: Color = Color::srgb(0.15, 0.45, 0.75);
|
||||||
|
|
||||||
/// Renders the in-game HUD: score counter, move counter, elapsed timer, draw-mode indicator, and the auto-complete badge that lights up when the game is solvable without further input.
|
/// Renders the in-game HUD: score counter, move counter, elapsed timer, draw-mode indicator, and the auto-complete badge that lights up when the game is solvable without further input.
|
||||||
pub struct HudPlugin;
|
pub struct HudPlugin;
|
||||||
|
|
||||||
impl Plugin for HudPlugin {
|
impl Plugin for HudPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.add_systems(Startup, (spawn_hud, spawn_new_game_button))
|
// The click handlers write to messages registered elsewhere by their
|
||||||
|
// owning plugins (`GamePlugin`, `PausePlugin`, `HelpPlugin`,
|
||||||
|
// `challenge_plugin`, `daily_challenge_plugin`, `time_attack_plugin`,
|
||||||
|
// `input_plugin`). Re-register defensively so the HUD plugin works in
|
||||||
|
// isolation under `MinimalPlugins` (tests). `add_message` is
|
||||||
|
// idempotent.
|
||||||
|
app.add_message::<NewGameRequestEvent>()
|
||||||
|
.add_message::<UndoRequestEvent>()
|
||||||
|
.add_message::<PauseRequestEvent>()
|
||||||
|
.add_message::<HelpRequestEvent>()
|
||||||
|
.add_message::<StartZenRequestEvent>()
|
||||||
|
.add_message::<StartChallengeRequestEvent>()
|
||||||
|
.add_message::<StartTimeAttackRequestEvent>()
|
||||||
|
.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(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))
|
||||||
.add_systems(Update, update_selection_hud)
|
.add_systems(Update, update_selection_hud)
|
||||||
.add_systems(Update, (handle_new_game_button, paint_new_game_button));
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
(
|
||||||
|
handle_new_game_button,
|
||||||
|
handle_undo_button,
|
||||||
|
handle_pause_button,
|
||||||
|
handle_help_button,
|
||||||
|
handle_modes_button,
|
||||||
|
handle_mode_option_click,
|
||||||
|
handle_menu_button,
|
||||||
|
handle_menu_option_click,
|
||||||
|
paint_action_buttons,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,15 +294,15 @@ fn spawn_hud(font_res: Option<Res<FontResource>>, mut commands: Commands) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawns the New Game action button anchored to the top-right of the
|
/// Spawns the action button bar anchored to the top-right of the window.
|
||||||
/// window. Click fires [`NewGameRequestEvent`]; the existing
|
/// Each child is a clickable button mirroring a keyboard accelerator —
|
||||||
/// `ConfirmNewGameScreen` modal in `GamePlugin` handles confirmation when
|
/// per the UI-first principle (CLAUDE.md / ARCHITECTURE.md §1) the buttons
|
||||||
/// a game is in progress, and starts a fresh deal otherwise.
|
/// are the primary entry point and the hotkeys are optional.
|
||||||
///
|
///
|
||||||
/// Per the UI-first principle (CLAUDE.md / ARCHITECTURE.md §1), this
|
/// Order (left → right): Undo, Pause, Help, New Game. New Game is rightmost
|
||||||
/// button is the primary entry point for starting a new game. The `N`
|
/// because it's the most consequential action; the destructive button sits
|
||||||
/// keyboard shortcut is an optional accelerator.
|
/// on its own visual edge.
|
||||||
fn spawn_new_game_button(font_res: Option<Res<FontResource>>, mut commands: Commands) {
|
fn spawn_action_buttons(font_res: Option<Res<FontResource>>, mut commands: Commands) {
|
||||||
let font = TextFont {
|
let font = TextFont {
|
||||||
font: font_res.as_ref().map(|f| f.0.clone()).unwrap_or_default(),
|
font: font_res.as_ref().map(|f| f.0.clone()).unwrap_or_default(),
|
||||||
font_size: 16.0,
|
font_size: 16.0,
|
||||||
@@ -202,35 +310,59 @@ fn spawn_new_game_button(font_res: Option<Res<FontResource>>, mut commands: Comm
|
|||||||
};
|
};
|
||||||
commands
|
commands
|
||||||
.spawn((
|
.spawn((
|
||||||
NewGameButton,
|
|
||||||
Button,
|
|
||||||
Node {
|
Node {
|
||||||
position_type: PositionType::Absolute,
|
position_type: PositionType::Absolute,
|
||||||
right: Val::Px(12.0),
|
right: Val::Px(12.0),
|
||||||
top: Val::Px(8.0),
|
top: Val::Px(8.0),
|
||||||
|
flex_direction: FlexDirection::Row,
|
||||||
|
column_gap: Val::Px(8.0),
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
ZIndex(Z_HUD),
|
||||||
|
))
|
||||||
|
.with_children(|row| {
|
||||||
|
spawn_action_button(row, MenuButton, "Menu \u{25BE}", &font);
|
||||||
|
spawn_action_button(row, UndoButton, "Undo", &font);
|
||||||
|
spawn_action_button(row, PauseButton, "Pause", &font);
|
||||||
|
spawn_action_button(row, HelpButton, "Help", &font);
|
||||||
|
spawn_action_button(row, ModesButton, "Modes \u{25BE}", &font);
|
||||||
|
spawn_action_button(row, NewGameButton, "New Game", &font);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawns a single action button as a child of `row`. Each button shares
|
||||||
|
/// the same node geometry, idle colour, and `ActionButton` marker so
|
||||||
|
/// `paint_action_buttons` can recolour all of them with one query.
|
||||||
|
fn spawn_action_button<M: Component>(
|
||||||
|
row: &mut ChildSpawnerCommands,
|
||||||
|
marker: M,
|
||||||
|
label: &str,
|
||||||
|
font: &TextFont,
|
||||||
|
) {
|
||||||
|
row.spawn((
|
||||||
|
marker,
|
||||||
|
ActionButton,
|
||||||
|
Button,
|
||||||
|
Node {
|
||||||
padding: UiRect::axes(Val::Px(14.0), Val::Px(8.0)),
|
padding: UiRect::axes(Val::Px(14.0), Val::Px(8.0)),
|
||||||
justify_content: JustifyContent::Center,
|
justify_content: JustifyContent::Center,
|
||||||
align_items: AlignItems::Center,
|
align_items: AlignItems::Center,
|
||||||
border_radius: BorderRadius::all(Val::Px(6.0)),
|
border_radius: BorderRadius::all(Val::Px(6.0)),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
BackgroundColor(NEW_GAME_BTN_IDLE),
|
BackgroundColor(ACTION_BTN_IDLE),
|
||||||
ZIndex(Z_HUD),
|
|
||||||
))
|
))
|
||||||
.with_children(|b| {
|
.with_children(|b| {
|
||||||
b.spawn((
|
b.spawn((Text::new(label), font.clone(), TextColor(Color::WHITE)));
|
||||||
Text::new("New Game"),
|
|
||||||
font,
|
|
||||||
TextColor(Color::WHITE),
|
|
||||||
));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Click handler for the New Game button — fires `NewGameRequestEvent`.
|
|
||||||
///
|
|
||||||
/// `Changed<Interaction>` filter ensures we only react on the frame the
|
/// `Changed<Interaction>` filter ensures we only react on the frame the
|
||||||
/// interaction state transitions, avoiding repeat events while the button
|
/// interaction state transitions, avoiding repeat events while the button
|
||||||
/// is held down.
|
/// is held down. Each click handler fires the corresponding request event,
|
||||||
|
/// which `pause_plugin` / `help_plugin` / `game_plugin` consume alongside
|
||||||
|
/// their existing keyboard handlers.
|
||||||
fn handle_new_game_button(
|
fn handle_new_game_button(
|
||||||
interaction_query: Query<&Interaction, (With<NewGameButton>, Changed<Interaction>)>,
|
interaction_query: Query<&Interaction, (With<NewGameButton>, Changed<Interaction>)>,
|
||||||
mut new_game: MessageWriter<NewGameRequestEvent>,
|
mut new_game: MessageWriter<NewGameRequestEvent>,
|
||||||
@@ -242,20 +374,322 @@ fn handle_new_game_button(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Visual feedback for the New Game button — paints idle / hover / pressed
|
fn handle_undo_button(
|
||||||
/// states by mutating the `BackgroundColor` whenever the interaction state
|
interaction_query: Query<&Interaction, (With<UndoButton>, Changed<Interaction>)>,
|
||||||
/// changes.
|
mut undo: MessageWriter<UndoRequestEvent>,
|
||||||
fn paint_new_game_button(
|
) {
|
||||||
|
for interaction in &interaction_query {
|
||||||
|
if *interaction == Interaction::Pressed {
|
||||||
|
undo.write(UndoRequestEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_pause_button(
|
||||||
|
interaction_query: Query<&Interaction, (With<PauseButton>, Changed<Interaction>)>,
|
||||||
|
mut pause: MessageWriter<PauseRequestEvent>,
|
||||||
|
) {
|
||||||
|
for interaction in &interaction_query {
|
||||||
|
if *interaction == Interaction::Pressed {
|
||||||
|
pause.write(PauseRequestEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_help_button(
|
||||||
|
interaction_query: Query<&Interaction, (With<HelpButton>, Changed<Interaction>)>,
|
||||||
|
mut help: MessageWriter<HelpRequestEvent>,
|
||||||
|
) {
|
||||||
|
for interaction in &interaction_query {
|
||||||
|
if *interaction == Interaction::Pressed {
|
||||||
|
help.write(HelpRequestEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toggles the [`ModesPopover`]: spawns it on first click, despawns it on
|
||||||
|
/// second click. Mode rows are populated per the player's current level so
|
||||||
|
/// only unlocked options appear.
|
||||||
|
fn handle_modes_button(
|
||||||
|
interaction_query: Query<&Interaction, (With<ModesButton>, Changed<Interaction>)>,
|
||||||
|
popovers: Query<Entity, With<ModesPopover>>,
|
||||||
|
progress: Option<Res<ProgressResource>>,
|
||||||
|
daily: Option<Res<DailyChallengeResource>>,
|
||||||
|
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_modes_popover(
|
||||||
|
&mut commands,
|
||||||
|
progress.as_deref(),
|
||||||
|
daily.as_deref(),
|
||||||
|
font_res.as_deref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawns the modes popover anchored just below the action bar's right
|
||||||
|
/// edge. Always includes Classic; includes Daily Challenge when a daily
|
||||||
|
/// resource is loaded; includes Zen / Challenge / Time Attack once the
|
||||||
|
/// player reaches the challenge unlock level.
|
||||||
|
fn spawn_modes_popover(
|
||||||
|
commands: &mut Commands,
|
||||||
|
progress: Option<&ProgressResource>,
|
||||||
|
daily: Option<&DailyChallengeResource>,
|
||||||
|
font_res: Option<&FontResource>,
|
||||||
|
) {
|
||||||
|
let level = progress.map_or(0, |p| p.0.level);
|
||||||
|
let font = TextFont {
|
||||||
|
font: font_res.map(|f| f.0.clone()).unwrap_or_default(),
|
||||||
|
font_size: 15.0,
|
||||||
|
..default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut rows: Vec<(ModeOption, &'static str)> = vec![(ModeOption::Classic, "Classic")];
|
||||||
|
if daily.is_some() {
|
||||||
|
rows.push((ModeOption::DailyChallenge, "Daily Challenge"));
|
||||||
|
}
|
||||||
|
if level >= CHALLENGE_UNLOCK_LEVEL {
|
||||||
|
rows.push((ModeOption::Zen, "Zen"));
|
||||||
|
rows.push((ModeOption::Challenge, "Challenge"));
|
||||||
|
rows.push((ModeOption::TimeAttack, "Time Attack"));
|
||||||
|
}
|
||||||
|
|
||||||
|
commands
|
||||||
|
.spawn((
|
||||||
|
ModesPopover,
|
||||||
|
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 popover row to the matching request event,
|
||||||
|
/// then despawns the popover.
|
||||||
|
///
|
||||||
|
/// Classic uses [`NewGameRequestEvent`] directly; the other modes use
|
||||||
|
/// their `Start*RequestEvent` so the existing keyboard handler runs
|
||||||
|
/// (level gates, `TimeAttackResource` setup, daily seed lookup, etc.) —
|
||||||
|
/// the popover stays a thin entry point and never duplicates that logic.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn handle_mode_option_click(
|
||||||
|
interaction_query: Query<(&Interaction, &ModeOption), Changed<Interaction>>,
|
||||||
|
popovers: Query<Entity, With<ModesPopover>>,
|
||||||
|
mut new_game: MessageWriter<NewGameRequestEvent>,
|
||||||
|
mut zen: MessageWriter<StartZenRequestEvent>,
|
||||||
|
mut challenge: MessageWriter<StartChallengeRequestEvent>,
|
||||||
|
mut time_attack: MessageWriter<StartTimeAttackRequestEvent>,
|
||||||
|
mut daily: MessageWriter<StartDailyChallengeRequestEvent>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
let mut clicked_any = false;
|
||||||
|
for (interaction, option) in &interaction_query {
|
||||||
|
if *interaction != Interaction::Pressed {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
clicked_any = true;
|
||||||
|
match option {
|
||||||
|
ModeOption::Classic => {
|
||||||
|
new_game.write(NewGameRequestEvent::default());
|
||||||
|
}
|
||||||
|
ModeOption::DailyChallenge => {
|
||||||
|
daily.write(StartDailyChallengeRequestEvent);
|
||||||
|
}
|
||||||
|
ModeOption::Zen => {
|
||||||
|
zen.write(StartZenRequestEvent);
|
||||||
|
}
|
||||||
|
ModeOption::Challenge => {
|
||||||
|
challenge.write(StartChallengeRequestEvent);
|
||||||
|
}
|
||||||
|
ModeOption::TimeAttack => {
|
||||||
|
time_attack.write(StartTimeAttackRequestEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if clicked_any
|
||||||
|
&& let Ok(entity) = popovers.single() {
|
||||||
|
commands.entity(entity).despawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
/// states by mutating `BackgroundColor` whenever the interaction state
|
||||||
|
/// changes. One query covers all action buttons via the shared
|
||||||
|
/// `ActionButton` marker.
|
||||||
|
fn paint_action_buttons(
|
||||||
mut buttons: Query<
|
mut buttons: Query<
|
||||||
(&Interaction, &mut BackgroundColor),
|
(&Interaction, &mut BackgroundColor),
|
||||||
(With<NewGameButton>, Changed<Interaction>),
|
(With<ActionButton>, Changed<Interaction>),
|
||||||
>,
|
>,
|
||||||
) {
|
) {
|
||||||
for (interaction, mut bg) in &mut buttons {
|
for (interaction, mut bg) in &mut buttons {
|
||||||
bg.0 = match interaction {
|
bg.0 = match interaction {
|
||||||
Interaction::Pressed => NEW_GAME_BTN_PRESSED,
|
Interaction::Pressed => ACTION_BTN_PRESSED,
|
||||||
Interaction::Hovered => NEW_GAME_BTN_HOVER,
|
Interaction::Hovered => ACTION_BTN_HOVER,
|
||||||
Interaction::None => NEW_GAME_BTN_IDLE,
|
Interaction::None => ACTION_BTN_IDLE,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ use solitaire_core::game_state::DrawMode;
|
|||||||
use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL;
|
use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL;
|
||||||
use crate::events::{
|
use crate::events::{
|
||||||
DrawRequestEvent, ForfeitEvent, HintVisualEvent, InfoToastEvent, MoveRejectedEvent,
|
DrawRequestEvent, ForfeitEvent, HintVisualEvent, InfoToastEvent, MoveRejectedEvent,
|
||||||
MoveRequestEvent, NewGameConfirmEvent, NewGameRequestEvent, StateChangedEvent, UndoRequestEvent,
|
MoveRequestEvent, NewGameConfirmEvent, NewGameRequestEvent, StartZenRequestEvent,
|
||||||
|
StateChangedEvent, UndoRequestEvent,
|
||||||
};
|
};
|
||||||
use crate::game_plugin::GameMutation;
|
use crate::game_plugin::GameMutation;
|
||||||
use crate::pause_plugin::PausedResource;
|
use crate::pause_plugin::PausedResource;
|
||||||
@@ -84,6 +85,7 @@ impl Plugin for InputPlugin {
|
|||||||
app.init_resource::<HintCycleIndex>()
|
app.init_resource::<HintCycleIndex>()
|
||||||
.init_resource::<KeyboardConfirmState>()
|
.init_resource::<KeyboardConfirmState>()
|
||||||
.add_message::<NewGameConfirmEvent>()
|
.add_message::<NewGameConfirmEvent>()
|
||||||
|
.add_message::<StartZenRequestEvent>()
|
||||||
.add_message::<InfoToastEvent>()
|
.add_message::<InfoToastEvent>()
|
||||||
.add_message::<ForfeitEvent>()
|
.add_message::<ForfeitEvent>()
|
||||||
.add_message::<HintVisualEvent>()
|
.add_message::<HintVisualEvent>()
|
||||||
@@ -147,6 +149,7 @@ fn handle_keyboard_core(
|
|||||||
mut ev: CoreKeyboardMessages<'_>,
|
mut ev: CoreKeyboardMessages<'_>,
|
||||||
mut time_attack: Option<ResMut<TimeAttackResource>>,
|
mut time_attack: Option<ResMut<TimeAttackResource>>,
|
||||||
selection: Option<Res<SelectionState>>,
|
selection: Option<Res<SelectionState>>,
|
||||||
|
mut zen_requests: MessageReader<StartZenRequestEvent>,
|
||||||
) {
|
) {
|
||||||
if paused.is_some_and(|p| p.0) {
|
if paused.is_some_and(|p| p.0) {
|
||||||
return;
|
return;
|
||||||
@@ -209,11 +212,13 @@ fn handle_keyboard_core(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if keys.just_pressed(KeyCode::KeyZ) {
|
let zen_clicked = zen_requests.read().count() > 0;
|
||||||
|
if keys.just_pressed(KeyCode::KeyZ) || zen_clicked {
|
||||||
// Cancel any pending forfeit when the player takes another action.
|
// Cancel any pending forfeit when the player takes another action.
|
||||||
confirm.forfeit_countdown = 0.0;
|
confirm.forfeit_countdown = 0.0;
|
||||||
// Zen / Challenge / Time Attack are gated to level >= CHALLENGE_UNLOCK_LEVEL.
|
// Zen / Challenge / Time Attack are gated to level >= CHALLENGE_UNLOCK_LEVEL.
|
||||||
// X is gated separately by ChallengePlugin.
|
// X is gated separately by ChallengePlugin. Either Z or the HUD
|
||||||
|
// Modes-popover "Zen" row reaches this branch.
|
||||||
let level = progress.as_ref().map_or(0, |p| p.0.level);
|
let level = progress.as_ref().map_or(0, |p| p.0.level);
|
||||||
if level >= CHALLENGE_UNLOCK_LEVEL {
|
if level >= CHALLENGE_UNLOCK_LEVEL {
|
||||||
ev.new_game.write(NewGameRequestEvent {
|
ev.new_game.write(NewGameRequestEvent {
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -67,14 +67,21 @@ pub use font_plugin::{FontPlugin, FontResource};
|
|||||||
pub use cursor_plugin::CursorPlugin;
|
pub use cursor_plugin::CursorPlugin;
|
||||||
pub use events::{
|
pub use events::{
|
||||||
AchievementUnlockedEvent, CardFaceRevealedEvent, CardFlippedEvent, DrawRequestEvent,
|
AchievementUnlockedEvent, CardFaceRevealedEvent, CardFlippedEvent, DrawRequestEvent,
|
||||||
ForfeitEvent, GameWonEvent, HintVisualEvent, InfoToastEvent, ManualSyncRequestEvent,
|
ForfeitEvent, GameWonEvent, HelpRequestEvent, HintVisualEvent, InfoToastEvent,
|
||||||
MoveRejectedEvent, MoveRequestEvent, NewGameConfirmEvent, NewGameRequestEvent,
|
ManualSyncRequestEvent, MoveRejectedEvent, MoveRequestEvent, NewGameConfirmEvent,
|
||||||
StateChangedEvent, SyncCompleteEvent, UndoRequestEvent, XpAwardedEvent,
|
NewGameRequestEvent, PauseRequestEvent, StartChallengeRequestEvent,
|
||||||
|
StartDailyChallengeRequestEvent, StartTimeAttackRequestEvent, StartZenRequestEvent,
|
||||||
|
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::{HudAutoComplete, HudPlugin};
|
pub use hud_plugin::{
|
||||||
|
ActionButton, HelpButton, HudAutoComplete, HudPlugin, MenuButton, MenuOption, MenuPopover,
|
||||||
|
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;
|
||||||
pub use onboarding_plugin::{OnboardingPlugin, OnboardingScreen};
|
pub use onboarding_plugin::{OnboardingPlugin, OnboardingScreen};
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ use bevy::prelude::*;
|
|||||||
use solitaire_core::game_state::DrawMode;
|
use solitaire_core::game_state::DrawMode;
|
||||||
use solitaire_data::save_game_state_to;
|
use solitaire_data::save_game_state_to;
|
||||||
|
|
||||||
use crate::events::StateChangedEvent;
|
use crate::events::{PauseRequestEvent, StateChangedEvent};
|
||||||
use crate::game_plugin::{GameOverScreen, GameStatePath};
|
use crate::game_plugin::{GameOverScreen, GameStatePath};
|
||||||
use crate::progress_plugin::ProgressResource;
|
use crate::progress_plugin::ProgressResource;
|
||||||
use crate::resources::{DragState, GameStateResource};
|
use crate::resources::{DragState, GameStateResource};
|
||||||
@@ -58,6 +58,7 @@ impl Plugin for PausePlugin {
|
|||||||
// events first, but calling add_event again is always safe.
|
// events first, but calling add_event again is always safe.
|
||||||
app.add_message::<SettingsChangedEvent>()
|
app.add_message::<SettingsChangedEvent>()
|
||||||
.add_message::<StateChangedEvent>()
|
.add_message::<StateChangedEvent>()
|
||||||
|
.add_message::<PauseRequestEvent>()
|
||||||
.init_resource::<PausedResource>()
|
.init_resource::<PausedResource>()
|
||||||
.add_systems(
|
.add_systems(
|
||||||
Update,
|
Update,
|
||||||
@@ -75,6 +76,7 @@ impl Plugin for PausePlugin {
|
|||||||
fn toggle_pause(
|
fn toggle_pause(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
keys: Res<ButtonInput<KeyCode>>,
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
|
mut requests: MessageReader<PauseRequestEvent>,
|
||||||
mut paused: ResMut<PausedResource>,
|
mut paused: ResMut<PausedResource>,
|
||||||
screens: Query<Entity, With<PauseScreen>>,
|
screens: Query<Entity, With<PauseScreen>>,
|
||||||
game_over_screens: Query<Entity, With<GameOverScreen>>,
|
game_over_screens: Query<Entity, With<GameOverScreen>>,
|
||||||
@@ -87,7 +89,11 @@ fn toggle_pause(
|
|||||||
mut changed: MessageWriter<StateChangedEvent>,
|
mut changed: MessageWriter<StateChangedEvent>,
|
||||||
selection: Option<Res<SelectionState>>,
|
selection: Option<Res<SelectionState>>,
|
||||||
) {
|
) {
|
||||||
if !keys.just_pressed(KeyCode::Escape) {
|
// 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.
|
||||||
|
let button_clicked = requests.read().count() > 0;
|
||||||
|
if !keys.just_pressed(KeyCode::Escape) && !button_clicked {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// If a card is currently selected, let SelectionPlugin handle this Escape
|
// If a card is currently selected, let SelectionPlugin handle this Escape
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ use bevy::prelude::*;
|
|||||||
use solitaire_core::game_state::GameMode;
|
use solitaire_core::game_state::GameMode;
|
||||||
|
|
||||||
use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL;
|
use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL;
|
||||||
use crate::events::{GameWonEvent, InfoToastEvent, NewGameRequestEvent};
|
use crate::events::{
|
||||||
|
GameWonEvent, InfoToastEvent, NewGameRequestEvent, StartTimeAttackRequestEvent,
|
||||||
|
};
|
||||||
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;
|
||||||
@@ -40,6 +42,7 @@ impl Plugin for TimeAttackPlugin {
|
|||||||
.add_message::<TimeAttackEndedEvent>()
|
.add_message::<TimeAttackEndedEvent>()
|
||||||
.add_message::<GameWonEvent>()
|
.add_message::<GameWonEvent>()
|
||||||
.add_message::<NewGameRequestEvent>()
|
.add_message::<NewGameRequestEvent>()
|
||||||
|
.add_message::<StartTimeAttackRequestEvent>()
|
||||||
.add_message::<InfoToastEvent>()
|
.add_message::<InfoToastEvent>()
|
||||||
.add_systems(
|
.add_systems(
|
||||||
Update,
|
Update,
|
||||||
@@ -52,12 +55,15 @@ impl Plugin for TimeAttackPlugin {
|
|||||||
|
|
||||||
fn handle_start_time_attack_request(
|
fn handle_start_time_attack_request(
|
||||||
keys: Res<ButtonInput<KeyCode>>,
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
|
mut requests: MessageReader<StartTimeAttackRequestEvent>,
|
||||||
progress: Res<ProgressResource>,
|
progress: Res<ProgressResource>,
|
||||||
mut session: ResMut<TimeAttackResource>,
|
mut session: ResMut<TimeAttackResource>,
|
||||||
mut new_game: MessageWriter<NewGameRequestEvent>,
|
mut new_game: MessageWriter<NewGameRequestEvent>,
|
||||||
mut info_toast: MessageWriter<InfoToastEvent>,
|
mut info_toast: MessageWriter<InfoToastEvent>,
|
||||||
) {
|
) {
|
||||||
if !keys.just_pressed(KeyCode::KeyT) {
|
// Either T or the HUD Modes-popover "Time Attack" row triggers this.
|
||||||
|
let button_clicked = requests.read().count() > 0;
|
||||||
|
if !keys.just_pressed(KeyCode::KeyT) && !button_clicked {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if progress.0.level < CHALLENGE_UNLOCK_LEVEL {
|
if progress.0.level < CHALLENGE_UNLOCK_LEVEL {
|
||||||
|
|||||||
Reference in New Issue
Block a user