diff --git a/Cargo.lock b/Cargo.lock index a8df0b2..74dd694 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7340,7 +7340,6 @@ dependencies = [ "jni 0.21.1", "jsonwebtoken", "keyring-core", - "klondike", "reqwest", "serde", "serde_json", @@ -7367,7 +7366,6 @@ dependencies = [ "image", "jni 0.21.1", "kira", - "klondike", "reqwest", "resvg", "ron", @@ -7427,7 +7425,6 @@ dependencies = [ "chrono", "console_error_panic_hook", "getrandom 0.3.4", - "klondike", "serde", "serde-wasm-bindgen", "serde_json", diff --git a/solitaire_data/Cargo.toml b/solitaire_data/Cargo.toml index cdd2e44..c7349d7 100644 --- a/solitaire_data/Cargo.toml +++ b/solitaire_data/Cargo.toml @@ -13,7 +13,6 @@ chrono = { workspace = true } thiserror = { workspace = true } async-trait = { workspace = true } uuid = { workspace = true } -klondike = { workspace = true } # These deps are not available / not needed on wasm32: # dirs — platform data directories (no filesystem on browser) @@ -36,10 +35,6 @@ keyring-core = { workspace = true } [target.'cfg(target_os = "android")'.dependencies] 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] solitaire_server = { path = "../solitaire_server" } diff --git a/solitaire_engine/Cargo.toml b/solitaire_engine/Cargo.toml index 6f6c29b..306c7e0 100644 --- a/solitaire_engine/Cargo.toml +++ b/solitaire_engine/Cargo.toml @@ -10,7 +10,6 @@ image = { workspace = true } solitaire_core = { workspace = true } solitaire_data = { workspace = true } solitaire_sync = { workspace = true } -klondike = { workspace = true } chrono = { workspace = true } uuid = { workspace = true } serde = { workspace = true } diff --git a/solitaire_engine/src/animation_plugin.rs b/solitaire_engine/src/animation_plugin.rs index 3e8ef42..87d6d68 100644 --- a/solitaire_engine/src/animation_plugin.rs +++ b/solitaire_engine/src/animation_plugin.rs @@ -1078,7 +1078,7 @@ mod tests { // Pairs the existing audio (`card_invalid.wav`) and visual // (`feedback_anim_plugin::queue_shake_for_rejected_move`) feedback // with an accessibility-focused readable text cue. - use klondike::{KlondikePile, Tableau}; + use solitaire_core::{KlondikePile, Tableau}; let mut app = App::new(); app.add_plugins(MinimalPlugins).add_plugins(AnimationPlugin); diff --git a/solitaire_engine/src/auto_complete_plugin.rs b/solitaire_engine/src/auto_complete_plugin.rs index 3409e03..353e2d0 100644 --- a/solitaire_engine/src/auto_complete_plugin.rs +++ b/solitaire_engine/src/auto_complete_plugin.rs @@ -167,7 +167,7 @@ mod tests { use super::*; use crate::game_plugin::GamePlugin; 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::game_state::{DrawMode, GameState}; diff --git a/solitaire_engine/src/card_plugin.rs b/solitaire_engine/src/card_plugin.rs index effd899..440667d 100644 --- a/solitaire_engine/src/card_plugin.rs +++ b/solitaire_engine/src/card_plugin.rs @@ -16,7 +16,7 @@ use bevy::color::Color; use bevy::prelude::*; use bevy::sprite::Anchor; 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::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 /// stock [`PileMarker`]) and its `Transform` is recomputed each frame from /// `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 /// ([`StockEmptyLabel`]) covers the recycle hint instead, so the two /// indicators never render simultaneously. @@ -301,13 +301,18 @@ pub struct CardShadow; #[derive(Component, Debug)] pub struct CardBackFrame; -/// Fill colour for the face-down card border frame. Medium gray so it reads as -/// a neutral "edge" without competing with the suit colours on face-up cards. -const CARD_BACK_FRAME_COLOR: Color = Color::srgb(0.38, 0.38, 0.38); +/// Fill colour for the face-down card border frame. Light-medium gray so it +/// reads as a clear "edge" without competing with the suit colours on face-up +/// 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 -/// the visible border. 3 world units ≈ 3 dp on a 1× screen. -const CARD_BACK_FRAME_PADDING: f32 = 3.0; +/// the visible border. Widened from `3.0` to `6.0` so the frame peeks out as a +/// 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 /// 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 // -// 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 // existing `StockEmptyLabel` (`↺` overlay) covers the empty-stock case, so // the badge hides itself when the stock has zero cards — the two indicators // never render at the same time. // --------------------------------------------------------------------------- -/// Inset (in pixels) from the top-right corner of the stock pile sprite to -/// the centre of the count badge. Must satisfy `|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); +/// Inset (in pixels) from the bottom-right corner of the stock pile sprite to +/// the centre of the count badge. Anchoring to the bottom-right keeps the chip +/// clear of the rank/suit pip in the card's top-left corner. Both components +/// move the centre *inward* from that corner: `x` is subtracted from the right +/// edge, `y` is added to the bottom edge. The `x` magnitude must satisfy +/// `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 /// 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, -/// given the current `Layout`. The badge sits at the top-right corner of -/// the stock pile sprite, inset by [`STOCK_BADGE_INSET`]. +/// given the current `Layout`. The badge sits at the bottom-right corner of +/// 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 { // 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. @@ -1939,8 +1948,9 @@ fn stock_badge_translation(layout: &Layout) -> Vec3 { .copied() .unwrap_or(Vec2::ZERO); let half = layout.card_size * 0.5; - let x = pile_pos.x + half.x + STOCK_BADGE_INSET.x; - let y = pile_pos.y + half.y + STOCK_BADGE_INSET.y; + // Anchor to the bottom-right corner, then move the centre inward. + 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) } @@ -2357,7 +2367,7 @@ fn update_tableau_fan_frac( .into_iter() .map(|tableau| { game.0 - .pile(klondike::KlondikePile::Tableau(tableau)) + .pile(solitaire_core::KlondikePile::Tableau(tableau)) .into_iter() .filter(|c| c.face_up) .count() diff --git a/solitaire_engine/src/cursor_plugin.rs b/solitaire_engine/src/cursor_plugin.rs index 2435806..531ac06 100644 --- a/solitaire_engine/src/cursor_plugin.rs +++ b/solitaire_engine/src/cursor_plugin.rs @@ -34,7 +34,7 @@ use bevy::prelude::*; 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 crate::card_plugin::RightClickHighlight; @@ -655,7 +655,7 @@ mod tests { .world_mut() .query::<&DropTargetOverlay>() .iter(app.world()) - .map(|o| o.0.clone()) + .map(|o| o.0) .collect(); assert!( !overlays.contains(&KlondikePile::Tableau(Tableau::Tableau3)), diff --git a/solitaire_engine/src/events.rs b/solitaire_engine/src/events.rs index d48a23d..5ad69a0 100644 --- a/solitaire_engine/src/events.rs +++ b/solitaire_engine/src/events.rs @@ -1,7 +1,7 @@ //! Cross-system events used by the engine's plugins. use bevy::prelude::Message; -use klondike::KlondikePile; +use solitaire_core::KlondikePile; use solitaire_core::card::Suit; use solitaire_core::game_state::GameMode; use solitaire_data::AchievementRecord; diff --git a/solitaire_engine/src/feedback_anim_plugin.rs b/solitaire_engine/src/feedback_anim_plugin.rs index fc19a15..b866811 100644 --- a/solitaire_engine/src/feedback_anim_plugin.rs +++ b/solitaire_engine/src/feedback_anim_plugin.rs @@ -43,7 +43,7 @@ use std::hash::{Hash, Hasher}; use bevy::prelude::*; use bevy::window::RequestRedraw; -use klondike::{Foundation, KlondikePile}; +use solitaire_core::{Foundation, KlondikePile}; use solitaire_data::AnimSpeed; use crate::animation_plugin::CardAnim; @@ -849,7 +849,7 @@ mod tests { #[test] fn shake_anim_skipped_under_reduce_motion() { use bevy::ecs::message::Messages; - use klondike::Tableau; + use solitaire_core::Tableau; use solitaire_core::game_state::{DrawMode, GameState}; use solitaire_data::Settings; diff --git a/solitaire_engine/src/game_plugin.rs b/solitaire_engine/src/game_plugin.rs index 85c34c2..aaa919e 100644 --- a/solitaire_engine/src/game_plugin.rs +++ b/solitaire_engine/src/game_plugin.rs @@ -13,7 +13,7 @@ use chrono::Utc; use bevy::prelude::*; use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future}; use bevy::window::AppLifecycle; -use klondike::KlondikePile; +use solitaire_core::KlondikePile; use solitaire_core::game_state::{DrawMode, GameMode, GameState}; use solitaire_core::solver::{SolverConfig, SolverResult, try_solve}; #[allow(deprecated)] @@ -521,7 +521,7 @@ fn handle_new_game( // hides that information and reads naturally as "dealt from the // deck." Skipped when LayoutResource isn't present (headless tests). 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 { tx.translation.x = stock.x; @@ -1014,12 +1014,12 @@ fn pile_cards(game: &GameState, pile: &KlondikePile) -> Vec Option { +fn foundation_slot(foundation: solitaire_core::Foundation) -> Option { match foundation { - klondike::Foundation::Foundation1 => Some(0), - klondike::Foundation::Foundation2 => Some(1), - klondike::Foundation::Foundation3 => Some(2), - klondike::Foundation::Foundation4 => Some(3), + solitaire_core::Foundation::Foundation1 => Some(0), + solitaire_core::Foundation::Foundation2 => Some(1), + solitaire_core::Foundation::Foundation3 => Some(2), + solitaire_core::Foundation::Foundation4 => Some(3), } } @@ -1294,7 +1294,7 @@ fn save_game_state_on_exit( #[cfg(test)] mod tests { use super::*; - use klondike::{Foundation, KlondikePile, Tableau}; + use solitaire_core::{Foundation, KlondikePile, Tableau}; use solitaire_core::klondike_adapter::{SavedKlondikePile, SavedTableau}; /// Build a minimal headless `App` with just `GamePlugin` installed. @@ -1423,8 +1423,10 @@ mod tests { "fresh game should inherit default take_from_foundation=true", ); - let mut settings = solitaire_data::Settings::default(); - settings.take_from_foundation = false; + let mut settings = solitaire_data::Settings { + take_from_foundation: false, + ..Default::default() + }; app.world_mut() .write_message(crate::settings_plugin::SettingsChangedEvent( settings.clone(), @@ -1951,15 +1953,15 @@ mod tests { ); } - /// Verify that the game-over overlay contains the expected header text and - /// action-hint strings so players understand why the overlay appeared and - /// what keys to press. + // Verify that the game-over overlay contains the expected header text and + // action-hint strings so players understand why the overlay appeared and + // what keys to press. // ----------------------------------------------------------------------- // Task #56 — Escape dismisses GameOverScreen and starts new game // ----------------------------------------------------------------------- - /// Pressing Escape while `GameOverScreen` is visible must fire - /// `NewGameRequestEvent` — identical behaviour to pressing N. + // Pressing Escape while `GameOverScreen` is visible must fire + // `NewGameRequestEvent` — identical behaviour to pressing N. // ----------------------------------------------------------------------- // Task #48 — Undo with empty stack fires InfoToastEvent // ----------------------------------------------------------------------- diff --git a/solitaire_engine/src/hud_plugin.rs b/solitaire_engine/src/hud_plugin.rs index 976f774..7aaa485 100644 --- a/solitaire_engine/src/hud_plugin.rs +++ b/solitaire_engine/src/hud_plugin.rs @@ -8,7 +8,7 @@ use bevy::prelude::*; use bevy::window::WindowResized; -use klondike::{Foundation, KlondikePile, Tableau}; +use solitaire_core::{Foundation, KlondikePile, Tableau}; use solitaire_core::card::Suit; 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 /// controls reference so both always agree. #[cfg(target_os = "android")] -pub(crate) const ANDROID_HINT_LABEL: &str = "!"; +pub(crate) const ANDROID_HINT_LABEL: &str = "Hint"; #[cfg(target_os = "android")] const ACTION_BAR_LABELS: [&str; 7] = [ - "\u{2261}", - "\u{2190}", - "||", - "?", + "Menu", + "Undo", + "Pause", + "Help", ANDROID_HINT_LABEL, - "M", - "+", + "Mode", + "New", ]; #[cfg(not(target_os = "android"))] const ACTION_BAR_LABELS: [&str; 7] = [ @@ -830,6 +830,8 @@ fn spawn_avatar_child( ) { const SIZE: f32 = 32.0; 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. commands.entity(parent).with_children(|b| { b.spawn(( @@ -850,6 +852,15 @@ fn spawn_avatar_child( }) .and_then(|c| c.to_uppercase().next()) .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| { b.spawn(( 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. /// 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. +#[cfg(not(target_os = "android"))] const ACTION_FADE_REVEAL_PX: f32 = HUD_BAND_HEIGHT + 32.0; /// Lerp rate for fading (per second). 6.0 ≈ 167 ms for a full /// transition — fast enough to feel responsive without flashing on /// brief cursor wanders into the reveal zone. +#[cfg(not(target_os = "android"))] const ACTION_FADE_RATE_PER_SEC: f32 = 6.0; /// 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 /// `target` at a fixed rate so the visual transition is smooth across /// variable framerates. +#[cfg(not(target_os = "android"))] fn update_action_fade(windows: Query<&Window>, time: Res