feat(engine): Today's Event callout on the Home Daily card
Phase B step 1 of the MSSC-inspired Home rework — surfaces today's
daily-challenge metadata on the Daily card so the picker reads as
"there's something fresh waiting" rather than a generic mode label.
- Date line "Today, May 6" pulled from DailyChallengeResource. Reads
in STATE_INFO blue while the run is still open.
- Server-fetched goal (when SyncPlugin is wired) appears underneath
as "Goal: Win in under 5 minutes", matching the toast that already
fires when the player presses C.
- Once the player has recorded today's completion, the date flips
to "Today, May 6 \u{2022} Done" in ACCENT_PRIMARY so the picker
reads as a reward state rather than a TODO.
Headless tests omit DailyChallengePlugin, so HomeContext.daily_today
defaults to None and the card falls back to its baseline layout.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@ use solitaire_core::game_state::DrawMode;
|
|||||||
use solitaire_data::save_settings_to;
|
use solitaire_data::save_settings_to;
|
||||||
|
|
||||||
use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL;
|
use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL;
|
||||||
|
use crate::daily_challenge_plugin::DailyChallengeResource;
|
||||||
use crate::events::{
|
use crate::events::{
|
||||||
InfoToastEvent, NewGameRequestEvent, StartChallengeRequestEvent,
|
InfoToastEvent, NewGameRequestEvent, StartChallengeRequestEvent,
|
||||||
StartDailyChallengeRequestEvent, StartTimeAttackRequestEvent, StartZenRequestEvent,
|
StartDailyChallengeRequestEvent, StartTimeAttackRequestEvent, StartZenRequestEvent,
|
||||||
@@ -258,6 +259,7 @@ fn spawn_home_on_launch(
|
|||||||
progress: Option<Res<ProgressResource>>,
|
progress: Option<Res<ProgressResource>>,
|
||||||
stats: Option<Res<StatsResource>>,
|
stats: Option<Res<StatsResource>>,
|
||||||
settings: Option<Res<SettingsResource>>,
|
settings: Option<Res<SettingsResource>>,
|
||||||
|
daily: Option<Res<DailyChallengeResource>>,
|
||||||
font_res: Option<Res<FontResource>>,
|
font_res: Option<Res<FontResource>>,
|
||||||
) {
|
) {
|
||||||
if shown.0
|
if shown.0
|
||||||
@@ -275,6 +277,7 @@ fn spawn_home_on_launch(
|
|||||||
progress.as_deref(),
|
progress.as_deref(),
|
||||||
stats.as_deref(),
|
stats.as_deref(),
|
||||||
settings.as_deref(),
|
settings.as_deref(),
|
||||||
|
daily.as_deref(),
|
||||||
font_res.as_deref(),
|
font_res.as_deref(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -285,12 +288,14 @@ fn spawn_home_on_launch(
|
|||||||
// M-key toggle
|
// M-key toggle
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn toggle_home_screen(
|
fn toggle_home_screen(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
keys: Res<ButtonInput<KeyCode>>,
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
progress: Option<Res<ProgressResource>>,
|
progress: Option<Res<ProgressResource>>,
|
||||||
stats: Option<Res<StatsResource>>,
|
stats: Option<Res<StatsResource>>,
|
||||||
settings: Option<Res<SettingsResource>>,
|
settings: Option<Res<SettingsResource>>,
|
||||||
|
daily: Option<Res<DailyChallengeResource>>,
|
||||||
font_res: Option<Res<FontResource>>,
|
font_res: Option<Res<FontResource>>,
|
||||||
screens: Query<Entity, With<HomeScreen>>,
|
screens: Query<Entity, With<HomeScreen>>,
|
||||||
) {
|
) {
|
||||||
@@ -306,6 +311,7 @@ fn toggle_home_screen(
|
|||||||
progress.as_deref(),
|
progress.as_deref(),
|
||||||
stats.as_deref(),
|
stats.as_deref(),
|
||||||
settings.as_deref(),
|
settings.as_deref(),
|
||||||
|
daily.as_deref(),
|
||||||
font_res.as_deref(),
|
font_res.as_deref(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -320,8 +326,20 @@ fn build_home_context<'a>(
|
|||||||
progress: Option<&ProgressResource>,
|
progress: Option<&ProgressResource>,
|
||||||
stats: Option<&StatsResource>,
|
stats: Option<&StatsResource>,
|
||||||
settings: Option<&SettingsResource>,
|
settings: Option<&SettingsResource>,
|
||||||
|
daily: Option<&DailyChallengeResource>,
|
||||||
font_res: Option<&'a FontResource>,
|
font_res: Option<&'a FontResource>,
|
||||||
) -> HomeContext<'a> {
|
) -> HomeContext<'a> {
|
||||||
|
let daily_today = daily.map(|d| {
|
||||||
|
let completed_today = progress
|
||||||
|
.and_then(|p| p.0.daily_challenge_last_completed)
|
||||||
|
.is_some_and(|d_last| d_last == d.date);
|
||||||
|
DailyToday {
|
||||||
|
date_label: d.date.format("%b %-d").to_string(),
|
||||||
|
goal: d.goal_description.clone(),
|
||||||
|
completed_today,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
HomeContext {
|
HomeContext {
|
||||||
level: progress.map_or(0, |p| p.0.level),
|
level: progress.map_or(0, |p| p.0.level),
|
||||||
total_xp: progress.map_or(0, |p| p.0.total_xp),
|
total_xp: progress.map_or(0, |p| p.0.total_xp),
|
||||||
@@ -330,6 +348,7 @@ fn build_home_context<'a>(
|
|||||||
classic_best: stats.map_or(0, |s| s.0.classic_best_score),
|
classic_best: stats.map_or(0, |s| s.0.classic_best_score),
|
||||||
zen_best: stats.map_or(0, |s| s.0.zen_best_score),
|
zen_best: stats.map_or(0, |s| s.0.zen_best_score),
|
||||||
challenge_best: stats.map_or(0, |s| s.0.challenge_best_score),
|
challenge_best: stats.map_or(0, |s| s.0.challenge_best_score),
|
||||||
|
daily_today,
|
||||||
draw_mode: settings
|
draw_mode: settings
|
||||||
.map(|s| s.0.draw_mode.clone())
|
.map(|s| s.0.draw_mode.clone())
|
||||||
.unwrap_or(DrawMode::DrawOne),
|
.unwrap_or(DrawMode::DrawOne),
|
||||||
@@ -458,6 +477,7 @@ fn handle_home_draw_mode_buttons(
|
|||||||
mut changed: MessageWriter<SettingsChangedEvent>,
|
mut changed: MessageWriter<SettingsChangedEvent>,
|
||||||
progress: Option<Res<ProgressResource>>,
|
progress: Option<Res<ProgressResource>>,
|
||||||
stats: Option<Res<StatsResource>>,
|
stats: Option<Res<StatsResource>>,
|
||||||
|
daily: Option<Res<DailyChallengeResource>>,
|
||||||
font_res: Option<Res<FontResource>>,
|
font_res: Option<Res<FontResource>>,
|
||||||
) {
|
) {
|
||||||
if screens.is_empty() {
|
if screens.is_empty() {
|
||||||
@@ -500,6 +520,7 @@ fn handle_home_draw_mode_buttons(
|
|||||||
progress.as_deref(),
|
progress.as_deref(),
|
||||||
stats.as_deref(),
|
stats.as_deref(),
|
||||||
Some(settings),
|
Some(settings),
|
||||||
|
daily.as_deref(),
|
||||||
font_res.as_deref(),
|
font_res.as_deref(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -615,10 +636,28 @@ struct HomeContext<'a> {
|
|||||||
zen_best: u32,
|
zen_best: u32,
|
||||||
challenge_best: u32,
|
challenge_best: u32,
|
||||||
daily_streak: u32,
|
daily_streak: u32,
|
||||||
|
daily_today: Option<DailyToday>,
|
||||||
draw_mode: DrawMode,
|
draw_mode: DrawMode,
|
||||||
font_res: Option<&'a FontResource>,
|
font_res: Option<&'a FontResource>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Today's daily-challenge metadata as the Home picker needs it. Only
|
||||||
|
/// populated when both [`DailyChallengeResource`] is present (the
|
||||||
|
/// plugin is wired) and we have something useful to show — otherwise
|
||||||
|
/// the Daily card falls back to its baseline description without a
|
||||||
|
/// dated callout.
|
||||||
|
struct DailyToday {
|
||||||
|
/// Short calendar label, e.g. `"May 6"`. Always populated.
|
||||||
|
date_label: String,
|
||||||
|
/// Server-supplied goal copy ("Win in under 5 minutes"). `None`
|
||||||
|
/// when no server backend is wired or the fetch hasn't returned.
|
||||||
|
goal: Option<String>,
|
||||||
|
/// `true` when the player has already recorded today's daily.
|
||||||
|
/// Surfaces a "Done" badge so the picker reads as reward-state
|
||||||
|
/// rather than "you still owe today's run".
|
||||||
|
completed_today: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// Spawns the Home modal with the player-stats header strip, draw-mode
|
/// Spawns the Home modal with the player-stats header strip, draw-mode
|
||||||
/// row, five mode cards, and a Cancel button.
|
/// row, five mode cards, and a Cancel button.
|
||||||
fn spawn_home_screen(commands: &mut Commands, ctx: HomeContext<'_>) {
|
fn spawn_home_screen(commands: &mut Commands, ctx: HomeContext<'_>) {
|
||||||
@@ -1029,6 +1068,42 @@ fn spawn_mode_card(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Daily-only "Today's Event" caption — date, optional
|
||||||
|
// server goal, and a "Done" badge once the player has
|
||||||
|
// already recorded today's completion. Only renders for
|
||||||
|
// the Daily card when DailyChallengeResource is present.
|
||||||
|
if matches!(mode, HomeMode::Daily)
|
||||||
|
&& unlocked
|
||||||
|
&& let Some(today) = ctx.daily_today.as_ref()
|
||||||
|
{
|
||||||
|
let date_text = if today.completed_today {
|
||||||
|
format!("Today, {} \u{2022} Done", today.date_label)
|
||||||
|
} else {
|
||||||
|
format!("Today, {}", today.date_label)
|
||||||
|
};
|
||||||
|
let date_color = if today.completed_today {
|
||||||
|
ACCENT_PRIMARY
|
||||||
|
} else {
|
||||||
|
STATE_INFO
|
||||||
|
};
|
||||||
|
c.spawn((
|
||||||
|
Text::new(date_text),
|
||||||
|
font_chip.clone(),
|
||||||
|
TextColor(date_color),
|
||||||
|
Node {
|
||||||
|
margin: UiRect::top(VAL_SPACE_1),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
));
|
||||||
|
if let Some(goal) = today.goal.as_ref() {
|
||||||
|
c.spawn((
|
||||||
|
Text::new(format!("Goal: {goal}")),
|
||||||
|
font_chip.clone(),
|
||||||
|
TextColor(TEXT_SECONDARY),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Locked footnote — explicit copy so the gate is unambiguous.
|
// Locked footnote — explicit copy so the gate is unambiguous.
|
||||||
if !unlocked {
|
if !unlocked {
|
||||||
c.spawn((
|
c.spawn((
|
||||||
|
|||||||
Reference in New Issue
Block a user