fix(engine): single-card double-click with no destination now plays the reject animation
handle_double_click had a coverage gap. The flow was:
- Priority 1: try moving the single top card to its best
destination (foundation, then tableau).
- Priority 2: if Priority 1 failed AND the player clicked the
base of a multi-card stack, try moving the whole stack.
`MoveRejectedEvent` was only fired inside the Priority 2 else-branch
— so a double-click on a single card with no legal destination
fell through both priorities silently: no card_invalid.wav, no
shake animation on the source pile, the player got zero feedback
that the click was acknowledged.
The fix collapses both priorities' failure paths into one
unconditional `MoveRejectedEvent` write at the end of the
double-click branch. Single-card miss now plays the same feedback
as multi-card-stack miss. The early `return` on each successful
move keeps the rejection branch from firing on the success path.
Pre-fix, a player double-clicking the 7♠ buried under a 6♥ on
column 5 (no foundation slot for 7s; no tableau column accepting
black 7) saw nothing happen. Post-fix, the source pile shakes
and the invalid-move sound plays, exactly like a drag-and-drop
rejection.
Workspace: 1170 passing tests / 0 failing. cargo clippy
--workspace --all-targets -- -D warnings clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1310,32 +1310,37 @@ fn handle_double_click(
|
|||||||
// Priority 2: if the player clicked the base of a multi-card face-up
|
// Priority 2: if the player clicked the base of a multi-card face-up
|
||||||
// stack (card_ids.len() > 1), try moving the whole stack to another
|
// stack (card_ids.len() > 1), try moving the whole stack to another
|
||||||
// tableau column.
|
// tableau column.
|
||||||
if card_ids.len() > 1 {
|
if card_ids.len() > 1
|
||||||
let Some(bottom_card) = game.0.piles.get(&pile)
|
&& let Some(bottom_card) = game.0.piles.get(&pile)
|
||||||
.and_then(|p| p.cards.get(stack_index)) else { return };
|
.and_then(|p| p.cards.get(stack_index))
|
||||||
if let Some((dest, count)) = best_tableau_destination_for_stack(
|
&& let Some((dest, count)) = best_tableau_destination_for_stack(
|
||||||
bottom_card,
|
bottom_card,
|
||||||
&pile,
|
&pile,
|
||||||
&game.0,
|
&game.0,
|
||||||
card_ids.len(),
|
card_ids.len(),
|
||||||
) {
|
)
|
||||||
moves.write(MoveRequestEvent {
|
{
|
||||||
from: pile,
|
moves.write(MoveRequestEvent {
|
||||||
to: dest,
|
from: pile,
|
||||||
count,
|
to: dest,
|
||||||
});
|
count,
|
||||||
} else {
|
});
|
||||||
// No legal destination for the stack — play the invalid-move
|
return;
|
||||||
// sound and shake the source pile cards as feedback.
|
|
||||||
// `MoveRejectedEvent` with `from == to` routes the shake to
|
|
||||||
// the source pile (which `start_shake_anim` reads from `ev.to`).
|
|
||||||
rejected.write(MoveRejectedEvent {
|
|
||||||
from: pile.clone(),
|
|
||||||
to: pile,
|
|
||||||
count: card_ids.len(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Both priorities failed — play the invalid-move sound and shake
|
||||||
|
// the source pile as feedback. `MoveRejectedEvent` with
|
||||||
|
// `from == to` routes the shake to the source pile (which
|
||||||
|
// `start_shake_anim` reads from `ev.to`). Pre-fix, this branch
|
||||||
|
// only fired for multi-card stacks, so a double-click on a
|
||||||
|
// single card with no legal destination did nothing — no
|
||||||
|
// sound, no shake. Now both single-card and stack misses get
|
||||||
|
// the same feedback.
|
||||||
|
rejected.write(MoveRejectedEvent {
|
||||||
|
from: pile.clone(),
|
||||||
|
to: pile,
|
||||||
|
count: card_ids.len(),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// Single click — record the time.
|
// Single click — record the time.
|
||||||
last_click.insert(top_card_id, now);
|
last_click.insert(top_card_id, now);
|
||||||
|
|||||||
Reference in New Issue
Block a user