Compare commits

...

2 Commits

Author SHA1 Message Date
funman300 76cf41e7a9 fix(ui): open sync-setup modal when Connect clicked from Settings
Android Release / build-apk (push) Successful in 3m49s
The sync-setup modal was silently blocked by its own guard:
other_modal_scrims checks for any ModalScrim without SyncSetupScreen,
but the Settings panel IS a ModalScrim, so clicking Connect from within
Settings always hit the guard and returned early.

Two fixes:
- handle_sync_buttons: set SettingsScreen.0 = false when ConnectSync
  is pressed so settings closes as the event is fired
- open_sync_setup_modal: exclude SettingsPanel from other_modal_scrims
  to handle the deferred-despawn timing window (settings scrim entity
  still exists in the world until command buffers flush at frame end)
- Make SettingsPanel pub so sync_setup_plugin can reference it

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 15:32:14 -07:00
funman300 fae5933d29 fix(engine): enable take-from-foundation for restored and startup games
Android Release / build-apk (push) Successful in 3m42s
GameState serializes take_from_foundation=false (the core default),
so saved games on disk and direct-loaded states never had the setting
applied from SettingsResource — only freshly dealt games did.

Two fixes:
- sync_settings_to_game: new system that reads SettingsChangedEvent
  and patches game.0.take_from_foundation on every settings change
  (covers initial settings load at startup and in-session toggles)
- handle_restore_prompt: apply settings immediately after game.0 =
  restored so the Continue path also respects the current setting
- Register SettingsChangedEvent in GamePlugin::build (idempotent with
  SettingsPlugin) so the message is available in headless test apps

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 15:26:42 -07:00
3 changed files with 39 additions and 4 deletions
+25
View File
@@ -202,6 +202,8 @@ impl Plugin for GamePlugin {
.add_message::<FoundationCompletedEvent>()
.add_message::<InfoToastEvent>()
.add_message::<AppLifecycle>()
// add_message is idempotent; SettingsPlugin also registers this.
.add_message::<crate::settings_plugin::SettingsChangedEvent>()
.add_systems(
Update,
poll_pending_new_game_seed.before(GameMutation),
@@ -228,6 +230,7 @@ impl Plugin for GamePlugin {
// GameMutation flow.
.add_systems(Update, spawn_restore_prompt_if_pending)
.add_systems(Update, handle_restore_prompt.before(GameMutation))
.add_systems(Update, sync_settings_to_game.before(GameMutation))
.init_resource::<AutoSaveTimer>()
.add_systems(Update, tick_elapsed_time)
.add_systems(Update, auto_save_game_state)
@@ -235,6 +238,23 @@ impl Plugin for GamePlugin {
}
}
/// Forwards `take_from_foundation` from [`SettingsResource`] to the live
/// [`GameStateResource`] every time [`SettingsChangedEvent`] fires.
///
/// This covers two cases that the new-game path misses:
/// 1. The initial settings load at startup: saves on disk default to `false`
/// but `Settings` defaults to `true`; the event fires once when the
/// settings file is first read.
/// 2. A user toggling the setting mid-session in the Settings panel.
fn sync_settings_to_game(
mut events: MessageReader<crate::settings_plugin::SettingsChangedEvent>,
mut game: ResMut<GameStateResource>,
) {
for ev in events.read() {
game.0.take_from_foundation = ev.0.take_from_foundation;
}
}
/// Pure, testable helper. Updates `elapsed_seconds` and drains the
/// fractional accumulator into whole-second ticks. No-op when `is_won`.
pub fn advance_elapsed(
@@ -614,6 +634,7 @@ fn handle_restore_prompt(
new_game_buttons: Query<&Interaction, (With<RestoreNewGameButton>, Changed<Interaction>)>,
mut pending: ResMut<PendingRestoredGame>,
mut game: ResMut<GameStateResource>,
settings: Option<Res<crate::settings_plugin::SettingsResource>>,
mut changed: MessageWriter<StateChangedEvent>,
mut new_game: MessageWriter<NewGameRequestEvent>,
mut launch_home_shown: Option<ResMut<crate::home_plugin::LaunchHomeShown>>,
@@ -639,6 +660,10 @@ fn handle_restore_prompt(
let resolved = if key_continue || click_continue {
if let Some(restored) = pending.0.take() {
game.0 = restored;
// Patch setting that serialized with the old core default of `false`.
if let Some(s) = settings.as_ref() {
game.0.take_from_foundation = s.0.take_from_foundation;
}
changed.write(StateChangedEvent);
}
for entity in &screens {
+8 -2
View File
@@ -94,7 +94,7 @@ pub struct SettingsChangedEvent(pub Settings);
/// Marker on the root Settings panel entity.
#[derive(Component, Debug)]
struct SettingsPanel;
pub struct SettingsPanel;
/// Marks the `Text` node showing the live SFX volume value.
#[derive(Component, Debug)]
@@ -1137,6 +1137,7 @@ fn handle_sync_buttons(
mut configure_sync: MessageWriter<SyncConfigureRequestEvent>,
mut logout_sync: MessageWriter<SyncLogoutRequestEvent>,
mut delete_account: MessageWriter<DeleteAccountRequestEvent>,
mut screen: ResMut<SettingsScreen>,
) {
for (interaction, button) in &interaction_query {
if *interaction != Interaction::Pressed {
@@ -1144,7 +1145,12 @@ fn handle_sync_buttons(
}
match button {
SettingsButton::SyncNow => { manual_sync.write(ManualSyncRequestEvent); }
SettingsButton::ConnectSync => { configure_sync.write(SyncConfigureRequestEvent); }
SettingsButton::ConnectSync => {
// Close settings before the sync-setup modal opens so the
// guard in open_sync_setup_modal doesn't block on our own scrim.
screen.0 = false;
configure_sync.write(SyncConfigureRequestEvent);
}
SettingsButton::DisconnectSync => { logout_sync.write(SyncLogoutRequestEvent); }
SettingsButton::DeleteAccount => { delete_account.write(DeleteAccountRequestEvent); }
_ => {}
+6 -2
View File
@@ -52,7 +52,7 @@ use crate::events::{
SyncLogoutRequestEvent,
};
use crate::font_plugin::FontResource;
use crate::settings_plugin::{SettingsResource, SettingsScreen, SettingsStoragePath};
use crate::settings_plugin::{SettingsPanel, SettingsResource, SettingsScreen, SettingsStoragePath};
use crate::resources::TokioRuntimeResource;
use crate::sync_plugin::SyncProviderResource;
use crate::ui_modal::{spawn_modal, ModalScrim};
@@ -205,10 +205,14 @@ impl Plugin for SyncSetupPlugin {
// ---------------------------------------------------------------------------
/// Opens the sync-setup modal when `SyncConfigureRequestEvent` is received.
#[allow(clippy::type_complexity)]
fn open_sync_setup_modal(
mut events: MessageReader<SyncConfigureRequestEvent>,
existing: Query<(), With<SyncSetupScreen>>,
other_modal_scrims: Query<(), (With<ModalScrim>, Without<SyncSetupScreen>)>,
// Exclude SettingsPanel: the Connect button closes settings in the same
// frame it fires SyncConfigureRequestEvent, but Bevy despawns are deferred
// so the settings scrim still exists in the world during this system.
other_modal_scrims: Query<(), (With<ModalScrim>, Without<SyncSetupScreen>, Without<SettingsPanel>)>,
mut commands: Commands,
mut focused: ResMut<SyncFocusedField>,
font_res: Option<Res<FontResource>>,