diff --git a/solitaire_engine/src/card_plugin.rs b/solitaire_engine/src/card_plugin.rs index 87060bb..ae65ad7 100644 --- a/solitaire_engine/src/card_plugin.rs +++ b/solitaire_engine/src/card_plugin.rs @@ -14,6 +14,7 @@ use std::collections::{HashMap, HashSet}; use bevy::color::Color; use bevy::prelude::*; +use bevy::window::WindowResized; use solitaire_core::card::{Card, Rank, Suit}; use solitaire_core::game_state::{DrawMode, GameState}; use solitaire_core::pile::PileType; @@ -23,7 +24,7 @@ use solitaire_core::rules::{can_place_on_foundation, can_place_on_tableau}; use crate::animation_plugin::{CardAnim, EffectiveSlideDuration}; use crate::events::{CardFaceRevealedEvent, CardFlippedEvent, StateChangedEvent}; use crate::game_plugin::GameMutation; -use crate::layout::{Layout, LayoutResource}; +use crate::layout::{Layout, LayoutResource, LayoutSystem}; use crate::pause_plugin::PausedResource; use crate::resources::{DragState, GameStateResource}; use crate::settings_plugin::{SettingsChangedEvent, SettingsResource}; @@ -191,6 +192,7 @@ impl Plugin for CardPlugin { clear_right_click_highlights_on_state_change.after(GameMutation), clear_right_click_highlights_on_pause, update_stock_empty_indicator.after(GameMutation), + snap_cards_on_window_resize.after(LayoutSystem::UpdateOnResize), ), ); } @@ -1114,6 +1116,63 @@ fn update_stock_empty_indicator( ); } +/// Snaps every card sprite to its target position when the window is resized. +/// +/// This replaces the old "fire `StateChangedEvent` from `on_window_resized`" +/// path. That path went through `sync_cards_on_change` → `update_card_entity`, +/// which inserts a `CardAnim` slide tween whenever the card moves more than +/// 1 unit. During a corner drag, every frame's `WindowResized` event +/// retargeted the tween from the card's mid-slide position, so cards never +/// reached steady state — the visible "snap back and forth" jitter. +/// +/// Cards now snap directly (no slide), matching the instant repositioning +/// already used for backgrounds and pile markers in +/// `table_plugin::on_window_resized`. Any in-flight `CardAnim` is removed so +/// it cannot keep writing the old target translation after the snap. +/// +/// The "↺" stock-empty label's `font_size` is derived from +/// `layout.card_size.x`, so this system also reapplies the stock indicator — +/// otherwise the label would not rescale on resize once +/// `update_stock_empty_indicator` stopped firing on resize. +/// +/// Scheduled `.after(LayoutSystem::UpdateOnResize)` so `LayoutResource` has +/// been refreshed by `table_plugin::on_window_resized` before this runs. +#[allow(clippy::type_complexity)] +fn snap_cards_on_window_resize( + mut events: MessageReader, + mut commands: Commands, + game: Option>, + layout: Option>, + mut entities: Query<(Entity, &CardEntity, &mut Transform)>, + mut pile_markers: Query<(Entity, &PileMarker, &mut Sprite)>, + label_children: Query<(Entity, &ChildOf), With>, +) { + if events.read().next().is_none() { + return; + } + let Some(game) = game else { return }; + let Some(layout) = layout else { return }; + + let mut targets: HashMap = HashMap::new(); + for (card, pos, z) in card_positions(&game.0, &layout.0) { + targets.insert(card.id, (pos, z)); + } + for (entity, marker, mut transform) in &mut entities { + if let Some(&(pos, z)) = targets.get(&marker.card_id) { + transform.translation = Vec3::new(pos.x, pos.y, z); + commands.entity(entity).remove::(); + } + } + + apply_stock_empty_indicator( + &mut commands, + &game.0, + &mut pile_markers, + &label_children, + &layout.0, + ); +} + #[cfg(test)] mod tests { use super::*; diff --git a/solitaire_engine/src/layout.rs b/solitaire_engine/src/layout.rs index 787e7c9..be6ba29 100644 --- a/solitaire_engine/src/layout.rs +++ b/solitaire_engine/src/layout.rs @@ -6,10 +6,22 @@ use std::collections::HashMap; use bevy::math::Vec2; -use bevy::prelude::Resource; +use bevy::prelude::{Resource, SystemSet}; use solitaire_core::card::Suit; use solitaire_core::pile::PileType; +/// Schedule labels for layout-related systems so cross-plugin ordering is +/// explicit instead of relying on Bevy's automatic resource-conflict ordering +/// (which only forces non-parallel execution, not a particular order). +#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum LayoutSystem { + /// The system that updates [`LayoutResource`], the table background, and + /// pile markers in response to a `WindowResized` event. Card-snap systems + /// (in `card_plugin`) run `.after(LayoutSystem::UpdateOnResize)` so they + /// see the fresh layout. + UpdateOnResize, +} + /// Minimum supported window dimensions. Layout is still computed below this /// size but cards will be small. pub const MIN_WINDOW: Vec2 = Vec2::new(800.0, 600.0); diff --git a/solitaire_engine/src/table_plugin.rs b/solitaire_engine/src/table_plugin.rs index fe95d11..181b450 100644 --- a/solitaire_engine/src/table_plugin.rs +++ b/solitaire_engine/src/table_plugin.rs @@ -10,7 +10,7 @@ use solitaire_core::card::Suit; use solitaire_core::pile::PileType; use crate::events::{HintVisualEvent, StateChangedEvent}; -use crate::layout::{compute_layout, Layout, LayoutResource}; +use crate::layout::{compute_layout, Layout, LayoutResource, LayoutSystem}; #[cfg(test)] use crate::layout::TABLE_COLOUR; use crate::settings_plugin::{SettingsChangedEvent, SettingsResource}; @@ -69,7 +69,7 @@ impl Plugin for TablePlugin { .add_systems( Update, ( - on_window_resized, + on_window_resized.in_set(LayoutSystem::UpdateOnResize), apply_theme_on_settings_change, apply_hint_pile_highlight, tick_hint_pile_highlights, @@ -275,12 +275,10 @@ fn spawn_pile_markers(commands: &mut Commands, layout: &Layout) { } } -#[allow(clippy::too_many_arguments)] #[allow(clippy::type_complexity)] fn on_window_resized( mut events: MessageReader, mut layout_res: Option>, - mut state_changed: MessageWriter, mut backgrounds: Query< (&mut Sprite, &mut Transform), (With, Without), @@ -311,8 +309,12 @@ fn on_window_resized( } } - // Reposition card sprites to the new layout. - state_changed.write(StateChangedEvent); + // Card sprites are repositioned by `card_plugin::snap_cards_on_window_resize` + // running `.after(LayoutSystem::UpdateOnResize)` — that system snaps card + // transforms directly to the new layout instead of going through + // `StateChangedEvent → sync_cards → CardAnim` which would retarget the + // slide tween every frame during a corner drag (the visible "snap back + // and forth" jitter). } // ---------------------------------------------------------------------------