Files
Ferrous-Solitaire/solitaire_engine/src/help_plugin.rs
T
funman300 ffc79447d4 fix+refactor+docs: P0–P3 todo list items
P0 fixes:
- Register WinSummaryPlugin, SelectionPlugin, CardAnimationPlugin in main.rs
  (all three were exported but never wired — features silently did nothing)
- game_state::draw(): increment move_count on waste→stock recycle, not just
  on normal draws; add move_count_increments_on_recycle regression test

P1 fixes:
- solitaire_server/Cargo.toml: remove duplicate dev-dependencies
  (solitaire_sync, uuid, chrono, jsonwebtoken were in both sections)

P2 — input_plugin refactor:
- Split 198-line handle_keyboard() into three focused systems under 110 lines each:
  handle_keyboard_core (U/N/Z/D/Space), handle_keyboard_hint (H), handle_keyboard_forfeit (G)
- Introduce KeyboardConfirmState resource to share countdown timers across systems
- Add three new unit tests: all_hints_suggests_draw_*, all_hints_is_empty_when_truly_stuck,
  new_game_confirm_window_is_positive

P2 — achievement predicate tests (solitaire_core):
- Add 10 direct unit tests for speed_demon, lightning, no_undo, high_scorer,
  on_a_roll, comeback predicates (previously only covered via check_achievements())
- 141 core tests now passing

P2 — server tests:
- solitaire_server/src/sync.rs: 4 unit tests for merge logic (no DB required)
- solitaire_server/src/leaderboard.rs: 2 unit tests for entry shape and sort order

P3 — documentation:
- Add struct-level ///  to 12 Plugin structs (ChallengePlugin, CursorPlugin,
  AnimationPlugin, HelpPlugin, PausePlugin, AudioPlugin, DailyChallengePlugin,
  HudPlugin, LeaderboardPlugin, OnboardingPlugin, TimeAttackPlugin, WeeklyGoalsPlugin)
- Add field-level /// to Card, Pile, Deck, GameState, AchievementContext, AchievementDef
- Add /// to WeeklyGoalKind, WeeklyGoalDef, WeeklyGoalContext, StatsExt::update_on_win

card_animation module (new files from previous session):
- chain.rs, diagnostics.rs, tuning.rs, updated interaction.rs/animation.rs/mod.rs/lib.rs
- Remove unused HOVER_SCALE_DEFAULT / DRAG_LIFT_SCALE_DEFAULT / HOVER_LERP_SPEED_DEFAULT constants
- Add handle_touch_stock_tap so touch users can draw from the stock pile

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 22:02:52 +00:00

153 lines
4.6 KiB
Rust

//! Toggleable on-screen help / cheat sheet showing keyboard bindings.
//!
//! Press **F1** 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;
/// Spawns and despawns the help/controls overlay shown when the player presses H (or the help button).
/// All hotkeys and gesture guides live here.
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>>,
) {
if !keys.just_pressed(KeyCode::F1) {
return;
}
if let Ok(entity) = screens.single() {
commands.entity(entity).despawn();
} 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 (N twice if in progress)".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 Stats & progression".to_string(),
" A Achievements".to_string(),
" L Leaderboard".to_string(),
" O Settings".to_string(),
" F1 This help screen".to_string(),
" F11 Toggle fullscreen".to_string(),
" Esc Pause / resume".to_string(),
" [ / ] SFX volume down / up".to_string(),
String::new(),
"Press F1 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_f1_spawns_help_screen() {
let mut app = headless_app();
app.world_mut()
.resource_mut::<ButtonInput<KeyCode>>()
.press(KeyCode::F1);
app.update();
assert_eq!(
app.world_mut()
.query::<&HelpScreen>()
.iter(app.world())
.count(),
1
);
}
#[test]
fn pressing_f1_twice_closes_help_screen() {
let mut app = headless_app();
app.world_mut()
.resource_mut::<ButtonInput<KeyCode>>()
.press(KeyCode::F1);
app.update();
{
let mut input = app.world_mut().resource_mut::<ButtonInput<KeyCode>>();
input.release(KeyCode::F1);
input.clear();
input.press(KeyCode::F1);
}
app.update();
assert_eq!(
app.world_mut()
.query::<&HelpScreen>()
.iter(app.world())
.count(),
0
);
}
}