feat(engine): add Undo, Pause, Help UI buttons in HUD action bar

Continues the UI-first pass started by the New Game button. Per the
design principle in CLAUDE.md / ARCHITECTURE.md §1, every player action
must be reachable from a visible UI control with the keyboard shortcut
as an optional accelerator. Refactor the single New Game button into a
flex-row "action bar" anchored top-right with four buttons: Undo,
Pause, Help, New Game (left → right; New Game rightmost as the most
consequential action).

Plumbing:
- New `PauseRequestEvent` and `HelpRequestEvent` in events.rs.
- pause_plugin::toggle_pause reads either Esc or PauseRequestEvent so
  the button and the keyboard accelerator drive the same code path
  (with the existing drag / game-over / selection guards).
- help_plugin::toggle_help_screen reads either F1 or HelpRequestEvent;
  also fix the stale module-doc claim that H toggles help (it's F1 —
  H is bound to hint cycle in input_plugin).
- hud_plugin now spawns four ActionButton-marked buttons via a
  ChildSpawnerCommands helper, with one click handler per button
  firing its respective request event. A single
  paint_action_buttons system covers hover/pressed colour for all of
  them via the shared ActionButton marker. The click handlers
  defensively re-register their request events so the plugin works in
  isolation under MinimalPlugins (tests). add_message is idempotent.
- ARCHITECTURE.md HudPlugin row updated to call out the action bar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
funman300
2026-04-29 23:38:54 +00:00
parent 62cd1cf924
commit 97f38085e3
6 changed files with 184 additions and 53 deletions
+8 -2
View File
@@ -19,7 +19,7 @@ use bevy::prelude::*;
use solitaire_core::game_state::DrawMode;
use solitaire_data::save_game_state_to;
use crate::events::StateChangedEvent;
use crate::events::{PauseRequestEvent, StateChangedEvent};
use crate::game_plugin::{GameOverScreen, GameStatePath};
use crate::progress_plugin::ProgressResource;
use crate::resources::{DragState, GameStateResource};
@@ -58,6 +58,7 @@ impl Plugin for PausePlugin {
// events first, but calling add_event again is always safe.
app.add_message::<SettingsChangedEvent>()
.add_message::<StateChangedEvent>()
.add_message::<PauseRequestEvent>()
.init_resource::<PausedResource>()
.add_systems(
Update,
@@ -75,6 +76,7 @@ impl Plugin for PausePlugin {
fn toggle_pause(
mut commands: Commands,
keys: Res<ButtonInput<KeyCode>>,
mut requests: MessageReader<PauseRequestEvent>,
mut paused: ResMut<PausedResource>,
screens: Query<Entity, With<PauseScreen>>,
game_over_screens: Query<Entity, With<GameOverScreen>>,
@@ -87,7 +89,11 @@ fn toggle_pause(
mut changed: MessageWriter<StateChangedEvent>,
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;
}
// If a card is currently selected, let SelectionPlugin handle this Escape