diff --git a/solitaire_engine/src/input_plugin.rs b/solitaire_engine/src/input_plugin.rs index a02a1bd..10245d3 100644 --- a/solitaire_engine/src/input_plugin.rs +++ b/solitaire_engine/src/input_plugin.rs @@ -105,6 +105,7 @@ impl Plugin for InputPlugin { // Touch drag pipeline (parallel path through DragState). touch_start_drag, touch_follow_drag, + handle_double_tap, // before touch_end_drag: reads drag state pre-clear touch_end_drag.before(GameMutation), ) .chain(), @@ -1204,12 +1205,16 @@ fn pile_drop_rect(pile: &PileType, layout: &Layout, game: &GameState) -> (Vec2, } // --------------------------------------------------------------------------- -// Task #27 — Double-click to auto-move +// Task #27 — Double-click / double-tap to auto-move // --------------------------------------------------------------------------- /// Maximum seconds between two clicks to count as a double-click. const DOUBLE_CLICK_WINDOW: f32 = 0.35; +/// Maximum seconds between two taps to count as a double-tap. +/// Slightly wider than the mouse window — touch screens have higher latency. +const DOUBLE_TAP_WINDOW: f32 = 0.5; + /// Find the best legal destination for `card` — Foundation first, then Tableau. /// /// Returns `None` if no legal move exists from the card's current location. @@ -1363,6 +1368,124 @@ fn handle_double_click( } } +// --------------------------------------------------------------------------- +// Task #27b — Double-tap to auto-move (touch equivalent of double-click) +// --------------------------------------------------------------------------- + +/// System that detects double-taps on face-up cards and fires `MoveRequestEvent` +/// to the best legal destination — the touch equivalent of [`handle_double_click`]. +/// +/// Must run **before** `touch_end_drag` in the system chain. At +/// `TouchPhase::Ended` the drag state still holds `active_touch_id`, +/// `cards`, and `origin_pile`; once `touch_end_drag` fires those fields +/// are cleared and the tap/drag distinction is permanently lost. +/// +/// A pure tap is identified by `drag.active_touch_id.is_some() && +/// !drag.committed`: the touch began (so `touch_start_drag` populated +/// `drag`) but the drag threshold was never crossed. +/// +/// Move priority matches [`handle_double_click`]: +/// 1. Move the single top card to its best foundation (or tableau). +/// 2. If no single-card move exists and the selection spans multiple +/// face-up cards, move the whole stack to the best tableau column. +/// 3. If both priorities fail, fire `MoveRejectedEvent` for audio + shake +/// feedback. +#[allow(clippy::too_many_arguments)] +fn handle_double_tap( + mut touch_events: MessageReader, + paused: Option>, + time: Res