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:
@@ -76,8 +76,8 @@ use crate::settings_plugin::SettingsResource;
|
||||
use crate::ui_theme::{
|
||||
ACCENT_PRIMARY, ACCENT_SECONDARY, BG_BASE, BORDER_SUBTLE, MOTION_SPLASH_FADE_SECS,
|
||||
MOTION_SPLASH_TOTAL_SECS, STATE_DANGER, STATE_INFO, STATE_SUCCESS, STATE_WARNING,
|
||||
TEXT_DISABLED, TEXT_PRIMARY, TYPE_CAPTION, TYPE_DISPLAY, VAL_SPACE_1, VAL_SPACE_2,
|
||||
VAL_SPACE_3, VAL_SPACE_5, VAL_SPACE_6, VAL_SPACE_7, Z_SPLASH,
|
||||
TEXT_DISABLED, TEXT_PRIMARY, TYPE_CAPTION, TYPE_DISPLAY, VAL_SPACE_1, VAL_SPACE_2, VAL_SPACE_3,
|
||||
VAL_SPACE_5, VAL_SPACE_6, VAL_SPACE_7, Z_SPLASH,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -99,12 +99,7 @@ impl Plugin for SplashPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(Startup, spawn_splash).add_systems(
|
||||
Update,
|
||||
(
|
||||
dismiss_splash_on_input,
|
||||
advance_splash,
|
||||
pulse_splash_cursor,
|
||||
)
|
||||
.chain(),
|
||||
(dismiss_splash_on_input, advance_splash, pulse_splash_cursor).chain(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -325,11 +320,7 @@ fn build_scanline_image() -> Image {
|
||||
// because `TextureFormat::pixel_size()` returns a `Result` in this
|
||||
// Bevy version and a `debug_assert_eq!` shouldn't carry the
|
||||
// unwrap noise.
|
||||
debug_assert_eq!(
|
||||
pixels.len(),
|
||||
16,
|
||||
"scanline pixel buffer must be 2x2 RGBA8",
|
||||
);
|
||||
debug_assert_eq!(pixels.len(), 16, "scanline pixel buffer must be 2x2 RGBA8",);
|
||||
Image::new(
|
||||
Extent3d {
|
||||
width: 2,
|
||||
@@ -376,13 +367,17 @@ fn spawn_header_section(parent: &mut ChildSpawnerCommands, font_handle: &Handle<
|
||||
})
|
||||
.with_children(|hdr| {
|
||||
hdr.spawn((
|
||||
SplashFadable { base_color: ACCENT_PRIMARY },
|
||||
SplashFadable {
|
||||
base_color: ACCENT_PRIMARY,
|
||||
},
|
||||
Text::new("|"), // ASCII terminal cursor.
|
||||
cursor_font,
|
||||
TextColor(transparent(ACCENT_PRIMARY)),
|
||||
));
|
||||
hdr.spawn((
|
||||
SplashFadable { base_color: TEXT_PRIMARY },
|
||||
SplashFadable {
|
||||
base_color: TEXT_PRIMARY,
|
||||
},
|
||||
Text::new("Ferrous Solitaire"),
|
||||
title_font,
|
||||
TextColor(transparent(TEXT_PRIMARY)),
|
||||
@@ -390,7 +385,9 @@ fn spawn_header_section(parent: &mut ChildSpawnerCommands, font_handle: &Handle<
|
||||
// Thin horizontal divider under the wordmark — same hue as
|
||||
// every other 1px chrome line in the design system.
|
||||
hdr.spawn((
|
||||
SplashFadableBg { base_color: BORDER_SUBTLE },
|
||||
SplashFadableBg {
|
||||
base_color: BORDER_SUBTLE,
|
||||
},
|
||||
Node {
|
||||
width: Val::Px(192.0),
|
||||
height: Val::Px(1.0),
|
||||
@@ -399,7 +396,9 @@ fn spawn_header_section(parent: &mut ChildSpawnerCommands, font_handle: &Handle<
|
||||
BackgroundColor(transparent(BORDER_SUBTLE)),
|
||||
));
|
||||
hdr.spawn((
|
||||
SplashFadable { base_color: TEXT_DISABLED },
|
||||
SplashFadable {
|
||||
base_color: TEXT_DISABLED,
|
||||
},
|
||||
Text::new("TERMINAL EDITION"),
|
||||
subtitle_font,
|
||||
TextColor(transparent(TEXT_DISABLED)),
|
||||
@@ -469,13 +468,17 @@ fn spawn_check_row(parent: &mut ChildSpawnerCommands, line_font: &TextFont, labe
|
||||
})
|
||||
.with_children(|row| {
|
||||
row.spawn((
|
||||
SplashFadable { base_color: STATE_SUCCESS },
|
||||
SplashFadable {
|
||||
base_color: STATE_SUCCESS,
|
||||
},
|
||||
Text::new("\u{2713}"), // ✓
|
||||
line_font.clone(),
|
||||
TextColor(transparent(STATE_SUCCESS)),
|
||||
));
|
||||
row.spawn((
|
||||
SplashFadable { base_color: TEXT_DISABLED },
|
||||
SplashFadable {
|
||||
base_color: TEXT_DISABLED,
|
||||
},
|
||||
Text::new(label.to_string()),
|
||||
line_font.clone(),
|
||||
TextColor(transparent(TEXT_DISABLED)),
|
||||
@@ -502,7 +505,9 @@ fn spawn_ready_row(parent: &mut ChildSpawnerCommands, line_font: &TextFont) {
|
||||
})
|
||||
.with_children(|row| {
|
||||
row.spawn((
|
||||
SplashFadable { base_color: TEXT_PRIMARY },
|
||||
SplashFadable {
|
||||
base_color: TEXT_PRIMARY,
|
||||
},
|
||||
Text::new("| ready_"), // ASCII ready prompt.
|
||||
line_font.clone(),
|
||||
TextColor(transparent(TEXT_PRIMARY)),
|
||||
@@ -513,7 +518,9 @@ fn spawn_ready_row(parent: &mut ChildSpawnerCommands, line_font: &TextFont) {
|
||||
// 6×12 px spec literally. Pulse animation lives in
|
||||
// `pulse_splash_cursor` for testability.
|
||||
row.spawn((
|
||||
SplashFadableBg { base_color: ACCENT_PRIMARY },
|
||||
SplashFadableBg {
|
||||
base_color: ACCENT_PRIMARY,
|
||||
},
|
||||
SplashCursorPulse,
|
||||
Node {
|
||||
width: Val::Px(6.0),
|
||||
@@ -542,7 +549,9 @@ fn spawn_progress_bar(parent: &mut ChildSpawnerCommands, line_font: &TextFont) {
|
||||
.with_children(|bar| {
|
||||
// Track.
|
||||
bar.spawn((
|
||||
SplashFadableBg { base_color: BORDER_SUBTLE },
|
||||
SplashFadableBg {
|
||||
base_color: BORDER_SUBTLE,
|
||||
},
|
||||
Node {
|
||||
width: Val::Percent(100.0),
|
||||
height: Val::Px(1.0),
|
||||
@@ -553,7 +562,9 @@ fn spawn_progress_bar(parent: &mut ChildSpawnerCommands, line_font: &TextFont) {
|
||||
.with_children(|track| {
|
||||
// Fill — 100 % of the track width = "complete".
|
||||
track.spawn((
|
||||
SplashFadableBg { base_color: ACCENT_PRIMARY },
|
||||
SplashFadableBg {
|
||||
base_color: ACCENT_PRIMARY,
|
||||
},
|
||||
Node {
|
||||
width: Val::Percent(100.0),
|
||||
height: Val::Percent(100.0),
|
||||
@@ -570,7 +581,9 @@ fn spawn_progress_bar(parent: &mut ChildSpawnerCommands, line_font: &TextFont) {
|
||||
})
|
||||
.with_children(|caption| {
|
||||
caption.spawn((
|
||||
SplashFadable { base_color: TEXT_DISABLED },
|
||||
SplashFadable {
|
||||
base_color: TEXT_DISABLED,
|
||||
},
|
||||
Text::new("DONE \u{00B7} 247 ASSETS"), // DONE · 247 ASSETS
|
||||
line_font.clone(),
|
||||
TextColor(transparent(TEXT_DISABLED)),
|
||||
@@ -598,14 +611,18 @@ fn spawn_footer_section(parent: &mut ChildSpawnerCommands, font_handle: &Handle<
|
||||
})
|
||||
.with_children(|footer| {
|
||||
footer.spawn((
|
||||
SplashFadable { base_color: TEXT_DISABLED },
|
||||
SplashFadable {
|
||||
base_color: TEXT_DISABLED,
|
||||
},
|
||||
Text::new("BASE16-EIGHTIES"),
|
||||
footer_font.clone(),
|
||||
TextColor(transparent(TEXT_DISABLED)),
|
||||
));
|
||||
spawn_palette_swatch_row(footer);
|
||||
footer.spawn((
|
||||
SplashFadable { base_color: TEXT_DISABLED },
|
||||
SplashFadable {
|
||||
base_color: TEXT_DISABLED,
|
||||
},
|
||||
Text::new(format!("v{}", env!("CARGO_PKG_VERSION"))),
|
||||
footer_font.clone(),
|
||||
TextColor(transparent(TEXT_DISABLED)),
|
||||
@@ -838,9 +855,8 @@ fn dismiss_splash_on_input(
|
||||
// Jump the age forward to the start of the fade-out so the
|
||||
// overlay dissolves cleanly. Saturating arithmetic on Duration
|
||||
// means an already-past-fade-out splash stays past fade-out.
|
||||
let fade_out_start = Duration::from_secs_f32(
|
||||
(MOTION_SPLASH_TOTAL_SECS - MOTION_SPLASH_FADE_SECS).max(0.0),
|
||||
);
|
||||
let fade_out_start =
|
||||
Duration::from_secs_f32((MOTION_SPLASH_TOTAL_SECS - MOTION_SPLASH_FADE_SECS).max(0.0));
|
||||
for mut age in &mut roots {
|
||||
if age.0 < fade_out_start {
|
||||
age.0 = fade_out_start;
|
||||
@@ -879,9 +895,9 @@ mod tests {
|
||||
/// Tells `TimePlugin` to advance the virtual clock by `secs` on the
|
||||
/// next `app.update()`. Mirrors the helper in `ui_tooltip::tests`.
|
||||
fn set_manual_time_step(app: &mut App, secs: f32) {
|
||||
app.insert_resource(TimeUpdateStrategy::ManualDuration(
|
||||
Duration::from_secs_f32(secs),
|
||||
));
|
||||
app.insert_resource(TimeUpdateStrategy::ManualDuration(Duration::from_secs_f32(
|
||||
secs,
|
||||
)));
|
||||
}
|
||||
|
||||
/// `Time<Virtual>` clamps per-tick deltas to `max_delta` (default
|
||||
@@ -1056,9 +1072,8 @@ mod tests {
|
||||
"alpha mid-hold must be exactly 1.0"
|
||||
);
|
||||
// Inside fade-out.
|
||||
let mid_fade_out = Duration::from_secs_f32(
|
||||
MOTION_SPLASH_TOTAL_SECS - MOTION_SPLASH_FADE_SECS / 2.0,
|
||||
);
|
||||
let mid_fade_out =
|
||||
Duration::from_secs_f32(MOTION_SPLASH_TOTAL_SECS - MOTION_SPLASH_FADE_SECS / 2.0);
|
||||
let mid_out_alpha = splash_alpha(mid_fade_out).unwrap();
|
||||
assert!(
|
||||
mid_out_alpha < 0.6 && mid_out_alpha > 0.4,
|
||||
@@ -1097,9 +1112,8 @@ mod tests {
|
||||
.next()
|
||||
.expect("splash should exist after one post-dismiss tick")
|
||||
.0;
|
||||
let fade_out_start = Duration::from_secs_f32(
|
||||
MOTION_SPLASH_TOTAL_SECS - MOTION_SPLASH_FADE_SECS,
|
||||
);
|
||||
let fade_out_start =
|
||||
Duration::from_secs_f32(MOTION_SPLASH_TOTAL_SECS - MOTION_SPLASH_FADE_SECS);
|
||||
assert!(
|
||||
age >= fade_out_start,
|
||||
"after a keypress dismiss the splash must be in fade-out (age >= {fade_out_start:?}); got {age:?}"
|
||||
@@ -1127,9 +1141,8 @@ mod tests {
|
||||
.next()
|
||||
.expect("splash should exist after one post-dismiss tick")
|
||||
.0;
|
||||
let fade_out_start = Duration::from_secs_f32(
|
||||
MOTION_SPLASH_TOTAL_SECS - MOTION_SPLASH_FADE_SECS,
|
||||
);
|
||||
let fade_out_start =
|
||||
Duration::from_secs_f32(MOTION_SPLASH_TOTAL_SECS - MOTION_SPLASH_FADE_SECS);
|
||||
assert!(
|
||||
age >= fade_out_start,
|
||||
"after a left-click dismiss the splash must be in fade-out; got {age:?}"
|
||||
@@ -1320,11 +1333,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// Trough — sin(TAU * 0.75) = -1 → normalised = 0 → factor = min.
|
||||
let trough = cursor_pulse_factor(
|
||||
Duration::from_secs_f32(period * 3.0 / 4.0),
|
||||
period,
|
||||
min,
|
||||
);
|
||||
let trough = cursor_pulse_factor(Duration::from_secs_f32(period * 3.0 / 4.0), period, min);
|
||||
assert!(
|
||||
(trough - min).abs() < 1e-5,
|
||||
"trough should fall to min ({min}); got {trough}"
|
||||
|
||||
Reference in New Issue
Block a user