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:
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user