feat(engine): convert HelpScreen to real-button modal with kbd-chip rows

Phase 3 step 5a of the UX overhaul. Replaces the old monospace
text-dump (a flat vertical column of "  D            Draw from
stock"-style lines) with a proper modal layout: section titles,
two-column rows where each shortcut renders inside a small
border-outlined chip alongside its description.

Modal contents:
- Header: "Controls"
- Body: three sections (Gameplay / New Game / Overlays), each with a
  section title in TEXT_SECONDARY plus a row per shortcut.
- Each row: a 64 px-min-width chip (caption font, border, radius-sm)
  carrying the key name, then the description in TEXT_PRIMARY at
  TYPE_BODY.
- Actions: a primary "Close" button (hotkey hint "F1").

CONTROL_SECTIONS is a static const-data table of `ControlRow`
records grouped into `ControlSection`s — easier to maintain than the
prior `Vec<String>` of free-form text and easier to extend.

handle_help_close_button is the click counterpart to F1; it
despawns the modal when the player clicks Close.

The audit identified the prior layout as the worst of the
"feels like a 2010 monospace debug dump" overlays. This
restructure is the largest visual upgrade so far in the overhaul.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
funman300
2026-04-30 01:06:00 +00:00
parent 242b5fef21
commit deb034c5fb
+157 -59
View File
@@ -7,11 +7,23 @@
use bevy::prelude::*;
use crate::events::HelpRequestEvent;
use crate::font_plugin::FontResource;
use crate::ui_modal::{
spawn_modal, spawn_modal_actions, spawn_modal_button, spawn_modal_header, ButtonVariant,
};
use crate::ui_theme::{
Z_MODAL_PANEL, BORDER_SUBTLE, RADIUS_SM, SPACE_2, TEXT_PRIMARY, TEXT_SECONDARY, TYPE_BODY,
TYPE_CAPTION, VAL_SPACE_1, VAL_SPACE_2, VAL_SPACE_3,
};
/// Marker on the help overlay root node.
#[derive(Component, Debug)]
pub struct HelpScreen;
/// Marker on the "Close" button inside the Help modal.
#[derive(Component, Debug)]
pub struct HelpCloseButton;
/// Spawns and despawns the help / controls overlay shown when the player
/// clicks the "Help" HUD button or presses `F1`. All hotkeys and gesture
/// guides live here.
@@ -20,7 +32,7 @@ pub struct HelpPlugin;
impl Plugin for HelpPlugin {
fn build(&self, app: &mut App) {
app.add_message::<HelpRequestEvent>()
.add_systems(Update, toggle_help_screen);
.add_systems(Update, (toggle_help_screen, handle_help_close_button));
}
}
@@ -29,6 +41,7 @@ fn toggle_help_screen(
keys: Res<ButtonInput<KeyCode>>,
mut requests: MessageReader<HelpRequestEvent>,
screens: Query<Entity, With<HelpScreen>>,
font_res: Option<Res<FontResource>>,
) {
// Either F1 or a click on the HUD "Help" button (which fires
// HelpRequestEvent) toggles the overlay.
@@ -39,70 +52,155 @@ fn toggle_help_screen(
if let Ok(entity) = screens.single() {
commands.entity(entity).despawn();
} else {
spawn_help_screen(&mut commands);
spawn_help_screen(&mut commands, font_res.as_deref());
}
}
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(),
];
/// Click handler for the modal's "Close" button. F1 toggles the overlay
/// the same way; this just exposes the close action to mouse / touch.
fn handle_help_close_button(
mut commands: Commands,
close_buttons: Query<&Interaction, (With<HelpCloseButton>, Changed<Interaction>)>,
screens: Query<Entity, With<HelpScreen>>,
) {
if !close_buttons.iter().any(|i| *i == Interaction::Pressed) {
return;
}
for entity in &screens {
commands.entity(entity).despawn();
}
}
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)),
));
/// Each entry in the controls reference table.
struct ControlRow {
keys: &'static str,
description: &'static str,
}
/// Each section of the controls reference. Sections render with a
/// section title and a vertically stacked list of `ControlRow`s.
struct ControlSection {
title: &'static str,
rows: &'static [ControlRow],
}
const CONTROL_SECTIONS: &[ControlSection] = &[
ControlSection {
title: "Gameplay",
rows: &[
ControlRow { keys: "Drag", description: "Move cards between piles" },
ControlRow { keys: "D / Space", description: "Draw from stock" },
ControlRow { keys: "U", description: "Undo last move" },
ControlRow { keys: "Click stock", description: "Draw" },
],
},
ControlSection {
title: "New Game",
rows: &[
ControlRow { keys: "N", description: "New Classic game (N twice if in progress)" },
ControlRow { keys: "C", description: "Start today's daily challenge" },
ControlRow { keys: "Z", description: "Start a Zen game (level 5+)" },
ControlRow { keys: "X", description: "Start the next Challenge (level 5+)" },
ControlRow { keys: "T", description: "Start a Time Attack session (level 5+)" },
],
},
ControlSection {
title: "Overlays",
rows: &[
ControlRow { keys: "S", description: "Stats & progression" },
ControlRow { keys: "A", description: "Achievements" },
ControlRow { keys: "L", description: "Leaderboard" },
ControlRow { keys: "O", description: "Settings" },
ControlRow { keys: "F1", description: "This help screen" },
ControlRow { keys: "F11", description: "Toggle fullscreen" },
ControlRow { keys: "Esc", description: "Pause / resume" },
ControlRow { keys: "[ / ]", description: "SFX volume down / up" },
],
},
];
fn spawn_help_screen(commands: &mut Commands, font_res: Option<&FontResource>) {
let font_handle = font_res.map(|f| f.0.clone()).unwrap_or_default();
let font_section = TextFont {
font: font_handle.clone(),
font_size: TYPE_BODY,
..default()
};
let font_row = font_section.clone();
let font_kbd = TextFont {
font: font_handle,
font_size: TYPE_CAPTION,
..default()
};
spawn_modal(commands, HelpScreen, Z_MODAL_PANEL, |card| {
spawn_modal_header(card, "Controls", font_res);
for section in CONTROL_SECTIONS {
// Section title in muted text — distinguishes from row content.
card.spawn((
Text::new(section.title),
font_section.clone(),
TextColor(TEXT_SECONDARY),
));
// Each row is a flex-row: kbd-style chip + description.
for row in section.rows {
card.spawn(Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
column_gap: VAL_SPACE_3,
..default()
})
.with_children(|line| {
// The hotkey rendered as a small chip with a border —
// visual cue that it's a key reference, not part of
// the description text.
line.spawn((
Node {
padding: UiRect::axes(VAL_SPACE_2, VAL_SPACE_1),
min_width: Val::Px(64.0),
justify_content: JustifyContent::Center,
border: UiRect::all(Val::Px(1.0)),
border_radius: BorderRadius::all(Val::Px(RADIUS_SM)),
..default()
},
BorderColor::all(BORDER_SUBTLE),
))
.with_children(|chip| {
chip.spawn((
Text::new(row.keys),
font_kbd.clone(),
TextColor(TEXT_PRIMARY),
));
});
line.spawn((
Text::new(row.description),
font_row.clone(),
TextColor(TEXT_PRIMARY),
));
});
}
// Section spacer — small empty box. Keeps each section
// visually grouped.
card.spawn(Node {
height: Val::Px(SPACE_2),
..default()
});
}
spawn_modal_actions(card, |actions| {
spawn_modal_button(
actions,
HelpCloseButton,
"Close",
Some("F1"),
ButtonVariant::Primary,
font_res,
);
});
});
}
#[cfg(test)]