fix(ux): 14 cross-platform UX/UI fixes from 500-game audit
Web client (game.js): - Restart game timer after undo exits auto-complete sequence - Pause timer while browser tab is hidden (visibilitychange) - Validate URL seed — NaN / negative falls back to randomSeed() - Guard onBoardClick/onBoardDblClick during win (snap.is_won) - Delay win overlay 320 ms so last card CSS transition finishes - Force reflow in flashIllegal() to restart shake on rapid re-trigger Android (safe_area.rs): - Preserve last-known insets on app resume instead of zeroing them; eliminates double layout flash on every foreground cycle All clients — Bevy engine: - Radial menu: clamp icon anchors to viewport bounds so icons are never placed off-screen on narrow phones - Auto-complete: deactivate state.active when is_auto_completable goes false (undo mid-sequence) to stop perpetual background retry - Touch selection: gate highlight rebuild on is_changed() — was despawning/respawning entities every frame unnecessarily - Input: fire "Tap a pile to move" InfoToast on first tap in TapToSelect mode; document cursor_world 1:1 viewport invariant - Drag threshold: raise desktop from 4 → 6 px to prevent accidental drags from cursor jitter on HiDPI displays Desktop / Android (solitaire_app): - Call cleanup_orphaned_tmp_files() at startup to remove .tmp files left by crashes between atomic write and rename Design clarification (klondike_adapter.rs): - Doc comment: Draw-1 recycling is penalty-only by design (never blocked) to avoid creating unwinnable positions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -51,15 +51,15 @@ impl Plugin for AutoCompletePlugin {
|
||||
app.init_resource::<AutoCompleteState>()
|
||||
.add_message::<RequestRedraw>()
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
detect_auto_complete,
|
||||
on_auto_complete_start,
|
||||
drive_auto_complete,
|
||||
)
|
||||
.chain()
|
||||
.after(GameMutation),
|
||||
);
|
||||
Update,
|
||||
(
|
||||
detect_auto_complete,
|
||||
on_auto_complete_start,
|
||||
drive_auto_complete,
|
||||
)
|
||||
.chain()
|
||||
.after(GameMutation),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,14 +83,21 @@ fn detect_auto_complete(
|
||||
if game.0.is_auto_completable && !state.active {
|
||||
state.active = true;
|
||||
state.cooldown = AUTO_COMPLETE_INITIAL_DELAY;
|
||||
} else if !game.0.is_auto_completable && state.active {
|
||||
// `is_auto_completable` only becomes false after an explicit undo
|
||||
// (which puts a card back on the tableau or re-fills the stock/waste)
|
||||
// or a new-game reset — never as a transient gap during a normal
|
||||
// auto-complete sequence. Deactivate here so `drive_auto_complete`
|
||||
// does not keep retrying indefinitely after the player undoes out of
|
||||
// the sequence.
|
||||
//
|
||||
// Note: the transient-`None` case mentioned in older versions of this
|
||||
// comment referred to `next_auto_complete_move()` returning `None`, not
|
||||
// to `is_auto_completable` being false. Those are independent fields;
|
||||
// `drive_auto_complete` still retries on a transient `None` return from
|
||||
// `next_auto_complete_move` because that check happens there, not here.
|
||||
state.active = false;
|
||||
}
|
||||
// Intentionally no `else if !is_auto_completable` branch here.
|
||||
// Deactivating on every frame where `is_auto_completable` is false
|
||||
// would hard-stop the sequence mid-flight whenever `next_auto_complete_move`
|
||||
// transiently returns `None` (e.g. while the previous move is still
|
||||
// in-flight). The `is_won` check above already handles the definitive
|
||||
// end-of-game case; `drive_auto_complete` simply retries next tick
|
||||
// when no move is available yet.
|
||||
}
|
||||
|
||||
/// Plays a distinct chime the moment auto-complete first activates.
|
||||
@@ -244,9 +251,7 @@ mod tests {
|
||||
|
||||
// Zero out the cooldown so drive fires on the next update regardless
|
||||
// of the initial delay constant.
|
||||
app.world_mut()
|
||||
.resource_mut::<AutoCompleteState>()
|
||||
.cooldown = 0.0;
|
||||
app.world_mut().resource_mut::<AutoCompleteState>().cooldown = 0.0;
|
||||
app.update(); // drive fires the move
|
||||
|
||||
let events = app.world().resource::<Messages<MoveRequestEvent>>();
|
||||
|
||||
Reference in New Issue
Block a user