From 65d595ad12939f3301ccc65c1ac428dcb7c789e8 Mon Sep 17 00:00:00 2001 From: funman300 Date: Thu, 30 Apr 2026 20:18:14 +0000 Subject: [PATCH] =?UTF-8?q?feat(engine):=20first-launch=20polish=20?= =?UTF-8?q?=E2=80=94=20em-dash=20zero=20stats=20and=20welcome=20line=20on?= =?UTF-8?q?=20profile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- solitaire_engine/src/profile_plugin.rs | 21 ++++++++++++ solitaire_engine/src/stats_plugin.rs | 46 ++++++++++++++++++++------ 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/solitaire_engine/src/profile_plugin.rs b/solitaire_engine/src/profile_plugin.rs index 678f1df..f8fb1be 100644 --- a/solitaire_engine/src/profile_plugin.rs +++ b/solitaire_engine/src/profile_plugin.rs @@ -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"), diff --git a/solitaire_engine/src/stats_plugin.rs b/solitaire_engine/src/stats_plugin.rs index 2e97a56..af6c6d5 100644 --- a/solitaire_engine/src/stats_plugin.rs +++ b/solitaire_engine/src/stats_plugin.rs @@ -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,