refactor(engine): ambiguity gate covers the full cluster — batches 1-3, 585 pairs burned to zero
Port the July 7 gate-extender branch onto current master (the You-hub, touch action bar, glass tab bar, Phase L, and hint-ghost work all landed since) and finish batch 3. cluster_app now includes the mode, replay, input, radial, tooltip, cursor, touch-selection, and safe-area plugins on top of batches 1-2; the 585 ambiguous pairs the expansion exposed are annotated down to zero and the gate stays assert_eq!(count, 0). New ordering machinery: - ReplayPlayback: playback driver chain on the pre-mutation spine (PointerInput < ReplayPlayback < overlay chain < GameMutation) - ModeStart: mode-start handlers between the stats abandon-recorder and GameMutation; ChallengeCompletion < DailyCompletion < WeeklyGoalsEval pinned inside ProgressUpdate so `.after(ProgressUpdate)` readers see settled progress - PointerInput: public set wrapping the input chain; AbandonRecord on the stats abandon-recorder; PendingHint around the async hint pipeline; HintGhostFx for the ghost chain (after the full visual spine) - Clubs: MoveRequestWriters, WarningToastWriters join the existing request/toast writer clubs; UiTextFx grows the overlay/tooltip/safe-area chrome painters; SettingsAccess grows the lag-tolerant settings readers Gate: cargo test --workspace green (993 engine tests), clippy --all-targets -D warnings clean, rustfmt applied. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -67,6 +67,16 @@ pub struct AchievementsResource(pub Vec<AchievementRecord>);
|
||||
#[derive(Resource, Debug, Clone)]
|
||||
pub struct AchievementsStoragePath(pub Option<PathBuf>);
|
||||
|
||||
/// Set wrapping the win-path achievement evaluator. It writes
|
||||
/// `AchievementsResource`/`ProgressResource` and emits unlock events; UI
|
||||
/// readers of those (HUD popovers, the stats/achievements panels) order
|
||||
/// themselves `.after(AchievementEval)`. The replay-completion evaluator is
|
||||
/// deliberately NOT a member: it runs after `AutoComplete`, and panel chains
|
||||
/// that need `.after(AchievementEval)` run before `AutoComplete` — putting
|
||||
/// both in one set would close an ordering cycle (#143).
|
||||
#[derive(bevy::ecs::schedule::SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct AchievementEval;
|
||||
|
||||
pub struct AchievementPlugin {
|
||||
pub storage_path: Option<PathBuf>,
|
||||
}
|
||||
@@ -119,9 +129,13 @@ impl Plugin for AchievementPlugin {
|
||||
.add_systems(
|
||||
Update,
|
||||
evaluate_on_win
|
||||
.in_set(AchievementEval)
|
||||
.after(GameMutation)
|
||||
.after(StatsUpdate)
|
||||
.after(ProgressUpdate),
|
||||
.after(ProgressUpdate)
|
||||
// The unlock/XP/level-up events it emits are drained by
|
||||
// the toast pipeline this frame (#143).
|
||||
.before(crate::animation_plugin::AnimFx),
|
||||
)
|
||||
// Achievement-onboarding cue: fires once after the player's very
|
||||
// first win to teach the Achievements panel exists. Must run
|
||||
@@ -131,20 +145,50 @@ impl Plugin for AchievementPlugin {
|
||||
Update,
|
||||
fire_achievement_onboarding_toast
|
||||
.after(GameMutation)
|
||||
.after(StatsUpdate),
|
||||
.after(StatsUpdate)
|
||||
.in_set(crate::game_plugin::InfoToastWriters)
|
||||
.ambiguous_with(crate::game_plugin::InfoToastWriters)
|
||||
// Writes the one-shot onboarding flag into
|
||||
// SettingsResource: board painters re-read settings, so
|
||||
// land the write first; UiTextFx members read disjoint
|
||||
// settings fields (reduce-motion), so writer/reader
|
||||
// order there is provably irrelevant (#143).
|
||||
.before(crate::card_plugin::BoardVisuals)
|
||||
.ambiguous_with(crate::ui_theme::UiTextFx)
|
||||
.in_set(crate::settings_plugin::SettingsAccess)
|
||||
.ambiguous_with(crate::settings_plugin::SettingsAccess),
|
||||
)
|
||||
// Open/close/tab handling moved to `you_hub_plugin`
|
||||
// (Phase E) — this plugin now owns body content + scroll.
|
||||
.add_systems(Update, scroll_achievements_panel)
|
||||
.add_systems(
|
||||
Update,
|
||||
crate::ui_modal::touch_scroll_panel::<AchievementsScrollable>,
|
||||
scroll_achievements_panel
|
||||
.in_set(crate::ui_modal::PanelScroll)
|
||||
.ambiguous_with(crate::ui_modal::PanelScroll),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
crate::ui_modal::touch_scroll_panel::<AchievementsScrollable>
|
||||
.in_set(crate::ui_modal::PanelScroll)
|
||||
.ambiguous_with(crate::ui_modal::PanelScroll),
|
||||
)
|
||||
// Event-driven unlock: observe `ReplayPlaybackState` and unlock
|
||||
// `cinephile` the first time playback runs to natural completion.
|
||||
// Reads the resource via `Option<Res<_>>` so headless tests that
|
||||
// omit `ReplayPlaybackPlugin` still build.
|
||||
.add_systems(Update, evaluate_cinephile_on_replay_completion);
|
||||
.add_systems(
|
||||
Update,
|
||||
// Shares the unlock stream and AchievementsResource with
|
||||
// evaluate_on_win; ordered after it and before the toast
|
||||
// pipeline that drains the unlock events (#143).
|
||||
evaluate_cinephile_on_replay_completion
|
||||
.after(evaluate_on_win)
|
||||
// The stats overlay's watch-replay button (ordered
|
||||
// before AutoComplete) writes ReplayPlaybackState this
|
||||
// system reads.
|
||||
.after(crate::auto_complete_plugin::AutoComplete)
|
||||
.before(crate::animation_plugin::AnimFx),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -230,6 +230,14 @@ fn adopt_toasts_into_stack(
|
||||
/// Drives all linear card animations (`CardAnim`), toast notifications, deal stagger, win cascade, and the auto-complete card-slide sequence.
|
||||
pub struct AnimationPlugin;
|
||||
|
||||
/// Set wrapping this plugin's Update chain (card slide/cascade animation and
|
||||
/// the toast pipeline). Ordered after [`crate::card_animation::CardTweenFx`]
|
||||
/// on the shared visual spine — members write card `Transform`s, which every
|
||||
/// other painter set also touches — and after the toast/stat/progress writer
|
||||
/// sets so the toast handlers drain this frame's events (#143).
|
||||
#[derive(bevy::ecs::schedule::SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct AnimFx;
|
||||
|
||||
impl Plugin for AnimationPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
// Register the events this plugin consumes so tests that don't include
|
||||
@@ -253,12 +261,36 @@ impl Plugin for AnimationPlugin {
|
||||
.init_resource::<ToastQueue>()
|
||||
.init_resource::<ActiveToast>()
|
||||
.add_systems(Startup, (init_slide_duration, spawn_toast_stack_root))
|
||||
.configure_sets(
|
||||
Update,
|
||||
AnimFx
|
||||
// Visual spine: …BoardVisuals < MarkerVisuals <
|
||||
// CardTweenFx < AnimFx < FeedbackFx (#143). GameMutation
|
||||
// ordering is inherited transitively.
|
||||
.after(crate::card_animation::CardTweenFx)
|
||||
.after(crate::table_plugin::MarkerVisuals)
|
||||
// Stats/progress must settle before the win/level-up
|
||||
// toast handlers read their events. (No edge against
|
||||
// InfoToastWriters: toast writers exist on both sides
|
||||
// of this set — enqueue_toasts drains via cursor and
|
||||
// joins the club instead; a toast surfacing one frame
|
||||
// later is invisible.)
|
||||
.after(crate::stats_plugin::StatsUpdate)
|
||||
.after(crate::progress_plugin::ProgressUpdate)
|
||||
.after(GameMutation)
|
||||
// HUD text pulses are disjoint from the card entities
|
||||
// this set animates (see UiTextFx × BoardVisuals, #143).
|
||||
.ambiguous_with(crate::ui_theme::UiTextFx),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
advance_card_anims,
|
||||
sync_slide_duration,
|
||||
handle_win_cascade,
|
||||
// Both drain SettingsChangedEvent / read anim-speed
|
||||
// fields via cursor; a home deal-option edit surfacing
|
||||
// one frame later is invisible (#143).
|
||||
sync_slide_duration.ambiguous_with(crate::settings_plugin::SettingsAccess),
|
||||
handle_win_cascade.ambiguous_with(crate::settings_plugin::SettingsAccess),
|
||||
handle_achievement_toast,
|
||||
handle_levelup_toast,
|
||||
handle_daily_goal_announcement_toast,
|
||||
@@ -266,16 +298,23 @@ impl Plugin for AnimationPlugin {
|
||||
handle_weekly_toast,
|
||||
handle_time_attack_toast,
|
||||
handle_challenge_toast,
|
||||
handle_settings_toast,
|
||||
handle_settings_toast.ambiguous_with(crate::settings_plugin::SettingsAccess),
|
||||
handle_auto_complete_toast,
|
||||
handle_xp_awarded_toast,
|
||||
handle_move_rejected_toast,
|
||||
handle_warning_toast,
|
||||
tick_toasts,
|
||||
(enqueue_toasts, drive_toast_display).chain(),
|
||||
(
|
||||
enqueue_toasts
|
||||
.in_set(crate::game_plugin::InfoToastWriters)
|
||||
.ambiguous_with(crate::game_plugin::InfoToastWriters),
|
||||
drive_toast_display,
|
||||
)
|
||||
.chain(),
|
||||
adopt_toasts_into_stack,
|
||||
)
|
||||
.after(GameMutation),
|
||||
.chain()
|
||||
.in_set(AnimFx),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,14 @@ use tuning::update_input_platform;
|
||||
/// long as no single entity carries both `CardAnim` and `CardAnimation`.
|
||||
pub struct CardAnimationPlugin;
|
||||
|
||||
/// Set wrapping this plugin's Update chain (curve tweens, hover/drag
|
||||
/// visuals). First anim stage on the visual spine: …BoardVisuals <
|
||||
/// MarkerVisuals < CardTweenFx < [`crate::animation_plugin::AnimFx`] <
|
||||
/// FeedbackFx — members write card `Transform`s, which every other painter
|
||||
/// set also touches (#143).
|
||||
#[derive(bevy::ecs::schedule::SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct CardTweenFx;
|
||||
|
||||
impl Plugin for CardAnimationPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
// Register events and resources idempotently — double-registration is
|
||||
@@ -115,7 +123,14 @@ impl Plugin for CardAnimationPlugin {
|
||||
// still present in the same frame it was removed (deferred
|
||||
// commands aren't applied until the next ApplyDeferred
|
||||
// point), causing a 1-frame gap between every chain step.
|
||||
ApplyDeferred,
|
||||
//
|
||||
// `ambiguous_with_all`: an explicit sync point conflicts
|
||||
// (on `World`) with every unordered system in the
|
||||
// schedule. Only the flush of THIS chain's commands needs
|
||||
// ordering — the chain edges provide that; whether an
|
||||
// unrelated system's commands flush here or at the next
|
||||
// sync point is immaterial to it (#143).
|
||||
ApplyDeferred.ambiguous_with_all(),
|
||||
// After each animation finishes, pop the next chain segment.
|
||||
advance_animation_chains,
|
||||
// Interaction visuals (run after animation for final positions).
|
||||
@@ -123,10 +138,24 @@ impl Plugin for CardAnimationPlugin {
|
||||
apply_hover_scale,
|
||||
apply_drag_visual,
|
||||
// Drain buffered inputs only when no animations remain.
|
||||
drain_input_buffer,
|
||||
// It re-emits buffered undo/new-game requests, so it
|
||||
// joins the self-ambiguous writer clubs (#143).
|
||||
drain_input_buffer
|
||||
.in_set(crate::game_plugin::UndoRequestWriters)
|
||||
.ambiguous_with(crate::game_plugin::UndoRequestWriters),
|
||||
)
|
||||
.chain()
|
||||
.after(GameMutation),
|
||||
.in_set(CardTweenFx),
|
||||
)
|
||||
.configure_sets(
|
||||
Update,
|
||||
CardTweenFx
|
||||
.after(GameMutation)
|
||||
.after(crate::table_plugin::MarkerVisuals)
|
||||
// HUD text pulses touch Transform on UI entities only —
|
||||
// disjoint from the card entities this set animates
|
||||
// (same rationale as UiTextFx × BoardVisuals, #143).
|
||||
.ambiguous_with(crate::ui_theme::UiTextFx),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -592,7 +592,9 @@ impl Plugin for CardPlugin {
|
||||
tick_right_click_highlights,
|
||||
clear_right_click_highlights_on_state_change,
|
||||
clear_right_click_highlights_on_pause,
|
||||
tick_hint_highlight,
|
||||
// Reads only the anim-speed/reduce-motion flags; a frame
|
||||
// of lag is invisible (#143).
|
||||
tick_hint_highlight.ambiguous_with(crate::settings_plugin::SettingsAccess),
|
||||
update_stock_empty_indicator,
|
||||
update_stock_count_badge.run_if(resource_changed::<GameStateResource>),
|
||||
collect_resize_events,
|
||||
|
||||
@@ -11,8 +11,7 @@ use solitaire_data::{challenge_count, challenge_seed_for, save_progress_to};
|
||||
use crate::events::{
|
||||
GameWonEvent, InfoToastEvent, NewGameRequestEvent, StartChallengeRequestEvent,
|
||||
};
|
||||
use crate::game_plugin::GameMutation;
|
||||
use crate::progress_plugin::{ProgressResource, ProgressStoragePath, ProgressUpdate};
|
||||
use crate::progress_plugin::{ProgressResource, ProgressStoragePath};
|
||||
use crate::resources::GameStateResource;
|
||||
|
||||
/// Minimum player level required to start a Challenge run.
|
||||
@@ -38,8 +37,28 @@ impl Plugin for ChallengePlugin {
|
||||
.add_message::<StartChallengeRequestEvent>()
|
||||
.add_message::<InfoToastEvent>()
|
||||
// Run after ProgressUpdate so we don't fight ProgressPlugin's add_xp.
|
||||
.add_systems(Update, advance_on_challenge_win.after(ProgressUpdate))
|
||||
.add_systems(Update, handle_start_challenge_request.before(GameMutation));
|
||||
// Ordering comes from the mode-completion chain configured in
|
||||
// progress_plugin; the toast it emits joins the writer club
|
||||
// drained by the toast pipeline this frame (#143).
|
||||
.add_systems(
|
||||
Update,
|
||||
advance_on_challenge_win
|
||||
.in_set(crate::progress_plugin::ChallengeCompletion)
|
||||
.in_set(crate::game_plugin::InfoToastWriters)
|
||||
.ambiguous_with(crate::game_plugin::InfoToastWriters),
|
||||
)
|
||||
// ModeStart places it between the stats abandon-recorder and
|
||||
// GameMutation; it shares the new-game request/toast streams
|
||||
// with the other writers (#143).
|
||||
.add_systems(
|
||||
Update,
|
||||
handle_start_challenge_request
|
||||
.in_set(crate::game_plugin::ModeStart)
|
||||
.in_set(crate::game_plugin::NewGameRequestWriters)
|
||||
.ambiguous_with(crate::game_plugin::NewGameRequestWriters)
|
||||
.in_set(crate::game_plugin::InfoToastWriters)
|
||||
.ambiguous_with(crate::game_plugin::InfoToastWriters),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,12 +76,31 @@ pub struct CursorPlugin;
|
||||
|
||||
impl Plugin for CursorPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
// All three read the DragState/game state the input chain and
|
||||
// mutation systems settle, so they run after both. The highlight
|
||||
// painters write card/overlay Sprites and join the visual spine
|
||||
// ahead of the board painters; the cursor icon touches only the
|
||||
// window cursor (#143).
|
||||
app.add_systems(
|
||||
Update,
|
||||
(
|
||||
update_cursor_icon,
|
||||
update_drop_highlights.run_if(resource_changed::<DragState>),
|
||||
update_drop_target_overlays,
|
||||
update_cursor_icon
|
||||
.after(crate::input_plugin::PointerInput)
|
||||
.after(crate::game_plugin::GameMutation)
|
||||
.after(crate::selection_plugin::SelectionFx)
|
||||
.before(crate::card_plugin::BoardVisuals),
|
||||
(
|
||||
update_drop_highlights.run_if(resource_changed::<DragState>),
|
||||
update_drop_target_overlays,
|
||||
)
|
||||
.chain()
|
||||
.after(crate::input_plugin::PointerInput)
|
||||
.after(crate::game_plugin::GameMutation)
|
||||
// Selection maintenance clears DragState and the hint
|
||||
// poll writes highlight sprites — both settle first.
|
||||
.after(crate::selection_plugin::SelectionFx)
|
||||
.after(crate::pending_hint::PendingHint)
|
||||
.before(crate::card_plugin::BoardVisuals),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@ use crate::events::{
|
||||
GameWonEvent, InfoToastEvent, NewGameRequestEvent, StartDailyChallengeRequestEvent,
|
||||
WarningToastEvent, XpAwardedEvent,
|
||||
};
|
||||
use crate::game_plugin::GameMutation;
|
||||
use crate::progress_plugin::{ProgressResource, ProgressStoragePath, ProgressUpdate};
|
||||
|
||||
use crate::progress_plugin::{ProgressResource, ProgressStoragePath};
|
||||
use crate::resources::GameStateResource;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::sync_plugin::SyncProviderResource;
|
||||
@@ -124,17 +124,51 @@ impl Plugin for DailyChallengePlugin {
|
||||
.add_message::<StartDailyChallengeRequestEvent>()
|
||||
.add_message::<WarningToastEvent>()
|
||||
.add_message::<XpAwardedEvent>()
|
||||
// record/award after the base ProgressUpdate so we don't fight
|
||||
// ProgressPlugin's add_xp on the same frame.
|
||||
.add_systems(Update, handle_daily_completion.after(ProgressUpdate))
|
||||
.add_systems(Update, handle_start_daily_request.before(GameMutation))
|
||||
.add_systems(Update, check_daily_expiry_warning)
|
||||
.add_systems(Update, check_date_rollover);
|
||||
// Ordering comes from the mode-completion chain configured in
|
||||
// progress_plugin; the toast it emits joins the writer club
|
||||
// drained by the toast pipeline this frame (#143).
|
||||
.add_systems(
|
||||
Update,
|
||||
handle_daily_completion
|
||||
.in_set(crate::progress_plugin::DailyCompletion)
|
||||
.in_set(crate::game_plugin::InfoToastWriters)
|
||||
.ambiguous_with(crate::game_plugin::InfoToastWriters),
|
||||
)
|
||||
// ModeStart places it between the stats abandon-recorder and
|
||||
// GameMutation; it shares the new-game request/toast streams
|
||||
// with the other writers (#143).
|
||||
.add_systems(
|
||||
Update,
|
||||
handle_start_daily_request
|
||||
.in_set(crate::game_plugin::ModeStart)
|
||||
.in_set(crate::game_plugin::NewGameRequestWriters)
|
||||
.ambiguous_with(crate::game_plugin::NewGameRequestWriters)
|
||||
.in_set(crate::game_plugin::InfoToastWriters)
|
||||
.ambiguous_with(crate::game_plugin::InfoToastWriters),
|
||||
)
|
||||
// One chain for the DailyChallengeResource writers/readers:
|
||||
// the local date rollover replaces today's challenge and the
|
||||
// expiry warning reads it. Before ModeStart so a rollover
|
||||
// lands before the start handler reads the seed this frame
|
||||
// (#143).
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
check_date_rollover,
|
||||
check_daily_expiry_warning
|
||||
.in_set(crate::game_plugin::WarningToastWriters)
|
||||
.ambiguous_with(crate::game_plugin::WarningToastWriters),
|
||||
)
|
||||
.chain()
|
||||
.before(crate::game_plugin::ModeStart),
|
||||
);
|
||||
|
||||
// Server-challenge fetch uses SyncProviderResource (reqwest), not available on wasm.
|
||||
// The poll replaces DailyChallengeResource, so it heads the same
|
||||
// pre-ModeStart timeline as the rollover/expiry chain above (#143).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
app.add_systems(Startup, fetch_server_challenge)
|
||||
.add_systems(Update, poll_server_challenge);
|
||||
.add_systems(Update, poll_server_challenge.before(check_date_rollover));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ use solitaire_core::game_state::{DifficultyLevel, GameMode};
|
||||
use solitaire_data::difficulty_seeds::seeds_for;
|
||||
|
||||
use crate::events::{NewGameRequestEvent, StartDifficultyRequestEvent};
|
||||
use crate::game_plugin::GameMutation;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Resources
|
||||
@@ -77,7 +76,16 @@ impl Plugin for DifficultyPlugin {
|
||||
app.init_resource::<DifficultyIndexResource>()
|
||||
.add_message::<StartDifficultyRequestEvent>()
|
||||
.add_message::<NewGameRequestEvent>()
|
||||
.add_systems(Update, handle_difficulty_request.before(GameMutation));
|
||||
// ModeStart places it between the stats abandon-recorder and
|
||||
// GameMutation; it shares the new-game request stream with the
|
||||
// other writers (#143).
|
||||
.add_systems(
|
||||
Update,
|
||||
handle_difficulty_request
|
||||
.in_set(crate::game_plugin::ModeStart)
|
||||
.in_set(crate::game_plugin::NewGameRequestWriters)
|
||||
.ambiguous_with(crate::game_plugin::NewGameRequestWriters),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -198,6 +198,22 @@ pub fn deal_stagger_jitter(card_id: u32) -> f32 {
|
||||
/// animation systems.
|
||||
pub struct FeedbackAnimPlugin;
|
||||
|
||||
/// Set wrapping the shake/settle/deal/flourish systems. Last anim stage on
|
||||
/// the visual spine: …MarkerVisuals < CardTweenFx <
|
||||
/// [`crate::animation_plugin::AnimFx`] < FeedbackFx. Runs after
|
||||
/// MarkerVisuals because the foundation flourish tints pile-marker sprites,
|
||||
/// which the marker painters also write; the tint must win within the frame
|
||||
/// (#143). GameMutation ordering is inherited transitively.
|
||||
#[derive(bevy::ecs::schedule::SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct FeedbackFx;
|
||||
|
||||
/// Set wrapping the hint-ghost chain (spawn → tick → despawn). Ghost
|
||||
/// entities are `Without<CardEntity>` sprites owned solely by this chain —
|
||||
/// systems that animate other entity domains (e.g. the win-summary camera
|
||||
/// shake) declare themselves `.ambiguous_with(HintGhostFx)` (#143).
|
||||
#[derive(bevy::ecs::schedule::SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct HintGhostFx;
|
||||
|
||||
impl Plugin for FeedbackAnimPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
// Register the events this plugin consumes so it can run in isolation
|
||||
@@ -211,22 +227,36 @@ impl Plugin for FeedbackAnimPlugin {
|
||||
.add_message::<HintVisualEvent>()
|
||||
.add_message::<StateChangedEvent>()
|
||||
.add_message::<RequestRedraw>()
|
||||
.configure_sets(
|
||||
Update,
|
||||
FeedbackFx
|
||||
.after(GameMutation)
|
||||
.after(crate::animation_plugin::AnimFx)
|
||||
// HUD text pulses are disjoint from the card/marker
|
||||
// entities this set animates (see UiTextFx ×
|
||||
// BoardVisuals, #143).
|
||||
.ambiguous_with(crate::ui_theme::UiTextFx),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
start_shake_anim.after(GameMutation),
|
||||
// Reads only the reduce-motion flag; a frame of lag is
|
||||
// invisible (#143).
|
||||
start_shake_anim.ambiguous_with(crate::settings_plugin::SettingsAccess),
|
||||
tick_shake_anim,
|
||||
start_settle_anim.after(GameMutation),
|
||||
start_settle_anim,
|
||||
// tick_foundation_flourish writes the full Transform.scale
|
||||
// (Vec3); tick_settle_anim writes only scale.y on top of
|
||||
// it. Ordering ensures the settle's y-only write always
|
||||
// applies last so it wins on the ~0.15 s overlap when both
|
||||
// components are present on the same King entity.
|
||||
tick_foundation_flourish.before(tick_settle_anim),
|
||||
tick_foundation_flourish,
|
||||
tick_settle_anim,
|
||||
start_deal_anim.after(GameMutation),
|
||||
start_foundation_flourish.after(GameMutation),
|
||||
),
|
||||
start_deal_anim,
|
||||
start_foundation_flourish,
|
||||
)
|
||||
.chain()
|
||||
.in_set(FeedbackFx),
|
||||
)
|
||||
// Hint ghost (Phase H): the spawn reads card Transform/Sprite,
|
||||
// so it orders after the board painters; the tick only touches
|
||||
@@ -240,7 +270,24 @@ impl Plugin for FeedbackAnimPlugin {
|
||||
tick_hint_ghosts,
|
||||
despawn_hint_ghosts_on_state_change.after(GameMutation),
|
||||
)
|
||||
.chain(),
|
||||
.chain()
|
||||
.in_set(HintGhostFx)
|
||||
// The spawn reads card Sprites the marker painters (theme
|
||||
// apply, pile highlights) also write — those settle first.
|
||||
// Ghost entities are Without<CardEntity> and never HUD
|
||||
// chrome, and the spawn reads only the reduce-motion flag
|
||||
// (frame-lag invisible) (#143).
|
||||
.after(crate::table_plugin::MarkerVisuals)
|
||||
// …and the card tweens/slides, whose Transform writes
|
||||
// the spawn snapshot must not race.
|
||||
.after(crate::card_animation::CardTweenFx)
|
||||
// FeedbackFx (deal slide, shake) is the last Transform
|
||||
// writer on the spine — the ghost snapshot reads after
|
||||
// the whole visual pipeline has settled.
|
||||
.after(FeedbackFx)
|
||||
.ambiguous_with(crate::ui_theme::UiTextFx)
|
||||
.in_set(crate::settings_plugin::SettingsAccess)
|
||||
.ambiguous_with(crate::settings_plugin::SettingsAccess),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,38 @@ pub struct DrawRequestWriters;
|
||||
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct InfoToastWriters;
|
||||
|
||||
/// Self-ambiguous club for writers of `MoveRequestEvent` (drag drops, stock
|
||||
/// clicks, radial menu, selection keys, replay step button). Consumers drain
|
||||
/// the whole queue — append order is meaningless (#143). Same rules as
|
||||
/// [`NewGameRequestWriters`]: do NOT hang ordering edges off this set.
|
||||
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct MoveRequestWriters;
|
||||
|
||||
/// Self-ambiguous club for writers of `WarningToastEvent` (daily-expiry
|
||||
/// warning, leaderboard opt-in/out failure polls). The toast pipeline
|
||||
/// drains the whole queue — append order is meaningless (#143). Same
|
||||
/// rules as [`InfoToastWriters`].
|
||||
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct WarningToastWriters;
|
||||
|
||||
/// Set + self-ambiguous club for the mode-start request handlers (challenge,
|
||||
/// daily, time-attack, play-by-seed, difficulty). Each drains its own
|
||||
/// request stream and resets `GameStateResource` for its mode; only one
|
||||
/// stream carries an event on any real frame, so member order is irrelevant
|
||||
/// (club), while the set as a whole runs between the stats abandon-recorder
|
||||
/// ([`crate::stats_plugin::AbandonRecord`], which must read the old game
|
||||
/// first) and [`GameMutation`] (#143).
|
||||
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ModeStart;
|
||||
|
||||
/// Set wrapping `GamePlugin`'s pre-mutation game-state spine (elapsed-time
|
||||
/// tick, settings sync, restore prompt). Sits between
|
||||
/// [`crate::settings_plugin::SettingsMutation`] and [`GameMutation`]; other
|
||||
/// pre-mutation readers of `GameStateResource` (e.g. the stats
|
||||
/// abandon-recorder) order themselves `.after(PreMutation)` (#143).
|
||||
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct PreMutation;
|
||||
|
||||
/// Persistence path for the in-progress game state file. `None` disables I/O.
|
||||
#[derive(Resource, Debug, Clone)]
|
||||
pub struct GameStatePath(pub Option<PathBuf>);
|
||||
@@ -267,7 +299,14 @@ impl Plugin for GamePlugin {
|
||||
.in_set(InfoToastWriters)
|
||||
.ambiguous_with(InfoToastWriters),
|
||||
)
|
||||
.add_systems(Update, record_replay_on_win.after(GameMutation))
|
||||
.add_systems(
|
||||
Update,
|
||||
record_replay_on_win
|
||||
.after(GameMutation)
|
||||
// Before the playback skip-recorder (which runs after
|
||||
// AutoComplete) — both touch RecordingReplay (#143).
|
||||
.before(crate::auto_complete_plugin::AutoComplete),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
@@ -301,9 +340,20 @@ impl Plugin for GamePlugin {
|
||||
.ambiguous_with(NewGameRequestWriters),
|
||||
)
|
||||
.chain()
|
||||
.in_set(PreMutation),
|
||||
)
|
||||
.configure_sets(
|
||||
Update,
|
||||
PreMutation
|
||||
.after(crate::settings_plugin::SettingsMutation)
|
||||
.before(GameMutation),
|
||||
)
|
||||
.configure_sets(
|
||||
Update,
|
||||
ModeStart
|
||||
.after(crate::stats_plugin::AbandonRecord)
|
||||
.before(GameMutation),
|
||||
)
|
||||
.init_resource::<AutoSaveTimer>()
|
||||
.add_systems(Update, auto_save_game_state.after(GameMutation))
|
||||
.add_systems(Last, save_game_state_on_exit);
|
||||
|
||||
@@ -54,12 +54,23 @@ impl Plugin for HelpPlugin {
|
||||
.add_message::<TouchInput>()
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
toggle_help_screen,
|
||||
handle_help_close_button,
|
||||
scroll_help_panel,
|
||||
crate::ui_modal::touch_scroll_panel::<HelpScrollable>,
|
||||
),
|
||||
(toggle_help_screen, handle_help_close_button)
|
||||
.chain()
|
||||
// FocusKeys consumes Esc after the modal handlers
|
||||
// (mirrors toggle_settings_screen, #143).
|
||||
.before(crate::ui_focus::FocusKeys),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
scroll_help_panel
|
||||
.in_set(crate::ui_modal::PanelScroll)
|
||||
.ambiguous_with(crate::ui_modal::PanelScroll),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
crate::ui_modal::touch_scroll_panel::<HelpScrollable>
|
||||
.in_set(crate::ui_modal::PanelScroll)
|
||||
.ambiguous_with(crate::ui_modal::PanelScroll),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,19 +334,45 @@ impl Plugin for HomePlugin {
|
||||
spawn_home_on_launch,
|
||||
toggle_home_screen,
|
||||
attach_focusable_to_home_mode_cards,
|
||||
handle_home_card_click,
|
||||
handle_home_new_game_hero,
|
||||
handle_home_card_click
|
||||
.in_set(crate::game_plugin::NewGameRequestWriters)
|
||||
.ambiguous_with(crate::game_plugin::NewGameRequestWriters)
|
||||
.in_set(crate::game_plugin::InfoToastWriters)
|
||||
.ambiguous_with(crate::game_plugin::InfoToastWriters),
|
||||
handle_home_new_game_hero
|
||||
.in_set(crate::game_plugin::NewGameRequestWriters)
|
||||
.ambiguous_with(crate::game_plugin::NewGameRequestWriters),
|
||||
handle_home_cancel_button,
|
||||
handle_home_profile_chip,
|
||||
handle_home_profile_chip
|
||||
.in_set(crate::profile_plugin::ProfileToggleWriters)
|
||||
.ambiguous_with(crate::profile_plugin::ProfileToggleWriters),
|
||||
handle_home_draw_mode_buttons,
|
||||
handle_home_winnable_toggle,
|
||||
handle_home_deal_options_toggle,
|
||||
handle_home_difficulty_chip_click,
|
||||
handle_home_digit_keys,
|
||||
handle_home_difficulty_chip_click
|
||||
.in_set(crate::game_plugin::NewGameRequestWriters)
|
||||
.ambiguous_with(crate::game_plugin::NewGameRequestWriters),
|
||||
handle_home_digit_keys
|
||||
.in_set(crate::game_plugin::NewGameRequestWriters)
|
||||
.ambiguous_with(crate::game_plugin::NewGameRequestWriters)
|
||||
.before(crate::ui_focus::FocusKeys),
|
||||
)
|
||||
.chain(),
|
||||
.chain()
|
||||
// The home context repaints from this frame's settled
|
||||
// progress/stats/achievements; settings access joins the
|
||||
// screen-plugin club (#143).
|
||||
.after(crate::progress_plugin::ProgressUpdate)
|
||||
.after(crate::stats_plugin::StatsUpdate)
|
||||
.after(crate::achievement_plugin::AchievementEval)
|
||||
.in_set(crate::settings_plugin::SettingsAccess)
|
||||
.ambiguous_with(crate::settings_plugin::SettingsAccess),
|
||||
)
|
||||
.add_systems(Update, scroll_home_panel);
|
||||
.add_systems(
|
||||
Update,
|
||||
scroll_home_panel
|
||||
.in_set(crate::ui_modal::PanelScroll)
|
||||
.ambiguous_with(crate::ui_modal::PanelScroll),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -517,13 +517,31 @@ impl Plugin for HudPlugin {
|
||||
Update,
|
||||
(restore_hud_on_modal, apply_hud_visibility)
|
||||
.chain()
|
||||
.before(LayoutSystem::UpdateOnResize),
|
||||
.before(LayoutSystem::UpdateOnResize)
|
||||
// Toggles Visibility on HUD chrome roots only — joins
|
||||
// the chrome club (#143).
|
||||
.in_set(UiTextFx)
|
||||
.ambiguous_with(UiTextFx),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
update_hud_avatar.after(crate::settings_plugin::SettingsMutation),
|
||||
handle_avatar_button.ambiguous_with(HudButtons),
|
||||
update_hud_avatar
|
||||
// Same-frame-exact after the settings spine, so NOT
|
||||
// a SettingsAccess member — but the club's readers
|
||||
// and the home deal-option writers touch settings
|
||||
// fields the avatar never reads (#143).
|
||||
.ambiguous_with(crate::settings_plugin::SettingsAccess)
|
||||
.after(crate::settings_plugin::SettingsMutation)
|
||||
// Two more settings writers arrived with the wider
|
||||
// ambiguity cluster: the pause overlay's draw-mode
|
||||
// toggle and the achievement onboarding flag (#143).
|
||||
.after(crate::pause_plugin::PauseMutation)
|
||||
.after(crate::game_plugin::InfoToastWriters),
|
||||
handle_avatar_button
|
||||
.ambiguous_with(HudButtons)
|
||||
.in_set(crate::profile_plugin::ProfileToggleWriters)
|
||||
.ambiguous_with(crate::profile_plugin::ProfileToggleWriters),
|
||||
),
|
||||
)
|
||||
.add_systems(
|
||||
@@ -538,7 +556,11 @@ impl Plugin for HudPlugin {
|
||||
// the board painters that resize card/label text (#143).
|
||||
.add_systems(
|
||||
Update,
|
||||
update_hud_typography.after(crate::card_plugin::BoardVisuals),
|
||||
update_hud_typography
|
||||
.after(crate::card_plugin::BoardVisuals)
|
||||
// Writes HUD TextFont only — chrome club (#143).
|
||||
.in_set(UiTextFx)
|
||||
.ambiguous_with(UiTextFx),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
@@ -551,16 +573,26 @@ impl Plugin for HudPlugin {
|
||||
.after(GameMutation)
|
||||
.in_set(UiTextFx)
|
||||
.ambiguous_with(UiTextFx)
|
||||
.ambiguous_with(crate::card_plugin::BoardVisuals),
|
||||
.ambiguous_with(crate::card_plugin::BoardVisuals)
|
||||
// Reads only the reduce-motion flag; a frame of lag is
|
||||
// invisible (#143).
|
||||
.in_set(crate::settings_plugin::SettingsAccess)
|
||||
.ambiguous_with(crate::settings_plugin::SettingsAccess),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
(start_streak_flourish, advance_streak_flourish)
|
||||
.chain()
|
||||
.after(GameMutation)
|
||||
// Reads the streak-milestone events StatsUpdate emits.
|
||||
.after(crate::stats_plugin::StatsUpdate)
|
||||
.in_set(UiTextFx)
|
||||
.ambiguous_with(UiTextFx)
|
||||
.ambiguous_with(crate::card_plugin::BoardVisuals),
|
||||
.ambiguous_with(crate::card_plugin::BoardVisuals)
|
||||
// Reads only the reduce-motion flag; a frame of lag is
|
||||
// invisible (#143).
|
||||
.in_set(crate::settings_plugin::SettingsAccess)
|
||||
.ambiguous_with(crate::settings_plugin::SettingsAccess),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
@@ -584,16 +616,28 @@ impl Plugin for HudPlugin {
|
||||
handle_help_button,
|
||||
handle_hint_button
|
||||
.after(GameMutation)
|
||||
// A press cancels/spawns the solver task before
|
||||
// this frame's drop/poll pass (#143).
|
||||
.before(crate::pending_hint::PendingHint)
|
||||
.in_set(crate::game_plugin::InfoToastWriters)
|
||||
.ambiguous_with(crate::game_plugin::InfoToastWriters),
|
||||
handle_modes_button,
|
||||
// The modes/menu popovers repaint the player level from
|
||||
// ProgressResource — read it after this frame's XP award
|
||||
// settles (#143).
|
||||
handle_modes_button
|
||||
.after(crate::progress_plugin::ProgressUpdate)
|
||||
.after(crate::achievement_plugin::AchievementEval),
|
||||
handle_mode_option_click
|
||||
.in_set(NewGameRequestWriters)
|
||||
.ambiguous_with(NewGameRequestWriters),
|
||||
handle_modes_backdrop_click,
|
||||
close_modes_popover_on_escape,
|
||||
handle_menu_button,
|
||||
handle_menu_option_click,
|
||||
handle_menu_option_click
|
||||
.after(crate::progress_plugin::ProgressUpdate)
|
||||
.after(crate::achievement_plugin::AchievementEval)
|
||||
.in_set(crate::profile_plugin::ProfileToggleWriters)
|
||||
.ambiguous_with(crate::profile_plugin::ProfileToggleWriters),
|
||||
handle_menu_backdrop_click,
|
||||
close_menu_popover_on_escape,
|
||||
paint_action_buttons,
|
||||
@@ -602,8 +646,18 @@ impl Plugin for HudPlugin {
|
||||
// Writes tab-label TextColor and reads TextFont — entity-
|
||||
// disjoint from the UiTextFx readout writers (tab labels
|
||||
// vs HUD readouts), which Bevy can't prove, so declare it.
|
||||
animate_tab_expansion.ambiguous_with(UiTextFx),
|
||||
duck_tab_bar_during_drag,
|
||||
animate_tab_expansion
|
||||
.ambiguous_with(UiTextFx)
|
||||
// Tab-label fonts are never card/corner-label fonts.
|
||||
.ambiguous_with(crate::card_plugin::BoardVisuals)
|
||||
// Reads only reduce-motion; a frame of lag is
|
||||
// invisible, so it joins the settings-reader club.
|
||||
.in_set(crate::settings_plugin::SettingsAccess)
|
||||
.ambiguous_with(crate::settings_plugin::SettingsAccess),
|
||||
// Reads only reduce-motion — settings-reader club (#143).
|
||||
duck_tab_bar_during_drag
|
||||
.in_set(crate::settings_plugin::SettingsAccess)
|
||||
.ambiguous_with(crate::settings_plugin::SettingsAccess),
|
||||
)
|
||||
.chain()
|
||||
.in_set(HudButtons)
|
||||
|
||||
@@ -58,6 +58,14 @@ use crate::touch_selection_plugin::TouchSelectionState;
|
||||
use crate::ui_theme::{MOTION_DRAG_REJECT_SECS, STATE_SUCCESS, STATE_WARNING};
|
||||
use solitaire_core::DrawStockConfig;
|
||||
|
||||
/// Set wrapping the core pointer/keyboard input chain (clicks, drags, touch,
|
||||
/// hotkeys). Runs between [`crate::game_plugin::PreMutation`] (it reads the
|
||||
/// ticked game state) and [`crate::selection_plugin::SelectionKeySet`] (the
|
||||
/// selection keys check `DragState::is_idle`); request events it emits are
|
||||
/// consumed at `GameMutation` in the same frame (#143).
|
||||
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct PointerInput;
|
||||
|
||||
/// System-set labels used to anchor external systems relative to the touch
|
||||
/// drag pipeline without duplicating the internal chain ordering.
|
||||
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
@@ -143,7 +151,13 @@ impl Plugin for InputPlugin {
|
||||
// `.chain()` still orders them relative to each other.
|
||||
handle_keyboard_core
|
||||
.in_set(crate::game_plugin::DrawRequestWriters)
|
||||
.ambiguous_with(crate::game_plugin::DrawRequestWriters),
|
||||
.ambiguous_with(crate::game_plugin::DrawRequestWriters)
|
||||
// N/Z/X/… and U also feed the shared new-game and
|
||||
// undo request streams (#143).
|
||||
.in_set(crate::game_plugin::NewGameRequestWriters)
|
||||
.ambiguous_with(crate::game_plugin::NewGameRequestWriters)
|
||||
.in_set(crate::game_plugin::UndoRequestWriters)
|
||||
.ambiguous_with(crate::game_plugin::UndoRequestWriters),
|
||||
handle_keyboard_hint,
|
||||
handle_keyboard_forfeit,
|
||||
handle_stock_click
|
||||
@@ -159,29 +173,61 @@ impl Plugin for InputPlugin {
|
||||
end_drag.before(GameMutation),
|
||||
// Touch drag pipeline (parallel path through DragState).
|
||||
touch_start_drag.in_set(TouchDragSet::AfterStartDrag),
|
||||
touch_follow_drag,
|
||||
// Moves the dragged card's Transform; UiTextFx members
|
||||
// animate chrome entities only — disjoint domains Bevy
|
||||
// can't prove (#143).
|
||||
touch_follow_drag.ambiguous_with(crate::ui_theme::UiTextFx),
|
||||
handle_double_tap, // before touch_end_drag: reads drag state pre-clear
|
||||
touch_end_drag
|
||||
.after(TouchDragSet::BeforeEndDrag)
|
||||
.before(GameMutation),
|
||||
// Reads StateChangedEvent via cursor — a reset one frame
|
||||
// after the mutation is invisible (the cycle index only
|
||||
// matters on the next H press), so it rides at the end
|
||||
// of the input chain instead of chasing every
|
||||
// StateChanged writer (#143).
|
||||
// It also drains the new-game request stream via its own
|
||||
// cursor, so it joins the writer club the same way
|
||||
// enqueue_toasts does (#143).
|
||||
reset_hint_cycle_on_state_change
|
||||
.in_set(crate::game_plugin::NewGameRequestWriters)
|
||||
.ambiguous_with(crate::game_plugin::NewGameRequestWriters),
|
||||
)
|
||||
.chain(),
|
||||
.chain()
|
||||
.in_set(PointerInput),
|
||||
)
|
||||
.add_systems(Update, reset_hint_cycle_on_state_change);
|
||||
.configure_sets(
|
||||
Update,
|
||||
PointerInput
|
||||
.after(crate::game_plugin::PreMutation)
|
||||
.before(crate::selection_plugin::SelectionKeySet)
|
||||
.before(crate::ui_focus::FocusKeys),
|
||||
);
|
||||
// F11 fullscreen toggle is desktop-only; Android windows are always full-screen.
|
||||
// It writes only `Window.mode`; no system reads the mode, and the
|
||||
// geometry change it triggers arrives via WindowResized events on a
|
||||
// later frame — order against Window readers is provably irrelevant
|
||||
// (#143).
|
||||
#[cfg(not(target_os = "android"))]
|
||||
app.add_systems(Update, handle_fullscreen);
|
||||
app.add_systems(Update, handle_fullscreen.ambiguous_with_all());
|
||||
app
|
||||
// Async hint pipeline: state-change drop runs before the
|
||||
// poll system so a move applied this frame cancels any
|
||||
// in-flight task before its result can be surfaced.
|
||||
// in-flight task before its result can be surfaced. The poll
|
||||
// emits the hint toast and the pile-highlight event the board
|
||||
// painters consume this frame (#143).
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
crate::pending_hint::drop_pending_hint_on_state_change,
|
||||
crate::pending_hint::poll_pending_hint_task,
|
||||
crate::pending_hint::poll_pending_hint_task
|
||||
.in_set(crate::game_plugin::InfoToastWriters)
|
||||
.ambiguous_with(crate::game_plugin::InfoToastWriters),
|
||||
)
|
||||
.chain(),
|
||||
.chain()
|
||||
.in_set(crate::pending_hint::PendingHint)
|
||||
.after(GameMutation)
|
||||
.before(crate::auto_complete_plugin::AutoComplete),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,18 +158,38 @@ impl Plugin for LeaderboardPlugin {
|
||||
poll_leaderboard_fetch,
|
||||
update_leaderboard_panel,
|
||||
handle_opt_in_button,
|
||||
poll_opt_in_task,
|
||||
// The polls share the warning-toast stream with the
|
||||
// daily-expiry check (drained wholesale, #143).
|
||||
poll_opt_in_task
|
||||
.in_set(crate::game_plugin::WarningToastWriters)
|
||||
.ambiguous_with(crate::game_plugin::WarningToastWriters),
|
||||
handle_opt_out_button,
|
||||
poll_opt_out_task,
|
||||
poll_opt_out_task
|
||||
.in_set(crate::game_plugin::WarningToastWriters)
|
||||
.ambiguous_with(crate::game_plugin::WarningToastWriters),
|
||||
handle_set_display_name_button,
|
||||
handle_display_name_text_input,
|
||||
handle_display_name_confirm,
|
||||
handle_display_name_cancel,
|
||||
update_leaderboard_public_name_label,
|
||||
// Writes one marker-filtered Text label — disjoint from
|
||||
// the HUD/stats text painters (UiTextFx club, #143).
|
||||
update_leaderboard_public_name_label
|
||||
.in_set(crate::ui_theme::UiTextFx)
|
||||
.ambiguous_with(crate::ui_theme::UiTextFx),
|
||||
)
|
||||
.chain(),
|
||||
.chain()
|
||||
// Opt-in state and display name live in
|
||||
// SettingsResource — the whole chain joins the
|
||||
// screen-plugin settings club (#143).
|
||||
.in_set(crate::settings_plugin::SettingsAccess)
|
||||
.ambiguous_with(crate::settings_plugin::SettingsAccess),
|
||||
)
|
||||
.add_systems(Update, scroll_leaderboard_panel);
|
||||
.add_systems(
|
||||
Update,
|
||||
scroll_leaderboard_panel
|
||||
.in_set(crate::ui_modal::PanelScroll)
|
||||
.ambiguous_with(crate::ui_modal::PanelScroll),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -98,12 +98,26 @@ pub struct OnboardingPlugin;
|
||||
|
||||
impl Plugin for OnboardingPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_resource::<OnboardingSlideIndex>()
|
||||
.add_systems(Update, spawn_if_first_run)
|
||||
.add_systems(
|
||||
Update,
|
||||
(handle_onboarding_buttons, handle_onboarding_keyboard).chain(),
|
||||
);
|
||||
app.init_resource::<OnboardingSlideIndex>().add_systems(
|
||||
Update,
|
||||
// One chain: all three touch the slide index and the
|
||||
// first-run flag in SettingsResource. Club membership
|
||||
// declares the settings access disjoint from the other
|
||||
// screen plugins'; the keyboard handler runs before
|
||||
// FocusKeys like every other Esc consumer (#143).
|
||||
(
|
||||
spawn_if_first_run,
|
||||
handle_onboarding_buttons,
|
||||
handle_onboarding_keyboard.before(crate::ui_focus::FocusKeys),
|
||||
)
|
||||
.chain()
|
||||
// Before the settings spine: the first-run flag write
|
||||
// lands before every same-frame settings reader (they
|
||||
// all sit at or after SettingsMutation transitively).
|
||||
.before(crate::settings_plugin::SettingsMutation)
|
||||
.in_set(crate::settings_plugin::SettingsAccess)
|
||||
.ambiguous_with(crate::settings_plugin::SettingsAccess),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -104,6 +104,15 @@ pub fn draw_mode_label(mode: DrawStockConfig) -> &'static str {
|
||||
/// to disk, and routes the Forfeit confirm-modal flow.
|
||||
pub struct PausePlugin;
|
||||
|
||||
/// Set wrapping every system that mutates [`PausedResource`] or drives the
|
||||
/// pause/forfeit modal flow. Runs as one deterministic chain between
|
||||
/// [`crate::settings_plugin::SettingsMutation`] (the pause overlay's
|
||||
/// draw-mode toggle writes `SettingsResource`) and
|
||||
/// [`crate::game_plugin::GameMutation`], so every system ordered after
|
||||
/// `GameMutation` observes this frame's pause state transitively (#143).
|
||||
#[derive(bevy::ecs::schedule::SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct PauseMutation;
|
||||
|
||||
impl Plugin for PausePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
// add_message is idempotent — other plugins may register these
|
||||
@@ -116,28 +125,55 @@ impl Plugin for PausePlugin {
|
||||
.add_message::<crate::events::ShowSolutionRequestEvent>()
|
||||
.add_message::<InfoToastEvent>()
|
||||
.init_resource::<PausedResource>()
|
||||
.configure_sets(
|
||||
Update,
|
||||
PauseMutation
|
||||
.after(crate::settings_plugin::SettingsMutation)
|
||||
// Before the pre-mutation game-state spine: the elapsed
|
||||
// timer must observe this frame's pause transition, and
|
||||
// the draw-mode toggle's settings write must land before
|
||||
// sync_settings_to_game forwards it.
|
||||
.before(crate::game_plugin::PreMutation)
|
||||
.before(crate::game_plugin::GameMutation)
|
||||
// toggle_pause must see SelectionState *before*
|
||||
// handle_selection_keys clears it, so it can skip Escape
|
||||
// when a card is selected. The other members mutate
|
||||
// PausedResource, which the selection keys read.
|
||||
.before(SelectionKeySet)
|
||||
// HUD buttons read PausedResource (e.g. hint is inert
|
||||
// while paused) — order them after this frame's pause
|
||||
// transition.
|
||||
.before(crate::hud_plugin::HudButtons),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
// toggle_pause must see SelectionState *before* handle_selection_keys
|
||||
// clears it, so it can skip Escape when a card is selected.
|
||||
// It must also run *before* handle_forfeit_keyboard so the
|
||||
// ForfeitConfirmScreen is still alive when toggle_pause's
|
||||
// early-return guard checks for it — otherwise an Esc that
|
||||
// closes the forfeit modal would also open pause in the
|
||||
// same frame.
|
||||
// toggle_pause must run *before* handle_forfeit_keyboard
|
||||
// so the ForfeitConfirmScreen is still alive when
|
||||
// toggle_pause's early-return guard checks for it —
|
||||
// otherwise an Esc that closes the forfeit modal would
|
||||
// also open pause in the same frame. The chain preserves
|
||||
// that edge and makes the whole pause/forfeit flow
|
||||
// deterministic (all members share PausedResource and
|
||||
// the forfeit message streams).
|
||||
// Reads only motion/display settings fields — disjoint
|
||||
// from the screen-plugin club's writes (#143).
|
||||
toggle_pause
|
||||
.before(SelectionKeySet)
|
||||
.before(handle_forfeit_keyboard),
|
||||
handle_pause_draw_buttons,
|
||||
.in_set(crate::settings_plugin::SettingsAccess)
|
||||
.ambiguous_with(crate::settings_plugin::SettingsAccess),
|
||||
auto_resume_on_overlay,
|
||||
handle_pause_draw_buttons
|
||||
.in_set(crate::settings_plugin::SettingsAccess)
|
||||
.ambiguous_with(crate::settings_plugin::SettingsAccess),
|
||||
handle_pause_resume_button,
|
||||
handle_pause_forfeit_button,
|
||||
handle_pause_solution_button,
|
||||
handle_forfeit_request,
|
||||
handle_forfeit_confirm_buttons,
|
||||
handle_forfeit_keyboard,
|
||||
auto_resume_on_overlay,
|
||||
),
|
||||
handle_forfeit_confirm_buttons,
|
||||
)
|
||||
.chain()
|
||||
.in_set(PauseMutation),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,13 @@ use crate::resources::{GameStateResource, HintCycleIndex};
|
||||
/// emits the hint visuals once the task completes. At most one task
|
||||
/// is ever in flight: a fresh H press while a previous task is
|
||||
/// running drops the previous task and queues the new one.
|
||||
/// Set wrapping the async hint pipeline (state-change drop → poll). The
|
||||
/// HUD hint button orders `.before` it so a press cancels/spawns before
|
||||
/// this frame's poll; the drag-highlight painters order `.after` it so
|
||||
/// the hint sprite writes are settled (#143).
|
||||
#[derive(bevy::ecs::schedule::SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct PendingHint;
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
pub struct PendingHintTask {
|
||||
/// `Some` while the solver is still working on a verdict.
|
||||
|
||||
@@ -115,10 +115,27 @@ impl Plugin for PlayBySeedPlugin {
|
||||
Update,
|
||||
(
|
||||
handle_open_dialog,
|
||||
handle_text_input,
|
||||
tick_debounce_and_spawn_solver_task,
|
||||
poll_solver_task,
|
||||
handle_confirm,
|
||||
// Writes the marker-filtered seed-dialog Text nodes —
|
||||
// disjoint from the other text painters (UiTextFx
|
||||
// club, #143).
|
||||
handle_text_input
|
||||
.in_set(crate::ui_theme::UiTextFx)
|
||||
.ambiguous_with(crate::ui_theme::UiTextFx),
|
||||
tick_debounce_and_spawn_solver_task
|
||||
.in_set(crate::ui_theme::UiTextFx)
|
||||
.ambiguous_with(crate::ui_theme::UiTextFx)
|
||||
// Reads deal settings for the solver spawn; a frame
|
||||
// of lag is invisible (#143).
|
||||
.in_set(crate::settings_plugin::SettingsAccess)
|
||||
.ambiguous_with(crate::settings_plugin::SettingsAccess),
|
||||
poll_solver_task
|
||||
.in_set(crate::ui_theme::UiTextFx)
|
||||
.ambiguous_with(crate::ui_theme::UiTextFx),
|
||||
// Shares the new-game request stream with the other
|
||||
// writers (drained wholesale at GameMutation, #143).
|
||||
handle_confirm
|
||||
.in_set(crate::game_plugin::NewGameRequestWriters)
|
||||
.ambiguous_with(crate::game_plugin::NewGameRequestWriters),
|
||||
handle_cancel,
|
||||
)
|
||||
.chain()
|
||||
|
||||
@@ -60,6 +60,14 @@ pub struct DailyCalendarDot {
|
||||
/// [`spawn_profile_body`] for this tab's content.
|
||||
pub struct ProfilePlugin;
|
||||
|
||||
/// Self-ambiguous club for writers of
|
||||
/// [`crate::events::ToggleProfileRequestEvent`] (HUD avatar button, home
|
||||
/// profile chip, HUD menu option). The You-hub reader drains the whole
|
||||
/// queue, so writer append order is irrelevant; `you_hub_plugin` orders
|
||||
/// its open/tab handling after the club (#143).
|
||||
#[derive(bevy::ecs::schedule::SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ProfileToggleWriters;
|
||||
|
||||
/// Marker on the scrollable body Node inside the Profile modal.
|
||||
///
|
||||
/// The Profile panel renders sync info, progression (incl. 14-day
|
||||
@@ -82,7 +90,12 @@ impl Plugin for ProfilePlugin {
|
||||
.add_message::<MouseWheel>()
|
||||
// Open/close/tab handling moved to `you_hub_plugin`
|
||||
// (Phase E) — this plugin now owns body content + scroll.
|
||||
.add_systems(Update, scroll_profile_panel);
|
||||
.add_systems(
|
||||
Update,
|
||||
scroll_profile_panel
|
||||
.in_set(crate::ui_modal::PanelScroll)
|
||||
.ambiguous_with(crate::ui_modal::PanelScroll),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,21 @@ pub struct LevelUpEvent {
|
||||
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ProgressUpdate;
|
||||
|
||||
/// Set for `challenge_plugin::advance_on_challenge_win`. See the
|
||||
/// mode-completion chain configured in `ProgressPlugin::build` (#143).
|
||||
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ChallengeCompletion;
|
||||
|
||||
/// Set for `daily_challenge_plugin::handle_daily_completion`. See the
|
||||
/// mode-completion chain configured in `ProgressPlugin::build` (#143).
|
||||
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct DailyCompletion;
|
||||
|
||||
/// Set for `weekly_goals_plugin::evaluate_weekly_goals`. See the
|
||||
/// mode-completion chain configured in `ProgressPlugin::build` (#143).
|
||||
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct WeeklyGoalsEval;
|
||||
|
||||
/// Bevy plugin that awards XP on `GameWonEvent`, persists `PlayerProgress`,
|
||||
/// and emits `LevelUpEvent` whenever a win crosses a level boundary.
|
||||
///
|
||||
@@ -74,7 +89,27 @@ impl Plugin for ProgressPlugin {
|
||||
.add_message::<GameWonEvent>()
|
||||
.add_systems(
|
||||
Update,
|
||||
award_xp_on_win.after(GameMutation).in_set(ProgressUpdate),
|
||||
award_xp_on_win
|
||||
.after(GameMutation)
|
||||
.in_set(ProgressUpdate)
|
||||
// Base win XP lands before the mode bonuses so the
|
||||
// level-up attribution is stable.
|
||||
.before(ChallengeCompletion),
|
||||
)
|
||||
// Mode-completion evaluators, pinned in one explicit chain
|
||||
// INSIDE ProgressUpdate — every `.after(ProgressUpdate)`
|
||||
// reader then sees fully-settled progress. Each evaluator
|
||||
// drains GameWonEvent via its own cursor and writes disjoint
|
||||
// ProgressResource fields plus XP, but a daily win also counts
|
||||
// toward weekly goals, so two members can fire on the same
|
||||
// frame — we pin an order rather than argue the XP/level-up
|
||||
// event stream is commutative (#143).
|
||||
.configure_sets(
|
||||
Update,
|
||||
(ChallengeCompletion, DailyCompletion, WeeklyGoalsEval)
|
||||
.chain()
|
||||
.in_set(ProgressUpdate)
|
||||
.after(GameMutation),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,16 +185,31 @@ impl Plugin for RadialMenuPlugin {
|
||||
// Tests inject `RadialCursorOverride` themselves; production
|
||||
// never touches it. We do not `init_resource` here so the
|
||||
// cursor-from-window path is the default.
|
||||
// The radial gesture reacts to pointer state the drag pipeline
|
||||
// settles, and its release handler feeds the shared move
|
||||
// request stream consumed at GameMutation this frame, so the
|
||||
// chain sits between PointerInput and GameMutation. The redraw
|
||||
// reads settings (reduce-motion) where a frame of lag is
|
||||
// invisible — SettingsAccess club (#143).
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
radial_open_on_right_click,
|
||||
radial_open_on_long_press,
|
||||
radial_track_cursor,
|
||||
radial_handle_release_or_cancel,
|
||||
radial_redraw_overlay,
|
||||
radial_handle_release_or_cancel
|
||||
.in_set(crate::game_plugin::MoveRequestWriters)
|
||||
.ambiguous_with(crate::game_plugin::MoveRequestWriters),
|
||||
radial_redraw_overlay
|
||||
.in_set(crate::settings_plugin::SettingsAccess)
|
||||
.ambiguous_with(crate::settings_plugin::SettingsAccess),
|
||||
)
|
||||
.chain(),
|
||||
.chain()
|
||||
.after(crate::input_plugin::PointerInput)
|
||||
// Selection keys also touch DragState (mutual pointer/
|
||||
// keyboard exclusion) — keys settle first.
|
||||
.after(crate::selection_plugin::SelectionKeySet)
|
||||
.before(crate::game_plugin::GameMutation),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,25 +464,71 @@ impl Plugin for ReplayOverlayPlugin {
|
||||
Update,
|
||||
(
|
||||
react_to_state_change,
|
||||
update_banner_label,
|
||||
update_progress_text,
|
||||
update_floating_progress_chip,
|
||||
update_scrub_fill,
|
||||
update_move_log_header,
|
||||
update_move_log_active_row,
|
||||
update_move_log_prev_rows,
|
||||
update_move_log_next_rows,
|
||||
update_mini_tableau_foundations,
|
||||
update_mini_tableau_stock_waste,
|
||||
update_pause_button_label,
|
||||
// Marker-filtered Text/Node repaints — each owns one
|
||||
// disjoint overlay marker, so they join the UiTextFx
|
||||
// club alongside the HUD text painters (#143).
|
||||
update_banner_label
|
||||
.in_set(crate::ui_theme::UiTextFx)
|
||||
.ambiguous_with(crate::ui_theme::UiTextFx),
|
||||
update_progress_text
|
||||
.in_set(crate::ui_theme::UiTextFx)
|
||||
.ambiguous_with(crate::ui_theme::UiTextFx),
|
||||
update_floating_progress_chip
|
||||
.in_set(crate::ui_theme::UiTextFx)
|
||||
.ambiguous_with(crate::ui_theme::UiTextFx),
|
||||
update_scrub_fill
|
||||
.in_set(crate::ui_theme::UiTextFx)
|
||||
.ambiguous_with(crate::ui_theme::UiTextFx),
|
||||
update_move_log_header
|
||||
.in_set(crate::ui_theme::UiTextFx)
|
||||
.ambiguous_with(crate::ui_theme::UiTextFx),
|
||||
update_move_log_active_row
|
||||
.in_set(crate::ui_theme::UiTextFx)
|
||||
.ambiguous_with(crate::ui_theme::UiTextFx),
|
||||
update_move_log_prev_rows
|
||||
.in_set(crate::ui_theme::UiTextFx)
|
||||
.ambiguous_with(crate::ui_theme::UiTextFx),
|
||||
update_move_log_next_rows
|
||||
.in_set(crate::ui_theme::UiTextFx)
|
||||
.ambiguous_with(crate::ui_theme::UiTextFx),
|
||||
update_mini_tableau_foundations
|
||||
.in_set(crate::ui_theme::UiTextFx)
|
||||
.ambiguous_with(crate::ui_theme::UiTextFx),
|
||||
update_mini_tableau_stock_waste
|
||||
.in_set(crate::ui_theme::UiTextFx)
|
||||
.ambiguous_with(crate::ui_theme::UiTextFx),
|
||||
update_pause_button_label
|
||||
.in_set(crate::ui_theme::UiTextFx)
|
||||
.ambiguous_with(crate::ui_theme::UiTextFx),
|
||||
handle_pause_button,
|
||||
handle_step_button,
|
||||
// Step/scrub dispatch into the shared move/draw/undo
|
||||
// request streams (drained wholesale at GameMutation),
|
||||
// so they join the writer clubs (#143).
|
||||
handle_step_button
|
||||
.in_set(crate::game_plugin::MoveRequestWriters)
|
||||
.ambiguous_with(crate::game_plugin::MoveRequestWriters)
|
||||
.in_set(crate::game_plugin::DrawRequestWriters)
|
||||
.ambiguous_with(crate::game_plugin::DrawRequestWriters),
|
||||
handle_pause_keyboard,
|
||||
handle_stop_keyboard,
|
||||
handle_arrow_keyboard,
|
||||
handle_arrow_keyboard
|
||||
.in_set(crate::game_plugin::MoveRequestWriters)
|
||||
.ambiguous_with(crate::game_plugin::MoveRequestWriters)
|
||||
.in_set(crate::game_plugin::UndoRequestWriters)
|
||||
.ambiguous_with(crate::game_plugin::UndoRequestWriters)
|
||||
.in_set(crate::game_plugin::DrawRequestWriters)
|
||||
.ambiguous_with(crate::game_plugin::DrawRequestWriters),
|
||||
handle_stop_button,
|
||||
)
|
||||
.chain(),
|
||||
.chain()
|
||||
// After the playback driver so painters render the
|
||||
// post-tick state; before GameMutation so the step /
|
||||
// scrub events are consumed this frame. Click handlers
|
||||
// writing ReplayPlaybackState stay at the end of the
|
||||
// chain — a press is observed by react_to_state_change
|
||||
// next frame (state-driven loop, see above) (#143).
|
||||
.after(crate::replay_playback::ReplayPlayback)
|
||||
.before(crate::game_plugin::GameMutation),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -550,6 +550,14 @@ fn fire_state_changed_on_completion(
|
||||
*last_was_completed = now_completed;
|
||||
}
|
||||
|
||||
/// Set wrapping the replay playback driver chain (tick → auto-clear →
|
||||
/// completion `StateChangedEvent`). The tick feeds `MoveRequestEvent` /
|
||||
/// `DrawRequestEvent` into this frame's [`GameMutation`], so the set sits
|
||||
/// on the pre-mutation spine; the overlay UI (`replay_overlay`) orders
|
||||
/// itself after this set so its painters render the post-tick state (#143).
|
||||
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ReplayPlayback;
|
||||
|
||||
/// Bevy plugin that initialises [`ReplayPlaybackState`] and drives
|
||||
/// playback ticks, completion linger, and the recording-pause guard.
|
||||
///
|
||||
@@ -562,18 +570,46 @@ impl Plugin for ReplayPlaybackPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_resource::<ReplayPlaybackState>()
|
||||
.add_message::<StateChangedEvent>()
|
||||
.add_systems(
|
||||
.configure_sets(
|
||||
Update,
|
||||
(
|
||||
tick_replay_playback,
|
||||
auto_clear_completed_replay,
|
||||
fire_state_changed_on_completion,
|
||||
)
|
||||
.chain(),
|
||||
ReplayPlayback
|
||||
.after(crate::game_plugin::PreMutation)
|
||||
// Input first: a stop/pause press or a drag-emitted
|
||||
// request this frame is observed before the tick fires
|
||||
// the next scripted move (#143).
|
||||
.after(crate::input_plugin::PointerInput)
|
||||
.before(GameMutation),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
record_replay_skip_during_playback.after(GameMutation),
|
||||
(
|
||||
// The tick shares the move/draw request streams with the
|
||||
// pointer/keyboard writers (drained wholesale at
|
||||
// GameMutation) and reads playback speed from settings.
|
||||
tick_replay_playback
|
||||
.in_set(crate::game_plugin::MoveRequestWriters)
|
||||
.ambiguous_with(crate::game_plugin::MoveRequestWriters)
|
||||
.in_set(crate::game_plugin::DrawRequestWriters)
|
||||
.ambiguous_with(crate::game_plugin::DrawRequestWriters)
|
||||
.in_set(crate::settings_plugin::SettingsAccess)
|
||||
.ambiguous_with(crate::settings_plugin::SettingsAccess),
|
||||
auto_clear_completed_replay,
|
||||
// Chained before GameMutation: explicit order against the
|
||||
// mutation systems' StateChangedEvent writes; readers
|
||||
// drain the whole queue after GameMutation.
|
||||
fire_state_changed_on_completion,
|
||||
)
|
||||
.chain()
|
||||
.in_set(ReplayPlayback),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
record_replay_skip_during_playback
|
||||
.after(GameMutation)
|
||||
// After AutoComplete so the stats overlay's watch-replay
|
||||
// handler (which resets ReplayPlaybackState) and the
|
||||
// win-path recorder have both settled (#143).
|
||||
.after(crate::auto_complete_plugin::AutoComplete),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,10 +74,28 @@ impl Plugin for SafeAreaInsetsPlugin {
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
apply_safe_area_anchors,
|
||||
apply_safe_area_bottom_anchors,
|
||||
apply_safe_area_to_modal_scrims,
|
||||
on_app_resumed,
|
||||
// All three write Node padding on disjoint chrome roots
|
||||
// (top anchors / bottom anchors / modal scrims); chained
|
||||
// for a deterministic pass, and they join the UiTextFx
|
||||
// chrome club — anchored roots are never the label/
|
||||
// tooltip/readout entities the other members touch,
|
||||
// which Bevy can't prove from the queries (#143).
|
||||
(
|
||||
apply_safe_area_anchors,
|
||||
apply_safe_area_bottom_anchors,
|
||||
apply_safe_area_to_modal_scrims,
|
||||
)
|
||||
.chain()
|
||||
.in_set(crate::ui_theme::UiTextFx)
|
||||
.ambiguous_with(crate::ui_theme::UiTextFx)
|
||||
// UiScale is written on the settings mutator spine.
|
||||
.after(crate::settings_plugin::SettingsMutation),
|
||||
// Emits a synthetic WindowResized on resume only; every
|
||||
// reader drains the queue via cursor, and a resume
|
||||
// already spans hundreds of ms, so whether the synthetic
|
||||
// event lands this frame or next is invisible — same
|
||||
// argument as handle_fullscreen (#143).
|
||||
on_app_resumed.ambiguous_with_all(),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -23,14 +23,43 @@ mod tests {
|
||||
use bevy::ecs::schedule::{LogLevel, ScheduleBuildSettings};
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::achievement_plugin::AchievementPlugin;
|
||||
use crate::animation_plugin::AnimationPlugin;
|
||||
use crate::auto_complete_plugin::AutoCompletePlugin;
|
||||
use crate::card_animation::CardAnimationPlugin;
|
||||
use crate::card_plugin::CardPlugin;
|
||||
use crate::challenge_plugin::ChallengePlugin;
|
||||
use crate::cursor_plugin::CursorPlugin;
|
||||
use crate::daily_challenge_plugin::DailyChallengePlugin;
|
||||
use crate::difficulty_plugin::DifficultyPlugin;
|
||||
use crate::feedback_anim_plugin::FeedbackAnimPlugin;
|
||||
use crate::game_plugin::GamePlugin;
|
||||
use crate::help_plugin::HelpPlugin;
|
||||
use crate::home_plugin::HomePlugin;
|
||||
use crate::hud_plugin::HudPlugin;
|
||||
use crate::input_plugin::InputPlugin;
|
||||
use crate::leaderboard_plugin::LeaderboardPlugin;
|
||||
use crate::onboarding_plugin::OnboardingPlugin;
|
||||
use crate::pause_plugin::PausePlugin;
|
||||
use crate::play_by_seed_plugin::PlayBySeedPlugin;
|
||||
use crate::profile_plugin::ProfilePlugin;
|
||||
use crate::progress_plugin::ProgressPlugin;
|
||||
use crate::radial_menu::RadialMenuPlugin;
|
||||
use crate::replay_overlay::ReplayOverlayPlugin;
|
||||
use crate::replay_playback::ReplayPlaybackPlugin;
|
||||
use crate::safe_area::SafeAreaInsetsPlugin;
|
||||
use crate::selection_plugin::SelectionPlugin;
|
||||
use crate::settings_plugin::SettingsPlugin;
|
||||
use crate::splash_plugin::SplashPlugin;
|
||||
use crate::stats_plugin::StatsPlugin;
|
||||
use crate::table_plugin::TablePlugin;
|
||||
use crate::time_attack_plugin::TimeAttackPlugin;
|
||||
use crate::touch_selection_plugin::TouchSelectionPlugin;
|
||||
use crate::ui_focus::UiFocusPlugin;
|
||||
use crate::ui_modal::UiModalPlugin;
|
||||
use crate::ui_tooltip::UiTooltipPlugin;
|
||||
use crate::weekly_goals_plugin::WeeklyGoalsPlugin;
|
||||
use crate::win_summary_plugin::WinSummaryPlugin;
|
||||
|
||||
/// The backlog (302 pairs on 2026-07-06) was burned down to ZERO the
|
||||
/// same day (#143, PRs #146–#149) — this is now a hard gate. If your
|
||||
@@ -51,7 +80,40 @@ mod tests {
|
||||
.add_plugins(AutoCompletePlugin)
|
||||
.add_plugins(UiModalPlugin)
|
||||
.add_plugins(UiFocusPlugin)
|
||||
.add_plugins(SettingsPlugin::headless());
|
||||
.add_plugins(SettingsPlugin::headless())
|
||||
// Batch: game-adjacent plugins (all already exercised headless
|
||||
// by the engine behaviour tests).
|
||||
.add_plugins(ProgressPlugin::headless())
|
||||
.add_plugins(StatsPlugin::headless())
|
||||
.add_plugins(AchievementPlugin::headless())
|
||||
.add_plugins(SelectionPlugin)
|
||||
.add_plugins(AnimationPlugin)
|
||||
.add_plugins(CardAnimationPlugin)
|
||||
.add_plugins(FeedbackAnimPlugin)
|
||||
.add_plugins(PausePlugin)
|
||||
// Batch: screen/overlay plugins.
|
||||
.add_plugins(HomePlugin::headless())
|
||||
.add_plugins(WinSummaryPlugin)
|
||||
.add_plugins(SplashPlugin)
|
||||
.add_plugins(HelpPlugin)
|
||||
.add_plugins(OnboardingPlugin)
|
||||
.add_plugins(ProfilePlugin)
|
||||
.add_plugins(LeaderboardPlugin)
|
||||
// Batch: mode/replay/input plugins.
|
||||
.add_plugins(ChallengePlugin)
|
||||
.add_plugins(DailyChallengePlugin)
|
||||
.add_plugins(TimeAttackPlugin)
|
||||
.add_plugins(WeeklyGoalsPlugin)
|
||||
.add_plugins(PlayBySeedPlugin)
|
||||
.add_plugins(DifficultyPlugin)
|
||||
.add_plugins(ReplayPlaybackPlugin)
|
||||
.add_plugins(ReplayOverlayPlugin)
|
||||
.add_plugins(RadialMenuPlugin)
|
||||
.add_plugins(InputPlugin)
|
||||
.add_plugins(TouchSelectionPlugin)
|
||||
.add_plugins(UiTooltipPlugin)
|
||||
.add_plugins(CursorPlugin)
|
||||
.add_plugins(SafeAreaInsetsPlugin);
|
||||
app.init_resource::<ButtonInput<KeyCode>>();
|
||||
app
|
||||
}
|
||||
|
||||
@@ -134,6 +134,14 @@ impl KeyboardDragState {
|
||||
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct SelectionKeySet;
|
||||
|
||||
/// Set wrapping the post-mutation selection maintenance chain (clearing the
|
||||
/// selection on state change, painting the highlight). Runs between
|
||||
/// [`crate::game_plugin::GameMutation`] and
|
||||
/// [`crate::card_plugin::BoardVisuals`] so the painters read the settled
|
||||
/// selection/drag state (#143).
|
||||
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct SelectionFx;
|
||||
|
||||
/// Marker component placed on the outline sprite used as the keyboard-selection
|
||||
/// highlight.
|
||||
///
|
||||
@@ -158,14 +166,35 @@ impl Plugin for SelectionPlugin {
|
||||
(
|
||||
handle_selection_keys
|
||||
.in_set(SelectionKeySet)
|
||||
.before(GameMutation),
|
||||
clear_selection_on_state_change.after(GameMutation),
|
||||
update_selection_highlight.after(GameMutation).run_if(
|
||||
resource_changed::<SelectionState>
|
||||
.or(resource_changed::<KeyboardDragState>)
|
||||
.or(resource_changed::<GameStateResource>),
|
||||
),
|
||||
.before(GameMutation)
|
||||
// Enter-to-move feeds the shared move request
|
||||
// stream (drained wholesale at GameMutation, #143).
|
||||
.in_set(crate::game_plugin::MoveRequestWriters)
|
||||
.ambiguous_with(crate::game_plugin::MoveRequestWriters),
|
||||
(
|
||||
clear_selection_on_state_change,
|
||||
update_selection_highlight.run_if(
|
||||
resource_changed::<SelectionState>
|
||||
.or(resource_changed::<KeyboardDragState>)
|
||||
.or(resource_changed::<GameStateResource>),
|
||||
),
|
||||
)
|
||||
.chain()
|
||||
.in_set(SelectionFx),
|
||||
),
|
||||
)
|
||||
.configure_sets(
|
||||
Update,
|
||||
SelectionFx
|
||||
.after(GameMutation)
|
||||
.before(crate::card_plugin::BoardVisuals),
|
||||
)
|
||||
.configure_sets(
|
||||
Update,
|
||||
// Selection reads GameStateResource (legal destinations) —
|
||||
// run after the pre-mutation spine that ticks/mutates it
|
||||
// (#143).
|
||||
SelectionKeySet.after(crate::game_plugin::PreMutation),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,19 @@ pub struct SettingsChangedEvent(pub Settings);
|
||||
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct SettingsMutation;
|
||||
|
||||
/// Self-ambiguous club for the screen/overlay systems that touch
|
||||
/// [`SettingsResource`] (onboarding first-run flag, home difficulty/draw
|
||||
/// chips, leaderboard opt-in and display name, pause draw toggle, splash
|
||||
/// reduce-motion reads, …). Each accessor touches its own disjoint fields,
|
||||
/// and the modal-stacking guard means at most one of these screens is
|
||||
/// interactive at a time — same-frame access order is provably irrelevant,
|
||||
/// so members declare `.in_set(SettingsAccess).ambiguous_with(SettingsAccess)`
|
||||
/// (#143). [`SettingsMutation`] is nested inside the club (its members keep
|
||||
/// their internal chain). Do NOT add members whose settings read must be
|
||||
/// same-frame-exact; order those `.after(SettingsMutation)` instead.
|
||||
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct SettingsAccess;
|
||||
|
||||
/// Marker on the root Settings panel entity.
|
||||
#[derive(Component, Debug)]
|
||||
pub struct SettingsPanel;
|
||||
@@ -459,7 +472,12 @@ impl Plugin for SettingsPlugin {
|
||||
Update,
|
||||
SettingsMutation
|
||||
.after(crate::layout::LayoutSystem::UpdateOnResize)
|
||||
.before(crate::game_plugin::GameMutation),
|
||||
.before(crate::game_plugin::GameMutation)
|
||||
// Nested in the club: members keep their chain, but
|
||||
// their pairs against other screen-plugin settings
|
||||
// accessors are declared irrelevant (#143).
|
||||
.in_set(SettingsAccess)
|
||||
.ambiguous_with(SettingsAccess),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
@@ -482,8 +500,12 @@ impl Plugin for SettingsPlugin {
|
||||
toggle_settings_screen
|
||||
.before(crate::ui_focus::FocusKeys)
|
||||
.ambiguous_with(crate::hud_plugin::HudButtons),
|
||||
scroll_settings_panel,
|
||||
crate::ui_modal::touch_scroll_panel::<SettingsPanelScrollable>,
|
||||
scroll_settings_panel
|
||||
.in_set(crate::ui_modal::PanelScroll)
|
||||
.ambiguous_with(crate::ui_modal::PanelScroll),
|
||||
crate::ui_modal::touch_scroll_panel::<SettingsPanelScrollable>
|
||||
.in_set(crate::ui_modal::PanelScroll)
|
||||
.ambiguous_with(crate::ui_modal::PanelScroll),
|
||||
)
|
||||
.chain(),
|
||||
);
|
||||
|
||||
@@ -99,7 +99,18 @@ impl Plugin for SplashPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(Startup, spawn_splash).add_systems(
|
||||
Update,
|
||||
(dismiss_splash_on_input, advance_splash, pulse_splash_cursor).chain(),
|
||||
// Splash text/fade writers touch UI entities only — they join
|
||||
// the UiTextFx club (disjoint from board/anim sets) and, for
|
||||
// the reduce-motion read in the pulse, the screen-plugin
|
||||
// settings club. Disjoint from the HUD button painters for the
|
||||
// same entity-domain reason (#143).
|
||||
(dismiss_splash_on_input, advance_splash, pulse_splash_cursor)
|
||||
.chain()
|
||||
.in_set(crate::ui_theme::UiTextFx)
|
||||
.ambiguous_with(crate::ui_theme::UiTextFx)
|
||||
.ambiguous_with(crate::hud_plugin::HudButtons)
|
||||
.in_set(crate::settings_plugin::SettingsAccess)
|
||||
.ambiguous_with(crate::settings_plugin::SettingsAccess),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,14 @@ pub struct StatsStoragePath(pub Option<PathBuf>);
|
||||
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct StatsUpdate;
|
||||
|
||||
/// Set holding the abandon-recorder (`update_stats_on_new_game`). It must
|
||||
/// read the outgoing game's `move_count` before any mode-start handler or
|
||||
/// `GameMutation` clobbers the game state — mode plugins order their reset
|
||||
/// handlers `.after(AbandonRecord)` via `crate::game_plugin::ModeStart`
|
||||
/// (#143).
|
||||
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct AbandonRecord;
|
||||
|
||||
/// Marker component on the stats overlay root node.
|
||||
#[derive(Component, Debug)]
|
||||
pub struct StatsScreen;
|
||||
@@ -205,10 +213,34 @@ impl Plugin for StatsPlugin {
|
||||
// constraints (win_summary_plugin: cache_win_data.before(StatsUpdate)),
|
||||
// and a system cannot be both inside a set and individually before a
|
||||
// set-level ordering constraint.
|
||||
.add_systems(Update, update_stats_on_new_game.before(GameMutation))
|
||||
.add_systems(
|
||||
Update,
|
||||
update_stats_on_win.after(GameMutation).in_set(StatsUpdate),
|
||||
update_stats_on_new_game
|
||||
.in_set(AbandonRecord)
|
||||
.before(GameMutation)
|
||||
// Reads the shared new-game request stream via its own
|
||||
// cursor — append order is irrelevant, so it joins the
|
||||
// self-ambiguous writer club (NewGameRequestWriters must
|
||||
// not carry set-level edges; see its doc). The explicit
|
||||
// edges pin it after the pause/settings/selection
|
||||
// mutators and the pre-mutation game-state spine it
|
||||
// reads `move_count` from (#143).
|
||||
.in_set(crate::game_plugin::NewGameRequestWriters)
|
||||
.ambiguous_with(crate::game_plugin::NewGameRequestWriters)
|
||||
.after(crate::game_plugin::PreMutation)
|
||||
.after(crate::pause_plugin::PauseMutation)
|
||||
.after(crate::settings_plugin::SettingsMutation)
|
||||
.after(crate::selection_plugin::SelectionKeySet),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
update_stats_on_win
|
||||
.after(GameMutation)
|
||||
.in_set(StatsUpdate)
|
||||
// Also emits toasts (streak milestones) — join the
|
||||
// self-ambiguous writer club (#143).
|
||||
.in_set(crate::game_plugin::InfoToastWriters)
|
||||
.ambiguous_with(crate::game_plugin::InfoToastWriters),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
@@ -216,29 +248,69 @@ impl Plugin for StatsPlugin {
|
||||
// the NewGameRequestEvent it emits is not visible to
|
||||
// update_stats_on_new_game in the same frame — otherwise
|
||||
// record_abandoned() fires twice on every forfeit (#21).
|
||||
// It drains the ForfeitEvent the pause/forfeit modal chain
|
||||
// emits, and shares the new-game request stream with the
|
||||
// other writers (#143).
|
||||
handle_forfeit
|
||||
.before(GameMutation)
|
||||
.before(update_stats_on_new_game),
|
||||
.before(update_stats_on_new_game)
|
||||
.after(crate::pause_plugin::PauseMutation)
|
||||
.after(crate::selection_plugin::SelectionKeySet)
|
||||
.in_set(crate::game_plugin::NewGameRequestWriters)
|
||||
.ambiguous_with(crate::game_plugin::NewGameRequestWriters),
|
||||
)
|
||||
// Open/close/tab handling moved to `you_hub_plugin`
|
||||
// (Phase E) — this plugin now owns body content, replay
|
||||
// selector behavior, and scroll.
|
||||
.add_systems(Update, refresh_replay_history_on_win.after(GameMutation))
|
||||
.add_systems(Update, handle_watch_replay_button)
|
||||
.add_systems(Update, handle_copy_share_link_button)
|
||||
//
|
||||
// Stats overlay UI: one deterministic chain — every member
|
||||
// touches the replay-selector state or the overlay's nodes, and
|
||||
// handle_watch_replay_button resets the live game, so the chain
|
||||
// sits between GameMutation and the auto-complete driver on the
|
||||
// game-state timeline (#143). The toast-emitting members join
|
||||
// the self-ambiguous InfoToastWriters set so the toast pipeline
|
||||
// (AnimFx) drains them this frame.
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
refresh_replay_history_on_win,
|
||||
handle_replay_selector_buttons,
|
||||
repaint_replay_selector_caption,
|
||||
repaint_replay_selector_detail,
|
||||
// Marker-filtered Text repaints — disjoint from the
|
||||
// HUD text painters (UiTextFx club, #143).
|
||||
repaint_replay_selector_caption
|
||||
.in_set(crate::ui_theme::UiTextFx)
|
||||
.ambiguous_with(crate::ui_theme::UiTextFx),
|
||||
repaint_replay_selector_detail
|
||||
.in_set(crate::ui_theme::UiTextFx)
|
||||
.ambiguous_with(crate::ui_theme::UiTextFx),
|
||||
handle_watch_replay_button
|
||||
.in_set(crate::game_plugin::InfoToastWriters)
|
||||
.ambiguous_with(crate::game_plugin::InfoToastWriters),
|
||||
handle_copy_share_link_button
|
||||
.in_set(crate::game_plugin::InfoToastWriters)
|
||||
.ambiguous_with(crate::game_plugin::InfoToastWriters),
|
||||
)
|
||||
.chain(),
|
||||
.chain()
|
||||
.after(GameMutation)
|
||||
// The overlay repaints read this frame's settled stats,
|
||||
// progress, and achievement evaluation (level line,
|
||||
// streak).
|
||||
.after(StatsUpdate)
|
||||
.after(crate::progress_plugin::ProgressUpdate)
|
||||
.after(crate::achievement_plugin::AchievementEval)
|
||||
.before(crate::auto_complete_plugin::AutoComplete),
|
||||
)
|
||||
.add_systems(Update, scroll_stats_panel)
|
||||
.add_systems(
|
||||
Update,
|
||||
crate::ui_modal::touch_scroll_panel::<StatsScrollable>,
|
||||
scroll_stats_panel
|
||||
.in_set(crate::ui_modal::PanelScroll)
|
||||
.ambiguous_with(crate::ui_modal::PanelScroll),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
crate::ui_modal::touch_scroll_panel::<StatsScrollable>
|
||||
.in_set(crate::ui_modal::PanelScroll)
|
||||
.ambiguous_with(crate::ui_modal::PanelScroll),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,12 @@ impl Plugin for TablePlugin {
|
||||
// paint pipeline — markers and cards share Sprite/
|
||||
// Transform access (#143).
|
||||
(
|
||||
apply_theme_on_settings_change,
|
||||
// Drains SettingsChangedEvent via cursor — a home
|
||||
// deal-option edit (post-mutation SettingsAccess
|
||||
// writer) surfacing one frame later repaints one
|
||||
// frame later, which is invisible (#143).
|
||||
apply_theme_on_settings_change
|
||||
.ambiguous_with(crate::settings_plugin::SettingsAccess),
|
||||
apply_hint_pile_highlight,
|
||||
tick_hint_pile_highlights,
|
||||
sync_pile_marker_visibility.after(GameMutation),
|
||||
|
||||
@@ -109,13 +109,41 @@ impl Plugin for TimeAttackPlugin {
|
||||
.add_message::<NewGameRequestEvent>()
|
||||
.add_message::<StartTimeAttackRequestEvent>()
|
||||
.add_message::<InfoToastEvent>()
|
||||
// ModeStart places it between the stats abandon-recorder and
|
||||
// GameMutation; it shares the new-game request/toast streams
|
||||
// with the other writers (#143).
|
||||
.add_systems(
|
||||
Update,
|
||||
handle_start_time_attack_request.before(GameMutation),
|
||||
handle_start_time_attack_request
|
||||
.in_set(crate::game_plugin::ModeStart)
|
||||
.in_set(crate::game_plugin::NewGameRequestWriters)
|
||||
.ambiguous_with(crate::game_plugin::NewGameRequestWriters)
|
||||
.in_set(crate::game_plugin::InfoToastWriters)
|
||||
.ambiguous_with(crate::game_plugin::InfoToastWriters),
|
||||
)
|
||||
// One post-mutation chain for the TimeAttackResource writers:
|
||||
// clock tick, win auto-deal, then the debounced disk save.
|
||||
// The start handler (ModeStart, pre-mutation) is ordered
|
||||
// against all three transitively. auto_deal's new-game request
|
||||
// is drained wholesale next frame, so it joins the writer club
|
||||
// (#143). PausedResource is settled: PauseMutation < GameMutation.
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
advance_time_attack,
|
||||
auto_deal_on_time_attack_win
|
||||
.in_set(crate::game_plugin::NewGameRequestWriters)
|
||||
.ambiguous_with(crate::game_plugin::NewGameRequestWriters),
|
||||
auto_save_time_attack_session,
|
||||
)
|
||||
.chain()
|
||||
.after(GameMutation)
|
||||
// The HUD clock (after AutoComplete) and the ended-toast
|
||||
// handler (AnimFx) read what this chain writes — settle
|
||||
// it before both (#143).
|
||||
.before(crate::auto_complete_plugin::AutoComplete)
|
||||
.before(crate::animation_plugin::AnimFx),
|
||||
)
|
||||
.add_systems(Update, advance_time_attack)
|
||||
.add_systems(Update, auto_deal_on_time_attack_win.after(GameMutation))
|
||||
.add_systems(Update, auto_save_time_attack_session)
|
||||
.add_systems(Last, save_time_attack_session_on_exit);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,11 @@ impl Plugin for TouchSelectionPlugin {
|
||||
update_touch_selection_highlight,
|
||||
)
|
||||
.chain()
|
||||
.after(GameMutation),
|
||||
.after(GameMutation)
|
||||
// Selection cleanup and highlight placement settle before
|
||||
// the board painters repaint (and before resync's synthetic
|
||||
// StateChanged / fan-frac's layout write land) (#143).
|
||||
.before(crate::card_plugin::BoardVisuals),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -546,6 +546,15 @@ pub fn spawn_tab_chip<M: Component>(
|
||||
// Generic touch-scroll helper
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Self-ambiguous writer set for the per-modal scroll systems (mouse-wheel
|
||||
/// and touch variants). Every member writes `ScrollPosition`, but each
|
||||
/// targets only its own modal's scrollable body via a marker component, and
|
||||
/// the modal-stacking guard means at most one such panel exists at a time —
|
||||
/// the writes are disjoint by design, so members declare
|
||||
/// `.in_set(PanelScroll).ambiguous_with(PanelScroll)` (#143).
|
||||
#[derive(bevy::ecs::schedule::SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct PanelScroll;
|
||||
|
||||
/// Scrolls any `Overflow::scroll_y()` panel marked with `M` via single-finger
|
||||
/// touch pan. Add this as a system for each scrollable modal panel:
|
||||
///
|
||||
@@ -840,7 +849,11 @@ impl Plugin for UiModalPlugin {
|
||||
.in_set(crate::ui_theme::UiTextFx)
|
||||
.ambiguous_with(crate::ui_theme::UiTextFx)
|
||||
.ambiguous_with(crate::card_plugin::BoardVisuals)
|
||||
.ambiguous_with(crate::hud_plugin::HudButtons),
|
||||
.ambiguous_with(crate::hud_plugin::HudButtons)
|
||||
// Reads only the anim-speed settings field — disjoint from
|
||||
// the screen-plugin club's writes (#143).
|
||||
.in_set(crate::settings_plugin::SettingsAccess)
|
||||
.ambiguous_with(crate::settings_plugin::SettingsAccess),
|
||||
);
|
||||
// Click-outside-to-dismiss is independent of the open
|
||||
// animation chain — it reads `just_pressed(Left)` and runs
|
||||
|
||||
@@ -93,7 +93,23 @@ impl Plugin for UiTooltipPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_resource::<TooltipState>()
|
||||
.add_systems(Startup, spawn_tooltip_overlay)
|
||||
.add_systems(Update, (track_tooltip_hover, show_or_hide_tooltip).chain());
|
||||
// The tooltip pair only touches the tooltip overlay entity and
|
||||
// reads settings for the enable flag — chrome-disjoint from the
|
||||
// other UiTextFx members, and a settings read that may lag a
|
||||
// frame joins the SettingsAccess club (#143).
|
||||
.add_systems(
|
||||
Update,
|
||||
(track_tooltip_hover, show_or_hide_tooltip)
|
||||
.chain()
|
||||
.in_set(crate::ui_theme::UiTextFx)
|
||||
.ambiguous_with(crate::ui_theme::UiTextFx)
|
||||
.in_set(crate::settings_plugin::SettingsAccess)
|
||||
.ambiguous_with(crate::settings_plugin::SettingsAccess)
|
||||
// The tooltip node is neither a board entity nor a pile
|
||||
// marker; the visibility writes can't collide (#143).
|
||||
.ambiguous_with(crate::card_plugin::BoardVisuals)
|
||||
.ambiguous_with(crate::table_plugin::MarkerVisuals),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ use solitaire_data::{
|
||||
};
|
||||
|
||||
use crate::events::{GameWonEvent, XpAwardedEvent};
|
||||
use crate::game_plugin::GameMutation;
|
||||
use crate::progress_plugin::{LevelUpEvent, ProgressResource, ProgressStoragePath, ProgressUpdate};
|
||||
|
||||
use crate::progress_plugin::{LevelUpEvent, ProgressResource, ProgressStoragePath};
|
||||
use crate::resources::GameStateResource;
|
||||
|
||||
/// Fired when the player has just completed a weekly goal.
|
||||
@@ -33,11 +33,11 @@ impl Plugin for WeeklyGoalsPlugin {
|
||||
.add_systems(Startup, roll_weekly_goals_on_startup)
|
||||
// Run after GameMutation (so GameWonEvent is available) and
|
||||
// ProgressUpdate (so we don't fight ProgressPlugin's add_xp).
|
||||
// Ordering comes from the mode-completion chain configured in
|
||||
// progress_plugin (#143).
|
||||
.add_systems(
|
||||
Update,
|
||||
evaluate_weekly_goals
|
||||
.after(GameMutation)
|
||||
.after(ProgressUpdate),
|
||||
evaluate_weekly_goals.in_set(crate::progress_plugin::WeeklyGoalsEval),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,20 +232,54 @@ impl Plugin for WinSummaryPlugin {
|
||||
// the player's old personal-best values before `StatsPlugin` overwrites them.
|
||||
.add_systems(
|
||||
Update,
|
||||
cache_win_data.after(GameMutation).before(StatsUpdate),
|
||||
cache_win_data
|
||||
.after(GameMutation)
|
||||
.before(StatsUpdate)
|
||||
// Must also read pre-award progress (old level for the
|
||||
// level-up row) and emits the personal-best toast.
|
||||
.before(crate::progress_plugin::ProgressUpdate)
|
||||
.in_set(crate::game_plugin::InfoToastWriters)
|
||||
.ambiguous_with(crate::game_plugin::InfoToastWriters),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
// One deterministic chain, late on the frame: after AnimFx
|
||||
// so the unlock/XP event streams it drains are settled by
|
||||
// both achievement evaluators (win + replay-completion run
|
||||
// before AnimFx). Members join the request/toast writer
|
||||
// clubs they append to; the camera shake and the score
|
||||
// reveal touch entity domains disjoint from the card/HUD
|
||||
// painters (#143).
|
||||
(
|
||||
collect_session_achievements,
|
||||
spawn_win_summary_after_delay,
|
||||
handle_win_summary_buttons,
|
||||
collect_session_achievements
|
||||
.in_set(crate::game_plugin::NewGameRequestWriters)
|
||||
.ambiguous_with(crate::game_plugin::NewGameRequestWriters),
|
||||
spawn_win_summary_after_delay
|
||||
.in_set(crate::settings_plugin::SettingsAccess)
|
||||
.ambiguous_with(crate::settings_plugin::SettingsAccess),
|
||||
handle_win_summary_buttons
|
||||
.in_set(crate::game_plugin::NewGameRequestWriters)
|
||||
.ambiguous_with(crate::game_plugin::NewGameRequestWriters)
|
||||
.in_set(crate::game_plugin::InfoToastWriters)
|
||||
.ambiguous_with(crate::game_plugin::InfoToastWriters),
|
||||
close_overlay_on_watch_replay,
|
||||
handle_win_summary_keyboard,
|
||||
apply_screen_shake,
|
||||
reveal_score_breakdown,
|
||||
handle_win_summary_keyboard
|
||||
.in_set(crate::game_plugin::NewGameRequestWriters)
|
||||
.ambiguous_with(crate::game_plugin::NewGameRequestWriters)
|
||||
.before(crate::ui_focus::FocusKeys),
|
||||
apply_screen_shake
|
||||
.ambiguous_with(crate::feedback_anim_plugin::FeedbackFx)
|
||||
// Shakes the camera; hint ghosts are their own
|
||||
// entity domain (#143).
|
||||
.ambiguous_with(crate::feedback_anim_plugin::HintGhostFx)
|
||||
.ambiguous_with(crate::ui_theme::UiTextFx),
|
||||
reveal_score_breakdown
|
||||
.in_set(crate::ui_theme::UiTextFx)
|
||||
.ambiguous_with(crate::ui_theme::UiTextFx),
|
||||
)
|
||||
.after(GameMutation),
|
||||
.chain()
|
||||
.after(GameMutation)
|
||||
.after(crate::animation_plugin::AnimFx),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user