Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| adece12cf1 | |||
| 2cfbc32715 |
+4
-1
@@ -51,6 +51,7 @@ Solitaire Quest is a cross-platform Klondike Solitaire game written in Rust, tar
|
|||||||
- **No panics in game logic.** Every state transition returns `Result<_, MoveError>`. Panics are only acceptable in startup/configuration code.
|
- **No panics in game logic.** Every state transition returns `Result<_, MoveError>`. Panics are only acceptable in startup/configuration code.
|
||||||
- **One language, one repo.** The game client, sync client, shared types, and sync server are all Rust crates in a single Cargo workspace.
|
- **One language, one repo.** The game client, sync client, shared types, and sync server are all Rust crates in a single Cargo workspace.
|
||||||
- **Plugin-based Bevy architecture.** Each major feature is a Bevy `Plugin`. Systems are small and single-purpose. Cross-system communication uses Bevy `Event`s.
|
- **Plugin-based Bevy architecture.** Each major feature is a Bevy `Plugin`. Systems are small and single-purpose. Cross-system communication uses Bevy `Event`s.
|
||||||
|
- **UI-first interaction.** Every player-triggered action — new game, undo, draw, pause, open stats / settings / help / profile / leaderboard, etc. — must be reachable from a visible UI control. Keyboard shortcuts exist only as optional accelerators for power users; they are never the sole entry point. A player using only mouse or touch must be able to perform every action. New gameplay features ship with the UI control alongside the system that backs it.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -235,7 +236,9 @@ Done
|
|||||||
|
|
||||||
### Bevy Plugins
|
### Bevy Plugins
|
||||||
|
|
||||||
| Plugin | Key | Responsibility |
|
The "Shortcut" column lists optional keyboard accelerators. Every action in this table must also be reachable from a visible UI control (button, menu item, on-screen affordance) per the UI-first design principle in §1; the shortcut is a power-user convenience, not the sole entry point.
|
||||||
|
|
||||||
|
| Plugin | Shortcut | Responsibility |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `CardPlugin` | — | Card entity spawning, sprite management, drag-and-drop |
|
| `CardPlugin` | — | Card entity spawning, sprite management, drag-and-drop |
|
||||||
| `TablePlugin` | — | Pile markers, background, layout calculation |
|
| `TablePlugin` | — | Pile markers, background, layout calculation |
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ cargo clippy -p solitaire_core -- -D warnings
|
|||||||
- Resources own shared state. Events communicate between systems. Components own per-entity data.
|
- Resources own shared state. Events communicate between systems. Components own per-entity data.
|
||||||
- All UI screens are built with Bevy UI (`bevy::ui`). Never mix UI layout and game logic in the same system.
|
- All UI screens are built with Bevy UI (`bevy::ui`). Never mix UI layout and game logic in the same system.
|
||||||
- Layout is recomputed on `WindowResized` — never assume a fixed window size.
|
- Layout is recomputed on `WindowResized` — never assume a fixed window size.
|
||||||
|
- **UI-first.** Every player-triggered action (new game, undo, draw, pause, open stats / settings / help / profile / leaderboard, switch mode, etc.) must be reachable from a visible UI control. Keyboard shortcuts are optional accelerators — never the sole entry point. New gameplay features ship with the UI control alongside the system that backs it; do not merge a feature that is keyboard-only.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use solitaire_core::pile::PileType;
|
|||||||
|
|
||||||
use crate::auto_complete_plugin::AutoCompleteState;
|
use crate::auto_complete_plugin::AutoCompleteState;
|
||||||
use crate::daily_challenge_plugin::DailyChallengeResource;
|
use crate::daily_challenge_plugin::DailyChallengeResource;
|
||||||
use crate::events::InfoToastEvent;
|
use crate::events::{InfoToastEvent, NewGameRequestEvent};
|
||||||
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,18 +84,30 @@ 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.
|
||||||
|
/// Click fires [`NewGameRequestEvent`]; the existing `ConfirmNewGameScreen`
|
||||||
|
/// modal then handles confirmation when a game is in progress.
|
||||||
|
#[derive(Component, Debug)]
|
||||||
|
pub struct NewGameButton;
|
||||||
|
|
||||||
/// 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.
|
||||||
|
const NEW_GAME_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 NEW_GAME_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)
|
app.add_systems(Startup, (spawn_hud, spawn_new_game_button))
|
||||||
.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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,6 +186,80 @@ fn spawn_hud(font_res: Option<Res<FontResource>>, mut commands: Commands) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Spawns the New Game action button anchored to the top-right of the
|
||||||
|
/// window. Click fires [`NewGameRequestEvent`]; the existing
|
||||||
|
/// `ConfirmNewGameScreen` modal in `GamePlugin` handles confirmation when
|
||||||
|
/// a game is in progress, and starts a fresh deal otherwise.
|
||||||
|
///
|
||||||
|
/// Per the UI-first principle (CLAUDE.md / ARCHITECTURE.md §1), this
|
||||||
|
/// button is the primary entry point for starting a new game. The `N`
|
||||||
|
/// keyboard shortcut is an optional accelerator.
|
||||||
|
fn spawn_new_game_button(font_res: Option<Res<FontResource>>, mut commands: Commands) {
|
||||||
|
let font = TextFont {
|
||||||
|
font: font_res.as_ref().map(|f| f.0.clone()).unwrap_or_default(),
|
||||||
|
font_size: 16.0,
|
||||||
|
..default()
|
||||||
|
};
|
||||||
|
commands
|
||||||
|
.spawn((
|
||||||
|
NewGameButton,
|
||||||
|
Button,
|
||||||
|
Node {
|
||||||
|
position_type: PositionType::Absolute,
|
||||||
|
right: Val::Px(12.0),
|
||||||
|
top: Val::Px(8.0),
|
||||||
|
padding: UiRect::axes(Val::Px(14.0), Val::Px(8.0)),
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
border_radius: BorderRadius::all(Val::Px(6.0)),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(NEW_GAME_BTN_IDLE),
|
||||||
|
ZIndex(Z_HUD),
|
||||||
|
))
|
||||||
|
.with_children(|b| {
|
||||||
|
b.spawn((
|
||||||
|
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
|
||||||
|
/// interaction state transitions, avoiding repeat events while the button
|
||||||
|
/// is held down.
|
||||||
|
fn handle_new_game_button(
|
||||||
|
interaction_query: Query<&Interaction, (With<NewGameButton>, Changed<Interaction>)>,
|
||||||
|
mut new_game: MessageWriter<NewGameRequestEvent>,
|
||||||
|
) {
|
||||||
|
for interaction in &interaction_query {
|
||||||
|
if *interaction == Interaction::Pressed {
|
||||||
|
new_game.write(NewGameRequestEvent::default());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Visual feedback for the New Game button — paints idle / hover / pressed
|
||||||
|
/// states by mutating the `BackgroundColor` whenever the interaction state
|
||||||
|
/// changes.
|
||||||
|
fn paint_new_game_button(
|
||||||
|
mut buttons: Query<
|
||||||
|
(&Interaction, &mut BackgroundColor),
|
||||||
|
(With<NewGameButton>, Changed<Interaction>),
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
for (interaction, mut bg) in &mut buttons {
|
||||||
|
bg.0 = match interaction {
|
||||||
|
Interaction::Pressed => NEW_GAME_BTN_PRESSED,
|
||||||
|
Interaction::Hovered => NEW_GAME_BTN_HOVER,
|
||||||
|
Interaction::None => NEW_GAME_BTN_IDLE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Formats a time-limit value in seconds as `"mm:ss"` for HUD display.
|
/// Formats a time-limit value in seconds as `"mm:ss"` for HUD display.
|
||||||
///
|
///
|
||||||
/// For example `format_time_limit(300)` returns `"5:00"`.
|
/// For example `format_time_limit(300)` returns `"5:00"`.
|
||||||
|
|||||||
Reference in New Issue
Block a user