Compare commits

...

2 Commits

Author SHA1 Message Date
funman300 38e4c0341e feat(engine): reactive render — animations drive RequestRedraw, focused_mode reactive on Android
All per-frame animation tick systems now write MessageWriter<RequestRedraw>
each frame they have active work, allowing WinitSettings focused_mode to
switch from Continuous to reactive_low_power(100 ms) on Android.

Systems updated:
- advance_card_animations (CardAnimationPlugin)
- advance_card_anims (AnimationPlugin — deal/win cascade)
- tick_shake_anim, tick_settle_anim, tick_foundation_flourish (FeedbackAnimPlugin)
- drive_toast_display (AnimationPlugin — toast countdown)
- drive_auto_complete (AutoCompletePlugin — step interval keepalive)

The 100 ms low-power ceiling means the game timer still ticks ~10×/s
with no input; animations self-sustain via the redraw chain at full
frame rate while active; and the GPU is completely idle between frames
when the board is static.

Each plugin registers add_message::<RequestRedraw>() so the message
type is available under MinimalPlugins in unit tests.

Closes #78, #79

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-29 13:54:54 -07:00
funman300 ccf280ea50 fix(engine): add missing modal scrim guard to leaderboard panel
Android Release / build-apk (push) Successful in 4m29s
toggle_leaderboard_screen was missing the other_modal_scrims guard that
all other panel-toggle systems have. Pressing L (or the HUD button) while
any other modal was open would spawn a second ModalScrim on top of the
existing one, breaking z-ordering and leaving the first modal un-dismissable.

Adds:
  other_modal_scrims: Query<(), (With<ModalScrim>, Without<LeaderboardScreen>)>
and the early-return guard before spawn_leaderboard_screen is called.

