fix(engine,server): safe area clamp, analytics batch, achievement save order, daily rollover, replay validation, leaderboard opt-in (#56, #60, #61, #62, #66, #68)
Build and Deploy / build-and-push (push) Successful in 3m54s
Build and Deploy / build-and-push (push) Successful in 3m54s
- #66: Clamp safe-area insets to 25% of window height with warn!() on excess - #68: Move fire_flush outside per-event loop in analytics (batch flush once) - #56: Persist progress before marking reward_granted to prevent XP loss on crash - #60: Add DateRolloverTimer + check_date_rollover system for midnight seed refresh - #62: Add validate_header() in replay upload with mode/draw_mode allowlists - #61: Restore two-query leaderboard opt-in check (SELECT then UPDATE); original queries already in .sqlx cache; EXISTS variant would require sqlx prepare Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -11,11 +11,11 @@ use solitaire_core::pile::PileType;
|
||||
|
||||
use crate::events::{HintVisualEvent, StateChangedEvent};
|
||||
use crate::hud_plugin::HudVisibility;
|
||||
use crate::layout::{compute_layout, Layout, LayoutResource, LayoutSystem};
|
||||
use crate::safe_area::SafeAreaInsets;
|
||||
use crate::resources::GameStateResource;
|
||||
#[cfg(test)]
|
||||
use crate::layout::TABLE_COLOUR;
|
||||
use crate::layout::{Layout, LayoutResource, LayoutSystem, compute_layout};
|
||||
use crate::resources::GameStateResource;
|
||||
use crate::safe_area::SafeAreaInsets;
|
||||
use crate::settings_plugin::{SettingsChangedEvent, SettingsResource};
|
||||
use crate::ui_theme::TEXT_PRIMARY;
|
||||
#[cfg(test)]
|
||||
@@ -101,7 +101,9 @@ fn load_background_images(asset_server: Option<Res<AssetServer>>, mut commands:
|
||||
let Some(asset_server) = asset_server else {
|
||||
// AssetServer absent (e.g. MinimalPlugins in tests) — insert an
|
||||
// empty set so setup_table can proceed using a default handle.
|
||||
commands.insert_resource(BackgroundImageSet { handles: Vec::new() });
|
||||
commands.insert_resource(BackgroundImageSet {
|
||||
handles: Vec::new(),
|
||||
});
|
||||
return;
|
||||
};
|
||||
let handles = (0..5)
|
||||
@@ -118,8 +120,8 @@ fn load_background_images(asset_server: Option<Res<AssetServer>>, mut commands:
|
||||
fn theme_colour(theme: &Theme) -> Color {
|
||||
match theme {
|
||||
Theme::Green => Color::srgb(TABLE_COLOUR[0], TABLE_COLOUR[1], TABLE_COLOUR[2]),
|
||||
Theme::Blue => Color::srgb(0.059, 0.196, 0.322),
|
||||
Theme::Dark => Color::srgb(0.08, 0.08, 0.10),
|
||||
Theme::Blue => Color::srgb(0.059, 0.196, 0.322),
|
||||
Theme::Dark => Color::srgb(0.08, 0.08, 0.10),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,10 +173,12 @@ fn setup_table(
|
||||
));
|
||||
}
|
||||
|
||||
let (window_size, scale) = windows.iter().next().map_or(
|
||||
(Vec2::new(1280.0, 800.0), 1.0f32),
|
||||
|w| (default_window_size(w), w.scale_factor()),
|
||||
);
|
||||
let (window_size, scale) = windows
|
||||
.iter()
|
||||
.next()
|
||||
.map_or((Vec2::new(1280.0, 800.0), 1.0f32), |w| {
|
||||
(default_window_size(w), w.scale_factor())
|
||||
});
|
||||
// Safe-area insets arrive from JNI asynchronously; they are almost always
|
||||
// 0 here (populated ~frame 2-3). on_safe_area_changed fires when they
|
||||
// arrive and issues a synthetic WindowResized to re-snap all game objects.
|
||||
@@ -249,10 +253,10 @@ fn apply_theme_on_settings_change(
|
||||
/// Matches the same ASCII convention used by `CardPlugin` for card labels.
|
||||
pub fn suit_symbol(suit: &Suit) -> &'static str {
|
||||
match suit {
|
||||
Suit::Spades => "S",
|
||||
Suit::Hearts => "H",
|
||||
Suit::Spades => "S",
|
||||
Suit::Hearts => "H",
|
||||
Suit::Diamonds => "D",
|
||||
Suit::Clubs => "C",
|
||||
Suit::Clubs => "C",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,7 +295,10 @@ fn spawn_pile_markers(commands: &mut Commands, layout: &Layout) {
|
||||
entity.with_children(|b| {
|
||||
b.spawn((
|
||||
Text2d::new("K"),
|
||||
TextFont { font_size, ..default() },
|
||||
TextFont {
|
||||
font_size,
|
||||
..default()
|
||||
},
|
||||
TextColor(TEXT_PRIMARY.with_alpha(0.35)),
|
||||
Transform::from_xyz(0.0, 0.0, 0.1),
|
||||
));
|
||||
@@ -301,7 +308,10 @@ fn spawn_pile_markers(commands: &mut Commands, layout: &Layout) {
|
||||
entity.with_children(|b| {
|
||||
b.spawn((
|
||||
Text2d::new("A"),
|
||||
TextFont { font_size, ..default() },
|
||||
TextFont {
|
||||
font_size,
|
||||
..default()
|
||||
},
|
||||
TextColor(TEXT_PRIMARY.with_alpha(0.35)),
|
||||
Transform::from_xyz(0.0, 0.0, 0.1),
|
||||
));
|
||||
@@ -375,7 +385,9 @@ fn on_safe_area_changed(
|
||||
windows: Query<(Entity, &Window)>,
|
||||
mut resize_events: MessageWriter<WindowResized>,
|
||||
) {
|
||||
let Some(safe_area) = safe_area else { return; };
|
||||
let Some(safe_area) = safe_area else {
|
||||
return;
|
||||
};
|
||||
if !safe_area.is_changed() {
|
||||
return;
|
||||
}
|
||||
@@ -597,18 +609,18 @@ mod tests {
|
||||
#[test]
|
||||
fn all_three_themes_produce_distinct_colours() {
|
||||
let green = theme_colour(&Theme::Green);
|
||||
let blue = theme_colour(&Theme::Blue);
|
||||
let dark = theme_colour(&Theme::Dark);
|
||||
let blue = theme_colour(&Theme::Blue);
|
||||
let dark = theme_colour(&Theme::Dark);
|
||||
assert_ne!(green, blue, "Green and Blue must differ");
|
||||
assert_ne!(green, dark, "Green and Dark must differ");
|
||||
assert_ne!(blue, dark, "Blue and Dark must differ");
|
||||
assert_ne!(blue, dark, "Blue and Dark must differ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_background_index_0_matches_theme_colour() {
|
||||
for theme in [Theme::Green, Theme::Blue, Theme::Dark] {
|
||||
let expected = theme_colour(&theme);
|
||||
let actual = effective_background_colour(&theme, 0);
|
||||
let actual = effective_background_colour(&theme, 0);
|
||||
assert_eq!(
|
||||
expected, actual,
|
||||
"index 0 must always return the theme colour for {:?}",
|
||||
@@ -623,7 +635,10 @@ mod tests {
|
||||
let theme_green = theme_colour(&Theme::Green);
|
||||
for idx in 1..=3 {
|
||||
let eff = effective_background_colour(&Theme::Green, idx);
|
||||
assert_ne!(eff, theme_green, "index {idx} must override the theme colour");
|
||||
assert_ne!(
|
||||
eff, theme_green,
|
||||
"index {idx} must override the theme colour"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -643,10 +658,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn suit_symbol_returns_correct_letters() {
|
||||
assert_eq!(suit_symbol(&Suit::Spades), "S");
|
||||
assert_eq!(suit_symbol(&Suit::Hearts), "H");
|
||||
assert_eq!(suit_symbol(&Suit::Spades), "S");
|
||||
assert_eq!(suit_symbol(&Suit::Hearts), "H");
|
||||
assert_eq!(suit_symbol(&Suit::Diamonds), "D");
|
||||
assert_eq!(suit_symbol(&Suit::Clubs), "C");
|
||||
assert_eq!(suit_symbol(&Suit::Clubs), "C");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -730,12 +745,29 @@ mod tests {
|
||||
/// `hint_pile_highlight_rgb_tracks_state_warning_token`.
|
||||
#[test]
|
||||
fn hint_pile_highlight_colour_is_gold() {
|
||||
let Srgba { red, green, blue, .. } = HINT_PILE_HIGHLIGHT_COLOUR.to_srgba();
|
||||
assert!(red >= 0.7, "gold hint colour must have red ≥ 0.7, got {red}");
|
||||
assert!(green >= 0.5, "gold hint colour must have green ≥ 0.5, got {green}");
|
||||
assert!(blue <= 0.6, "gold hint colour must have blue ≤ 0.6, got {blue}");
|
||||
assert!(red > blue, "gold hint colour must be warmer than cool, got r={red} b={blue}");
|
||||
assert!(green > blue, "gold hint colour must be warmer than cool, got g={green} b={blue}");
|
||||
let Srgba {
|
||||
red, green, blue, ..
|
||||
} = HINT_PILE_HIGHLIGHT_COLOUR.to_srgba();
|
||||
assert!(
|
||||
red >= 0.7,
|
||||
"gold hint colour must have red ≥ 0.7, got {red}"
|
||||
);
|
||||
assert!(
|
||||
green >= 0.5,
|
||||
"gold hint colour must have green ≥ 0.5, got {green}"
|
||||
);
|
||||
assert!(
|
||||
blue <= 0.6,
|
||||
"gold hint colour must have blue ≤ 0.6, got {blue}"
|
||||
);
|
||||
assert!(
|
||||
red > blue,
|
||||
"gold hint colour must be warmer than cool, got r={red} b={blue}"
|
||||
);
|
||||
assert!(
|
||||
green > blue,
|
||||
"gold hint colour must be warmer than cool, got g={green} b={blue}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user