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:
@@ -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,69 +52,154 @@ 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();
|
||||
}
|
||||
}
|
||||
|
||||
/// 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" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
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),
|
||||
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()
|
||||
},
|
||||
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,
|
||||
};
|
||||
let font_row = font_section.clone();
|
||||
let font_kbd = TextFont {
|
||||
font: font_handle,
|
||||
font_size: TYPE_CAPTION,
|
||||
..default()
|
||||
},
|
||||
TextColor(Color::srgb(0.95, 0.95, 0.90)),
|
||||
};
|
||||
|
||||
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,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user