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:
@@ -140,11 +140,7 @@ impl Plugin for UiFocusPlugin {
|
||||
// resource.
|
||||
.add_systems(
|
||||
PostUpdate,
|
||||
(
|
||||
attach_focusable_to_modal_buttons,
|
||||
auto_focus_on_modal_open,
|
||||
)
|
||||
.chain(),
|
||||
(attach_focusable_to_modal_buttons, auto_focus_on_modal_open).chain(),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
@@ -433,11 +429,7 @@ fn handle_focus_keys(
|
||||
// it matches the visual left → right layout.
|
||||
let row_cycle: Vec<Entity> = siblings
|
||||
.iter()
|
||||
.filter(|e| {
|
||||
focusables
|
||||
.get(*e)
|
||||
.is_ok_and(|(_, disabled)| !disabled)
|
||||
})
|
||||
.filter(|e| focusables.get(*e).is_ok_and(|(_, disabled)| !disabled))
|
||||
.collect();
|
||||
if !row_cycle.is_empty()
|
||||
&& let Some(idx) = row_cycle.iter().position(|e| *e == target)
|
||||
@@ -465,23 +457,23 @@ fn handle_focus_keys(
|
||||
// 1. Any modal open ⇒ Modal(topmost scrim)
|
||||
// 2. Any Hud-grouped focusable hovered ⇒ Hud
|
||||
// 3. Otherwise ⇒ no-op
|
||||
let active_group: FocusGroup = if let Some(active_scrim) = scrims.iter().max_by_key(|e| e.index()) {
|
||||
// Pick the topmost modal as the active group. With multiple
|
||||
// modals stacked (Pause + Forfeit confirm) the most-recently-
|
||||
// spawned scrim has the highest entity index — same heuristic
|
||||
// Phase 1 used.
|
||||
FocusGroup::Modal(active_scrim)
|
||||
} else if hud_interactions.iter().any(|(_, interaction, focusable)| {
|
||||
matches!(interaction, Interaction::Hovered) && focusable.group == FocusGroup::Hud
|
||||
}) {
|
||||
FocusGroup::Hud
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
let active_group: FocusGroup =
|
||||
if let Some(active_scrim) = scrims.iter().max_by_key(|e| e.index()) {
|
||||
// Pick the topmost modal as the active group. With multiple
|
||||
// modals stacked (Pause + Forfeit confirm) the most-recently-
|
||||
// spawned scrim has the highest entity index — same heuristic
|
||||
// Phase 1 used.
|
||||
FocusGroup::Modal(active_scrim)
|
||||
} else if hud_interactions.iter().any(|(_, interaction, focusable)| {
|
||||
matches!(interaction, Interaction::Hovered) && focusable.group == FocusGroup::Hud
|
||||
}) {
|
||||
FocusGroup::Hud
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let tab_pressed = keys.just_pressed(KeyCode::Tab);
|
||||
let activate_pressed =
|
||||
keys.just_pressed(KeyCode::Enter) || keys.just_pressed(KeyCode::Space);
|
||||
let activate_pressed = keys.just_pressed(KeyCode::Enter) || keys.just_pressed(KeyCode::Space);
|
||||
|
||||
if !tab_pressed && !activate_pressed {
|
||||
return;
|
||||
@@ -657,28 +649,37 @@ fn update_focus_overlay(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ui_modal::{
|
||||
spawn_modal, spawn_modal_actions, spawn_modal_button, ButtonVariant, UiModalPlugin,
|
||||
ButtonVariant, UiModalPlugin, spawn_modal, spawn_modal_actions, spawn_modal_button,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn focus_ring_pulse_factor_at_zero_is_mid_point() {
|
||||
// sin(0) = 0 → factor = 0.825 (mid of [0.65, 1.0]).
|
||||
let f = focus_ring_pulse_factor(0.0);
|
||||
assert!((f - 0.825).abs() < 1e-5, "factor at t=0 should be 0.825, got {f}");
|
||||
assert!(
|
||||
(f - 0.825).abs() < 1e-5,
|
||||
"factor at t=0 should be 0.825, got {f}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn focus_ring_pulse_factor_peaks_at_quarter_period() {
|
||||
// sin(τ/4) = 1 → factor = 1.0.
|
||||
let f = focus_ring_pulse_factor(MOTION_FOCUS_PULSE_SECS / 4.0);
|
||||
assert!((f - 1.0).abs() < 1e-4, "factor at peak should be 1.0, got {f}");
|
||||
assert!(
|
||||
(f - 1.0).abs() < 1e-4,
|
||||
"factor at peak should be 1.0, got {f}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn focus_ring_pulse_factor_troughs_at_three_quarter_period() {
|
||||
// sin(3τ/4) = -1 → factor = 0.65.
|
||||
let f = focus_ring_pulse_factor(MOTION_FOCUS_PULSE_SECS * 3.0 / 4.0);
|
||||
assert!((f - 0.65).abs() < 1e-4, "factor at trough should be 0.65, got {f}");
|
||||
assert!(
|
||||
(f - 0.65).abs() < 1e-4,
|
||||
"factor at trough should be 0.65, got {f}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -755,12 +756,16 @@ mod tests {
|
||||
// `auto_focus_on_modal_open` execute.
|
||||
app.update();
|
||||
|
||||
let mut a_query = app.world_mut().query_filtered::<Entity, With<TestButtonA>>();
|
||||
let mut a_query = app
|
||||
.world_mut()
|
||||
.query_filtered::<Entity, With<TestButtonA>>();
|
||||
let a = a_query
|
||||
.iter(app.world())
|
||||
.next()
|
||||
.expect("button A should have been spawned");
|
||||
let mut b_query = app.world_mut().query_filtered::<Entity, With<TestButtonB>>();
|
||||
let mut b_query = app
|
||||
.world_mut()
|
||||
.query_filtered::<Entity, With<TestButtonB>>();
|
||||
let b = b_query
|
||||
.iter(app.world())
|
||||
.next()
|
||||
@@ -807,11 +812,17 @@ mod tests {
|
||||
};
|
||||
app.update();
|
||||
|
||||
let mut q_a = app.world_mut().query_filtered::<Entity, With<TestButtonA>>();
|
||||
let mut q_a = app
|
||||
.world_mut()
|
||||
.query_filtered::<Entity, With<TestButtonA>>();
|
||||
let a = q_a.iter(app.world()).next().expect("A spawned");
|
||||
let mut q_b = app.world_mut().query_filtered::<Entity, With<TestButtonB>>();
|
||||
let mut q_b = app
|
||||
.world_mut()
|
||||
.query_filtered::<Entity, With<TestButtonB>>();
|
||||
let b = q_b.iter(app.world()).next().expect("B spawned");
|
||||
let mut q_c = app.world_mut().query_filtered::<Entity, With<TestButtonC>>();
|
||||
let mut q_c = app
|
||||
.world_mut()
|
||||
.query_filtered::<Entity, With<TestButtonC>>();
|
||||
let c = q_c.iter(app.world()).next().expect("C spawned");
|
||||
(scrim, a, b, c)
|
||||
}
|
||||
@@ -864,10 +875,7 @@ mod tests {
|
||||
/// Crucially this system has **no** ordering relationship with
|
||||
/// `UiFocusPlugin`'s chain — exactly the situation that surfaces the
|
||||
/// "focus arrives one frame late" bug in production.
|
||||
fn spawn_modal_via_system(
|
||||
mut commands: Commands,
|
||||
mut trigger: ResMut<SpawnModalTrigger>,
|
||||
) {
|
||||
fn spawn_modal_via_system(mut commands: Commands, mut trigger: ResMut<SpawnModalTrigger>) {
|
||||
if !trigger.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user