feat(engine): add HelpPlugin (H/?) and Challenge cleared toast
- HelpPlugin: full-window cheat sheet listing every keybinding, toggled with H or ?. Three unit tests cover open/close/slash. - AnimationPlugin: ChallengeAdvancedEvent now surfaces as a 3-second "Challenge N cleared!" toast. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+10
-6
@@ -2,7 +2,7 @@
|
||||
|
||||
> Last updated: 2026-04-25
|
||||
> Branch: `master` — pushed to https://git.aleshym.co/funman300/Rusty_Solitare.git
|
||||
> Test count: **222 passing** (83 core + 54 data + 85 engine), `cargo clippy --workspace -- -D warnings` clean
|
||||
> Test count: **225 passing** (83 core + 54 data + 88 engine), `cargo clippy --workspace -- -D warnings` clean
|
||||
|
||||
---
|
||||
|
||||
@@ -148,14 +148,18 @@ All sub-phases (3A–3F) done. Plugins: `GamePlugin`, `TablePlugin`, `CardPlugin
|
||||
- `StatsPlugin` overlay (**S**) appends an "Unlocks" subsection (card backs / backgrounds, sorted/deduped, "None" when empty) and a live "Time Attack" panel showing remaining minutes/seconds + wins while a session is active.
|
||||
- Helper `format_id_list` factored out + tested.
|
||||
|
||||
### Phase 7 (part 1) — Help Overlay + Challenge Toast ✅ COMPLETE
|
||||
|
||||
- `HelpPlugin`: **H** or `?` toggles a full-window cheat sheet listing all keybindings (gameplay, mode hotkeys, overlays). 3 unit tests.
|
||||
- `AnimationPlugin` now surfaces `ChallengeAdvancedEvent` as a 3-second toast ("Challenge N cleared!").
|
||||
|
||||
## What Is Next
|
||||
|
||||
### Phase 7 — Audio + Polish
|
||||
### Phase 7 (part 2+) — Audio + Pause Menu
|
||||
|
||||
- Audio (`kira`): card deal/flip/place/invalid SFX, win fanfare, ambient loop. Volume sliders in a Settings overlay.
|
||||
- Onboarding: first-run hint overlay (rules summary + key list).
|
||||
- Pause menu (Esc currently logs a placeholder).
|
||||
- Optional: ChallengeAdvancedEvent → toast in `AnimationPlugin`.
|
||||
- Audio (`kira`): card deal/flip/place/invalid SFX, win fanfare, ambient loop. Volume sliders in a Settings overlay. **Blocker:** asset files are not yet in the repo; sourcing/recording these is the first step.
|
||||
- Pause menu: Esc currently logs a placeholder. Likely a small overlay similar to `HelpPlugin` with a `Paused` resource that gates `Time::delta_secs` propagation in `tick_elapsed_time` / `advance_time_attack`.
|
||||
- Onboarding: first-run banner pointing at the **H**/`?` cheat sheet (single-shot via `Settings.first_run_complete`).
|
||||
|
||||
### Phase 8 — Sync
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use bevy::prelude::*;
|
||||
use solitaire_engine::{
|
||||
AchievementPlugin, AnimationPlugin, CardPlugin, ChallengePlugin, DailyChallengePlugin,
|
||||
GamePlugin, InputPlugin, ProgressPlugin, StatsPlugin, TablePlugin, TimeAttackPlugin,
|
||||
GamePlugin, HelpPlugin, InputPlugin, ProgressPlugin, StatsPlugin, TablePlugin, TimeAttackPlugin,
|
||||
WeeklyGoalsPlugin,
|
||||
};
|
||||
|
||||
@@ -29,5 +29,6 @@ fn main() {
|
||||
.add_plugins(WeeklyGoalsPlugin)
|
||||
.add_plugins(ChallengePlugin)
|
||||
.add_plugins(TimeAttackPlugin)
|
||||
.add_plugins(HelpPlugin)
|
||||
.run();
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use bevy::prelude::*;
|
||||
|
||||
use crate::achievement_plugin::display_name_for;
|
||||
use crate::card_plugin::CardEntity;
|
||||
use crate::challenge_plugin::ChallengeAdvancedEvent;
|
||||
use crate::daily_challenge_plugin::DailyChallengeCompletedEvent;
|
||||
use crate::events::{AchievementUnlockedEvent, GameWonEvent};
|
||||
use crate::game_plugin::GameMutation;
|
||||
@@ -24,6 +25,7 @@ const LEVELUP_TOAST_SECS: f32 = 3.0;
|
||||
const DAILY_TOAST_SECS: f32 = 3.0;
|
||||
const WEEKLY_TOAST_SECS: f32 = 3.0;
|
||||
const TIME_ATTACK_TOAST_SECS: f32 = 5.0;
|
||||
const CHALLENGE_TOAST_SECS: f32 = 3.0;
|
||||
const CASCADE_STAGGER: f32 = 0.05;
|
||||
const CASCADE_DURATION: f32 = 0.5;
|
||||
|
||||
@@ -62,6 +64,7 @@ impl Plugin for AnimationPlugin {
|
||||
.add_event::<DailyChallengeCompletedEvent>()
|
||||
.add_event::<WeeklyGoalCompletedEvent>()
|
||||
.add_event::<TimeAttackEndedEvent>()
|
||||
.add_event::<ChallengeAdvancedEvent>()
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
@@ -72,6 +75,7 @@ impl Plugin for AnimationPlugin {
|
||||
handle_daily_toast,
|
||||
handle_weekly_toast,
|
||||
handle_time_attack_toast,
|
||||
handle_challenge_toast,
|
||||
tick_toasts,
|
||||
)
|
||||
.after(GameMutation),
|
||||
@@ -198,6 +202,19 @@ fn handle_time_attack_toast(
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_challenge_toast(
|
||||
mut commands: Commands,
|
||||
mut events: EventReader<ChallengeAdvancedEvent>,
|
||||
) {
|
||||
for ev in events.read() {
|
||||
spawn_toast(
|
||||
&mut commands,
|
||||
format!("Challenge {} cleared!", ev.previous_index.saturating_add(1)),
|
||||
CHALLENGE_TOAST_SECS,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn tick_toasts(
|
||||
mut commands: Commands,
|
||||
time: Res<Time>,
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
//! Toggleable on-screen help / cheat sheet showing keyboard bindings.
|
||||
//!
|
||||
//! Press **H** (or `?`) to toggle. Listed shortcuts are grouped by intent —
|
||||
//! gameplay, modes, and overlays.
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
/// Marker on the help overlay root node.
|
||||
#[derive(Component, Debug)]
|
||||
pub struct HelpScreen;
|
||||
|
||||
pub struct HelpPlugin;
|
||||
|
||||
impl Plugin for HelpPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(Update, toggle_help_screen);
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_help_screen(
|
||||
mut commands: Commands,
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
screens: Query<Entity, With<HelpScreen>>,
|
||||
) {
|
||||
let pressed_help = keys.just_pressed(KeyCode::KeyH) || keys.just_pressed(KeyCode::Slash);
|
||||
if !pressed_help {
|
||||
return;
|
||||
}
|
||||
if let Ok(entity) = screens.get_single() {
|
||||
commands.entity(entity).despawn_recursive();
|
||||
} else {
|
||||
spawn_help_screen(&mut commands);
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_help_screen(commands: &mut Commands) {
|
||||
let lines: Vec<String> = vec![
|
||||
"=== Controls ===".to_string(),
|
||||
String::new(),
|
||||
"-- Gameplay --".to_string(),
|
||||
" D Draw from stock".to_string(),
|
||||
" U Undo last move".to_string(),
|
||||
" Drag Move cards between piles".to_string(),
|
||||
" Click stock Draw".to_string(),
|
||||
String::new(),
|
||||
"-- New Game --".to_string(),
|
||||
" N New Classic game".to_string(),
|
||||
" C Start today's daily challenge".to_string(),
|
||||
" Z Start a Zen game (level 5+)".to_string(),
|
||||
" X Start the next Challenge (level 5+)".to_string(),
|
||||
" T Start a Time Attack session (level 5+)".to_string(),
|
||||
String::new(),
|
||||
"-- Overlays --".to_string(),
|
||||
" S Toggle stats / progression".to_string(),
|
||||
" H or ? Toggle this help".to_string(),
|
||||
" Esc Pause (placeholder)".to_string(),
|
||||
String::new(),
|
||||
"Press H or ? to close".to_string(),
|
||||
];
|
||||
|
||||
commands
|
||||
.spawn((
|
||||
HelpScreen,
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
left: Val::Percent(0.0),
|
||||
top: Val::Percent(0.0),
|
||||
width: Val::Percent(100.0),
|
||||
height: Val::Percent(100.0),
|
||||
flex_direction: FlexDirection::Column,
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Center,
|
||||
row_gap: Val::Px(4.0),
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.88)),
|
||||
ZIndex(210),
|
||||
))
|
||||
.with_children(|b| {
|
||||
for line in lines {
|
||||
b.spawn((
|
||||
Text::new(line),
|
||||
TextFont {
|
||||
font_size: 22.0,
|
||||
..default()
|
||||
},
|
||||
TextColor(Color::srgb(0.95, 0.95, 0.90)),
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn headless_app() -> App {
|
||||
let mut app = App::new();
|
||||
app.add_plugins(MinimalPlugins).add_plugins(HelpPlugin);
|
||||
app.init_resource::<ButtonInput<KeyCode>>();
|
||||
app.update();
|
||||
app
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pressing_h_spawns_help_screen() {
|
||||
let mut app = headless_app();
|
||||
app.world_mut()
|
||||
.resource_mut::<ButtonInput<KeyCode>>()
|
||||
.press(KeyCode::KeyH);
|
||||
app.update();
|
||||
|
||||
assert_eq!(
|
||||
app.world_mut()
|
||||
.query::<&HelpScreen>()
|
||||
.iter(app.world())
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pressing_h_twice_closes_help_screen() {
|
||||
let mut app = headless_app();
|
||||
app.world_mut()
|
||||
.resource_mut::<ButtonInput<KeyCode>>()
|
||||
.press(KeyCode::KeyH);
|
||||
app.update();
|
||||
|
||||
{
|
||||
let mut input = app.world_mut().resource_mut::<ButtonInput<KeyCode>>();
|
||||
input.release(KeyCode::KeyH);
|
||||
input.clear();
|
||||
input.press(KeyCode::KeyH);
|
||||
}
|
||||
app.update();
|
||||
|
||||
assert_eq!(
|
||||
app.world_mut()
|
||||
.query::<&HelpScreen>()
|
||||
.iter(app.world())
|
||||
.count(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pressing_slash_also_toggles_help() {
|
||||
let mut app = headless_app();
|
||||
app.world_mut()
|
||||
.resource_mut::<ButtonInput<KeyCode>>()
|
||||
.press(KeyCode::Slash);
|
||||
app.update();
|
||||
|
||||
assert_eq!(
|
||||
app.world_mut()
|
||||
.query::<&HelpScreen>()
|
||||
.iter(app.world())
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ pub mod challenge_plugin;
|
||||
pub mod daily_challenge_plugin;
|
||||
pub mod events;
|
||||
pub mod game_plugin;
|
||||
pub mod help_plugin;
|
||||
pub mod input_plugin;
|
||||
pub mod layout;
|
||||
pub mod progress_plugin;
|
||||
@@ -32,6 +33,7 @@ pub use events::{
|
||||
NewGameRequestEvent, StateChangedEvent, UndoRequestEvent,
|
||||
};
|
||||
pub use game_plugin::{GameMutation, GamePlugin};
|
||||
pub use help_plugin::{HelpPlugin, HelpScreen};
|
||||
pub use input_plugin::InputPlugin;
|
||||
pub use layout::{compute_layout, Layout, LayoutResource};
|
||||
pub use resources::{DragState, GameStateResource, SyncStatus, SyncStatusResource};
|
||||
|
||||
Reference in New Issue
Block a user