feat(engine): first-launch polish — em-dash zero stats and welcome line on profile

On first launch the Stats grid previously mixed "0" cells (Games
Played / Won / Lost) with "—" cells (Best Score / Win Rate / Avg
Time), reading as inconsistent. Now every cell renders an em-dash
when games_played == 0, and a "Play a game to start tracking stats."
caption sits above the grid using the existing TYPE_CAPTION /
TEXT_SECONDARY tokens. Once a game has been played the original
formatters resume.

The Profile screen gains a one-line welcome ("Welcome! Play games to
earn XP and unlock achievements.") that renders only when both
total_xp and the daily streak are zero, breaking up the wall of
zero-valued readouts that greeted users on first launch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
funman300
2026-04-30 20:18:14 +00:00
parent abeb4e5cdf
commit 65d595ad12
2 changed files with 57 additions and 10 deletions
+21
View File
@@ -111,6 +111,27 @@ fn spawn_profile_screen(
spawn_modal(commands, ProfileScreen, Z_MODAL_PANEL, |card| {
spawn_modal_header(card, "Profile", font_res);
// First-launch welcome — only when the player has zero XP and
// zero daily streak, so the profile doesn't read as a wall of
// zeros to a brand-new player.
if let Some(p) = progress
&& p.0.total_xp == 0
&& p.0.daily_challenge_streak == 0
{
card.spawn((
Text::new("Welcome! Play games to earn XP and unlock achievements."),
font_section.clone(),
TextColor(ACCENT_PRIMARY),
Node {
margin: UiRect {
bottom: VAL_SPACE_2,
..default()
},
..default()
},
));
}
// ── Sync section ────────────────────────────────────────────
card.spawn((
Text::new("Sync"),
+36 -10
View File
@@ -30,8 +30,8 @@ use crate::ui_modal::{
};
use crate::ui_theme::{
ACCENT_PRIMARY, BORDER_SUBTLE, RADIUS_SM, STATE_INFO, STATE_WARNING, TEXT_PRIMARY,
TEXT_SECONDARY, TYPE_BODY, TYPE_BODY_LG, TYPE_HEADLINE, VAL_SPACE_2, VAL_SPACE_3, VAL_SPACE_4,
Z_MODAL_PANEL,
TEXT_SECONDARY, TYPE_BODY, TYPE_BODY_LG, TYPE_CAPTION, TYPE_HEADLINE, VAL_SPACE_2, VAL_SPACE_3,
VAL_SPACE_4, Z_MODAL_PANEL,
};
/// Bevy resource wrapping the current stats.
@@ -247,14 +247,19 @@ fn spawn_stats_screen(
font_res: Option<&FontResource>,
) {
// --- primary stat cells ---
let win_rate_str = format_win_rate(stats);
let played_str = format_stat_value(stats.games_played);
let won_str = format_stat_value(stats.games_won);
let lost_str = format_stat_value(stats.games_lost);
let fastest_str = format_fastest_win(stats.fastest_win_seconds);
let avg_time_str = format_avg_time(stats);
let best_score_str = format_optional_u32(stats.best_single_score);
let best_streak_str = format_stat_value(stats.win_streak_best);
// First-launch zero-state: when no games have been played yet, render
// every top-level cell as an em-dash so the panel doesn't read as a
// mix of "0" counters and "—" sentinels (which feels buggy).
let is_first_launch = stats.games_played == 0;
let dash = "\u{2014}".to_string();
let win_rate_str = if is_first_launch { dash.clone() } else { format_win_rate(stats) };
let played_str = if is_first_launch { dash.clone() } else { format_stat_value(stats.games_played) };
let won_str = if is_first_launch { dash.clone() } else { format_stat_value(stats.games_won) };
let lost_str = if is_first_launch { dash.clone() } else { format_stat_value(stats.games_lost) };
let fastest_str = if is_first_launch { dash.clone() } else { format_fastest_win(stats.fastest_win_seconds) };
let avg_time_str = if is_first_launch { dash.clone() } else { format_avg_time(stats) };
let best_score_str = if is_first_launch { dash.clone() } else { format_optional_u32(stats.best_single_score) };
let best_streak_str = if is_first_launch { dash.clone() } else { format_stat_value(stats.win_streak_best) };
let font_handle = font_res.map(|f| f.0.clone()).unwrap_or_default();
let font_section = TextFont {
@@ -271,6 +276,27 @@ fn spawn_stats_screen(
spawn_modal(commands, StatsScreen, Z_MODAL_PANEL, |card| {
spawn_modal_header(card, "Statistics", font_res);
// First-launch caption — sits above the grid as gentle nudge so
// the wall of em-dashes reads as "nothing to track yet" rather
// than as broken state.
if is_first_launch {
card.spawn((
Text::new("Play a game to start tracking stats."),
TextFont {
font_size: TYPE_CAPTION,
..default()
},
TextColor(TEXT_SECONDARY),
Node {
margin: UiRect {
bottom: VAL_SPACE_2,
..default()
},
..default()
},
));
}
// --- primary stat cells grid ---
card.spawn(Node {
flex_direction: FlexDirection::Row,