feat(engine): tooltips on every HUD readout and action button
Applies the tooltip infrastructure to the HUD: ten readouts (Score, Moves, Time, Mode, daily-challenge target, draw cycle, undo count, recycle count, auto-complete badge, keyboard selection chip) and the six action-bar buttons (Menu, Undo, Pause, Help, Modes, New Game) each gain a one-sentence tooltip in the established Balatro voice. The strings earn their keep by surfacing information that isn't visible: the link between the undo counter and the No Undo achievement, the recycle counter and Comeback, the dual count-up / countdown semantics of the timer in Time Attack, and the keyboard shortcuts plus side-effects on action buttons. spawn_action_button now requires a tooltip parameter so every action bar entry gets one — there is no opt-out, by design. The popover Mode and Menu rows are intentionally skipped: they're inside ephemeral overlays whose hover surfaces are brief and already labeled. Adds hud_elements_carry_expected_tooltip_strings, asserting the exact text on each of the 16 instrumented elements. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,7 @@ use crate::resources::GameStateResource;
|
|||||||
use crate::selection_plugin::SelectionState;
|
use crate::selection_plugin::SelectionState;
|
||||||
use crate::time_attack_plugin::TimeAttackResource;
|
use crate::time_attack_plugin::TimeAttackResource;
|
||||||
use crate::ui_focus::{FocusGroup, Focusable};
|
use crate::ui_focus::{FocusGroup, Focusable};
|
||||||
|
use crate::ui_tooltip::Tooltip;
|
||||||
|
|
||||||
/// Marker on the score text node.
|
/// Marker on the score text node.
|
||||||
#[derive(Component, Debug)]
|
#[derive(Component, Debug)]
|
||||||
@@ -343,18 +344,23 @@ fn spawn_hud(font_res: Option<Res<FontResource>>, mut commands: Commands) {
|
|||||||
hud.spawn(row_node()).with_children(|t1| {
|
hud.spawn(row_node()).with_children(|t1| {
|
||||||
t1.spawn((
|
t1.spawn((
|
||||||
HudScore,
|
HudScore,
|
||||||
|
Tooltip::new("Points earned this game. Hidden in Zen mode."),
|
||||||
Text::new("Score: 0"),
|
Text::new("Score: 0"),
|
||||||
font_score.clone(),
|
font_score.clone(),
|
||||||
TextColor(TEXT_PRIMARY),
|
TextColor(TEXT_PRIMARY),
|
||||||
));
|
));
|
||||||
t1.spawn((
|
t1.spawn((
|
||||||
HudMoves,
|
HudMoves,
|
||||||
|
Tooltip::new(
|
||||||
|
"Moves you've made this game. Counts placements and stock draws.",
|
||||||
|
),
|
||||||
Text::new("Moves: 0"),
|
Text::new("Moves: 0"),
|
||||||
font_lg.clone(),
|
font_lg.clone(),
|
||||||
TextColor(TEXT_SECONDARY),
|
TextColor(TEXT_SECONDARY),
|
||||||
));
|
));
|
||||||
t1.spawn((
|
t1.spawn((
|
||||||
HudTime,
|
HudTime,
|
||||||
|
Tooltip::new("Time on this game. Counts down in Time Attack."),
|
||||||
Text::new("0:00"),
|
Text::new("0:00"),
|
||||||
font_lg.clone(),
|
font_lg.clone(),
|
||||||
TextColor(TEXT_SECONDARY),
|
TextColor(TEXT_SECONDARY),
|
||||||
@@ -367,18 +373,21 @@ fn spawn_hud(font_res: Option<Res<FontResource>>, mut commands: Commands) {
|
|||||||
hud.spawn(row_node()).with_children(|t2| {
|
hud.spawn(row_node()).with_children(|t2| {
|
||||||
t2.spawn((
|
t2.spawn((
|
||||||
HudMode,
|
HudMode,
|
||||||
|
Tooltip::new("Active game mode. Click Modes to switch."),
|
||||||
Text::new(""),
|
Text::new(""),
|
||||||
font_body.clone(),
|
font_body.clone(),
|
||||||
TextColor(ACCENT_PRIMARY),
|
TextColor(ACCENT_PRIMARY),
|
||||||
));
|
));
|
||||||
t2.spawn((
|
t2.spawn((
|
||||||
HudChallenge,
|
HudChallenge,
|
||||||
|
Tooltip::new("Today's daily challenge target. Beat it for bonus XP."),
|
||||||
Text::new(""),
|
Text::new(""),
|
||||||
font_body.clone(),
|
font_body.clone(),
|
||||||
TextColor(STATE_INFO),
|
TextColor(STATE_INFO),
|
||||||
));
|
));
|
||||||
t2.spawn((
|
t2.spawn((
|
||||||
HudDrawCycle,
|
HudDrawCycle,
|
||||||
|
Tooltip::new("Cards drawn on the next stock click in Draw-Three."),
|
||||||
Text::new(""),
|
Text::new(""),
|
||||||
font_body.clone(),
|
font_body.clone(),
|
||||||
TextColor(STATE_INFO),
|
TextColor(STATE_INFO),
|
||||||
@@ -391,18 +400,25 @@ fn spawn_hud(font_res: Option<Res<FontResource>>, mut commands: Commands) {
|
|||||||
hud.spawn(row_node()).with_children(|t3| {
|
hud.spawn(row_node()).with_children(|t3| {
|
||||||
t3.spawn((
|
t3.spawn((
|
||||||
HudUndos,
|
HudUndos,
|
||||||
|
Tooltip::new(
|
||||||
|
"Undos used this game. Any undo blocks the No Undo achievement.",
|
||||||
|
),
|
||||||
Text::new(""),
|
Text::new(""),
|
||||||
font_body.clone(),
|
font_body.clone(),
|
||||||
TextColor(STATE_WARNING),
|
TextColor(STATE_WARNING),
|
||||||
));
|
));
|
||||||
t3.spawn((
|
t3.spawn((
|
||||||
HudRecycles,
|
HudRecycles,
|
||||||
|
Tooltip::new(
|
||||||
|
"Times you've recycled the stock. Three or more unlocks Comeback.",
|
||||||
|
),
|
||||||
Text::new(""),
|
Text::new(""),
|
||||||
font_body.clone(),
|
font_body.clone(),
|
||||||
TextColor(STATE_WARNING),
|
TextColor(STATE_WARNING),
|
||||||
));
|
));
|
||||||
t3.spawn((
|
t3.spawn((
|
||||||
HudAutoComplete,
|
HudAutoComplete,
|
||||||
|
Tooltip::new("Board is solvable from here. Press Enter to auto-finish."),
|
||||||
Text::new(""),
|
Text::new(""),
|
||||||
font_body.clone(),
|
font_body.clone(),
|
||||||
TextColor(STATE_SUCCESS),
|
TextColor(STATE_SUCCESS),
|
||||||
@@ -414,6 +430,7 @@ fn spawn_hud(font_res: Option<Res<FontResource>>, mut commands: Commands) {
|
|||||||
hud.spawn(row_node()).with_children(|t4| {
|
hud.spawn(row_node()).with_children(|t4| {
|
||||||
t4.spawn((
|
t4.spawn((
|
||||||
HudSelection,
|
HudSelection,
|
||||||
|
Tooltip::new("Pile selected with Tab. Use arrows or Enter to act."),
|
||||||
Text::new(""),
|
Text::new(""),
|
||||||
font_body,
|
font_body,
|
||||||
TextColor(ACCENT_SECONDARY),
|
TextColor(ACCENT_SECONDARY),
|
||||||
@@ -458,12 +475,60 @@ fn spawn_action_buttons(font_res: Option<Res<FontResource>>, mut commands: Comma
|
|||||||
// visual reading order (left → right). It feeds
|
// visual reading order (left → right). It feeds
|
||||||
// `Focusable { group: Hud, order }` so Tab cycles the action
|
// `Focusable { group: Hud, order }` so Tab cycles the action
|
||||||
// bar in the same order the eye scans it.
|
// bar in the same order the eye scans it.
|
||||||
spawn_action_button(row, MenuButton, "Menu \u{25BE}", None, &font, 0);
|
spawn_action_button(
|
||||||
spawn_action_button(row, UndoButton, "Undo", Some("U"), &font, 1);
|
row,
|
||||||
spawn_action_button(row, PauseButton, "Pause", Some("Esc"), &font, 2);
|
MenuButton,
|
||||||
spawn_action_button(row, HelpButton, "Help", Some("F1"), &font, 3);
|
"Menu \u{25BE}",
|
||||||
spawn_action_button(row, ModesButton, "Modes \u{25BE}", None, &font, 4);
|
None,
|
||||||
spawn_action_button(row, NewGameButton, "New Game", Some("N"), &font, 5);
|
"Open Stats, Achievements, Profile, Settings, or Leaderboard.",
|
||||||
|
&font,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
spawn_action_button(
|
||||||
|
row,
|
||||||
|
UndoButton,
|
||||||
|
"Undo",
|
||||||
|
Some("U"),
|
||||||
|
"Take back your last move. Costs points and blocks No Undo.",
|
||||||
|
&font,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
spawn_action_button(
|
||||||
|
row,
|
||||||
|
PauseButton,
|
||||||
|
"Pause",
|
||||||
|
Some("Esc"),
|
||||||
|
"Pause the game and freeze the timer.",
|
||||||
|
&font,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
spawn_action_button(
|
||||||
|
row,
|
||||||
|
HelpButton,
|
||||||
|
"Help",
|
||||||
|
Some("F1"),
|
||||||
|
"Show controls, rules, and keyboard shortcuts.",
|
||||||
|
&font,
|
||||||
|
3,
|
||||||
|
);
|
||||||
|
spawn_action_button(
|
||||||
|
row,
|
||||||
|
ModesButton,
|
||||||
|
"Modes \u{25BE}",
|
||||||
|
None,
|
||||||
|
"Switch modes: Classic, Daily, Zen, Challenge, Time Attack.",
|
||||||
|
&font,
|
||||||
|
4,
|
||||||
|
);
|
||||||
|
spawn_action_button(
|
||||||
|
row,
|
||||||
|
NewGameButton,
|
||||||
|
"New Game",
|
||||||
|
Some("N"),
|
||||||
|
"Start a fresh deal. Confirms first if a game is in progress.",
|
||||||
|
&font,
|
||||||
|
5,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,11 +539,18 @@ fn spawn_action_buttons(font_res: Option<Res<FontResource>>, mut commands: Comma
|
|||||||
/// `order` is the button's index inside the action bar (0 for the
|
/// `order` is the button's index inside the action bar (0 for the
|
||||||
/// leftmost). It propagates into the [`Focusable`] this function inserts
|
/// leftmost). It propagates into the [`Focusable`] this function inserts
|
||||||
/// so Phase 2's keyboard focus ring cycles the HUD in visual order.
|
/// so Phase 2's keyboard focus ring cycles the HUD in visual order.
|
||||||
|
///
|
||||||
|
/// `tooltip` is the hover-reveal caption attached via [`Tooltip`]. Every
|
||||||
|
/// action button ships with one — there is no opt-out — because each button
|
||||||
|
/// represents a player-triggered action and benefits from a one-line
|
||||||
|
/// reminder of what it does.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn spawn_action_button<M: Component>(
|
fn spawn_action_button<M: Component>(
|
||||||
row: &mut ChildSpawnerCommands,
|
row: &mut ChildSpawnerCommands,
|
||||||
marker: M,
|
marker: M,
|
||||||
label: &str,
|
label: &str,
|
||||||
hotkey: Option<&'static str>,
|
hotkey: Option<&'static str>,
|
||||||
|
tooltip: &'static str,
|
||||||
font: &TextFont,
|
font: &TextFont,
|
||||||
order: i32,
|
order: i32,
|
||||||
) {
|
) {
|
||||||
@@ -491,6 +563,7 @@ fn spawn_action_button<M: Component>(
|
|||||||
marker,
|
marker,
|
||||||
ActionButton,
|
ActionButton,
|
||||||
Button,
|
Button,
|
||||||
|
Tooltip::new(tooltip),
|
||||||
// Joins the `Hud` focus group at the supplied order so Tab
|
// Joins the `Hud` focus group at the supplied order so Tab
|
||||||
// cycles HUD buttons left-to-right under Phase 2. The HUD focus
|
// cycles HUD buttons left-to-right under Phase 2. The HUD focus
|
||||||
// ring still only engages when a HUD button is hovered (or in
|
// ring still only engages when a HUD button is hovered (or in
|
||||||
@@ -1860,6 +1933,113 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the tooltip string carried by the unique entity matching
|
||||||
|
/// marker `M`. Panics if zero or more than one such entity exists,
|
||||||
|
/// which is the invariant we want to enforce for HUD readouts and
|
||||||
|
/// action buttons (each marker is spawned exactly once).
|
||||||
|
fn tooltip_for<M: Component>(app: &mut App) -> String {
|
||||||
|
let mut q = app
|
||||||
|
.world_mut()
|
||||||
|
.query_filtered::<&Tooltip, With<M>>();
|
||||||
|
let world = app.world();
|
||||||
|
let mut iter = q.iter(world);
|
||||||
|
let first = iter
|
||||||
|
.next()
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
panic!(
|
||||||
|
"expected a Tooltip on the {} entity",
|
||||||
|
std::any::type_name::<M>()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.0
|
||||||
|
.clone()
|
||||||
|
.into_owned();
|
||||||
|
assert!(
|
||||||
|
iter.next().is_none(),
|
||||||
|
"expected exactly one Tooltip-bearing entity for {}",
|
||||||
|
std::any::type_name::<M>()
|
||||||
|
);
|
||||||
|
first
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every HUD readout and action button must spawn with a `Tooltip`
|
||||||
|
/// carrying the approved canonical microcopy. Mirrors the structure
|
||||||
|
/// of `hud_buttons_get_focusable_marker` (Phase 2 focus test) so the
|
||||||
|
/// invariant — one marker entity, one tooltip, exact text — is
|
||||||
|
/// asserted consistently across every element.
|
||||||
|
#[test]
|
||||||
|
fn hud_elements_carry_expected_tooltip_strings() {
|
||||||
|
let mut app = headless_app();
|
||||||
|
|
||||||
|
// HUD readouts (left column, top to bottom).
|
||||||
|
assert_eq!(
|
||||||
|
tooltip_for::<HudScore>(&mut app),
|
||||||
|
"Points earned this game. Hidden in Zen mode."
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
tooltip_for::<HudMoves>(&mut app),
|
||||||
|
"Moves you've made this game. Counts placements and stock draws."
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
tooltip_for::<HudTime>(&mut app),
|
||||||
|
"Time on this game. Counts down in Time Attack."
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
tooltip_for::<HudMode>(&mut app),
|
||||||
|
"Active game mode. Click Modes to switch."
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
tooltip_for::<HudChallenge>(&mut app),
|
||||||
|
"Today's daily challenge target. Beat it for bonus XP."
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
tooltip_for::<HudDrawCycle>(&mut app),
|
||||||
|
"Cards drawn on the next stock click in Draw-Three."
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
tooltip_for::<HudUndos>(&mut app),
|
||||||
|
"Undos used this game. Any undo blocks the No Undo achievement."
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
tooltip_for::<HudRecycles>(&mut app),
|
||||||
|
"Times you've recycled the stock. Three or more unlocks Comeback."
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
tooltip_for::<HudAutoComplete>(&mut app),
|
||||||
|
"Board is solvable from here. Press Enter to auto-finish."
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
tooltip_for::<HudSelection>(&mut app),
|
||||||
|
"Pile selected with Tab. Use arrows or Enter to act."
|
||||||
|
);
|
||||||
|
|
||||||
|
// Action bar (left to right).
|
||||||
|
assert_eq!(
|
||||||
|
tooltip_for::<MenuButton>(&mut app),
|
||||||
|
"Open Stats, Achievements, Profile, Settings, or Leaderboard."
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
tooltip_for::<UndoButton>(&mut app),
|
||||||
|
"Take back your last move. Costs points and blocks No Undo."
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
tooltip_for::<PauseButton>(&mut app),
|
||||||
|
"Pause the game and freeze the timer."
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
tooltip_for::<HelpButton>(&mut app),
|
||||||
|
"Show controls, rules, and keyboard shortcuts."
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
tooltip_for::<ModesButton>(&mut app),
|
||||||
|
"Switch modes: Classic, Daily, Zen, Challenge, Time Attack."
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
tooltip_for::<NewGameButton>(&mut app),
|
||||||
|
"Start a fresh deal. Confirms first if a game is in progress."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hud_button_order_matches_spawn_order() {
|
fn hud_button_order_matches_spawn_order() {
|
||||||
let mut app = headless_app();
|
let mut app = headless_app();
|
||||||
|
|||||||
Reference in New Issue
Block a user