refactor(engine,wasm,data): route all klondike/card_game imports through solitaire_core
All downstream crates now import Foundation, KlondikePile, Tableau, Klondike, Session, Suit, Rank exclusively from solitaire_core. solitaire_core is the single version-pin point for the upstream crates. - solitaire_engine: 19 files updated, klondike direct dep removed - solitaire_wasm: use statement updated, klondike direct dep removed - solitaire_data: unused klondike dep removed - Cargo.lock: klondike no longer a direct dep of engine/wasm/data - Full workspace clippy clean, all tests pass Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Generated
-3
@@ -7340,7 +7340,6 @@ dependencies = [
|
|||||||
"jni 0.21.1",
|
"jni 0.21.1",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"keyring-core",
|
"keyring-core",
|
||||||
"klondike",
|
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -7367,7 +7366,6 @@ dependencies = [
|
|||||||
"image",
|
"image",
|
||||||
"jni 0.21.1",
|
"jni 0.21.1",
|
||||||
"kira",
|
"kira",
|
||||||
"klondike",
|
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"resvg",
|
"resvg",
|
||||||
"ron",
|
"ron",
|
||||||
@@ -7427,7 +7425,6 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
"klondike",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde-wasm-bindgen",
|
"serde-wasm-bindgen",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ chrono = { workspace = true }
|
|||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
klondike = { workspace = true }
|
|
||||||
|
|
||||||
# These deps are not available / not needed on wasm32:
|
# These deps are not available / not needed on wasm32:
|
||||||
# dirs — platform data directories (no filesystem on browser)
|
# dirs — platform data directories (no filesystem on browser)
|
||||||
@@ -36,10 +35,6 @@ keyring-core = { workspace = true }
|
|||||||
|
|
||||||
[target.'cfg(target_os = "android")'.dependencies]
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
jni = { workspace = true }
|
jni = { workspace = true }
|
||||||
# android_keystore.rs uses bevy::android::ANDROID_APP to obtain the
|
|
||||||
# process-wide JavaVM handle for JNI. Must be listed here so the
|
|
||||||
# symbol resolves when cross-compiling for Android targets.
|
|
||||||
bevy = { workspace = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
solitaire_server = { path = "../solitaire_server" }
|
solitaire_server = { path = "../solitaire_server" }
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ image = { workspace = true }
|
|||||||
solitaire_core = { workspace = true }
|
solitaire_core = { workspace = true }
|
||||||
solitaire_data = { workspace = true }
|
solitaire_data = { workspace = true }
|
||||||
solitaire_sync = { workspace = true }
|
solitaire_sync = { workspace = true }
|
||||||
klondike = { workspace = true }
|
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
|
|||||||
@@ -1078,7 +1078,7 @@ mod tests {
|
|||||||
// Pairs the existing audio (`card_invalid.wav`) and visual
|
// Pairs the existing audio (`card_invalid.wav`) and visual
|
||||||
// (`feedback_anim_plugin::queue_shake_for_rejected_move`) feedback
|
// (`feedback_anim_plugin::queue_shake_for_rejected_move`) feedback
|
||||||
// with an accessibility-focused readable text cue.
|
// with an accessibility-focused readable text cue.
|
||||||
use klondike::{KlondikePile, Tableau};
|
use solitaire_core::{KlondikePile, Tableau};
|
||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
app.add_plugins(MinimalPlugins).add_plugins(AnimationPlugin);
|
app.add_plugins(MinimalPlugins).add_plugins(AnimationPlugin);
|
||||||
|
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::game_plugin::GamePlugin;
|
use crate::game_plugin::GamePlugin;
|
||||||
use crate::table_plugin::TablePlugin;
|
use crate::table_plugin::TablePlugin;
|
||||||
use klondike::{Foundation, KlondikePile, Tableau};
|
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
||||||
use solitaire_core::card::{Rank, Suit};
|
use solitaire_core::card::{Rank, Suit};
|
||||||
use solitaire_core::game_state::{DrawMode, GameState};
|
use solitaire_core::game_state::{DrawMode, GameState};
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use bevy::color::Color;
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::sprite::Anchor;
|
use bevy::sprite::Anchor;
|
||||||
use bevy::window::WindowResized;
|
use bevy::window::WindowResized;
|
||||||
use klondike::{Foundation, KlondikePile, Tableau};
|
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
||||||
use solitaire_core::card::{Card, Rank, Suit};
|
use solitaire_core::card::{Card, Rank, Suit};
|
||||||
use solitaire_core::game_state::{DrawMode, GameState};
|
use solitaire_core::game_state::{DrawMode, GameState};
|
||||||
|
|
||||||
@@ -230,7 +230,7 @@ pub struct StockEmptyLabel;
|
|||||||
/// The badge is spawned as a *top-level* world entity (not parented to the
|
/// The badge is spawned as a *top-level* world entity (not parented to the
|
||||||
/// stock [`PileMarker`]) and its `Transform` is recomputed each frame from
|
/// stock [`PileMarker`]) and its `Transform` is recomputed each frame from
|
||||||
/// `LayoutResource` so it tracks the stock pile through window resizes.
|
/// `LayoutResource` so it tracks the stock pile through window resizes.
|
||||||
/// The chip sits in the top-right corner of the stock pile and is hidden
|
/// The chip sits in the bottom-right corner of the stock pile and is hidden
|
||||||
/// while the stock is empty — the existing `↺` overlay
|
/// while the stock is empty — the existing `↺` overlay
|
||||||
/// ([`StockEmptyLabel`]) covers the recycle hint instead, so the two
|
/// ([`StockEmptyLabel`]) covers the recycle hint instead, so the two
|
||||||
/// indicators never render simultaneously.
|
/// indicators never render simultaneously.
|
||||||
@@ -301,13 +301,18 @@ pub struct CardShadow;
|
|||||||
#[derive(Component, Debug)]
|
#[derive(Component, Debug)]
|
||||||
pub struct CardBackFrame;
|
pub struct CardBackFrame;
|
||||||
|
|
||||||
/// Fill colour for the face-down card border frame. Medium gray so it reads as
|
/// Fill colour for the face-down card border frame. Light-medium gray so it
|
||||||
/// a neutral "edge" without competing with the suit colours on face-up cards.
|
/// reads as a clear "edge" without competing with the suit colours on face-up
|
||||||
const CARD_BACK_FRAME_COLOR: Color = Color::srgb(0.38, 0.38, 0.38);
|
/// cards. Brightened from `0.38` to `0.48` (≈ #7a7a7a) after a Pixel_7 smoke
|
||||||
|
/// test showed face-down `back_0.png` (≈ #1a1a1a) was nearly invisible against
|
||||||
|
/// the very dark `#151515` felt — the old gray was too close to the back fill
|
||||||
|
/// to define a crisp perimeter.
|
||||||
|
const CARD_BACK_FRAME_COLOR: Color = Color::srgb(0.48, 0.48, 0.48);
|
||||||
|
|
||||||
/// Extra width/height (in world units) added to each side of the card to form
|
/// Extra width/height (in world units) added to each side of the card to form
|
||||||
/// the visible border. 3 world units ≈ 3 dp on a 1× screen.
|
/// the visible border. Widened from `3.0` to `6.0` so the frame peeks out as a
|
||||||
const CARD_BACK_FRAME_PADDING: f32 = 3.0;
|
/// clearly readable perimeter at phone density (420 dpi) rather than a hairline.
|
||||||
|
const CARD_BACK_FRAME_PADDING: f32 = 6.0;
|
||||||
|
|
||||||
/// Returns the `(offset, padding, alpha)` triple used to paint a per-card
|
/// Returns the `(offset, padding, alpha)` triple used to paint a per-card
|
||||||
/// shadow given whether its parent card is currently part of the dragged
|
/// shadow given whether its parent card is currently part of the dragged
|
||||||
@@ -1901,19 +1906,22 @@ fn update_stock_empty_indicator(
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Stock-pile remaining-count badge
|
// Stock-pile remaining-count badge
|
||||||
//
|
//
|
||||||
// Shows a small "N" chip pinned to the top-right corner of the stock pile so
|
// Shows a small "N" chip pinned to the bottom-right corner of the stock pile so
|
||||||
// the player can see how many cards remain before the next recycle. The
|
// the player can see how many cards remain before the next recycle. The
|
||||||
// existing `StockEmptyLabel` (`↺` overlay) covers the empty-stock case, so
|
// existing `StockEmptyLabel` (`↺` overlay) covers the empty-stock case, so
|
||||||
// the badge hides itself when the stock has zero cards — the two indicators
|
// the badge hides itself when the stock has zero cards — the two indicators
|
||||||
// never render at the same time.
|
// never render at the same time.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Inset (in pixels) from the top-right corner of the stock pile sprite to
|
/// Inset (in pixels) from the bottom-right corner of the stock pile sprite to
|
||||||
/// the centre of the count badge. Must satisfy `|x| >= STOCK_BADGE_SIZE.x / 2`
|
/// the centre of the count badge. Anchoring to the bottom-right keeps the chip
|
||||||
/// so the badge right edge stays inside the stock pile and never overlaps the
|
/// clear of the rank/suit pip in the card's top-left corner. Both components
|
||||||
/// adjacent waste pile — critical on Android where `H_GAP_DIVISOR = 32` gives
|
/// move the centre *inward* from that corner: `x` is subtracted from the right
|
||||||
/// an inter-pile gap of only ~4 px.
|
/// edge, `y` is added to the bottom edge. The `x` magnitude must satisfy
|
||||||
const STOCK_BADGE_INSET: Vec2 = Vec2::new(-20.0, -8.0);
|
/// `x >= STOCK_BADGE_SIZE.x / 2` so the badge right edge stays inside the stock
|
||||||
|
/// pile and never overlaps the adjacent waste pile — critical on Android where
|
||||||
|
/// `H_GAP_DIVISOR = 32` gives an inter-pile gap of only ~4 px.
|
||||||
|
const STOCK_BADGE_INSET: Vec2 = Vec2::new(20.0, 8.0);
|
||||||
|
|
||||||
/// Width / height of the badge background sprite, in world pixels. Sized so
|
/// Width / height of the badge background sprite, in world pixels. Sized so
|
||||||
/// a 2-digit count (max "24") fits comfortably with `TYPE_BODY` (14 pt) text.
|
/// a 2-digit count (max "24") fits comfortably with `TYPE_BODY` (14 pt) text.
|
||||||
@@ -1928,8 +1936,9 @@ fn stock_card_count(game: &GameState) -> usize {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the world-space `Vec3` for the centre of the stock-count badge,
|
/// Returns the world-space `Vec3` for the centre of the stock-count badge,
|
||||||
/// given the current `Layout`. The badge sits at the top-right corner of
|
/// given the current `Layout`. The badge sits at the bottom-right corner of
|
||||||
/// the stock pile sprite, inset by [`STOCK_BADGE_INSET`].
|
/// the stock pile sprite, inset by [`STOCK_BADGE_INSET`], so it stays clear of
|
||||||
|
/// the rank/suit pip in the card's top-left corner.
|
||||||
fn stock_badge_translation(layout: &Layout) -> Vec3 {
|
fn stock_badge_translation(layout: &Layout) -> Vec3 {
|
||||||
// Empty layouts don't contain a Stock entry — fall back to origin so
|
// Empty layouts don't contain a Stock entry — fall back to origin so
|
||||||
// the badge stays in a deterministic spot until the layout is filled.
|
// the badge stays in a deterministic spot until the layout is filled.
|
||||||
@@ -1939,8 +1948,9 @@ fn stock_badge_translation(layout: &Layout) -> Vec3 {
|
|||||||
.copied()
|
.copied()
|
||||||
.unwrap_or(Vec2::ZERO);
|
.unwrap_or(Vec2::ZERO);
|
||||||
let half = layout.card_size * 0.5;
|
let half = layout.card_size * 0.5;
|
||||||
let x = pile_pos.x + half.x + STOCK_BADGE_INSET.x;
|
// Anchor to the bottom-right corner, then move the centre inward.
|
||||||
let y = pile_pos.y + half.y + STOCK_BADGE_INSET.y;
|
let x = pile_pos.x + half.x - STOCK_BADGE_INSET.x;
|
||||||
|
let y = pile_pos.y - half.y + STOCK_BADGE_INSET.y;
|
||||||
Vec3::new(x, y, Z_STOCK_BADGE)
|
Vec3::new(x, y, Z_STOCK_BADGE)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2357,7 +2367,7 @@ fn update_tableau_fan_frac(
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|tableau| {
|
.map(|tableau| {
|
||||||
game.0
|
game.0
|
||||||
.pile(klondike::KlondikePile::Tableau(tableau))
|
.pile(solitaire_core::KlondikePile::Tableau(tableau))
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|c| c.face_up)
|
.filter(|c| c.face_up)
|
||||||
.count()
|
.count()
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::window::{CursorIcon, PrimaryWindow, SystemCursorIcon};
|
use bevy::window::{CursorIcon, PrimaryWindow, SystemCursorIcon};
|
||||||
use klondike::{Foundation, KlondikePile, Tableau};
|
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
||||||
use solitaire_core::game_state::{DrawMode, GameState};
|
use solitaire_core::game_state::{DrawMode, GameState};
|
||||||
|
|
||||||
use crate::card_plugin::RightClickHighlight;
|
use crate::card_plugin::RightClickHighlight;
|
||||||
@@ -655,7 +655,7 @@ mod tests {
|
|||||||
.world_mut()
|
.world_mut()
|
||||||
.query::<&DropTargetOverlay>()
|
.query::<&DropTargetOverlay>()
|
||||||
.iter(app.world())
|
.iter(app.world())
|
||||||
.map(|o| o.0.clone())
|
.map(|o| o.0)
|
||||||
.collect();
|
.collect();
|
||||||
assert!(
|
assert!(
|
||||||
!overlays.contains(&KlondikePile::Tableau(Tableau::Tableau3)),
|
!overlays.contains(&KlondikePile::Tableau(Tableau::Tableau3)),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
//! Cross-system events used by the engine's plugins.
|
//! Cross-system events used by the engine's plugins.
|
||||||
|
|
||||||
use bevy::prelude::Message;
|
use bevy::prelude::Message;
|
||||||
use klondike::KlondikePile;
|
use solitaire_core::KlondikePile;
|
||||||
use solitaire_core::card::Suit;
|
use solitaire_core::card::Suit;
|
||||||
use solitaire_core::game_state::GameMode;
|
use solitaire_core::game_state::GameMode;
|
||||||
use solitaire_data::AchievementRecord;
|
use solitaire_data::AchievementRecord;
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ use std::hash::{Hash, Hasher};
|
|||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::window::RequestRedraw;
|
use bevy::window::RequestRedraw;
|
||||||
use klondike::{Foundation, KlondikePile};
|
use solitaire_core::{Foundation, KlondikePile};
|
||||||
use solitaire_data::AnimSpeed;
|
use solitaire_data::AnimSpeed;
|
||||||
|
|
||||||
use crate::animation_plugin::CardAnim;
|
use crate::animation_plugin::CardAnim;
|
||||||
@@ -849,7 +849,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn shake_anim_skipped_under_reduce_motion() {
|
fn shake_anim_skipped_under_reduce_motion() {
|
||||||
use bevy::ecs::message::Messages;
|
use bevy::ecs::message::Messages;
|
||||||
use klondike::Tableau;
|
use solitaire_core::Tableau;
|
||||||
use solitaire_core::game_state::{DrawMode, GameState};
|
use solitaire_core::game_state::{DrawMode, GameState};
|
||||||
use solitaire_data::Settings;
|
use solitaire_data::Settings;
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use chrono::Utc;
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future};
|
use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future};
|
||||||
use bevy::window::AppLifecycle;
|
use bevy::window::AppLifecycle;
|
||||||
use klondike::KlondikePile;
|
use solitaire_core::KlondikePile;
|
||||||
use solitaire_core::game_state::{DrawMode, GameMode, GameState};
|
use solitaire_core::game_state::{DrawMode, GameMode, GameState};
|
||||||
use solitaire_core::solver::{SolverConfig, SolverResult, try_solve};
|
use solitaire_core::solver::{SolverConfig, SolverResult, try_solve};
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
@@ -521,7 +521,7 @@ fn handle_new_game(
|
|||||||
// hides that information and reads naturally as "dealt from the
|
// hides that information and reads naturally as "dealt from the
|
||||||
// deck." Skipped when LayoutResource isn't present (headless tests).
|
// deck." Skipped when LayoutResource isn't present (headless tests).
|
||||||
if let Some(layout) = layout.as_ref()
|
if let Some(layout) = layout.as_ref()
|
||||||
&& let Some(stock) = layout.0.pile_positions.get(&klondike::KlondikePile::Stock)
|
&& let Some(stock) = layout.0.pile_positions.get(&solitaire_core::KlondikePile::Stock)
|
||||||
{
|
{
|
||||||
for mut tx in &mut card_transforms {
|
for mut tx in &mut card_transforms {
|
||||||
tx.translation.x = stock.x;
|
tx.translation.x = stock.x;
|
||||||
@@ -1014,12 +1014,12 @@ fn pile_cards(game: &GameState, pile: &KlondikePile) -> Vec<solitaire_core::card
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn foundation_slot(foundation: klondike::Foundation) -> Option<u8> {
|
fn foundation_slot(foundation: solitaire_core::Foundation) -> Option<u8> {
|
||||||
match foundation {
|
match foundation {
|
||||||
klondike::Foundation::Foundation1 => Some(0),
|
solitaire_core::Foundation::Foundation1 => Some(0),
|
||||||
klondike::Foundation::Foundation2 => Some(1),
|
solitaire_core::Foundation::Foundation2 => Some(1),
|
||||||
klondike::Foundation::Foundation3 => Some(2),
|
solitaire_core::Foundation::Foundation3 => Some(2),
|
||||||
klondike::Foundation::Foundation4 => Some(3),
|
solitaire_core::Foundation::Foundation4 => Some(3),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1294,7 +1294,7 @@ fn save_game_state_on_exit(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use klondike::{Foundation, KlondikePile, Tableau};
|
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
||||||
use solitaire_core::klondike_adapter::{SavedKlondikePile, SavedTableau};
|
use solitaire_core::klondike_adapter::{SavedKlondikePile, SavedTableau};
|
||||||
|
|
||||||
/// Build a minimal headless `App` with just `GamePlugin` installed.
|
/// Build a minimal headless `App` with just `GamePlugin` installed.
|
||||||
@@ -1423,8 +1423,10 @@ mod tests {
|
|||||||
"fresh game should inherit default take_from_foundation=true",
|
"fresh game should inherit default take_from_foundation=true",
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut settings = solitaire_data::Settings::default();
|
let mut settings = solitaire_data::Settings {
|
||||||
settings.take_from_foundation = false;
|
take_from_foundation: false,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
app.world_mut()
|
app.world_mut()
|
||||||
.write_message(crate::settings_plugin::SettingsChangedEvent(
|
.write_message(crate::settings_plugin::SettingsChangedEvent(
|
||||||
settings.clone(),
|
settings.clone(),
|
||||||
@@ -1951,15 +1953,15 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verify that the game-over overlay contains the expected header text and
|
// Verify that the game-over overlay contains the expected header text and
|
||||||
/// action-hint strings so players understand why the overlay appeared and
|
// action-hint strings so players understand why the overlay appeared and
|
||||||
/// what keys to press.
|
// what keys to press.
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Task #56 — Escape dismisses GameOverScreen and starts new game
|
// Task #56 — Escape dismisses GameOverScreen and starts new game
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
/// Pressing Escape while `GameOverScreen` is visible must fire
|
// Pressing Escape while `GameOverScreen` is visible must fire
|
||||||
/// `NewGameRequestEvent` — identical behaviour to pressing N.
|
// `NewGameRequestEvent` — identical behaviour to pressing N.
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Task #48 — Undo with empty stack fires InfoToastEvent
|
// Task #48 — Undo with empty stack fires InfoToastEvent
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::window::WindowResized;
|
use bevy::window::WindowResized;
|
||||||
use klondike::{Foundation, KlondikePile, Tableau};
|
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
||||||
use solitaire_core::card::Suit;
|
use solitaire_core::card::Suit;
|
||||||
use solitaire_core::game_state::{DrawMode, GameMode};
|
use solitaire_core::game_state::{DrawMode, GameMode};
|
||||||
|
|
||||||
@@ -315,17 +315,17 @@ pub struct HintButton;
|
|||||||
/// Android HUD label for the Hint button — shared with the help screen's
|
/// Android HUD label for the Hint button — shared with the help screen's
|
||||||
/// controls reference so both always agree.
|
/// controls reference so both always agree.
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
pub(crate) const ANDROID_HINT_LABEL: &str = "!";
|
pub(crate) const ANDROID_HINT_LABEL: &str = "Hint";
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
const ACTION_BAR_LABELS: [&str; 7] = [
|
const ACTION_BAR_LABELS: [&str; 7] = [
|
||||||
"\u{2261}",
|
"Menu",
|
||||||
"\u{2190}",
|
"Undo",
|
||||||
"||",
|
"Pause",
|
||||||
"?",
|
"Help",
|
||||||
ANDROID_HINT_LABEL,
|
ANDROID_HINT_LABEL,
|
||||||
"M",
|
"Mode",
|
||||||
"+",
|
"New",
|
||||||
];
|
];
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
const ACTION_BAR_LABELS: [&str; 7] = [
|
const ACTION_BAR_LABELS: [&str; 7] = [
|
||||||
@@ -830,6 +830,8 @@ fn spawn_avatar_child(
|
|||||||
) {
|
) {
|
||||||
const SIZE: f32 = 32.0;
|
const SIZE: f32 = 32.0;
|
||||||
if let Some(handle) = avatar.and_then(|a| a.0.clone()) {
|
if let Some(handle) = avatar.and_then(|a| a.0.clone()) {
|
||||||
|
// Logged-in with a downloaded avatar: keep the accent disc behind it.
|
||||||
|
commands.entity(parent).insert(BackgroundColor(ACCENT_PRIMARY));
|
||||||
// Image fills the circle container; border_radius clips it to a disc.
|
// Image fills the circle container; border_radius clips it to a disc.
|
||||||
commands.entity(parent).with_children(|b| {
|
commands.entity(parent).with_children(|b| {
|
||||||
b.spawn((
|
b.spawn((
|
||||||
@@ -850,6 +852,15 @@ fn spawn_avatar_child(
|
|||||||
})
|
})
|
||||||
.and_then(|c| c.to_uppercase().next())
|
.and_then(|c| c.to_uppercase().next())
|
||||||
.unwrap_or('?');
|
.unwrap_or('?');
|
||||||
|
// Real initial (logged in) keeps the red accent disc; the '?'
|
||||||
|
// unauthenticated fallback uses a neutral grey so it reads as a
|
||||||
|
// "tap to log in" affordance rather than an error.
|
||||||
|
let disc_bg = if initial == '?' {
|
||||||
|
BG_ELEVATED_HI
|
||||||
|
} else {
|
||||||
|
ACCENT_PRIMARY
|
||||||
|
};
|
||||||
|
commands.entity(parent).insert(BackgroundColor(disc_bg));
|
||||||
commands.entity(parent).with_children(|b| {
|
commands.entity(parent).with_children(|b| {
|
||||||
b.spawn((
|
b.spawn((
|
||||||
Text::new(initial.to_string()),
|
Text::new(initial.to_string()),
|
||||||
@@ -1651,11 +1662,13 @@ impl Default for HudActionFade {
|
|||||||
/// How many pixels from the bottom edge the cursor must be to reveal the bar.
|
/// How many pixels from the bottom edge the cursor must be to reveal the bar.
|
||||||
/// Set slightly taller than `HUD_BAND_HEIGHT` so the bar fades in as the
|
/// Set slightly taller than `HUD_BAND_HEIGHT` so the bar fades in as the
|
||||||
/// cursor approaches, not only when it crosses into the band itself.
|
/// cursor approaches, not only when it crosses into the band itself.
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
const ACTION_FADE_REVEAL_PX: f32 = HUD_BAND_HEIGHT + 32.0;
|
const ACTION_FADE_REVEAL_PX: f32 = HUD_BAND_HEIGHT + 32.0;
|
||||||
|
|
||||||
/// Lerp rate for fading (per second). 6.0 ≈ 167 ms for a full
|
/// Lerp rate for fading (per second). 6.0 ≈ 167 ms for a full
|
||||||
/// transition — fast enough to feel responsive without flashing on
|
/// transition — fast enough to feel responsive without flashing on
|
||||||
/// brief cursor wanders into the reveal zone.
|
/// brief cursor wanders into the reveal zone.
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
const ACTION_FADE_RATE_PER_SEC: f32 = 6.0;
|
const ACTION_FADE_RATE_PER_SEC: f32 = 6.0;
|
||||||
|
|
||||||
/// Updates the fade state from cursor position. Sets `target = 1.0` if
|
/// Updates the fade state from cursor position. Sets `target = 1.0` if
|
||||||
@@ -1663,6 +1676,7 @@ const ACTION_FADE_RATE_PER_SEC: f32 = 6.0;
|
|||||||
/// (player is using keyboard); `0.0` otherwise. Lerps `alpha` toward
|
/// (player is using keyboard); `0.0` otherwise. Lerps `alpha` toward
|
||||||
/// `target` at a fixed rate so the visual transition is smooth across
|
/// `target` at a fixed rate so the visual transition is smooth across
|
||||||
/// variable framerates.
|
/// variable framerates.
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
fn update_action_fade(windows: Query<&Window>, time: Res<Time>, mut fade: ResMut<HudActionFade>) {
|
fn update_action_fade(windows: Query<&Window>, time: Res<Time>, mut fade: ResMut<HudActionFade>) {
|
||||||
let Ok(window) = windows.single() else {
|
let Ok(window) = windows.single() else {
|
||||||
return;
|
return;
|
||||||
@@ -1687,6 +1701,7 @@ fn update_action_fade(windows: Query<&Window>, time: Res<Time>, mut fade: ResMut
|
|||||||
/// `Last` (after `paint_action_buttons`) so a hover-state change in the
|
/// `Last` (after `paint_action_buttons`) so a hover-state change in the
|
||||||
/// same frame doesn't override the fade with an opaque idle / hover
|
/// same frame doesn't override the fade with an opaque idle / hover
|
||||||
/// colour.
|
/// colour.
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
fn apply_action_fade(
|
fn apply_action_fade(
|
||||||
fade: Res<HudActionFade>,
|
fade: Res<HudActionFade>,
|
||||||
@@ -2567,10 +2582,18 @@ fn restore_hud_on_modal(
|
|||||||
/// Returns the action-bar label font size for a given logical window width.
|
/// Returns the action-bar label font size for a given logical window width.
|
||||||
fn action_bar_font_size(window_width: f32) -> f32 {
|
fn action_bar_font_size(window_width: f32) -> f32 {
|
||||||
if USE_TOUCH_UI_LAYOUT {
|
if USE_TOUCH_UI_LAYOUT {
|
||||||
// ~1/40 of the window width gives ~22 px on a 900 logical-px phone.
|
// Seven word-labels ("Menu","Undo","Pause","Help","Hint","Mode","New")
|
||||||
// Clamped so it never goes too tiny on narrow viewports or too large
|
// must share one row. The widest characters are in FiraMono (a
|
||||||
// on landscape tablets.
|
// monospace whose advance is ~0.62 of the font size). On a 900
|
||||||
(window_width / 40.0).clamp(16.0, 30.0)
|
// logical-px phone the row budget after bar padding (2*12) and six
|
||||||
|
// 4 px column gaps is ~852 px for ~28 label chars + 7*2*3 px button
|
||||||
|
// padding. Solving 28*0.62*size + 42 <= 852 gives size <= ~46, so the
|
||||||
|
// labels are advance-bound only on very narrow viewports; the real
|
||||||
|
// constraint is legibility, not fit. ~1/60 of the width yields ~15 px
|
||||||
|
// at 900 px — comfortably one row with margin to spare — clamped so it
|
||||||
|
// never drops below the 12 px legibility floor or grows past 18 px on
|
||||||
|
// landscape tablets where it would crowd the row again.
|
||||||
|
(window_width / 60.0).clamp(12.0, 18.0)
|
||||||
} else {
|
} else {
|
||||||
TYPE_BODY
|
TYPE_BODY
|
||||||
}
|
}
|
||||||
@@ -2578,9 +2601,14 @@ fn action_bar_font_size(window_width: f32) -> f32 {
|
|||||||
|
|
||||||
fn action_button_metrics() -> (UiRect, Val, Val) {
|
fn action_button_metrics() -> (UiRect, Val, Val) {
|
||||||
if USE_TOUCH_UI_LAYOUT {
|
if USE_TOUCH_UI_LAYOUT {
|
||||||
|
// Tight 3 px horizontal padding (down from 4) trims 14 px off the row
|
||||||
|
// total across 7 buttons, and a 44 px min_width (down from 52) lets the
|
||||||
|
// shortest labels ("New", "Help") shrink to their text rather than
|
||||||
|
// padding the row out past the 900 logical-px viewport. min_height
|
||||||
|
// stays at 44 px to preserve the comfortable touch target.
|
||||||
(
|
(
|
||||||
UiRect::axes(Val::Px(4.0), Val::Px(4.0)),
|
UiRect::axes(Val::Px(3.0), Val::Px(4.0)),
|
||||||
Val::Px(52.0),
|
Val::Px(44.0),
|
||||||
Val::Px(44.0),
|
Val::Px(44.0),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ use bevy::prelude::*;
|
|||||||
use bevy::window::PrimaryWindow;
|
use bevy::window::PrimaryWindow;
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
use bevy::window::{MonitorSelection, WindowMode};
|
use bevy::window::{MonitorSelection, WindowMode};
|
||||||
use klondike::{Foundation, KlondikePile, Tableau};
|
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
||||||
use solitaire_core::card::{Card, Suit};
|
use solitaire_core::card::{Card, Suit};
|
||||||
use solitaire_core::game_state::GameState;
|
use solitaire_core::game_state::GameState;
|
||||||
|
|
||||||
@@ -1788,7 +1788,7 @@ const _VEC3_REFERENCED: Option<Vec3> = None;
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::layout::compute_layout;
|
use crate::layout::compute_layout;
|
||||||
use klondike::{Foundation, Tableau};
|
use solitaire_core::{Foundation, Tableau};
|
||||||
use solitaire_core::game_state::{DrawMode, GameState};
|
use solitaire_core::game_state::{DrawMode, GameState};
|
||||||
|
|
||||||
fn clear_test_piles(game: &mut GameState) {
|
fn clear_test_piles(game: &mut GameState) {
|
||||||
@@ -2276,8 +2276,8 @@ mod tests {
|
|||||||
assert_eq!(*count, 1);
|
assert_eq!(*count, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `all_hints` must be empty when both stock and waste are empty and no
|
// `all_hints` must be empty when both stock and waste are empty and no
|
||||||
/// pile-to-pile move exists — the game is truly stuck.
|
// pile-to-pile move exists — the game is truly stuck.
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Drag-rejection return tween — `CardAnimation` replaces the legacy
|
// Drag-rejection return tween — `CardAnimation` replaces the legacy
|
||||||
// `ShakeAnim` on the dragged cards. The audio cue
|
// `ShakeAnim` on the dragged cards. The audio cue
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
use bevy::math::Vec2;
|
use bevy::math::Vec2;
|
||||||
use bevy::prelude::{Resource, SystemSet};
|
use bevy::prelude::{Resource, SystemSet};
|
||||||
use klondike::{Foundation, KlondikePile, Tableau};
|
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
||||||
|
|
||||||
/// Schedule labels for layout-related systems so cross-plugin ordering is
|
/// Schedule labels for layout-related systems so cross-plugin ordering is
|
||||||
/// explicit instead of relying on Bevy's automatic resource-conflict ordering
|
/// explicit instead of relying on Bevy's automatic resource-conflict ordering
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future};
|
use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future};
|
||||||
use klondike::KlondikePile;
|
use solitaire_core::KlondikePile;
|
||||||
use solitaire_core::game_state::GameState;
|
use solitaire_core::game_state::GameState;
|
||||||
use solitaire_core::solver::{SolverConfig, SolverResult, try_solve_from_state};
|
use solitaire_core::solver::{SolverConfig, SolverResult, try_solve_from_state};
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::events::HintVisualEvent;
|
use crate::events::HintVisualEvent;
|
||||||
use crate::input_plugin::HintSolverConfig;
|
use crate::input_plugin::HintSolverConfig;
|
||||||
use klondike::{Foundation, Tableau};
|
use solitaire_core::{Foundation, Tableau};
|
||||||
use solitaire_core::card::{Card, Rank, Suit};
|
use solitaire_core::card::{Card, Rank, Suit};
|
||||||
use solitaire_core::game_state::{DrawMode, GameState};
|
use solitaire_core::game_state::{DrawMode, GameState};
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ use bevy::input::touch::Touches;
|
|||||||
use bevy::math::Vec2;
|
use bevy::math::Vec2;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::window::PrimaryWindow;
|
use bevy::window::PrimaryWindow;
|
||||||
use klondike::{Foundation, KlondikePile, Tableau};
|
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
||||||
use solitaire_core::card::Card;
|
use solitaire_core::card::Card;
|
||||||
use solitaire_core::game_state::GameState;
|
use solitaire_core::game_state::GameState;
|
||||||
|
|
||||||
@@ -1037,7 +1037,7 @@ mod tests {
|
|||||||
let (dest_pile, anchor) = match app.world().resource::<RightClickRadialState>() {
|
let (dest_pile, anchor) = match app.world().resource::<RightClickRadialState>() {
|
||||||
RightClickRadialState::Active {
|
RightClickRadialState::Active {
|
||||||
legal_destinations, ..
|
legal_destinations, ..
|
||||||
} => legal_destinations[0].clone(),
|
} => legal_destinations[0],
|
||||||
_ => panic!("expected Active"),
|
_ => panic!("expected Active"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use super::ReplayPlaybackState;
|
use super::ReplayPlaybackState;
|
||||||
use chrono::Datelike;
|
use chrono::Datelike;
|
||||||
use klondike::{Foundation, KlondikePile, Tableau};
|
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
||||||
use solitaire_core::card::{Card, Rank, Suit};
|
use solitaire_core::card::{Card, Rank, Suit};
|
||||||
use solitaire_core::game_state::GameState;
|
use solitaire_core::game_state::GameState;
|
||||||
use solitaire_core::klondike_adapter::SavedKlondikePile;
|
use solitaire_core::klondike_adapter::SavedKlondikePile;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use klondike::{Foundation, KlondikePile, Tableau};
|
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
||||||
use solitaire_core::card::{Rank, Suit};
|
use solitaire_core::card::{Rank, Suit};
|
||||||
use solitaire_core::game_state::{DrawMode, GameMode};
|
use solitaire_core::game_state::{DrawMode, GameMode};
|
||||||
use solitaire_core::klondike_adapter::{SavedKlondikePile, SavedTableau};
|
use solitaire_core::klondike_adapter::{SavedKlondikePile, SavedTableau};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use super::*;
|
|||||||
use crate::layout::LayoutResource;
|
use crate::layout::LayoutResource;
|
||||||
use crate::replay_playback::ReplayPlaybackState;
|
use crate::replay_playback::ReplayPlaybackState;
|
||||||
use crate::resources::GameStateResource;
|
use crate::resources::GameStateResource;
|
||||||
use klondike::KlondikePile;
|
use solitaire_core::KlondikePile;
|
||||||
use solitaire_data::ReplayMove;
|
use solitaire_data::ReplayMove;
|
||||||
|
|
||||||
/// Overwrites the banner label whenever the resource changes — covers the
|
/// Overwrites the banner label whenever the resource changes — covers the
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
//! flag is threaded through, no every-callsite gate is added.
|
//! flag is threaded through, no every-callsite gate is added.
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use klondike::KlondikePile;
|
use solitaire_core::KlondikePile;
|
||||||
use solitaire_data::{Replay, ReplayMove};
|
use solitaire_data::{Replay, ReplayMove};
|
||||||
|
|
||||||
use crate::events::{DrawRequestEvent, MoveRequestEvent, StateChangedEvent, UndoRequestEvent};
|
use crate::events::{DrawRequestEvent, MoveRequestEvent, StateChangedEvent, UndoRequestEvent};
|
||||||
@@ -555,7 +555,7 @@ mod tests {
|
|||||||
use crate::game_plugin::GamePlugin;
|
use crate::game_plugin::GamePlugin;
|
||||||
use bevy::time::TimeUpdateStrategy;
|
use bevy::time::TimeUpdateStrategy;
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use klondike::{KlondikePile, Tableau};
|
use solitaire_core::{KlondikePile, Tableau};
|
||||||
use solitaire_core::game_state::{DrawMode, GameMode};
|
use solitaire_core::game_state::{DrawMode, GameMode};
|
||||||
use solitaire_core::klondike_adapter::{SavedKlondikePile, SavedTableau};
|
use solitaire_core::klondike_adapter::{SavedKlondikePile, SavedTableau};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use std::sync::Arc;
|
|||||||
use bevy::math::Vec2;
|
use bevy::math::Vec2;
|
||||||
use bevy::prelude::Resource;
|
use bevy::prelude::Resource;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use klondike::KlondikePile;
|
use solitaire_core::KlondikePile;
|
||||||
use solitaire_core::game_state::GameState;
|
use solitaire_core::game_state::GameState;
|
||||||
|
|
||||||
/// Wraps the currently active `GameState`. Single source of truth for the in-progress game.
|
/// Wraps the currently active `GameState`. Single source of truth for the in-progress game.
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
|
|
||||||
use bevy::input::ButtonInput;
|
use bevy::input::ButtonInput;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use klondike::{Foundation, KlondikePile, Tableau};
|
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
||||||
use solitaire_core::card::Card;
|
use solitaire_core::card::Card;
|
||||||
use solitaire_core::game_state::GameState;
|
use solitaire_core::game_state::GameState;
|
||||||
|
|
||||||
@@ -1110,8 +1110,7 @@ mod tests {
|
|||||||
let selected = app
|
let selected = app
|
||||||
.world()
|
.world()
|
||||||
.resource::<SelectionState>()
|
.resource::<SelectionState>()
|
||||||
.selected_pile
|
.selected_pile;
|
||||||
.clone();
|
|
||||||
// The cycle order starts at Waste, but Waste is empty so the next
|
// The cycle order starts at Waste, but Waste is empty so the next
|
||||||
// available pile (Tableau(0)) is selected.
|
// available pile (Tableau(0)) is selected.
|
||||||
assert_eq!(selected, Some(KlondikePile::Tableau(Tableau::Tableau1)));
|
assert_eq!(selected, Some(KlondikePile::Tableau(Tableau::Tableau1)));
|
||||||
@@ -1277,15 +1276,13 @@ mod tests {
|
|||||||
let before = app
|
let before = app
|
||||||
.world()
|
.world()
|
||||||
.resource::<SelectionState>()
|
.resource::<SelectionState>()
|
||||||
.selected_pile
|
.selected_pile;
|
||||||
.clone();
|
|
||||||
press_key(&mut app, KeyCode::Tab);
|
press_key(&mut app, KeyCode::Tab);
|
||||||
app.update();
|
app.update();
|
||||||
let after = app
|
let after = app
|
||||||
.world()
|
.world()
|
||||||
.resource::<SelectionState>()
|
.resource::<SelectionState>()
|
||||||
.selected_pile
|
.selected_pile;
|
||||||
.clone();
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
before, after,
|
before, after,
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
//! On startup, the plugin spawns an async pull task on [`AsyncComputeTaskPool`]
|
//! On startup, the plugin spawns an async pull task on [`AsyncComputeTaskPool`]
|
||||||
//! that fetches the remote payload from the active [`SyncProvider`]. Once the
|
//! that fetches the remote payload from the active [`SyncProvider`]. Once the
|
||||||
//! task resolves, the merged result is written to disk and the in-world
|
//! task resolves, the merged result is written to disk and the in-world
|
||||||
//! resources are updated. On app exit, a blocking push sends the current local
|
//! resources are updated. On app exit, a best-effort async push sends the
|
||||||
//! state to the backend.
|
//! current local state to the backend without blocking the Bevy main thread.
|
||||||
//!
|
//!
|
||||||
//! The plugin is completely backend-agnostic: the caller (usually
|
//! The plugin is completely backend-agnostic: the caller (usually
|
||||||
//! `solitaire_app`) constructs the right [`SyncProvider`] implementation and
|
//! `solitaire_app`) constructs the right [`SyncProvider`] implementation and
|
||||||
@@ -79,8 +79,8 @@ struct PendingReplayUpload(Option<Task<Result<String, SyncError>>>);
|
|||||||
/// - **Update** — polls the task each frame; on completion merges the remote
|
/// - **Update** — polls the task each frame; on completion merges the remote
|
||||||
/// payload with local data, persists the result, and updates in-world
|
/// payload with local data, persists the result, and updates in-world
|
||||||
/// resources.
|
/// resources.
|
||||||
/// - **Last** — on [`AppExit`], performs a blocking push of the current local
|
/// - **Last** — on [`AppExit`], starts a best-effort async push of the current
|
||||||
/// state to the active backend.
|
/// local state to the active backend without blocking shutdown.
|
||||||
///
|
///
|
||||||
/// Construct via [`SyncPlugin::new`], passing any type that implements
|
/// Construct via [`SyncPlugin::new`], passing any type that implements
|
||||||
/// [`SyncProvider`].
|
/// [`SyncProvider`].
|
||||||
@@ -272,11 +272,12 @@ fn poll_pull_result(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Last-schedule system: pushes the current local state on [`AppExit`].
|
/// Last-schedule system: starts a best-effort push of the current local state
|
||||||
|
/// on [`AppExit`] without blocking the Bevy main thread.
|
||||||
///
|
///
|
||||||
/// A blocking push is acceptable here — ARCHITECTURE.md §4 explicitly notes
|
/// The detached task may be cut short by process teardown, so local atomic
|
||||||
/// that blocking on exit is permitted because the game loop is already
|
/// persistence remains the durable source of truth even if the final remote
|
||||||
/// shutting down.
|
/// push does not complete.
|
||||||
fn push_on_exit(
|
fn push_on_exit(
|
||||||
mut exit_events: MessageReader<AppExit>,
|
mut exit_events: MessageReader<AppExit>,
|
||||||
provider: Res<SyncProviderResource>,
|
provider: Res<SyncProviderResource>,
|
||||||
@@ -291,20 +292,16 @@ fn push_on_exit(
|
|||||||
exit_events.clear();
|
exit_events.clear();
|
||||||
|
|
||||||
let payload = build_payload(&stats.0, &achievements.0, &progress.0);
|
let payload = build_payload(&stats.0, &achievements.0, &progress.0);
|
||||||
let result = rt.0.block_on(provider.0.push(&payload));
|
let provider = provider.0.clone();
|
||||||
match result {
|
let rt = rt.0.clone();
|
||||||
Ok(_) => {}
|
AsyncComputeTaskPool::get()
|
||||||
// `UnsupportedPlatform` is the expected response of
|
.spawn(async move {
|
||||||
// `LocalOnlyProvider`; treat it the same as the pull path does —
|
match rt.block_on(provider.push(&payload)) {
|
||||||
// no backend configured is not a failure.
|
Ok(_) | Err(SyncError::UnsupportedPlatform) => {}
|
||||||
Err(SyncError::UnsupportedPlatform) => {}
|
Err(e) => warn!("sync push on exit failed: {e}"),
|
||||||
Err(e) => {
|
}
|
||||||
// Log real push failures on exit so they appear in crash/log
|
})
|
||||||
// reports. We cannot surface them to the UI at this point (game
|
.detach();
|
||||||
// loop is done).
|
|
||||||
warn!("sync push on exit failed: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update-schedule system: on each `GameWonEvent` push the just-completed
|
/// Update-schedule system: on each `GameWonEvent` push the just-completed
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::window::WindowResized;
|
use bevy::window::WindowResized;
|
||||||
use klondike::{Foundation, KlondikePile, Tableau};
|
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
||||||
use solitaire_core::card::Suit;
|
use solitaire_core::card::Suit;
|
||||||
|
|
||||||
use crate::events::{HintVisualEvent, StateChangedEvent};
|
use crate::events::{HintVisualEvent, StateChangedEvent};
|
||||||
@@ -22,15 +22,28 @@ use crate::ui_theme::TEXT_PRIMARY;
|
|||||||
use solitaire_data::Theme;
|
use solitaire_data::Theme;
|
||||||
|
|
||||||
/// Default tint applied to every empty-pile marker sprite. Pure white
|
/// Default tint applied to every empty-pile marker sprite. Pure white
|
||||||
/// at 8% alpha — soft enough that the marker reads as a "hint of a
|
/// at 15% alpha — soft enough that the marker reads as a "hint of a
|
||||||
/// slot" rather than a panel, but visible against every felt
|
/// slot" rather than a panel, but discernible even against a very dark
|
||||||
/// background.
|
/// felt background under bright ambient light (the old 8% alpha vanished
|
||||||
|
/// on a #151515 felt during on-device Android testing).
|
||||||
///
|
///
|
||||||
/// Re-exported as the source of truth for `cursor_plugin::MARKER_DEFAULT`,
|
/// Re-exported as the source of truth for `cursor_plugin::MARKER_DEFAULT`,
|
||||||
/// which used to duplicate the literal alongside a "kept in sync" doc
|
/// which used to duplicate the literal alongside a "kept in sync" doc
|
||||||
/// comment. Pulling both call sites through this const makes drift a
|
/// comment. Pulling both call sites through this const makes drift a
|
||||||
/// compile error instead of a stale comment.
|
/// compile error instead of a stale comment.
|
||||||
pub const PILE_MARKER_DEFAULT_COLOUR: Color = Color::srgba(1.0, 1.0, 1.0, 0.08);
|
pub const PILE_MARKER_DEFAULT_COLOUR: Color = Color::srgba(1.0, 1.0, 1.0, 0.15);
|
||||||
|
|
||||||
|
/// Tint applied to the thin outline rectangle sitting behind every
|
||||||
|
/// empty-pile marker. A slightly brighter white at 28% alpha gives the
|
||||||
|
/// slot a defined edge — the standard solitaire "empty pile" affordance —
|
||||||
|
/// without competing with real cards. Rendered as a marginally larger
|
||||||
|
/// child rectangle one z-step behind the fill, so the fill overlaps it
|
||||||
|
/// and only a hairline frame remains visible.
|
||||||
|
const PILE_MARKER_OUTLINE_COLOUR: Color = Color::srgba(1.0, 1.0, 1.0, 0.28);
|
||||||
|
|
||||||
|
/// Width in logical pixels of the visible outline frame around an empty
|
||||||
|
/// pile marker (the outline rect is this much larger on each side).
|
||||||
|
const PILE_MARKER_OUTLINE_WIDTH: f32 = 2.0;
|
||||||
|
|
||||||
/// Holds pre-loaded [`Handle<Image>`]s for the 5 selectable table backgrounds.
|
/// Holds pre-loaded [`Handle<Image>`]s for the 5 selectable table backgrounds.
|
||||||
///
|
///
|
||||||
@@ -286,6 +299,22 @@ fn spawn_pile_markers(commands: &mut Commands, layout: &Layout) {
|
|||||||
PileMarker(pile),
|
PileMarker(pile),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// Outline frame: a marginally larger rectangle sitting one z-step
|
||||||
|
// behind the fill. The fill overlaps its centre, leaving only a
|
||||||
|
// hairline border visible — a defined slot edge without an extra
|
||||||
|
// asset or 9-slice. Untagged so the `PileMarker` count is unchanged.
|
||||||
|
let outline_size = marker_size + Vec2::splat(PILE_MARKER_OUTLINE_WIDTH * 2.0);
|
||||||
|
entity.with_children(|b| {
|
||||||
|
b.spawn((
|
||||||
|
Sprite {
|
||||||
|
color: PILE_MARKER_OUTLINE_COLOUR,
|
||||||
|
custom_size: Some(outline_size),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Transform::from_xyz(0.0, 0.0, -0.05),
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
// Tableau markers show "K" (only a King may start an empty column).
|
// Tableau markers show "K" (only a King may start an empty column).
|
||||||
// Foundation markers show "A" (only an Ace may claim an empty slot).
|
// Foundation markers show "A" (only an Ace may claim an empty slot).
|
||||||
// Neither label carries a suit because any suit may start any slot.
|
// Neither label carries a suit because any suit may start any slot.
|
||||||
@@ -577,7 +606,7 @@ mod tests {
|
|||||||
.world_mut()
|
.world_mut()
|
||||||
.query::<&PileMarker>()
|
.query::<&PileMarker>()
|
||||||
.iter(app.world())
|
.iter(app.world())
|
||||||
.map(|m| m.0.clone())
|
.map(|m| m.0)
|
||||||
.collect();
|
.collect();
|
||||||
types.sort_by_key(|p| format!("{p:?}"));
|
types.sort_by_key(|p| format!("{p:?}"));
|
||||||
types.dedup();
|
types.dedup();
|
||||||
@@ -607,9 +636,9 @@ mod tests {
|
|||||||
let mut visible_piles: Vec<KlondikePile> = Vec::new();
|
let mut visible_piles: Vec<KlondikePile> = Vec::new();
|
||||||
for (marker, visibility) in q.iter(app.world()) {
|
for (marker, visibility) in q.iter(app.world()) {
|
||||||
if matches!(visibility, Visibility::Hidden) {
|
if matches!(visibility, Visibility::Hidden) {
|
||||||
hidden_piles.push(marker.0.clone());
|
hidden_piles.push(marker.0);
|
||||||
} else {
|
} else {
|
||||||
visible_piles.push(marker.0.clone());
|
visible_piles.push(marker.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
use bevy::ecs::message::MessageReader;
|
use bevy::ecs::message::MessageReader;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use klondike::KlondikePile;
|
use solitaire_core::KlondikePile;
|
||||||
|
|
||||||
use crate::card_plugin::CardEntity;
|
use crate::card_plugin::CardEntity;
|
||||||
use crate::events::StateChangedEvent;
|
use crate::events::StateChangedEvent;
|
||||||
@@ -192,7 +192,7 @@ fn spawn_touch_highlight(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use klondike::Tableau;
|
use solitaire_core::Tableau;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn selection_state_default_is_idle() {
|
fn selection_state_default_is_idle() {
|
||||||
|
|||||||
@@ -216,13 +216,14 @@ where
|
|||||||
// modal at `Z_PAUSE` (220) in some scenes.
|
// modal at `Z_PAUSE` (220) in some scenes.
|
||||||
GlobalZIndex(z_panel),
|
GlobalZIndex(z_panel),
|
||||||
ZIndex(z_panel),
|
ZIndex(z_panel),
|
||||||
// B0004: ModalCard carries Transform (for the scale animation).
|
// B0004: ModalCard carries Transform (for the scale animation)
|
||||||
// Bevy's GlobalTransform hook fires B0004 when a child has
|
// and visibility-related UI components. Bevy validates that
|
||||||
// GlobalTransform but the parent does not. Adding Identity
|
// GlobalTransform / InheritedVisibility parents carry the same
|
||||||
// Transform here gives the scrim GlobalTransform so the check
|
// hierarchy components, so the scrim root explicitly carries the
|
||||||
// passes. UI layout still uses UiTransform; this has no layout
|
// matching identity components. UI layout still uses UiTransform;
|
||||||
// effect.
|
// this has no layout effect.
|
||||||
Transform::default(),
|
Transform::default(),
|
||||||
|
Visibility::default(),
|
||||||
))
|
))
|
||||||
.with_children(|root| {
|
.with_children(|root| {
|
||||||
root.spawn((
|
root.spawn((
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ solitaire_core = { path = "../solitaire_core" }
|
|||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
klondike = { workspace = true }
|
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
serde-wasm-bindgen = "0.6"
|
serde-wasm-bindgen = "0.6"
|
||||||
console_error_panic_hook = { version = "0.1", optional = true }
|
console_error_panic_hook = { version = "0.1", optional = true }
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
//! is the contract.
|
//! is the contract.
|
||||||
|
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use klondike::{Foundation, KlondikePile, Tableau};
|
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use solitaire_core::card::Suit;
|
use solitaire_core::card::Suit;
|
||||||
use solitaire_core::error::MoveError;
|
use solitaire_core::error::MoveError;
|
||||||
|
|||||||
Reference in New Issue
Block a user