refactor(engine): ambiguity burn-down batch 3 — board paint chain (171 → 47) #148

Merged
funman300 merged 1 commits from refactor/ambiguity-board-visuals into master 2026-07-07 03:57:21 +00:00
7 changed files with 78 additions and 27 deletions
+39 -18
View File
@@ -533,6 +533,17 @@ fn should_apply_resize(now_secs: f32, last_applied_secs: f32) -> bool {
/// Renders cards by reading `GameStateResource` on `StateChangedEvent`.
pub struct CardPlugin;
/// System set for everything that paints the board: card sprites, pile
/// markers, shadows, highlights, badges. Members mutate `Sprite` /
/// `Transform` on board entities and run as a deterministic chain (see the
/// registration in [`CardPlugin`]'s `build`); table-plugin marker painters
/// order themselves after this set. UI-domain systems that touch `Sprite`/
/// `Transform` on non-board entities (HUD text pulses, modal cards) declare
/// `.ambiguous_with(BoardVisuals)` instead — the entity domains are
/// disjoint by design (#143).
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
pub struct BoardVisuals;
impl Plugin for CardPlugin {
fn build(&self, app: &mut App) {
// PostStartup ensures TablePlugin's Startup system has inserted
@@ -558,35 +569,45 @@ impl Plugin for CardPlugin {
update_stock_empty_indicator_startup,
),
)
// Layout recompute (UpdateOnResize) always precedes board
// painting, and the painters run as ONE deterministic chain in
// data-flow order: layout refinement → card authority → anims →
// shadows → highlights → indicators → resize snapping → labels.
// Every painter mutates card/marker Sprite+Transform, so without
// the chain each pair is a scheduler ambiguity (#143). All
// members are cheap and mostly change-gated; sequential
// execution is not a cost that matters here.
.configure_sets(
Update,
LayoutSystem::UpdateOnResize.before(BoardVisuals),
)
.add_systems(
Update,
(
update_tableau_fan_frac
.after(GameMutation)
.before(sync_cards_on_change),
sync_cards_on_change.after(GameMutation),
resync_cards_on_settings_change.before(sync_cards_on_change),
start_flip_anim.after(GameMutation),
update_tableau_fan_frac,
resync_cards_on_settings_change,
sync_cards_on_change,
start_flip_anim,
tick_flip_anim,
update_drag_shadow,
update_card_shadows_on_drag.after(sync_cards_on_change),
tick_hint_highlight.after(GameMutation),
handle_right_click.after(GameMutation),
update_card_shadows_on_drag,
handle_right_click,
tick_right_click_highlights,
clear_right_click_highlights_on_state_change.after(GameMutation),
clear_right_click_highlights_on_state_change,
clear_right_click_highlights_on_pause,
update_stock_empty_indicator.after(GameMutation),
tick_hint_highlight,
update_stock_empty_indicator,
update_stock_count_badge
.after(GameMutation)
.run_if(resource_changed::<GameStateResource>),
collect_resize_events.after(LayoutSystem::UpdateOnResize),
snap_cards_on_window_resize
.after(collect_resize_events)
.after(GameMutation),
),
collect_resize_events,
snap_cards_on_window_resize,
resize_android_corner_labels,
)
.chain()
.in_set(BoardVisuals)
.after(GameMutation),
);
app.add_systems(Update, resize_android_corner_labels);
app.add_systems(PostUpdate, rebuild_card_entity_index);
}
}
+9 -2
View File
@@ -54,6 +54,7 @@ use crate::time_attack_plugin::TimeAttackResource;
use crate::ui_focus::{FocusGroup, Focusable};
use crate::ui_modal::ModalScrim;
use crate::ui_theme::SPACE_2;
use crate::ui_theme::UiTextFx;
use crate::ui_theme::{
ACCENT_PRIMARY, ACCENT_SECONDARY, BG_ELEVATED, BG_ELEVATED_HI, BG_ELEVATED_PRESSED,
BG_HUD_BAND, BORDER_SUBTLE, HighContrastBorder, MOTION_SCORE_PULSE_SECS,
@@ -500,13 +501,19 @@ impl Plugin for HudPlugin {
advance_score_floater,
)
.chain()
.after(GameMutation),
.after(GameMutation)
.in_set(UiTextFx)
.ambiguous_with(UiTextFx)
.ambiguous_with(crate::card_plugin::BoardVisuals),
)
.add_systems(
Update,
(start_streak_flourish, advance_streak_flourish)
.chain()
.after(GameMutation),
.after(GameMutation)
.in_set(UiTextFx)
.ambiguous_with(UiTextFx)
.ambiguous_with(crate::card_plugin::BoardVisuals),
)
.add_systems(
Update,
+1 -1
View File
@@ -38,7 +38,7 @@ mod tests {
/// ordering edge — add `.before`/`.after` (order matters) or
/// `.ambiguous_with` (provably order-independent) at the registration
/// site. When triage lowers the real count, lower this constant too.
const AMBIGUITY_BASELINE: usize = 171;
const AMBIGUITY_BASELINE: usize = 47;
fn cluster_app() -> App {
let mut app = App::new();
+11 -4
View File
@@ -101,10 +101,17 @@ impl Plugin for TablePlugin {
(
on_safe_area_changed.before(LayoutSystem::UpdateOnResize),
on_window_resized.in_set(LayoutSystem::UpdateOnResize),
apply_theme_on_settings_change,
apply_hint_pile_highlight,
tick_hint_pile_highlights,
sync_pile_marker_visibility.after(GameMutation),
// Marker painters: deterministic chain after the card
// paint pipeline — markers and cards share Sprite/
// Transform access (#143).
(
apply_theme_on_settings_change,
apply_hint_pile_highlight,
tick_hint_pile_highlights,
sync_pile_marker_visibility.after(GameMutation),
)
.chain()
.after(crate::card_plugin::BoardVisuals),
),
);
}
+5 -1
View File
@@ -149,7 +149,11 @@ impl Plugin for UiFocusPlugin {
clear_hud_focus_on_unhover,
handle_focus_keys,
update_focus_overlay,
pulse_focus_overlay.after(crate::settings_plugin::SettingsMutation),
pulse_focus_overlay
.after(crate::settings_plugin::SettingsMutation)
.in_set(crate::ui_theme::UiTextFx)
.ambiguous_with(crate::ui_theme::UiTextFx)
.ambiguous_with(crate::card_plugin::BoardVisuals),
)
.chain(),
);
+4 -1
View File
@@ -696,7 +696,10 @@ impl Plugin for UiModalPlugin {
paint_modal_buttons,
)
.chain()
.after(crate::settings_plugin::SettingsMutation),
.after(crate::settings_plugin::SettingsMutation)
.in_set(crate::ui_theme::UiTextFx)
.ambiguous_with(crate::ui_theme::UiTextFx)
.ambiguous_with(crate::card_plugin::BoardVisuals),
);
// Click-outside-to-dismiss is independent of the open
// animation chain — it reads `just_pressed(Left)` and runs
+9
View File
@@ -698,3 +698,12 @@ mod tests {
assert_eq!(scaled_duration(0.18, AnimSpeed::Instant), 0.0);
}
}
/// System set for text/UI visual effects that animate `Transform`/`Sprite`
/// on chrome entities (HUD score pulse, streak flourish, modal enter, focus
/// ring). These never touch board entities, so members are declared
/// `.ambiguous_with(BoardVisuals)` and `.ambiguous_with(UiTextFx)` — the
/// entity domains are disjoint by construction and relative order within a
/// frame is invisible (#143).
#[derive(bevy::ecs::schedule::SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
pub struct UiTextFx;