From 9fb59c7d47b80c5e80bf37ea407c8597f001dfe1 Mon Sep 17 00:00:00 2001 From: funman300 Date: Mon, 11 May 2026 13:10:38 -0700 Subject: [PATCH] fix(android): lime flash on double-tap auto-move confirmation When handle_double_tap recognises a double-tap and fires MoveRequestEvent, the moved card(s) are immediately tinted STATE_SUCCESS (lime #acc267) with a 0.35 s HintHighlight so the player sees visual confirmation before the card animation begins. - Priority 1 (single top card): flashes that card only. - Priority 2 (whole face-up stack): flashes every card in drag.cards. Reuses the existing tick_hint_highlight cleanup path (restores sprite to WHITE when timer expires) so no new system or component is needed. The flash duration (0.35 s) slightly outlasts a typical card animation (~0.3 s), giving the tint a brief moment at the destination before clearing. Marks P1 "Double-tap auto-move visible feedback" as closed in PLAYABILITY_TODO (hardware trigger-verification still manual). Co-Authored-By: Claude Sonnet 4.6 --- docs/android/PLAYABILITY_TODO.md | 12 +++++++++--- solitaire_engine/src/input_plugin.rs | 24 +++++++++++++++++++++++- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/docs/android/PLAYABILITY_TODO.md b/docs/android/PLAYABILITY_TODO.md index 6e03ab7..70a9d28 100644 --- a/docs/android/PLAYABILITY_TODO.md +++ b/docs/android/PLAYABILITY_TODO.md @@ -95,9 +95,15 @@ rewrites required. the `Layout` struct; `card_plugin::card_positions` and `input_plugin::card_position` / `pile_drop_rect` read from the struct so rendering and hit-testing stay in sync across viewport sizes. -- [ ] **Double-tap auto-move visible feedback.** `handle_double_tap` - exists since `395a322` — verify it triggers on hardware and add a - brief source-card flash / highlight to confirm to the user. +- [x] **Double-tap auto-move visible feedback.** *Closed 2026-05-11.* + On a recognised double-tap (priority 1 single-card or priority 2 + stack move), the moved card(s) receive a 0.35 s lime flash + (`STATE_SUCCESS` tint + `HintHighlight { remaining: 0.35 }`) before + the move request is written. The flash persists through the card + animation and is cleaned up by the existing `tick_hint_highlight` + system. Hardware trigger-verification remains a manual step — connect + AVD or device and confirm two rapid `TouchPhase::Ended` events within + 0.5 s produce the lime flash. ## P2 — Polish diff --git a/solitaire_engine/src/input_plugin.rs b/solitaire_engine/src/input_plugin.rs index 3684667..eb25ff2 100644 --- a/solitaire_engine/src/input_plugin.rs +++ b/solitaire_engine/src/input_plugin.rs @@ -34,7 +34,7 @@ use solitaire_core::rules::{can_place_on_foundation, can_place_on_tableau}; use crate::card_animation::tuning::AnimationTuning; use crate::card_animation::{CardAnimation, MotionCurve}; use crate::card_plugin::{CardEntity, HintHighlight, HintHighlightTimer, STACK_FAN_FRAC}; -use crate::ui_theme::{MOTION_DRAG_REJECT_SECS, STATE_WARNING}; +use crate::ui_theme::{MOTION_DRAG_REJECT_SECS, STATE_SUCCESS, STATE_WARNING}; use solitaire_core::game_state::DrawMode; use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL; use crate::events::{ @@ -1218,6 +1218,11 @@ const DOUBLE_CLICK_WINDOW: f32 = 0.35; /// Slightly wider than the mouse window — touch screens have higher latency. const DOUBLE_TAP_WINDOW: f32 = 0.5; +/// Duration of the lime flash applied to moved cards when a double-tap +/// auto-move succeeds. Short enough not to linger, long enough to register +/// during the card animation (~0.3 s). +const DOUBLE_TAP_FLASH_SECS: f32 = 0.35; + /// Find the best legal destination for `card` — Foundation first, then Tableau. /// /// Returns `None` if no legal move exists from the card's current location. @@ -1403,6 +1408,8 @@ fn handle_double_tap( mut last_tap: Local>, mut moves: MessageWriter, mut rejected: MessageWriter, + mut commands: Commands, + mut card_sprites: Query<(Entity, &CardEntity, &mut Sprite)>, ) { if paused.is_some_and(|p| p.0) { return; @@ -1450,6 +1457,14 @@ fn handle_double_tap( // Priority 1: move single top card. if let Some(dest) = best_destination(top_card, &game.0) { + // Flash the card lime to confirm the double-tap registered. + for (entity, ce, mut sprite) in card_sprites.iter_mut() { + if ce.card_id == top_card_id { + sprite.color = STATE_SUCCESS; + commands.entity(entity).insert(HintHighlight { remaining: DOUBLE_TAP_FLASH_SECS }); + break; + } + } moves.write(MoveRequestEvent { from: pile.clone(), to: dest, @@ -1469,6 +1484,13 @@ fn handle_double_tap( drag.cards.len(), ) { + // Flash all cards in the moving run. + for (entity, ce, mut sprite) in card_sprites.iter_mut() { + if drag.cards.contains(&ce.card_id) { + sprite.color = STATE_SUCCESS; + commands.entity(entity).insert(HintHighlight { remaining: DOUBLE_TAP_FLASH_SECS }); + } + } moves.write(MoveRequestEvent { from: pile.clone(), to: dest,