feat(engine): Phase F touch action bar — thumb-reach Undo/Draw/Hint + hold-to-repeat undo
Touch layout (USE_TOUCH_UI_LAYOUT) restructures the bottom action bar to five buttons: an enlarged Undo / Draw / Hint trio (96x64px targets, 1.35x labels) between compact Menu and Pause. Draw is new — it fires the same DrawRequestEvent as tapping the stock, so the most frequent action no longer needs a reach to the top of a tall folded screen. Help, Modes, and New Game leave the touch bar (they live in Menu -> System, the Home grid, and Home's hero respectively). Desktop keeps the seven-button bar unchanged (decision 5: touch-only). Holding Undo now steps back repeatedly after a 0.45s delay (5.5/s), each step through the normal request path so the scoring penalty applies. New self-ambiguous DrawRequestWriters set keeps the ambiguity gate at zero with the fourth DrawRequestEvent writer. 6 new hud_plugin tests; workspace suite + clippy green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,86 @@ pub(super) fn handle_undo_button(
|
||||
}
|
||||
}
|
||||
|
||||
/// Seconds the Undo button must be continuously held before hold-to-repeat
|
||||
/// kicks in. Long enough that a normal tap (press + release inside one or
|
||||
/// two frames) never triggers a second undo.
|
||||
const UNDO_HOLD_INITIAL_DELAY_SECS: f32 = 0.45;
|
||||
|
||||
/// Interval between repeated undos while the hold continues. ~5.5 undos/s —
|
||||
/// fast enough to unwind a long line, slow enough to release in time when
|
||||
/// the board reaches the state the player wants.
|
||||
const UNDO_HOLD_REPEAT_INTERVAL_SECS: f32 = 0.18;
|
||||
|
||||
/// Hold-to-repeat undo (Phase F): while the Undo button stays pressed,
|
||||
/// fire additional [`UndoRequestEvent`]s after an initial delay, one per
|
||||
/// repeat interval. The plain tap path stays in [`handle_undo_button`]
|
||||
/// (its `Changed<Interaction>` filter fires exactly once per press);
|
||||
/// this system only adds events once the hold outlives the delay, so a
|
||||
/// tap never double-undoes.
|
||||
///
|
||||
/// Each repeat goes through the normal request queue — the scoring
|
||||
/// penalty and No-Undo-mode gating in the consumer apply to every step.
|
||||
pub(super) fn repeat_undo_on_hold(
|
||||
time: Res<Time>,
|
||||
buttons: Query<&Interaction, With<UndoButton>>,
|
||||
mut state: ResMut<UndoHoldState>,
|
||||
mut undo: MessageWriter<UndoRequestEvent>,
|
||||
) {
|
||||
let held = buttons.iter().any(|i| *i == Interaction::Pressed);
|
||||
if !held {
|
||||
if state.held_secs != 0.0 {
|
||||
state.held_secs = 0.0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let before = state.held_secs;
|
||||
state.held_secs += time.delta_secs();
|
||||
if undo_hold_crossed_fire_boundary(before, state.held_secs) {
|
||||
undo.write(UndoRequestEvent);
|
||||
}
|
||||
}
|
||||
|
||||
/// `true` when a hold that lasted `before` seconds at the previous frame
|
||||
/// and `now` seconds this frame should fire a repeat undo: once when the
|
||||
/// hold first outlives the initial delay, then once per repeat-interval
|
||||
/// boundary. Pure so the timing contract is unit-testable without
|
||||
/// fighting `Time`.
|
||||
pub(super) fn undo_hold_crossed_fire_boundary(before: f32, now: f32) -> bool {
|
||||
if now < UNDO_HOLD_INITIAL_DELAY_SECS {
|
||||
return false;
|
||||
}
|
||||
if before < UNDO_HOLD_INITIAL_DELAY_SECS {
|
||||
// Crossed the initial-delay threshold this frame — first repeat.
|
||||
return true;
|
||||
}
|
||||
// Fire once each time the hold crosses another repeat-interval boundary.
|
||||
let intervals_before =
|
||||
((before - UNDO_HOLD_INITIAL_DELAY_SECS) / UNDO_HOLD_REPEAT_INTERVAL_SECS).floor();
|
||||
let intervals_now =
|
||||
((now - UNDO_HOLD_INITIAL_DELAY_SECS) / UNDO_HOLD_REPEAT_INTERVAL_SECS).floor();
|
||||
intervals_now > intervals_before
|
||||
}
|
||||
|
||||
/// Click on the touch action bar's Draw button — same
|
||||
/// [`DrawRequestEvent`] the stock-pile tap writes, so the consumer's
|
||||
/// rules (recycle, won-game rejection) apply identically. Skipped while
|
||||
/// paused, mirroring `handle_stock_click`'s guard.
|
||||
pub(super) fn handle_draw_button(
|
||||
interaction_query: Query<&Interaction, (With<DrawButton>, Changed<Interaction>)>,
|
||||
paused: Option<Res<PausedResource>>,
|
||||
mut draw: MessageWriter<DrawRequestEvent>,
|
||||
) {
|
||||
if paused.is_some_and(|p| p.0) {
|
||||
return;
|
||||
}
|
||||
for interaction in &interaction_query {
|
||||
if *interaction == Interaction::Pressed {
|
||||
draw.write(DrawRequestEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn handle_pause_button(
|
||||
interaction_query: Query<&Interaction, (With<PauseButton>, Changed<Interaction>)>,
|
||||
mut pause: MessageWriter<PauseRequestEvent>,
|
||||
|
||||
Reference in New Issue
Block a user