fix(android): UX pass — pause stacking, timer, help content, achievement glyphs
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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::<crate::events::AchievementUnlockedEvent>()
|
||||
.add_message::<FoundationCompletedEvent>()
|
||||
.add_message::<InfoToastEvent>()
|
||||
.add_message::<AppLifecycle>()
|
||||
.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<Time>,
|
||||
mut game: ResMut<GameStateResource>,
|
||||
mut accumulator: Local<f32>,
|
||||
mut skip_next_delta: Local<bool>,
|
||||
paused: Option<Res<crate::pause_plugin::PausedResource>>,
|
||||
home_screens: Query<(), With<crate::home_plugin::HomeScreen>>,
|
||||
onboarding_screens: Query<(), With<crate::onboarding_plugin::OnboardingScreen>>,
|
||||
mut lifecycle: MessageReader<AppLifecycle>,
|
||||
) {
|
||||
for event in lifecycle.read() {
|
||||
if matches!(event, AppLifecycle::WillSuspend | AppLifecycle::Suspended) {
|
||||
*skip_next_delta = true;
|
||||
}
|
||||
}
|
||||
if paused.is_some_and(|p| p.0)
|
||||
|| !home_screens.is_empty()
|
||||
|| !onboarding_screens.is_empty()
|
||||
{
|
||||
return;
|
||||
}
|
||||
if *skip_next_delta {
|
||||
*skip_next_delta = false;
|
||||
return;
|
||||
}
|
||||
let is_won = game.0.is_won;
|
||||
advance_elapsed(
|
||||
&mut game.0.elapsed_seconds,
|
||||
|
||||
Reference in New Issue
Block a user