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 bevy::prelude::*;
|
||||||
|
|
||||||
use crate::events::HelpRequestEvent;
|
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.
|
/// Marker on the help overlay root node.
|
||||||
#[derive(Component, Debug)]
|
#[derive(Component, Debug)]
|
||||||
pub struct HelpScreen;
|
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
|
/// Spawns and despawns the help / controls overlay shown when the player
|
||||||
/// clicks the "Help" HUD button or presses `F1`. All hotkeys and gesture
|
/// clicks the "Help" HUD button or presses `F1`. All hotkeys and gesture
|
||||||
/// guides live here.
|
/// guides live here.
|
||||||
@@ -20,7 +32,7 @@ pub struct HelpPlugin;
|
|||||||
impl Plugin for HelpPlugin {
|
impl Plugin for HelpPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.add_message::<HelpRequestEvent>()
|
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>>,
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
mut requests: MessageReader<HelpRequestEvent>,
|
mut requests: MessageReader<HelpRequestEvent>,
|
||||||
screens: Query<Entity, With<HelpScreen>>,
|
screens: Query<Entity, With<HelpScreen>>,
|
||||||
|
font_res: Option<Res<FontResource>>,
|
||||||
) {
|
) {
|
||||||
// Either F1 or a click on the HUD "Help" button (which fires
|
// Either F1 or a click on the HUD "Help" button (which fires
|
||||||
// HelpRequestEvent) toggles the overlay.
|
// HelpRequestEvent) toggles the overlay.
|
||||||
@@ -39,70 +52,155 @@ fn toggle_help_screen(
|
|||||||
if let Ok(entity) = screens.single() {
|
if let Ok(entity) = screens.single() {
|
||||||
commands.entity(entity).despawn();
|
commands.entity(entity).despawn();
|
||||||
} else {
|
} else {
|
||||||
spawn_help_screen(&mut commands);
|
spawn_help_screen(&mut commands, font_res.as_deref());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_help_screen(commands: &mut Commands) {
|
/// Click handler for the modal's "Close" button. F1 toggles the overlay
|
||||||
let lines: Vec<String> = vec![
|
/// the same way; this just exposes the close action to mouse / touch.
|
||||||
"=== Controls ===".to_string(),
|
fn handle_help_close_button(
|
||||||
String::new(),
|
mut commands: Commands,
|
||||||
"-- Gameplay --".to_string(),
|
close_buttons: Query<&Interaction, (With<HelpCloseButton>, Changed<Interaction>)>,
|
||||||
" D Draw from stock".to_string(),
|
screens: Query<Entity, With<HelpScreen>>,
|
||||||
" U Undo last move".to_string(),
|
) {
|
||||||
" Drag Move cards between piles".to_string(),
|
if !close_buttons.iter().any(|i| *i == Interaction::Pressed) {
|
||||||
" Click stock Draw".to_string(),
|
return;
|
||||||
String::new(),
|
}
|
||||||
"-- New Game --".to_string(),
|
for entity in &screens {
|
||||||
" N New Classic game (N twice if in progress)".to_string(),
|
commands.entity(entity).despawn();
|
||||||
" 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
|
/// Each entry in the controls reference table.
|
||||||
.spawn((
|
struct ControlRow {
|
||||||
HelpScreen,
|
keys: &'static str,
|
||||||
Node {
|
description: &'static str,
|
||||||
position_type: PositionType::Absolute,
|
}
|
||||||
left: Val::Percent(0.0),
|
|
||||||
top: Val::Percent(0.0),
|
/// Each section of the controls reference. Sections render with a
|
||||||
width: Val::Percent(100.0),
|
/// section title and a vertically stacked list of `ControlRow`s.
|
||||||
height: Val::Percent(100.0),
|
struct ControlSection {
|
||||||
flex_direction: FlexDirection::Column,
|
title: &'static str,
|
||||||
justify_content: JustifyContent::Center,
|
rows: &'static [ControlRow],
|
||||||
align_items: AlignItems::Center,
|
}
|
||||||
row_gap: Val::Px(4.0),
|
|
||||||
..default()
|
const CONTROL_SECTIONS: &[ControlSection] = &[
|
||||||
},
|
ControlSection {
|
||||||
BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.88)),
|
title: "Gameplay",
|
||||||
ZIndex(210),
|
rows: &[
|
||||||
))
|
ControlRow { keys: "Drag", description: "Move cards between piles" },
|
||||||
.with_children(|b| {
|
ControlRow { keys: "D / Space", description: "Draw from stock" },
|
||||||
for line in lines {
|
ControlRow { keys: "U", description: "Undo last move" },
|
||||||
b.spawn((
|
ControlRow { keys: "Click stock", description: "Draw" },
|
||||||
Text::new(line),
|
],
|
||||||
TextFont {
|
},
|
||||||
font_size: 22.0,
|
ControlSection {
|
||||||
..default()
|
title: "New Game",
|
||||||
},
|
rows: &[
|
||||||
TextColor(Color::srgb(0.95, 0.95, 0.90)),
|
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)]
|
#[cfg(test)]
|
||||||
|
|||||||
Reference in New Issue
Block a user