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:
funman300
2026-04-25 22:39:08 -07:00
parent 193410200e
commit 7dfbff45d1
5 changed files with 194 additions and 7 deletions
+10 -6
View File
@@ -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 (3A3F) 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
+2 -1
View File
@@ -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();
}
+17
View File
@@ -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>,
+163
View File
@@ -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
);
}
}
+2
View File
@@ -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};