Closes #77

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 15:52:47 -07:00
6 changed files with 24 additions and 6 deletions
+8 -5
View File
@@ -172,13 +172,16 @@ fn build_app_with_settings(
// a 1-second ceiling when the app is backgrounded cuts wake-up frequency // a 1-second ceiling when the app is backgrounded cuts wake-up frequency
// from ~60 Hz to ≤1 Hz, dramatically reducing background battery drain. // from ~60 Hz to ≤1 Hz, dramatically reducing background battery drain.
// //
// The focused mode stays Continuous so that card-slide animations remain // focused_mode uses reactive_low_power(100 ms) so the CPU only wakes when
// smooth. PresentMode::AutoVsync (set above) keeps the GPU capped at the // an event arrives (touch, resize, etc.) or an animation system writes
// display refresh rate (~60 Hz) when foregrounded, which already prevents // RequestRedraw. The 100 ms ceiling is a fallback that ensures the game
// the GPU from spinning at 200+ fps between vsync intervals. // timer ticks at least 10×/s even with no input, while keeping the GPU
// completely idle between frames when the board is static.
// PresentMode::AutoVsync (set above) still caps the GPU at the display
// refresh rate when frames do render.
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
app.insert_resource(WinitSettings { app.insert_resource(WinitSettings {
focused_mode: UpdateMode::Continuous, focused_mode: UpdateMode::reactive_low_power(std::time::Duration::from_millis(100)),
unfocused_mode: UpdateMode::reactive_low_power(std::time::Duration::from_secs(1)), unfocused_mode: UpdateMode::reactive_low_power(std::time::Duration::from_secs(1)),
}); });
+2
View File
@@ -13,6 +13,7 @@
use std::collections::VecDeque; use std::collections::VecDeque;
use bevy::prelude::*; use bevy::prelude::*;
use bevy::window::RequestRedraw;
use solitaire_data::{AnimSpeed, Settings}; use solitaire_data::{AnimSpeed, Settings};
use crate::achievement_plugin::display_name_for; use crate::achievement_plugin::display_name_for;
@@ -180,6 +181,7 @@ impl Plugin for AnimationPlugin {
.add_message::<MoveRejectedEvent>() .add_message::<MoveRejectedEvent>()
.add_message::<WarningToastEvent>() .add_message::<WarningToastEvent>()
.add_message::<XpAwardedEvent>() .add_message::<XpAwardedEvent>()
.add_message::<RequestRedraw>()
.init_resource::<EffectiveSlideDuration>() .init_resource::<EffectiveSlideDuration>()
.init_resource::<ToastQueue>() .init_resource::<ToastQueue>()
.init_resource::<ActiveToast>() .init_resource::<ActiveToast>()
+4 -1
View File
@@ -9,6 +9,7 @@
//! returns `None` (e.g. a transient state), the plugin retries next tick. //! returns `None` (e.g. a transient state), the plugin retries next tick.
use bevy::prelude::*; use bevy::prelude::*;
use bevy::window::RequestRedraw;
use crate::audio_plugin::{AudioState, SoundLibrary}; use crate::audio_plugin::{AudioState, SoundLibrary};
use crate::events::{MoveRequestEvent, StateChangedEvent}; use crate::events::{MoveRequestEvent, StateChangedEvent};
@@ -39,7 +40,9 @@ pub struct AutoCompletePlugin;
impl Plugin for AutoCompletePlugin { impl Plugin for AutoCompletePlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.init_resource::<AutoCompleteState>().add_systems( app.init_resource::<AutoCompleteState>()
.add_message::<RequestRedraw>()
.add_systems(
Update, Update,
( (
detect_auto_complete, detect_auto_complete,
@@ -92,6 +92,7 @@ pub use timing::{
pub use tuning::{AnimationTuning, InputPlatform}; pub use tuning::{AnimationTuning, InputPlatform};
use bevy::prelude::*; use bevy::prelude::*;
use bevy::window::RequestRedraw;
use crate::card_plugin::CardEntity; use crate::card_plugin::CardEntity;
use crate::events::{DrawRequestEvent, GameWonEvent, MoveRequestEvent, UndoRequestEvent}; use crate::events::{DrawRequestEvent, GameWonEvent, MoveRequestEvent, UndoRequestEvent};
@@ -125,6 +126,7 @@ impl Plugin for CardAnimationPlugin {
.add_message::<DrawRequestEvent>() .add_message::<DrawRequestEvent>()
.add_message::<UndoRequestEvent>() .add_message::<UndoRequestEvent>()
.add_message::<GameWonEvent>() .add_message::<GameWonEvent>()
.add_message::<RequestRedraw>()
.init_resource::<DragState>() .init_resource::<DragState>()
.init_resource::<HoverState>() .init_resource::<HoverState>()
.init_resource::<InputBuffer>() .init_resource::<InputBuffer>()
@@ -42,6 +42,7 @@ use std::f32::consts::PI;
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use bevy::prelude::*; use bevy::prelude::*;
use bevy::window::RequestRedraw;
use solitaire_core::pile::PileType; use solitaire_core::pile::PileType;
use solitaire_data::AnimSpeed; use solitaire_data::AnimSpeed;
@@ -204,6 +205,7 @@ impl Plugin for FeedbackAnimPlugin {
.add_message::<MoveRejectedEvent>() .add_message::<MoveRejectedEvent>()
.add_message::<NewGameRequestEvent>() .add_message::<NewGameRequestEvent>()
.add_message::<FoundationCompletedEvent>() .add_message::<FoundationCompletedEvent>()
.add_message::<RequestRedraw>()
.add_systems( .add_systems(
Update, Update,
( (
@@ -191,6 +191,7 @@ fn toggle_leaderboard_screen(
keys: Res<ButtonInput<KeyCode>>, keys: Res<ButtonInput<KeyCode>>,
mut requests: MessageReader<ToggleLeaderboardRequestEvent>, mut requests: MessageReader<ToggleLeaderboardRequestEvent>,
screens: Query<Entity, With<LeaderboardScreen>>, screens: Query<Entity, With<LeaderboardScreen>>,
other_modal_scrims: Query<(), (With<ModalScrim>, Without<LeaderboardScreen>)>,
data: Res<LeaderboardResource>, data: Res<LeaderboardResource>,
provider: Option<Res<SyncProviderResource>>, provider: Option<Res<SyncProviderResource>>,
settings: Option<Res<SettingsResource>>, settings: Option<Res<SettingsResource>>,
@@ -208,6 +209,11 @@ fn toggle_leaderboard_screen(
return; return;
} }
// Don't stack a second modal scrim over one that is already open.
if !other_modal_scrims.is_empty() {
return;
}
// Spawn the panel immediately with whatever data we have so far. // Spawn the panel immediately with whatever data we have so far.
let remote_available = provider let remote_available = provider
.as_ref() .as_ref()