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) <noreply@anthropic.com>
This commit is contained in:
funman300
2026-05-06 06:16:37 +00:00
parent cc635328be
commit 56e2e6f151
3 changed files with 33 additions and 2 deletions
@@ -474,9 +474,29 @@ fn spawn_achievements_screen(
..default() ..default()
}; };
let any_unlocked = records.iter().any(|r| r.unlocked);
let scrim = spawn_modal(commands, AchievementsScreen, Z_MODAL_PANEL, |card| { let scrim = spawn_modal(commands, AchievementsScreen, Z_MODAL_PANEL, |card| {
spawn_modal_header(card, header, font_res); 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 // Scrollable body — the achievements list grows to ~19 rows which
// overflows the modal on the 800x600 minimum window. Wrapping the // overflows the modal on the 800x600 minimum window. Wrapping the
// row list in an `Overflow::scroll_y()` Node with a constrained // row list in an `Overflow::scroll_y()` Node with a constrained
+6 -1
View File
@@ -501,7 +501,12 @@ fn spawn_leaderboard_screen(
} }
LeaderboardResource::Loaded(rows) if rows.is_empty() => { LeaderboardResource::Loaded(rows) if rows.is_empty() => {
body.spawn(( 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(), font_row.clone(),
TextColor(TEXT_SECONDARY), TextColor(TEXT_SECONDARY),
)); ));
+7 -1
View File
@@ -22,7 +22,7 @@ use solitaire_data::{
TOOLTIP_DELAY_STEP_SECS, TOOLTIP_DELAY_STEP_SECS,
}; };
use crate::events::{ManualSyncRequestEvent, ToggleSettingsRequestEvent}; use crate::events::{InfoToastEvent, ManualSyncRequestEvent, ToggleSettingsRequestEvent};
use crate::font_plugin::FontResource; use crate::font_plugin::FontResource;
use crate::progress_plugin::ProgressResource; use crate::progress_plugin::ProgressResource;
use crate::resources::{SettingsScrollPos, SyncStatus, SyncStatusResource}; use crate::resources::{SettingsScrollPos, SyncStatus, SyncStatusResource};
@@ -296,6 +296,7 @@ impl Plugin for SettingsPlugin {
.add_message::<SettingsChangedEvent>() .add_message::<SettingsChangedEvent>()
.add_message::<ManualSyncRequestEvent>() .add_message::<ManualSyncRequestEvent>()
.add_message::<ToggleSettingsRequestEvent>() .add_message::<ToggleSettingsRequestEvent>()
.add_message::<InfoToastEvent>()
.add_message::<bevy::input::mouse::MouseWheel>() .add_message::<bevy::input::mouse::MouseWheel>()
// `WindowResized` / `WindowMoved` are real Bevy window events // `WindowResized` / `WindowMoved` are real Bevy window events
// and emitted by the windowing backend under `DefaultPlugins`, // and emitted by the windowing backend under `DefaultPlugins`,
@@ -383,6 +384,7 @@ fn handle_volume_keys(
mut settings: ResMut<SettingsResource>, mut settings: ResMut<SettingsResource>,
path: Res<SettingsStoragePath>, path: Res<SettingsStoragePath>,
mut changed: MessageWriter<SettingsChangedEvent>, mut changed: MessageWriter<SettingsChangedEvent>,
mut toast: MessageWriter<InfoToastEvent>,
) { ) {
let mut delta = 0.0_f32; let mut delta = 0.0_f32;
if keys.just_pressed(KeyCode::BracketLeft) { if keys.just_pressed(KeyCode::BracketLeft) {
@@ -401,6 +403,10 @@ fn handle_volume_keys(
} }
persist(&path, &settings.0); persist(&path, &settings.0);
changed.write(SettingsChangedEvent(settings.0.clone())); 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 /// Opens or closes the Settings panel — `O` keyboard accelerator or