feat(engine): hold-/ hotkey cheat sheet + unified binding table (Phase J)
Phase J audit findings: the visible focus ring the doc asked for already exists (FocusOverlay singleton, 2px accent ring, breathing pulse, reduce-motion aware — landed after the doc was written), and post-Phase-C every modal already opens and dismisses keyboard-only. The real gaps were two silently diverging static hotkey tables (onboarding's slide vs Help's list — onboarding still said 'Mode Launcher (then 1-5)') and no way to see the bindings mid-game. - New crate::hotkeys module owns THE binding table (21 rows, verified against a grep inventory of every just_pressed(KeyCode::..) handler); rows carry an `essential` flag — the onboarding slide teaches that subset, the cheat sheet shows everything. - New cheat_sheet_plugin: hold `/` for a right-anchored reference overlay of every binding; release hides it. Deliberately not a spawn_modal modal (momentary reference, closer to a tooltip), never spawns while a modal owns the screen (also keeps it out of the seed-entry field), inert on touch builds. - Onboarding's stale local table deleted in favour of the shared one (copy updated: Home naming, hold-to-repeat undo, added H). 5 new tests (table integrity, essential-subset size, uniqueness, show/hide driver, modal suppression). Workspace + clippy green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -84,66 +84,11 @@ struct OnboardingSkipButton;
|
||||
pub struct OnboardingSlideIndex(pub u8);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Slide data — hotkey rows are taken verbatim from `help_plugin.rs` so the
|
||||
// two screens stay in sync without a shared abstraction.
|
||||
// Slide data — the hotkey slide renders the essential subset of the
|
||||
// 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
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -446,8 +391,10 @@ fn spawn_slide_hotkeys(commands: &mut Commands, font_res: Option<&FontResource>)
|
||||
spawn_modal(commands, OnboardingScreen, Z_ONBOARDING, |card| {
|
||||
spawn_modal_header(card, "Keyboard shortcuts", font_res);
|
||||
|
||||
// Vertical list of `key — description` rows, same chip style as HelpScreen.
|
||||
for row in HOTKEYS {
|
||||
// Vertical list of `key — description` rows, same chip style as
|
||||
// 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 {
|
||||
flex_direction: FlexDirection::Row,
|
||||
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]
|
||||
fn hotkey_list_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!(
|
||||
!row.description.is_empty(),
|
||||
"hotkey description must not be empty"
|
||||
);
|
||||
}
|
||||
fn hotkey_slide_subset_is_non_empty() {
|
||||
assert!(
|
||||
crate::hotkeys::HOTKEYS.iter().any(|r| r.essential),
|
||||
"the onboarding slide needs at least one essential hotkey"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user