From 56e2e6f151527a82bc2bf2ec12d637886ebcba35 Mon Sep 17 00:00:00 2001 From: funman300 Date: Wed, 6 May 2026 06:16:37 +0000 Subject: [PATCH] feat(engine): empty-state copy + onboarding hints across panels - Leaderboard empty state: replace single muted line with a two-tier "Be the first on the leaderboard." headline + body invite. - Achievements panel: surface a first-launch hint above the grid until the player unlocks anything, so the greyed-out rows aren't context-free. - Volume hotkeys ([/]): emit an InfoToastEvent with the new percentage so off-panel adjustments give visible feedback (previously silent). Co-Authored-By: Claude Opus 4.7 (1M context) --- solitaire_engine/src/achievement_plugin.rs | 20 ++++++++++++++++++++ solitaire_engine/src/leaderboard_plugin.rs | 7 ++++++- solitaire_engine/src/settings_plugin.rs | 8 +++++++- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/solitaire_engine/src/achievement_plugin.rs b/solitaire_engine/src/achievement_plugin.rs index 48d92d5..f377558 100644 --- a/solitaire_engine/src/achievement_plugin.rs +++ b/solitaire_engine/src/achievement_plugin.rs @@ -474,9 +474,29 @@ fn spawn_achievements_screen( ..default() }; + let any_unlocked = records.iter().any(|r| r.unlocked); + let scrim = spawn_modal(commands, AchievementsScreen, Z_MODAL_PANEL, |card| { spawn_modal_header(card, header, font_res); + // First-time hint — shown until the player has unlocked anything. + // The list itself describes individual rewards, but a top-level + // explanation gives newer players context for the otherwise dense + // greyed-out grid. + if !any_unlocked { + card.spawn(( + Text::new( + "Complete games and try new modes to unlock achievements and rewards.", + ), + TextFont { + font: font_res.map(|f| f.0.clone()).unwrap_or_default(), + font_size: TYPE_CAPTION, + ..default() + }, + TextColor(TEXT_SECONDARY), + )); + } + // Scrollable body — the achievements list grows to ~19 rows which // overflows the modal on the 800x600 minimum window. Wrapping the // row list in an `Overflow::scroll_y()` Node with a constrained diff --git a/solitaire_engine/src/leaderboard_plugin.rs b/solitaire_engine/src/leaderboard_plugin.rs index 1f59d5e..2d08868 100644 --- a/solitaire_engine/src/leaderboard_plugin.rs +++ b/solitaire_engine/src/leaderboard_plugin.rs @@ -501,7 +501,12 @@ fn spawn_leaderboard_screen( } LeaderboardResource::Loaded(rows) if rows.is_empty() => { body.spawn(( - Text::new("No entries yet \u{2014} sync and opt in to appear here."), + Text::new("Be the first on the leaderboard."), + font_status.clone(), + TextColor(TEXT_PRIMARY), + )); + body.spawn(( + Text::new("Win a game and opt in to appear here."), font_row.clone(), TextColor(TEXT_SECONDARY), )); diff --git a/solitaire_engine/src/settings_plugin.rs b/solitaire_engine/src/settings_plugin.rs index f25edb0..16ddb83 100644 --- a/solitaire_engine/src/settings_plugin.rs +++ b/solitaire_engine/src/settings_plugin.rs @@ -22,7 +22,7 @@ use solitaire_data::{ TOOLTIP_DELAY_STEP_SECS, }; -use crate::events::{ManualSyncRequestEvent, ToggleSettingsRequestEvent}; +use crate::events::{InfoToastEvent, ManualSyncRequestEvent, ToggleSettingsRequestEvent}; use crate::font_plugin::FontResource; use crate::progress_plugin::ProgressResource; use crate::resources::{SettingsScrollPos, SyncStatus, SyncStatusResource}; @@ -296,6 +296,7 @@ impl Plugin for SettingsPlugin { .add_message::() .add_message::() .add_message::() + .add_message::() .add_message::() // `WindowResized` / `WindowMoved` are real Bevy window events // and emitted by the windowing backend under `DefaultPlugins`, @@ -383,6 +384,7 @@ fn handle_volume_keys( mut settings: ResMut, path: Res, mut changed: MessageWriter, + mut toast: MessageWriter, ) { let mut delta = 0.0_f32; if keys.just_pressed(KeyCode::BracketLeft) { @@ -401,6 +403,10 @@ fn handle_volume_keys( } persist(&path, &settings.0); changed.write(SettingsChangedEvent(settings.0.clone())); + toast.write(InfoToastEvent(format!( + "SFX volume: {}%", + (after * 100.0).round() as i32 + ))); } /// Opens or closes the Settings panel — `O` keyboard accelerator or