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:
funman300
2026-06-01 21:23:52 -07:00
parent 20e5222148
commit 64f975ed6d
9 changed files with 571 additions and 216 deletions
+21 -15
View File
@@ -77,8 +77,9 @@ impl TouchSelectionState {
/// Marker component placed on the highlight sprite child of a selected source card.
///
/// Despawned and respawned each frame by [`update_touch_selection_highlight`] so
/// stale highlights never linger after a game-state change.
/// Despawned and respawned by [`update_touch_selection_highlight`] whenever
/// [`TouchSelectionState`] changes. The system is gated on `is_changed()` so it
/// is a no-op every frame that the selection is stable.
#[derive(Component)]
pub struct TouchSelectionHighlight;
@@ -91,16 +92,15 @@ pub struct TouchSelectionPlugin;
impl Plugin for TouchSelectionPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<TouchSelectionState>()
.add_systems(
Update,
(
clear_touch_selection_on_state_change,
update_touch_selection_highlight,
)
.chain()
.after(GameMutation),
);
app.init_resource::<TouchSelectionState>().add_systems(
Update,
(
clear_touch_selection_on_state_change,
update_touch_selection_highlight,
)
.chain()
.after(GameMutation),
);
}
}
@@ -121,9 +121,9 @@ pub(crate) fn clear_touch_selection_on_state_change(
/// Maintains the `TouchSelectionHighlight` outline sprite on the selected source card.
///
/// All existing `TouchSelectionHighlight` entities are despawned each frame and
/// a new one is spawned on the top card of the selected pile (if any). This
/// matches the pattern used by `selection_plugin::update_selection_highlight`.
/// Rebuilds the highlight set only when [`TouchSelectionState`] or the layout
/// actually changes — not every frame. Existing highlights are despawned first,
/// then a fresh highlight is spawned on every card in the selected stack.
pub(crate) fn update_touch_selection_highlight(
mut commands: Commands,
selection: Res<TouchSelectionState>,
@@ -131,6 +131,12 @@ pub(crate) fn update_touch_selection_highlight(
highlights: Query<Entity, With<TouchSelectionHighlight>>,
layout: Option<Res<LayoutResource>>,
) {
// Skip when neither the selection nor the layout changed this frame.
let layout_changed = layout.as_ref().map(|l| l.is_changed()).unwrap_or(false);
if !selection.is_changed() && !layout_changed {
return;
}
// Despawn stale highlights first.
for entity in &highlights {
commands.entity(entity).despawn();