From 04f3dab5637892d836f62bc326f7187c259cfeff Mon Sep 17 00:00:00 2001 From: funman300 Date: Tue, 12 May 2026 20:02:39 -0700 Subject: [PATCH] =?UTF-8?q?fix(android):=20UX=20pass=20=E2=80=94=20pause?= =?UTF-8?q?=20stacking,=20timer,=20help=20content,=20achievement=20glyphs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BUG-1: pause_plugin — auto_resume_on_overlay system despawns PauseScreen whenever any other ModalScrim becomes live; fixes Pause modal stacking on top of Stats / Settings / Help / Achievements / Profile overlays opened from the HUD menu while paused. BUG-2: game_plugin — tick_elapsed_time skips the first delta_secs after AppLifecycle::WillSuspend/Suspended so the Android post-resume frame spike (equal to the full suspension duration) no longer inflates the in-game timer. UX-2: help_plugin — Android build gets a touch-specific CONTROL_SECTIONS (Tap / New Game / HUD buttons); desktop sections (Mouse, Keyboard drag, Mode Launcher, Overlays) remain on non-Android builds only. UX-3: achievement_plugin — replace \u{25CB} (○) and \u{2713} (✓) prefixes with ASCII "- " / "+ "; both Geometric Shapes codepoints are absent from FiraMono and rendered as the fallback letter "o". Phase 8 work from previous session (already compiled, not yet committed): hud_plugin — HUD visual hierarchy (Undo/Pause bright, nav buttons dim); menu popover — Help + Game Modes entries added (7 items total). card_plugin — stock badge drops "·" prefix, shows plain count. pause_plugin — Draw Mode segmented control (Draw 1 / Draw 3 explicit buttons). Co-Authored-By: Claude Sonnet 4.6 --- solitaire_engine/src/achievement_plugin.rs | 4 +- solitaire_engine/src/card_plugin.rs | 12 +- solitaire_engine/src/game_plugin.rs | 19 +++ solitaire_engine/src/help_plugin.rs | 30 +++++ solitaire_engine/src/hud_plugin.rs | 60 +++++++-- solitaire_engine/src/pause_plugin.rs | 148 ++++++++++++++------- 6 files changed, 207 insertions(+), 66 deletions(-) diff --git a/solitaire_engine/src/achievement_plugin.rs b/solitaire_engine/src/achievement_plugin.rs index 9419a6e..b7f2b92 100644 --- a/solitaire_engine/src/achievement_plugin.rs +++ b/solitaire_engine/src/achievement_plugin.rs @@ -533,9 +533,9 @@ fn spawn_achievements_screen( } let (name_color, desc_color, prefix) = if record.unlocked { - (ACCENT_PRIMARY, TEXT_PRIMARY, "\u{2713} ") + (ACCENT_PRIMARY, TEXT_PRIMARY, "+ ") } else { - (TEXT_DISABLED, TEXT_DISABLED, "\u{25CB} ") + (TEXT_DISABLED, TEXT_DISABLED, "- ") }; let tooltip_text = tooltip_for_row(record.unlocked, def); diff --git a/solitaire_engine/src/card_plugin.rs b/solitaire_engine/src/card_plugin.rs index e9ccd82..1a521c4 100644 --- a/solitaire_engine/src/card_plugin.rs +++ b/solitaire_engine/src/card_plugin.rs @@ -1484,7 +1484,7 @@ fn update_stock_empty_indicator( // --------------------------------------------------------------------------- // Stock-pile remaining-count badge // -// Shows a small "·N" chip pinned to the top-right corner of the stock pile so +// Shows a small "N" chip pinned to the top-right corner of the stock pile so // the player can see how many cards remain before the next recycle. The // existing `StockEmptyLabel` (`↺` overlay) covers the empty-stock case, so // the badge hides itself when the stock has zero cards — the two indicators @@ -1562,7 +1562,7 @@ fn spawn_stock_count_badge( .with_children(|b| { b.spawn(( StockCountBadgeText, - Text2d::new(format!("·{count}")), + Text2d::new(format!("{count}")), text_font, TextColor(STOCK_BADGE_FG), // Slightly above the chip background so the digits aren't @@ -1624,7 +1624,7 @@ fn update_stock_count_badge( if let Ok(badge_children) = children.get(entity) { for child in badge_children.iter() { if let Ok(mut text) = texts.get_mut(child) { - let new = format!("·{count}"); + let new = format!("{count}"); if text.0 != new { text.0 = new; } @@ -2811,7 +2811,7 @@ mod tests { // First update inside `app()` runs the spawn path; run one more to // confirm the in-place update path is also stable. app.update(); - assert_eq!(stock_badge_text(&mut app), "·24"); + assert_eq!(stock_badge_text(&mut app), "24"); assert!(matches!(stock_badge_visibility(&mut app), Visibility::Inherited)); } @@ -2837,7 +2837,7 @@ mod tests { // initial 24) and assert the badge text follows. let mut app = app(); // Sanity-check the starting count. - assert_eq!(stock_badge_text(&mut app), "·24"); + assert_eq!(stock_badge_text(&mut app), "24"); { let mut game = app.world_mut().resource_mut::(); if let Some(stock) = game.0.piles.get_mut(&PileType::Stock) { @@ -2845,7 +2845,7 @@ mod tests { } } app.update(); - assert_eq!(stock_badge_text(&mut app), "·23"); + assert_eq!(stock_badge_text(&mut app), "23"); assert!(matches!(stock_badge_visibility(&mut app), Visibility::Inherited)); } diff --git a/solitaire_engine/src/game_plugin.rs b/solitaire_engine/src/game_plugin.rs index 4784325..a77354d 100644 --- a/solitaire_engine/src/game_plugin.rs +++ b/solitaire_engine/src/game_plugin.rs @@ -11,6 +11,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use bevy::prelude::*; use bevy::tasks::{futures_lite::future, AsyncComputeTaskPool, Task}; +use bevy::window::AppLifecycle; use chrono::Utc; use solitaire_core::game_state::{DrawMode, GameMode, GameState}; use solitaire_core::pile::PileType; @@ -200,6 +201,7 @@ impl Plugin for GamePlugin { .add_message::() .add_message::() .add_message::() + .add_message::() .add_systems( Update, poll_pending_new_game_seed.before(GameMutation), @@ -259,20 +261,37 @@ pub fn advance_elapsed( /// timer doesn't tick before the player commits to a deal; stops while /// the onboarding modal is visible so a new player's first-game time /// isn't inflated by reading the tutorial. +/// +/// On Android the first frame after the app is resumed from background +/// can carry a very large `delta_secs` equal to the entire suspension +/// period. `skip_next_delta` is set to `true` on `WillSuspend` / +/// `Suspended` so that frame's delta is dropped instead of applied. +#[allow(clippy::too_many_arguments)] fn tick_elapsed_time( time: Res