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:
@@ -111,6 +111,27 @@ fn spawn_profile_screen(
|
|||||||
spawn_modal(commands, ProfileScreen, Z_MODAL_PANEL, |card| {
|
spawn_modal(commands, ProfileScreen, Z_MODAL_PANEL, |card| {
|
||||||
spawn_modal_header(card, "Profile", font_res);
|
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 ────────────────────────────────────────────
|
// ── Sync section ────────────────────────────────────────────
|
||||||
card.spawn((
|
card.spawn((
|
||||||
Text::new("Sync"),
|
Text::new("Sync"),
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ use crate::ui_modal::{
|
|||||||
};
|
};
|
||||||
use crate::ui_theme::{
|
use crate::ui_theme::{
|
||||||
ACCENT_PRIMARY, BORDER_SUBTLE, RADIUS_SM, STATE_INFO, STATE_WARNING, TEXT_PRIMARY,
|
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,
|
TEXT_SECONDARY, TYPE_BODY, TYPE_BODY_LG, TYPE_CAPTION, TYPE_HEADLINE, VAL_SPACE_2, VAL_SPACE_3,
|
||||||
Z_MODAL_PANEL,
|
VAL_SPACE_4, Z_MODAL_PANEL,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Bevy resource wrapping the current stats.
|
/// Bevy resource wrapping the current stats.
|
||||||
@@ -247,14 +247,19 @@ fn spawn_stats_screen(
|
|||||||
font_res: Option<&FontResource>,
|
font_res: Option<&FontResource>,
|
||||||
) {
|
) {
|
||||||
// --- primary stat cells ---
|
// --- primary stat cells ---
|
||||||
let win_rate_str = format_win_rate(stats);
|
// First-launch zero-state: when no games have been played yet, render
|
||||||
let played_str = format_stat_value(stats.games_played);
|
// every top-level cell as an em-dash so the panel doesn't read as a
|
||||||
let won_str = format_stat_value(stats.games_won);
|
// mix of "0" counters and "—" sentinels (which feels buggy).
|
||||||
let lost_str = format_stat_value(stats.games_lost);
|
let is_first_launch = stats.games_played == 0;
|
||||||
let fastest_str = format_fastest_win(stats.fastest_win_seconds);
|
let dash = "\u{2014}".to_string();
|
||||||
let avg_time_str = format_avg_time(stats);
|
let win_rate_str = if is_first_launch { dash.clone() } else { format_win_rate(stats) };
|
||||||
let best_score_str = format_optional_u32(stats.best_single_score);
|
let played_str = if is_first_launch { dash.clone() } else { format_stat_value(stats.games_played) };
|
||||||
let best_streak_str = format_stat_value(stats.win_streak_best);
|
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_handle = font_res.map(|f| f.0.clone()).unwrap_or_default();
|
||||||
let font_section = TextFont {
|
let font_section = TextFont {
|
||||||
@@ -271,6 +276,27 @@ fn spawn_stats_screen(
|
|||||||
spawn_modal(commands, StatsScreen, Z_MODAL_PANEL, |card| {
|
spawn_modal(commands, StatsScreen, Z_MODAL_PANEL, |card| {
|
||||||
spawn_modal_header(card, "Statistics", font_res);
|
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 ---
|
// --- primary stat cells grid ---
|
||||||
card.spawn(Node {
|
card.spawn(Node {
|
||||||
flex_direction: FlexDirection::Row,
|
flex_direction: FlexDirection::Row,
|
||||||
|
|||||||
Reference in New Issue
Block a user