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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user