Compare commits
10 Commits
710555bd7e
...
v0.42.0
| Author | SHA1 | Date | |
|---|---|---|---|
| c0cd7c2c15 | |||
| ac002d8255 | |||
| 0fc1fa139e | |||
| 4f0c5bb808 | |||
| a6b22df666 | |||
| b402c01918 | |||
| be478acde7 | |||
| 379873765d | |||
| 713a292057 | |||
| 58c2dfd0a9 |
@@ -6,6 +6,47 @@ project follows [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.42.0] — 2026-07-06
|
||||
|
||||
### Added
|
||||
|
||||
- **CI workspace gate.** New `test.yml` workflow runs clippy (deny warnings)
|
||||
and the full test suite on every master push and PR — previously no CI ran
|
||||
tests at all. Caught its own first bug (missing Bevy native deps) on its
|
||||
own PR. (#135)
|
||||
- **Schedule ambiguity gate.** A headless test builds the gameplay plugin
|
||||
cluster with Bevy ambiguity detection promoted to error. The initial
|
||||
measurement found 302 system pairs with conflicting data access and no
|
||||
ordering; four burn-down batches (PRs #146–#149) took it to ZERO the same
|
||||
day, and the gate now enforces 0. Keyboard consumption, board painting,
|
||||
and HUD updates all have deterministic order for the first time.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Browser canvas 36% smaller.** `canvas_bg.wasm` shrank 36.2 MB → 23.2 MB
|
||||
via a size-focused `wasm-release` profile (fat LTO, single codegen unit,
|
||||
opt-level "s"); verified visually identical in production. (#134)
|
||||
- **Quaternions API adoption.** Canonical `FOUNDATIONS`/`TABLEAUS` consts in
|
||||
`solitaire_core` replace five scattered enum lists; upstream
|
||||
`Suit::SUITS`/`Rank::RANKS` replace nine hand-rolled arrays, with the
|
||||
texture-atlas indexing re-keyed through tested canonical helpers. Net
|
||||
−177 lines. (#137)
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Sync push race.** The server's load→merge→store cycle now runs in one
|
||||
transaction; concurrent pushes from two devices can no longer overwrite
|
||||
each other's merge. (#136)
|
||||
- **Refresh-token rotation is single-use under concurrency** — rotation
|
||||
gates on the DELETE's row count, so a stolen-then-replayed refresh token
|
||||
loses the race and gets 401. (#136)
|
||||
- **Exit sync push actually completes.** Was a detached task killed by
|
||||
process teardown; now a bounded 2-second blocking wait on the app's final
|
||||
frame. (#138)
|
||||
- **Server auth hardening.** Login timing no longer reveals whether a
|
||||
username exists; concurrent duplicate registration returns 409 instead of
|
||||
500; avatar uploads are magic-byte checked. (#144, issues #139–#141)
|
||||
|
||||
## [0.41.1] — 2026-07-06
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -46,6 +46,11 @@ pub struct AutoCompleteState {
|
||||
/// Plugin that drives the auto-complete sequence.
|
||||
pub struct AutoCompletePlugin;
|
||||
|
||||
/// Set wrapping the auto-complete detect/drive chain; HUD readers of
|
||||
/// [`AutoCompleteState`] order themselves after it (#143).
|
||||
#[derive(bevy::ecs::schedule::SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct AutoComplete;
|
||||
|
||||
impl Plugin for AutoCompletePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_resource::<AutoCompleteState>()
|
||||
@@ -58,7 +63,9 @@ impl Plugin for AutoCompletePlugin {
|
||||
drive_auto_complete,
|
||||
)
|
||||
.chain()
|
||||
.after(GameMutation),
|
||||
.in_set(AutoComplete)
|
||||
.after(GameMutation)
|
||||
.before(crate::card_plugin::BoardVisuals),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,17 @@ pub struct GameMutation;
|
||||
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct NewGameRequestWriters;
|
||||
|
||||
/// Self-ambiguous set for writers of `UndoRequestEvent` — same rationale as
|
||||
/// [`NewGameRequestWriters`]: consumers drain the queue, append order is
|
||||
/// meaningless (#143).
|
||||
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct UndoRequestWriters;
|
||||
|
||||
/// Self-ambiguous set for writers of `InfoToastEvent` — toasts queue in
|
||||
/// arrival order and any same-frame order is fine (#143).
|
||||
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct InfoToastWriters;
|
||||
|
||||
/// Persistence path for the in-progress game state file. `None` disables I/O.
|
||||
#[derive(Resource, Debug, Clone)]
|
||||
pub struct GameStatePath(pub Option<PathBuf>);
|
||||
@@ -233,7 +244,14 @@ impl Plugin for GamePlugin {
|
||||
.chain()
|
||||
.in_set(GameMutation),
|
||||
)
|
||||
.add_systems(Update, check_no_moves.after(GameMutation))
|
||||
.add_systems(
|
||||
Update,
|
||||
check_no_moves
|
||||
.after(GameMutation)
|
||||
.before(crate::card_plugin::BoardVisuals)
|
||||
.in_set(InfoToastWriters)
|
||||
.ambiguous_with(InfoToastWriters),
|
||||
)
|
||||
.add_systems(Update, record_replay_on_win.after(GameMutation))
|
||||
.add_systems(
|
||||
Update,
|
||||
@@ -244,14 +262,16 @@ impl Plugin for GamePlugin {
|
||||
handle_game_over_button_input,
|
||||
)
|
||||
.after(GameMutation)
|
||||
.before(crate::ui_focus::FocusKeys)
|
||||
.in_set(NewGameRequestWriters)
|
||||
.ambiguous_with(NewGameRequestWriters),
|
||||
.ambiguous_with(NewGameRequestWriters)
|
||||
.in_set(UndoRequestWriters)
|
||||
.ambiguous_with(UndoRequestWriters),
|
||||
)
|
||||
// Restore prompt: spawn the modal once the splash is gone,
|
||||
// route Continue / New Game intents back into the existing
|
||||
// GameMutation flow.
|
||||
.add_systems(Update, spawn_restore_prompt_if_pending)
|
||||
// All three pre-mutation game-state writers are chained: elapsed
|
||||
// All pre-mutation game-state writers are chained: elapsed
|
||||
// time ticks first, settings sync next, then the restore prompt —
|
||||
// a deterministic spine instead of three unordered ResMut holders
|
||||
// (ambiguity burn-down, #143).
|
||||
@@ -260,11 +280,13 @@ impl Plugin for GamePlugin {
|
||||
(
|
||||
tick_elapsed_time,
|
||||
sync_settings_to_game,
|
||||
spawn_restore_prompt_if_pending,
|
||||
handle_restore_prompt
|
||||
.in_set(NewGameRequestWriters)
|
||||
.ambiguous_with(NewGameRequestWriters),
|
||||
)
|
||||
.chain()
|
||||
.after(crate::settings_plugin::SettingsMutation)
|
||||
.before(GameMutation),
|
||||
)
|
||||
.init_resource::<AutoSaveTimer>()
|
||||
|
||||
@@ -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,
|
||||
@@ -153,6 +154,13 @@ pub struct HudColumn;
|
||||
#[derive(Component, Debug)]
|
||||
pub struct HudActionBar;
|
||||
|
||||
/// Set wrapping the chained HUD button/popover interaction systems. Other
|
||||
/// keyboard consumers order themselves around it (e.g.
|
||||
/// [`crate::ui_focus::FocusKeys`] runs after) so input-consumption order is
|
||||
/// deterministic (#143).
|
||||
#[derive(bevy::ecs::schedule::SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct HudButtons;
|
||||
|
||||
/// Marker on the text node inside each touch-layout action-bar button.
|
||||
/// Used by `resize_action_bar_labels` to update font size on window resize.
|
||||
#[derive(Component, Debug)]
|
||||
@@ -467,25 +475,56 @@ impl Plugin for HudPlugin {
|
||||
// defensively so the HUD plugin works standalone in tests.
|
||||
.add_message::<WindowResized>()
|
||||
.add_systems(Startup, (spawn_hud_band, spawn_hud, spawn_action_buttons, spawn_hud_avatar))
|
||||
.add_systems(Update, update_hud.after(GameMutation))
|
||||
// HUD text updaters run as one deterministic chain (they write
|
||||
// disjoint Text nodes, but Bevy can't prove it); update_hud also
|
||||
// reads AutoCompleteState, so the chain sits after the
|
||||
// auto-complete detect/drive chain (#143).
|
||||
.add_systems(
|
||||
Update,
|
||||
apply_hud_visibility.before(LayoutSystem::UpdateOnResize),
|
||||
)
|
||||
.add_systems(Update, restore_hud_on_modal)
|
||||
.add_systems(Update, (update_hud_avatar, handle_avatar_button))
|
||||
.add_systems(Update, update_won_previously.after(GameMutation))
|
||||
.add_systems(Update, announce_auto_complete.after(GameMutation))
|
||||
.add_systems(
|
||||
Update,
|
||||
update_selection_hud
|
||||
.after(GameMutation)
|
||||
.run_if(
|
||||
(
|
||||
update_hud,
|
||||
update_selection_hud.run_if(
|
||||
resource_exists_and_changed::<SelectionState>
|
||||
.or(resource_exists_and_changed::<GameStateResource>),
|
||||
),
|
||||
update_won_previously,
|
||||
)
|
||||
.chain()
|
||||
.after(GameMutation)
|
||||
.after(crate::auto_complete_plugin::AutoComplete)
|
||||
.in_set(UiTextFx)
|
||||
.ambiguous_with(UiTextFx),
|
||||
)
|
||||
// HUD chrome visibility: modal-restore writes HudVisibility, the
|
||||
// applier consumes it, and the layout recompute reads it — a
|
||||
// fixed chain instead of three racing systems (#143).
|
||||
.add_systems(
|
||||
Update,
|
||||
(restore_hud_on_modal, apply_hud_visibility)
|
||||
.chain()
|
||||
.before(LayoutSystem::UpdateOnResize),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
update_hud_avatar.after(crate::settings_plugin::SettingsMutation),
|
||||
handle_avatar_button.ambiguous_with(HudButtons),
|
||||
),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
announce_auto_complete
|
||||
.after(GameMutation)
|
||||
.after(crate::auto_complete_plugin::AutoComplete)
|
||||
.in_set(crate::game_plugin::InfoToastWriters)
|
||||
.ambiguous_with(crate::game_plugin::InfoToastWriters),
|
||||
)
|
||||
// Typography rescale touches HUD TextFont only, but orders after
|
||||
// the board painters that resize card/label text (#143).
|
||||
.add_systems(
|
||||
Update,
|
||||
update_hud_typography.after(crate::card_plugin::BoardVisuals),
|
||||
)
|
||||
.add_systems(Update, update_hud_typography)
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
@@ -494,13 +533,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,
|
||||
@@ -508,10 +553,16 @@ impl Plugin for HudPlugin {
|
||||
handle_new_game_button
|
||||
.in_set(NewGameRequestWriters)
|
||||
.ambiguous_with(NewGameRequestWriters),
|
||||
handle_undo_button,
|
||||
handle_undo_button
|
||||
.in_set(crate::game_plugin::UndoRequestWriters)
|
||||
.ambiguous_with(crate::game_plugin::UndoRequestWriters)
|
||||
.before(GameMutation),
|
||||
handle_pause_button,
|
||||
handle_help_button,
|
||||
handle_hint_button.after(GameMutation),
|
||||
handle_hint_button
|
||||
.after(GameMutation)
|
||||
.in_set(crate::game_plugin::InfoToastWriters)
|
||||
.ambiguous_with(crate::game_plugin::InfoToastWriters),
|
||||
handle_modes_button,
|
||||
handle_mode_option_click
|
||||
.in_set(NewGameRequestWriters)
|
||||
@@ -523,7 +574,10 @@ impl Plugin for HudPlugin {
|
||||
handle_menu_backdrop_click,
|
||||
close_menu_popover_on_escape,
|
||||
paint_action_buttons,
|
||||
),
|
||||
)
|
||||
.chain()
|
||||
.in_set(HudButtons)
|
||||
.before(crate::ui_focus::FocusKeys),
|
||||
)
|
||||
// Fade lives in `Last` so it always overrides whatever the
|
||||
// hover/paint pass set on `BackgroundColor` this frame.
|
||||
|
||||
@@ -32,13 +32,14 @@ mod tests {
|
||||
use crate::ui_focus::UiFocusPlugin;
|
||||
use crate::ui_modal::UiModalPlugin;
|
||||
|
||||
/// Legacy ambiguity backlog measured 2026-07-06 (issue #143). This
|
||||
/// number may only decrease. If your change trips this assertion you
|
||||
/// have added a pair of systems with conflicting data access and no
|
||||
/// 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 = 198;
|
||||
/// 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
|
||||
/// change trips this assertion you have added a pair of systems with
|
||||
/// conflicting data access and no ordering edge: add `.before`/`.after`
|
||||
/// where order matters, or `.ambiguous_with` the relevant domain set
|
||||
/// (BoardVisuals, MarkerVisuals, UiTextFx, HudButtons, writer sets)
|
||||
/// where it provably does not. Do not raise this constant.
|
||||
const AMBIGUITY_BASELINE: usize = 0;
|
||||
|
||||
fn cluster_app() -> App {
|
||||
let mut app = App::new();
|
||||
@@ -96,10 +97,12 @@ mod tests {
|
||||
}
|
||||
};
|
||||
|
||||
assert!(
|
||||
count <= AMBIGUITY_BASELINE,
|
||||
"system-order ambiguities grew: {count} > baseline {AMBIGUITY_BASELINE}. \
|
||||
Add .before/.after or .ambiguous_with at the new registration site.",
|
||||
assert_eq!(
|
||||
count, AMBIGUITY_BASELINE,
|
||||
"system-order ambiguities changed from the enforced baseline. \
|
||||
Add .before/.after or .ambiguous_with at the new registration site \
|
||||
(or, if the count legitimately dropped below a nonzero baseline, \
|
||||
lower AMBIGUITY_BASELINE).",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,14 @@ pub struct PendingWindowGeometry {
|
||||
#[derive(Message, Debug, Clone)]
|
||||
pub struct SettingsChangedEvent(pub Settings);
|
||||
|
||||
/// System set for the systems that mutate [`SettingsResource`] every frame
|
||||
/// (hotkeys and window-geometry persistence). Ordered before
|
||||
/// [`crate::game_plugin::GameMutation`]; readers of settings should sit
|
||||
/// after this set (directly, or transitively via `.after(GameMutation)`)
|
||||
/// so they observe the current frame's settings deterministically (#143).
|
||||
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct SettingsMutation;
|
||||
|
||||
/// Marker on the root Settings panel entity.
|
||||
#[derive(Component, Debug)]
|
||||
pub struct SettingsPanel;
|
||||
@@ -372,16 +380,37 @@ impl Plugin for SettingsPlugin {
|
||||
// also runs cleanly under `MinimalPlugins` (tests).
|
||||
.add_message::<WindowResized>()
|
||||
.add_message::<WindowMoved>()
|
||||
// Settings changes land before game logic runs: the mutator
|
||||
// chain (volume keys → geometry record → geometry persist) is a
|
||||
// deterministic spine, and the whole set precedes GameMutation so
|
||||
// every reader already ordered after GameMutation sees this
|
||||
// frame's settings transitively (ambiguity burn-down, #143).
|
||||
.configure_sets(
|
||||
Update,
|
||||
SettingsMutation
|
||||
.after(crate::layout::LayoutSystem::UpdateOnResize)
|
||||
.before(crate::game_plugin::GameMutation),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
handle_volume_keys,
|
||||
toggle_settings_screen,
|
||||
scroll_settings_panel,
|
||||
crate::ui_modal::touch_scroll_panel::<SettingsPanelScrollable>,
|
||||
record_window_geometry_changes,
|
||||
persist_window_geometry_after_debounce,
|
||||
),
|
||||
)
|
||||
.chain()
|
||||
.in_set(SettingsMutation),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
toggle_settings_screen
|
||||
.before(crate::ui_focus::FocusKeys)
|
||||
.ambiguous_with(crate::hud_plugin::HudButtons),
|
||||
scroll_settings_panel,
|
||||
crate::ui_modal::touch_scroll_panel::<SettingsPanelScrollable>,
|
||||
)
|
||||
.chain(),
|
||||
);
|
||||
|
||||
if self.ui_enabled {
|
||||
|
||||
@@ -85,6 +85,13 @@ pub struct HintPileHighlight {
|
||||
/// Registers the table background and pile-marker rendering.
|
||||
pub struct TablePlugin;
|
||||
|
||||
/// Set wrapping the pile-marker painter chain (theme, hint highlights,
|
||||
/// visibility). Runs after [`crate::card_plugin::BoardVisuals`]; chrome-fx
|
||||
/// systems that touch `Visibility` on UI entities declare themselves
|
||||
/// ambiguous with it (#143).
|
||||
#[derive(bevy::ecs::schedule::SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct MarkerVisuals;
|
||||
|
||||
impl Plugin for TablePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
// Register WindowResized so the plugin works under MinimalPlugins in
|
||||
@@ -101,10 +108,18 @@ 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()
|
||||
.in_set(MarkerVisuals)
|
||||
.after(crate::card_plugin::BoardVisuals),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,6 +117,13 @@ pub struct FocusedButton(pub Option<Entity>);
|
||||
/// gains keyboard navigation without per-plugin wiring.
|
||||
pub struct UiFocusPlugin;
|
||||
|
||||
/// Set on [`handle_focus_keys`], the focus-ring keyboard navigator. It runs
|
||||
/// AFTER every app-level keyboard consumer (HUD buttons/popovers, restore
|
||||
/// prompt, settings toggle) so Esc/Tab consumption order is defined instead
|
||||
/// of scheduler-dependent (#143).
|
||||
#[derive(bevy::ecs::schedule::SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct FocusKeys;
|
||||
|
||||
impl Plugin for UiFocusPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_resource::<FocusedButton>()
|
||||
@@ -147,9 +154,19 @@ impl Plugin for UiFocusPlugin {
|
||||
(
|
||||
sync_focus_on_mouse_click,
|
||||
clear_hud_focus_on_unhover,
|
||||
handle_focus_keys,
|
||||
update_focus_overlay,
|
||||
pulse_focus_overlay,
|
||||
handle_focus_keys
|
||||
.in_set(FocusKeys)
|
||||
.after(crate::game_plugin::GameMutation),
|
||||
update_focus_overlay
|
||||
.in_set(crate::ui_theme::UiTextFx)
|
||||
.ambiguous_with(crate::ui_theme::UiTextFx)
|
||||
.ambiguous_with(crate::card_plugin::BoardVisuals)
|
||||
.ambiguous_with(crate::table_plugin::MarkerVisuals),
|
||||
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(),
|
||||
);
|
||||
|
||||
@@ -695,7 +695,12 @@ impl Plugin for UiModalPlugin {
|
||||
advance_modal_enter,
|
||||
paint_modal_buttons,
|
||||
)
|
||||
.chain(),
|
||||
.chain()
|
||||
.after(crate::settings_plugin::SettingsMutation)
|
||||
.in_set(crate::ui_theme::UiTextFx)
|
||||
.ambiguous_with(crate::ui_theme::UiTextFx)
|
||||
.ambiguous_with(crate::card_plugin::BoardVisuals)
|
||||
.ambiguous_with(crate::hud_plugin::HudButtons),
|
||||
);
|
||||
// Click-outside-to-dismiss is independent of the open
|
||||
// animation chain — it reads `just_pressed(Left)` and runs
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user