feat(engine): Android polish sweep + hint button + watch replay
Draw-Three waste fan: slot.saturating_sub(1) was a constant shift that
hid slot-0 even when the pile had fewer cards than visible. Fixed to
slot.saturating_sub(rendered_len.saturating_sub(visible)) so small piles
fan correctly and only a genuine buffer card gets hidden. New regression
test covers the small-pile case.
Android toast: game-over "press D / N" message now shows touch-friendly
copy ("Tap the stock...") on Android via cfg gate.
Onboarding: SLIDE_COUNT drops from 3 to 2 on Android so first-time
users skip the keyboard-shortcuts slide (irrelevant on touchscreen).
spawn_slide dispatch is gated identically.
Hint button: added HintButton to the HUD action bar (order 4, between
Help and Modes). Clicking it triggers the async solver hint — same path
as the H key — via optional resources so headless tests stay clean.
All button-order and tooltip tests updated for the new 7-button bar.
Watch Replay: win-summary modal now shows a "Watch Replay" secondary
button alongside "Play Again". It loads the most recent entry from
ReplayHistoryResource and hands it to start_replay_playback, dismissing
the modal. Falls back to an info toast when the replay or playback
plugin is unavailable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -242,6 +242,11 @@ pub struct PauseButton;
|
||||
#[derive(Component, Debug)]
|
||||
pub struct HelpButton;
|
||||
|
||||
/// Marker on the "Hint" action button. Click spawns an async solver task
|
||||
/// (same as the `H` keyboard accelerator) and highlights the suggested card.
|
||||
#[derive(Component, Debug)]
|
||||
pub struct HintButton;
|
||||
|
||||
/// Marker on the "Modes" action button. Click toggles the [`ModesPopover`]
|
||||
/// (a small dropdown panel) below the action bar. Each popover row starts
|
||||
/// the corresponding game mode.
|
||||
@@ -367,6 +372,7 @@ impl Plugin for HudPlugin {
|
||||
handle_undo_button,
|
||||
handle_pause_button,
|
||||
handle_help_button,
|
||||
handle_hint_button,
|
||||
handle_modes_button,
|
||||
handle_mode_option_click,
|
||||
handle_modes_backdrop_click,
|
||||
@@ -702,6 +708,15 @@ fn spawn_action_buttons(
|
||||
&font,
|
||||
3,
|
||||
);
|
||||
spawn_action_button(
|
||||
row,
|
||||
HintButton,
|
||||
"Hint",
|
||||
Some("H"),
|
||||
"Highlight a suggested move. Cycles through alternatives on repeat taps.",
|
||||
&font,
|
||||
4,
|
||||
);
|
||||
spawn_action_button(
|
||||
row,
|
||||
ModesButton,
|
||||
@@ -709,7 +724,7 @@ fn spawn_action_buttons(
|
||||
None,
|
||||
"Switch modes: Classic, Daily, Zen, Challenge, Time Attack.",
|
||||
&font,
|
||||
4,
|
||||
5,
|
||||
);
|
||||
spawn_action_button(
|
||||
row,
|
||||
@@ -718,7 +733,7 @@ fn spawn_action_buttons(
|
||||
Some("N"),
|
||||
"Start a fresh deal. Confirms first if a game is in progress.",
|
||||
&font,
|
||||
5,
|
||||
6,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -857,6 +872,36 @@ fn handle_help_button(
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_hint_button(
|
||||
interaction_query: Query<&Interaction, (With<HintButton>, Changed<Interaction>)>,
|
||||
paused: Option<Res<crate::PausedResource>>,
|
||||
game: Option<Res<GameStateResource>>,
|
||||
solver_config: Option<Res<crate::input_plugin::HintSolverConfig>>,
|
||||
mut pending_hint: Option<ResMut<crate::pending_hint::PendingHintTask>>,
|
||||
mut info_toast: MessageWriter<InfoToastEvent>,
|
||||
) {
|
||||
for interaction in &interaction_query {
|
||||
if *interaction != Interaction::Pressed {
|
||||
continue;
|
||||
}
|
||||
if paused.as_ref().is_some_and(|p| p.0) {
|
||||
return;
|
||||
}
|
||||
let Some(ref g) = game else { return };
|
||||
if g.0.is_won {
|
||||
#[cfg(target_os = "android")]
|
||||
let won_msg = "Game won! Tap New Game to play again";
|
||||
#[cfg(not(target_os = "android"))]
|
||||
let won_msg = "Game won! Press N for a new game";
|
||||
info_toast.write(InfoToastEvent(won_msg.to_string()));
|
||||
return;
|
||||
}
|
||||
if let (Some(cfg), Some(hint)) = (solver_config.as_ref(), pending_hint.as_mut()) {
|
||||
hint.spawn(g.0.clone(), cfg.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggles the [`ModesPopover`]: spawns it on first click, despawns it on
|
||||
/// second click. Mode rows are populated per the player's current level so
|
||||
/// only unlocked options appear.
|
||||
@@ -2648,6 +2693,7 @@ mod tests {
|
||||
focusable_for::<UndoButton>(&mut app),
|
||||
focusable_for::<PauseButton>(&mut app),
|
||||
focusable_for::<HelpButton>(&mut app),
|
||||
focusable_for::<HintButton>(&mut app),
|
||||
focusable_for::<ModesButton>(&mut app),
|
||||
focusable_for::<NewGameButton>(&mut app),
|
||||
] {
|
||||
@@ -2756,6 +2802,10 @@ mod tests {
|
||||
tooltip_for::<HelpButton>(&mut app),
|
||||
"Show controls, rules, and keyboard shortcuts."
|
||||
);
|
||||
assert_eq!(
|
||||
tooltip_for::<HintButton>(&mut app),
|
||||
"Highlight a suggested move. Cycles through alternatives on repeat taps."
|
||||
);
|
||||
assert_eq!(
|
||||
tooltip_for::<ModesButton>(&mut app),
|
||||
"Switch modes: Classic, Daily, Zen, Challenge, Time Attack."
|
||||
@@ -2875,14 +2925,15 @@ mod tests {
|
||||
fn hud_button_order_matches_spawn_order() {
|
||||
let mut app = headless_app();
|
||||
// Visual reading order (left → right): Menu, Undo, Pause, Help,
|
||||
// Modes, New Game. Their `order` fields must be 0..=5 in that
|
||||
// order so Tab cycles them as the player reads them.
|
||||
// Hint, Modes, New Game. Their `order` fields must be 0..=6 in
|
||||
// that order so Tab cycles them as the player reads them.
|
||||
assert_eq!(focusable_for::<MenuButton>(&mut app).order, 0);
|
||||
assert_eq!(focusable_for::<UndoButton>(&mut app).order, 1);
|
||||
assert_eq!(focusable_for::<PauseButton>(&mut app).order, 2);
|
||||
assert_eq!(focusable_for::<HelpButton>(&mut app).order, 3);
|
||||
assert_eq!(focusable_for::<ModesButton>(&mut app).order, 4);
|
||||
assert_eq!(focusable_for::<NewGameButton>(&mut app).order, 5);
|
||||
assert_eq!(focusable_for::<HintButton>(&mut app).order, 4);
|
||||
assert_eq!(focusable_for::<ModesButton>(&mut app).order, 5);
|
||||
assert_eq!(focusable_for::<NewGameButton>(&mut app).order, 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user