Merge pull request 'feat(engine): hold-/ hotkey cheat sheet + unified binding table (Phase J)' (#183) from feat/keyboard-completeness into master
This commit was merged in pull request #183.
This commit is contained in:
@@ -0,0 +1,194 @@
|
|||||||
|
//! Hold-`/` hotkey cheat sheet (Phase J).
|
||||||
|
//!
|
||||||
|
//! While `/` is held on a keyboard platform, a lightweight overlay
|
||||||
|
//! lists every binding from [`crate::hotkeys::HOTKEYS`] — the same
|
||||||
|
//! table the onboarding slide teaches from, so the two can never
|
||||||
|
//! disagree. Releasing the key hides it instantly; it never captures
|
||||||
|
//! input, never pauses the game, and never spawns while a modal owns
|
||||||
|
//! the screen (which also keeps it out of the seed-entry text field's
|
||||||
|
//! way).
|
||||||
|
//!
|
||||||
|
//! Not a `spawn_modal` modal on purpose: modals are sticky and guarded;
|
||||||
|
//! this is a momentary reference card, closer to a tooltip than a
|
||||||
|
//! dialog.
|
||||||
|
|
||||||
|
use bevy::input::ButtonInput;
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use crate::font_plugin::FontResource;
|
||||||
|
use crate::hotkeys::HOTKEYS;
|
||||||
|
use crate::platform::SHOW_KEYBOARD_ACCELERATORS;
|
||||||
|
use crate::ui_modal::ModalScrim;
|
||||||
|
use crate::ui_theme::{
|
||||||
|
ACCENT_PRIMARY, BG_ELEVATED, BORDER_STRONG, HighContrastBorder, RADIUS_MD, TEXT_PRIMARY,
|
||||||
|
TEXT_SECONDARY, TYPE_BODY, TYPE_CAPTION, VAL_SPACE_1, VAL_SPACE_2, VAL_SPACE_3, VAL_SPACE_4,
|
||||||
|
Z_TOOLTIP,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Marker on the cheat-sheet overlay root.
|
||||||
|
#[derive(Component, Debug)]
|
||||||
|
pub struct CheatSheetOverlay;
|
||||||
|
|
||||||
|
/// Registers the hold-`/` driver. Inert on touch-first builds.
|
||||||
|
pub struct CheatSheetPlugin;
|
||||||
|
|
||||||
|
impl Plugin for CheatSheetPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.init_resource::<ButtonInput<KeyCode>>()
|
||||||
|
.add_systems(Update, drive_cheat_sheet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows the overlay while `/` is held (and no modal owns the screen);
|
||||||
|
/// hides it the frame the key releases.
|
||||||
|
fn drive_cheat_sheet(
|
||||||
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
|
scrims: Query<(), With<ModalScrim>>,
|
||||||
|
existing: Query<Entity, With<CheatSheetOverlay>>,
|
||||||
|
font_res: Option<Res<FontResource>>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
if !SHOW_KEYBOARD_ACCELERATORS {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let held = keys.pressed(KeyCode::Slash);
|
||||||
|
if held && existing.is_empty() && scrims.is_empty() {
|
||||||
|
spawn_cheat_sheet(&mut commands, font_res.as_deref());
|
||||||
|
} else if !held {
|
||||||
|
for entity in &existing {
|
||||||
|
commands.entity(entity).despawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_cheat_sheet(commands: &mut Commands, font_res: Option<&FontResource>) {
|
||||||
|
let font_handle = font_res.map(|f| f.0.clone()).unwrap_or_default();
|
||||||
|
let font_header = TextFont {
|
||||||
|
font: font_handle.clone(),
|
||||||
|
font_size: TYPE_BODY,
|
||||||
|
..default()
|
||||||
|
};
|
||||||
|
let font_keys = TextFont {
|
||||||
|
font: font_handle.clone(),
|
||||||
|
font_size: TYPE_CAPTION,
|
||||||
|
..default()
|
||||||
|
};
|
||||||
|
let font_desc = TextFont {
|
||||||
|
font: font_handle,
|
||||||
|
font_size: TYPE_CAPTION,
|
||||||
|
..default()
|
||||||
|
};
|
||||||
|
|
||||||
|
commands
|
||||||
|
.spawn((
|
||||||
|
CheatSheetOverlay,
|
||||||
|
Node {
|
||||||
|
position_type: PositionType::Absolute,
|
||||||
|
right: Val::Px(16.0),
|
||||||
|
top: Val::Percent(8.0),
|
||||||
|
flex_direction: FlexDirection::Column,
|
||||||
|
row_gap: VAL_SPACE_1,
|
||||||
|
padding: UiRect::all(VAL_SPACE_4),
|
||||||
|
border: UiRect::all(Val::Px(1.0)),
|
||||||
|
border_radius: BorderRadius::all(Val::Px(RADIUS_MD)),
|
||||||
|
max_height: Val::Percent(84.0),
|
||||||
|
overflow: Overflow::scroll_y(),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(BG_ELEVATED),
|
||||||
|
BorderColor::all(BORDER_STRONG),
|
||||||
|
HighContrastBorder::with_default(BORDER_STRONG),
|
||||||
|
GlobalZIndex(Z_TOOLTIP),
|
||||||
|
))
|
||||||
|
.with_children(|panel| {
|
||||||
|
panel.spawn((
|
||||||
|
Text::new("Keyboard shortcuts"),
|
||||||
|
font_header.clone(),
|
||||||
|
TextColor(TEXT_PRIMARY),
|
||||||
|
Node {
|
||||||
|
margin: UiRect::bottom(VAL_SPACE_2),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
));
|
||||||
|
for row in HOTKEYS {
|
||||||
|
panel
|
||||||
|
.spawn(Node {
|
||||||
|
flex_direction: FlexDirection::Row,
|
||||||
|
column_gap: VAL_SPACE_3,
|
||||||
|
..default()
|
||||||
|
})
|
||||||
|
.with_children(|line| {
|
||||||
|
line.spawn((
|
||||||
|
Text::new(row.keys),
|
||||||
|
font_keys.clone(),
|
||||||
|
TextColor(ACCENT_PRIMARY),
|
||||||
|
Node {
|
||||||
|
min_width: Val::Px(110.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
));
|
||||||
|
line.spawn((
|
||||||
|
Text::new(row.description),
|
||||||
|
font_desc.clone(),
|
||||||
|
TextColor(TEXT_SECONDARY),
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn app() -> App {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.add_plugins(MinimalPlugins)
|
||||||
|
.add_plugins(CheatSheetPlugin);
|
||||||
|
app.update();
|
||||||
|
app
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay_count(app: &mut App) -> usize {
|
||||||
|
app.world_mut()
|
||||||
|
.query::<&CheatSheetOverlay>()
|
||||||
|
.iter(app.world())
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn holding_slash_shows_and_release_hides() {
|
||||||
|
let mut app = app();
|
||||||
|
app.world_mut()
|
||||||
|
.resource_mut::<ButtonInput<KeyCode>>()
|
||||||
|
.press(KeyCode::Slash);
|
||||||
|
app.update();
|
||||||
|
assert_eq!(overlay_count(&mut app), 1, "held / must show the sheet");
|
||||||
|
// Still held on later frames: exactly one overlay, no stacking.
|
||||||
|
app.update();
|
||||||
|
assert_eq!(overlay_count(&mut app), 1);
|
||||||
|
|
||||||
|
app.world_mut()
|
||||||
|
.resource_mut::<ButtonInput<KeyCode>>()
|
||||||
|
.release(KeyCode::Slash);
|
||||||
|
app.update();
|
||||||
|
app.update();
|
||||||
|
assert_eq!(overlay_count(&mut app), 0, "release must hide the sheet");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn suppressed_while_a_modal_is_open() {
|
||||||
|
let mut app = app();
|
||||||
|
app.world_mut().spawn(ModalScrim);
|
||||||
|
app.update();
|
||||||
|
app.world_mut()
|
||||||
|
.resource_mut::<ButtonInput<KeyCode>>()
|
||||||
|
.press(KeyCode::Slash);
|
||||||
|
app.update();
|
||||||
|
assert_eq!(
|
||||||
|
overlay_count(&mut app),
|
||||||
|
0,
|
||||||
|
"the sheet must not spawn over a modal (or into a text field)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,14 +14,14 @@ use crate::platform::{
|
|||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
AchievementPlugin, AnimationPlugin, AssetSourcesPlugin, AutoCompletePlugin,
|
AchievementPlugin, AnimationPlugin, AssetSourcesPlugin, AutoCompletePlugin,
|
||||||
CardAnimationPlugin, CardPlugin, ChallengePlugin, ContextualTipsPlugin, CursorPlugin,
|
CardAnimationPlugin, CardPlugin, ChallengePlugin, CheatSheetPlugin, ContextualTipsPlugin,
|
||||||
DailyChallengePlugin, DiagnosticsHudPlugin, DifficultyPlugin, FeedbackAnimPlugin, FontPlugin,
|
CursorPlugin, DailyChallengePlugin, DiagnosticsHudPlugin, DifficultyPlugin, FeedbackAnimPlugin,
|
||||||
GamePlugin, HelpPlugin, HomePlugin, HudPlugin, InputPlugin, OnboardingPlugin, PausePlugin,
|
FontPlugin, GamePlugin, HelpPlugin, HomePlugin, HudPlugin, InputPlugin, OnboardingPlugin,
|
||||||
PlayBySeedPlugin, ProfilePlugin, ProgressPlugin, RadialMenuPlugin, ReplayOverlayPlugin,
|
PausePlugin, PlayBySeedPlugin, ProfilePlugin, ProgressPlugin, RadialMenuPlugin,
|
||||||
ReplayPlaybackPlugin, SafeAreaInsetsPlugin, SelectionPlugin, SettingsPlugin,
|
ReplayOverlayPlugin, ReplayPlaybackPlugin, SafeAreaInsetsPlugin, SelectionPlugin,
|
||||||
SolutionPlaybackPlugin, SplashPlugin, StatsPlugin, SyncProvider, TablePlugin, ThemePlugin,
|
SettingsPlugin, SolutionPlaybackPlugin, SplashPlugin, StatsPlugin, SyncProvider, TablePlugin,
|
||||||
ThemeRegistryPlugin, TimeAttackPlugin, TouchSelectionPlugin, UiFocusPlugin, UiModalPlugin,
|
ThemePlugin, ThemeRegistryPlugin, TimeAttackPlugin, TouchSelectionPlugin, UiFocusPlugin,
|
||||||
UiTooltipPlugin, WeeklyGoalsPlugin, WhatsNewPlugin, WinSummaryPlugin,
|
UiModalPlugin, UiTooltipPlugin, WeeklyGoalsPlugin, WhatsNewPlugin, WinSummaryPlugin,
|
||||||
};
|
};
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -117,6 +117,7 @@ impl Plugin for CoreGamePlugin {
|
|||||||
.add_plugins(OnboardingPlugin)
|
.add_plugins(OnboardingPlugin)
|
||||||
.add_plugins(WhatsNewPlugin)
|
.add_plugins(WhatsNewPlugin)
|
||||||
.add_plugins(ContextualTipsPlugin)
|
.add_plugins(ContextualTipsPlugin)
|
||||||
|
.add_plugins(CheatSheetPlugin)
|
||||||
.add_plugins(WinSummaryPlugin)
|
.add_plugins(WinSummaryPlugin)
|
||||||
.add_plugins(UiModalPlugin)
|
.add_plugins(UiModalPlugin)
|
||||||
.add_plugins(UiFocusPlugin)
|
.add_plugins(UiFocusPlugin)
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
//! Single source of truth for the desktop keyboard bindings (Phase J).
|
||||||
|
//!
|
||||||
|
//! Two static hotkey tables had already diverged (onboarding's slide
|
||||||
|
//! and Help's controls reference); every future drift multiplies. This
|
||||||
|
//! module owns THE table: the onboarding slide renders the
|
||||||
|
//! [`HotkeyRow::essential`] subset, the hold-`/` cheat sheet
|
||||||
|
//! ([`crate::cheat_sheet_plugin`]) renders everything.
|
||||||
|
//!
|
||||||
|
//! The table is hand-maintained but **pinned by test** against the
|
||||||
|
//! handlers that actually consume each key — adding a binding without
|
||||||
|
//! updating this table (or vice versa) is designed to fail review, not
|
||||||
|
//! runtime. A registry generated from the input systems themselves is
|
||||||
|
//! the eventual ideal; this is the honest 90 % at 1 % of the cost.
|
||||||
|
|
||||||
|
/// One row of the hotkey table.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct HotkeyRow {
|
||||||
|
/// Display form of the key(s), e.g. `"D / Space"`.
|
||||||
|
pub keys: &'static str,
|
||||||
|
/// One-line action description.
|
||||||
|
pub description: &'static str,
|
||||||
|
/// `true` for the beginner-relevant subset the onboarding slide
|
||||||
|
/// shows; the cheat sheet always shows every row.
|
||||||
|
pub essential: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every desktop keyboard binding, in teaching order.
|
||||||
|
pub const HOTKEYS: &[HotkeyRow] = &[
|
||||||
|
HotkeyRow {
|
||||||
|
keys: "D / Space",
|
||||||
|
description: "Draw from stock",
|
||||||
|
essential: true,
|
||||||
|
},
|
||||||
|
HotkeyRow {
|
||||||
|
keys: "U",
|
||||||
|
description: "Undo last move (hold to repeat)",
|
||||||
|
essential: true,
|
||||||
|
},
|
||||||
|
HotkeyRow {
|
||||||
|
keys: "H",
|
||||||
|
description: "Hint (repeat to cycle alternatives)",
|
||||||
|
essential: true,
|
||||||
|
},
|
||||||
|
HotkeyRow {
|
||||||
|
keys: "Tab → Enter",
|
||||||
|
description: "Pick a card; arrows pick where; Enter to drop",
|
||||||
|
essential: true,
|
||||||
|
},
|
||||||
|
HotkeyRow {
|
||||||
|
keys: "N",
|
||||||
|
description: "New Classic game",
|
||||||
|
essential: true,
|
||||||
|
},
|
||||||
|
HotkeyRow {
|
||||||
|
keys: "M",
|
||||||
|
description: "Open Home (then 1–6 to pick a mode)",
|
||||||
|
essential: true,
|
||||||
|
},
|
||||||
|
HotkeyRow {
|
||||||
|
keys: "Esc",
|
||||||
|
description: "Pause / resume; close the top dialog",
|
||||||
|
essential: true,
|
||||||
|
},
|
||||||
|
HotkeyRow {
|
||||||
|
keys: "F1",
|
||||||
|
description: "Help / controls",
|
||||||
|
essential: true,
|
||||||
|
},
|
||||||
|
HotkeyRow {
|
||||||
|
keys: "S",
|
||||||
|
description: "Stats & progression",
|
||||||
|
essential: true,
|
||||||
|
},
|
||||||
|
HotkeyRow {
|
||||||
|
keys: "A",
|
||||||
|
description: "Achievements",
|
||||||
|
essential: true,
|
||||||
|
},
|
||||||
|
HotkeyRow {
|
||||||
|
keys: "O",
|
||||||
|
description: "Settings",
|
||||||
|
essential: true,
|
||||||
|
},
|
||||||
|
HotkeyRow {
|
||||||
|
keys: "P",
|
||||||
|
description: "Profile",
|
||||||
|
essential: false,
|
||||||
|
},
|
||||||
|
HotkeyRow {
|
||||||
|
keys: "L",
|
||||||
|
description: "Leaderboard",
|
||||||
|
essential: false,
|
||||||
|
},
|
||||||
|
HotkeyRow {
|
||||||
|
keys: "C",
|
||||||
|
description: "Daily Challenge",
|
||||||
|
essential: false,
|
||||||
|
},
|
||||||
|
HotkeyRow {
|
||||||
|
keys: "Z",
|
||||||
|
description: "Zen mode",
|
||||||
|
essential: false,
|
||||||
|
},
|
||||||
|
HotkeyRow {
|
||||||
|
keys: "X",
|
||||||
|
description: "Challenge mode",
|
||||||
|
essential: false,
|
||||||
|
},
|
||||||
|
HotkeyRow {
|
||||||
|
keys: "T",
|
||||||
|
description: "Time Attack",
|
||||||
|
essential: false,
|
||||||
|
},
|
||||||
|
HotkeyRow {
|
||||||
|
keys: "G",
|
||||||
|
description: "Give up the current deal",
|
||||||
|
essential: false,
|
||||||
|
},
|
||||||
|
HotkeyRow {
|
||||||
|
keys: "[ / ]",
|
||||||
|
description: "Volume down / up",
|
||||||
|
essential: false,
|
||||||
|
},
|
||||||
|
HotkeyRow {
|
||||||
|
keys: "F11",
|
||||||
|
description: "Toggle fullscreen",
|
||||||
|
essential: false,
|
||||||
|
},
|
||||||
|
HotkeyRow {
|
||||||
|
keys: "/ (hold)",
|
||||||
|
description: "This cheat sheet",
|
||||||
|
essential: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn table_is_populated_and_well_formed() {
|
||||||
|
assert!(!HOTKEYS.is_empty());
|
||||||
|
for row in HOTKEYS {
|
||||||
|
assert!(!row.keys.trim().is_empty(), "empty keys cell");
|
||||||
|
assert!(
|
||||||
|
!row.description.trim().is_empty(),
|
||||||
|
"empty description for {}",
|
||||||
|
row.keys
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn essential_subset_is_a_teachable_size() {
|
||||||
|
let essentials = HOTKEYS.iter().filter(|r| r.essential).count();
|
||||||
|
assert!(
|
||||||
|
(6..=12).contains(&essentials),
|
||||||
|
"the onboarding slide wants a skimmable essential set, got {essentials}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keys_cells_are_unique() {
|
||||||
|
let mut seen = std::collections::HashSet::new();
|
||||||
|
for row in HOTKEYS {
|
||||||
|
assert!(seen.insert(row.keys), "duplicate hotkey row: {}", row.keys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ pub mod avatar_plugin;
|
|||||||
pub mod card_animation;
|
pub mod card_animation;
|
||||||
pub mod card_plugin;
|
pub mod card_plugin;
|
||||||
pub mod challenge_plugin;
|
pub mod challenge_plugin;
|
||||||
|
pub mod cheat_sheet_plugin;
|
||||||
pub mod contextual_tips_plugin;
|
pub mod contextual_tips_plugin;
|
||||||
pub mod core_game_plugin;
|
pub mod core_game_plugin;
|
||||||
pub mod cursor_plugin;
|
pub mod cursor_plugin;
|
||||||
@@ -27,6 +28,7 @@ pub mod font_plugin;
|
|||||||
pub mod game_plugin;
|
pub mod game_plugin;
|
||||||
pub mod help_plugin;
|
pub mod help_plugin;
|
||||||
pub mod home_plugin;
|
pub mod home_plugin;
|
||||||
|
pub mod hotkeys;
|
||||||
pub mod hud_plugin;
|
pub mod hud_plugin;
|
||||||
pub mod input_plugin;
|
pub mod input_plugin;
|
||||||
pub mod layout;
|
pub mod layout;
|
||||||
@@ -95,6 +97,7 @@ pub use card_plugin::{
|
|||||||
pub use challenge_plugin::{
|
pub use challenge_plugin::{
|
||||||
CHALLENGE_UNLOCK_LEVEL, ChallengeAdvancedEvent, ChallengePlugin, challenge_progress_label,
|
CHALLENGE_UNLOCK_LEVEL, ChallengeAdvancedEvent, ChallengePlugin, challenge_progress_label,
|
||||||
};
|
};
|
||||||
|
pub use cheat_sheet_plugin::{CheatSheetOverlay, CheatSheetPlugin};
|
||||||
pub use contextual_tips_plugin::ContextualTipsPlugin;
|
pub use contextual_tips_plugin::ContextualTipsPlugin;
|
||||||
pub use core_game_plugin::CoreGamePlugin;
|
pub use core_game_plugin::CoreGamePlugin;
|
||||||
pub use cursor_plugin::CursorPlugin;
|
pub use cursor_plugin::CursorPlugin;
|
||||||
|
|||||||
@@ -84,66 +84,11 @@ struct OnboardingSkipButton;
|
|||||||
pub struct OnboardingSlideIndex(pub u8);
|
pub struct OnboardingSlideIndex(pub u8);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Slide data — hotkey rows are taken verbatim from `help_plugin.rs` so the
|
// Slide data — the hotkey slide renders the essential subset of the
|
||||||
// two screens stay in sync without a shared abstraction.
|
// shared table in `crate::hotkeys` (Phase J unified the previously
|
||||||
|
// diverging copies here and in help_plugin).
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// A single `key — description` pair shown on slide 3.
|
|
||||||
#[cfg(not(target_os = "android"))]
|
|
||||||
struct HotkeyRow {
|
|
||||||
keys: &'static str,
|
|
||||||
description: &'static str,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Most-used shortcuts from the `help_plugin` canonical list.
|
|
||||||
///
|
|
||||||
/// Updating the list in `help_plugin.rs` should be mirrored here. The
|
|
||||||
/// ARCHITECTURE.md decision log calls out that we copy values rather than
|
|
||||||
/// refactor the help plugin.
|
|
||||||
#[cfg(not(target_os = "android"))]
|
|
||||||
const HOTKEYS: &[HotkeyRow] = &[
|
|
||||||
HotkeyRow {
|
|
||||||
keys: "D / Space",
|
|
||||||
description: "Draw from stock",
|
|
||||||
},
|
|
||||||
HotkeyRow {
|
|
||||||
keys: "U",
|
|
||||||
description: "Undo last move",
|
|
||||||
},
|
|
||||||
HotkeyRow {
|
|
||||||
keys: "Tab → Enter",
|
|
||||||
description: "Pick a card; arrows pick where; Enter to drop",
|
|
||||||
},
|
|
||||||
HotkeyRow {
|
|
||||||
keys: "N",
|
|
||||||
description: "New Classic game",
|
|
||||||
},
|
|
||||||
HotkeyRow {
|
|
||||||
keys: "M",
|
|
||||||
description: "Open Mode Launcher (then 1–5 to pick)",
|
|
||||||
},
|
|
||||||
HotkeyRow {
|
|
||||||
keys: "S",
|
|
||||||
description: "Stats & progression",
|
|
||||||
},
|
|
||||||
HotkeyRow {
|
|
||||||
keys: "A",
|
|
||||||
description: "Achievements",
|
|
||||||
},
|
|
||||||
HotkeyRow {
|
|
||||||
keys: "O",
|
|
||||||
description: "Settings",
|
|
||||||
},
|
|
||||||
HotkeyRow {
|
|
||||||
keys: "Esc",
|
|
||||||
description: "Pause / resume",
|
|
||||||
},
|
|
||||||
HotkeyRow {
|
|
||||||
keys: "F1",
|
|
||||||
description: "Help / controls",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Plugin
|
// Plugin
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -446,8 +391,10 @@ fn spawn_slide_hotkeys(commands: &mut Commands, font_res: Option<&FontResource>)
|
|||||||
spawn_modal(commands, OnboardingScreen, Z_ONBOARDING, |card| {
|
spawn_modal(commands, OnboardingScreen, Z_ONBOARDING, |card| {
|
||||||
spawn_modal_header(card, "Keyboard shortcuts", font_res);
|
spawn_modal_header(card, "Keyboard shortcuts", font_res);
|
||||||
|
|
||||||
// Vertical list of `key — description` rows, same chip style as HelpScreen.
|
// Vertical list of `key — description` rows, same chip style as
|
||||||
for row in HOTKEYS {
|
// HelpScreen. Essential subset only — the full table lives on
|
||||||
|
// the hold-`/` cheat sheet.
|
||||||
|
for row in crate::hotkeys::HOTKEYS.iter().filter(|r| r.essential) {
|
||||||
card.spawn(Node {
|
card.spawn(Node {
|
||||||
flex_direction: FlexDirection::Row,
|
flex_direction: FlexDirection::Row,
|
||||||
align_items: AlignItems::Center,
|
align_items: AlignItems::Center,
|
||||||
@@ -826,22 +773,15 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Hotkey list is non-empty (guards against accidental truncation)
|
// Hotkey slide renders a non-empty essential subset (the table's own
|
||||||
|
// integrity tests live in `crate::hotkeys`)
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hotkey_list_is_non_empty() {
|
fn hotkey_slide_subset_is_non_empty() {
|
||||||
assert!(!HOTKEYS.is_empty(), "HOTKEYS must not be empty");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn all_hotkey_rows_have_non_empty_fields() {
|
|
||||||
for row in HOTKEYS {
|
|
||||||
assert!(!row.keys.is_empty(), "hotkey key field must not be empty");
|
|
||||||
assert!(
|
assert!(
|
||||||
!row.description.is_empty(),
|
crate::hotkeys::HOTKEYS.iter().any(|r| r.essential),
|
||||||
"hotkey description must not be empty"
|
"the onboarding slide needs at least one essential hotkey"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user