feat(engine): surface daily/weekly completions as toasts + progression panel

Phase 6 part 3 (partial):
- AnimationPlugin now shows a 3-second toast on DailyChallengeCompletedEvent
  and WeeklyGoalCompletedEvent.
- Stats overlay (S key) appends a Progression section with level, total XP,
  daily streak, and a live Weekly Goals list pulling from WEEKLY_GOALS.

Special modes (Time Attack / Challenge / Zen) and unlock UI deferred.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-04-24 19:28:13 -07:00
parent 0609d4eef3
commit 363ddc9b75
2 changed files with 76 additions and 6 deletions
+34
View File
@@ -7,10 +7,12 @@ use bevy::prelude::*;
use crate::achievement_plugin::display_name_for; use crate::achievement_plugin::display_name_for;
use crate::card_plugin::CardEntity; use crate::card_plugin::CardEntity;
use crate::daily_challenge_plugin::DailyChallengeCompletedEvent;
use crate::events::{AchievementUnlockedEvent, GameWonEvent}; use crate::events::{AchievementUnlockedEvent, GameWonEvent};
use crate::game_plugin::GameMutation; use crate::game_plugin::GameMutation;
use crate::layout::LayoutResource; use crate::layout::LayoutResource;
use crate::progress_plugin::LevelUpEvent; use crate::progress_plugin::LevelUpEvent;
use crate::weekly_goals_plugin::WeeklyGoalCompletedEvent;
/// Duration of a card slide (move) animation in seconds. /// Duration of a card slide (move) animation in seconds.
pub const SLIDE_SECS: f32 = 0.15; pub const SLIDE_SECS: f32 = 0.15;
@@ -18,6 +20,8 @@ pub const SLIDE_SECS: f32 = 0.15;
const WIN_TOAST_SECS: f32 = 4.0; const WIN_TOAST_SECS: f32 = 4.0;
const ACHIEVEMENT_TOAST_SECS: f32 = 3.0; const ACHIEVEMENT_TOAST_SECS: f32 = 3.0;
const LEVELUP_TOAST_SECS: f32 = 3.0; const LEVELUP_TOAST_SECS: f32 = 3.0;
const DAILY_TOAST_SECS: f32 = 3.0;
const WEEKLY_TOAST_SECS: f32 = 3.0;
const CASCADE_STAGGER: f32 = 0.05; const CASCADE_STAGGER: f32 = 0.05;
const CASCADE_DURATION: f32 = 0.5; const CASCADE_DURATION: f32 = 0.5;
@@ -53,6 +57,8 @@ impl Plugin for AnimationPlugin {
app.add_event::<GameWonEvent>() app.add_event::<GameWonEvent>()
.add_event::<AchievementUnlockedEvent>() .add_event::<AchievementUnlockedEvent>()
.add_event::<LevelUpEvent>() .add_event::<LevelUpEvent>()
.add_event::<DailyChallengeCompletedEvent>()
.add_event::<WeeklyGoalCompletedEvent>()
.add_systems( .add_systems(
Update, Update,
( (
@@ -60,6 +66,8 @@ impl Plugin for AnimationPlugin {
handle_win_cascade, handle_win_cascade,
handle_achievement_toast, handle_achievement_toast,
handle_levelup_toast, handle_levelup_toast,
handle_daily_toast,
handle_weekly_toast,
tick_toasts, tick_toasts,
) )
.after(GameMutation), .after(GameMutation),
@@ -147,6 +155,32 @@ fn handle_levelup_toast(mut commands: Commands, mut events: EventReader<LevelUpE
} }
} }
fn handle_daily_toast(
mut commands: Commands,
mut events: EventReader<DailyChallengeCompletedEvent>,
) {
for ev in events.read() {
spawn_toast(
&mut commands,
format!("Daily Challenge Complete! (Streak: {})", ev.streak),
DAILY_TOAST_SECS,
);
}
}
fn handle_weekly_toast(
mut commands: Commands,
mut events: EventReader<WeeklyGoalCompletedEvent>,
) {
for ev in events.read() {
spawn_toast(
&mut commands,
format!("Weekly Goal: {}", ev.description),
WEEKLY_TOAST_SECS,
);
}
}
fn tick_toasts( fn tick_toasts(
mut commands: Commands, mut commands: Commands,
time: Res<Time>, time: Res<Time>,
+42 -6
View File
@@ -10,10 +10,13 @@ use std::path::PathBuf;
use bevy::input::ButtonInput; use bevy::input::ButtonInput;
use bevy::prelude::*; use bevy::prelude::*;
use solitaire_data::{load_stats_from, save_stats_to, stats_file_path, StatsSnapshot}; use solitaire_data::{
load_stats_from, save_stats_to, stats_file_path, PlayerProgress, StatsSnapshot, WEEKLY_GOALS,
};
use crate::events::{GameWonEvent, NewGameRequestEvent}; use crate::events::{GameWonEvent, NewGameRequestEvent};
use crate::game_plugin::GameMutation; use crate::game_plugin::GameMutation;
use crate::progress_plugin::ProgressResource;
use crate::resources::GameStateResource; use crate::resources::GameStateResource;
/// Bevy resource wrapping the current stats. /// Bevy resource wrapping the current stats.
@@ -123,6 +126,7 @@ fn toggle_stats_screen(
mut commands: Commands, mut commands: Commands,
keys: Res<ButtonInput<KeyCode>>, keys: Res<ButtonInput<KeyCode>>,
stats: Res<StatsResource>, stats: Res<StatsResource>,
progress: Option<Res<ProgressResource>>,
screens: Query<Entity, With<StatsScreen>>, screens: Query<Entity, With<StatsScreen>>,
) { ) {
if !keys.just_pressed(KeyCode::KeyS) { if !keys.just_pressed(KeyCode::KeyS) {
@@ -131,11 +135,15 @@ fn toggle_stats_screen(
if let Ok(entity) = screens.get_single() { if let Ok(entity) = screens.get_single() {
commands.entity(entity).despawn_recursive(); commands.entity(entity).despawn_recursive();
} else { } else {
spawn_stats_screen(&mut commands, &stats.0); spawn_stats_screen(&mut commands, &stats.0, progress.as_deref().map(|p| &p.0));
} }
} }
fn spawn_stats_screen(commands: &mut Commands, stats: &StatsSnapshot) { fn spawn_stats_screen(
commands: &mut Commands,
stats: &StatsSnapshot,
progress: Option<&PlayerProgress>,
) {
let win_rate = stats let win_rate = stats
.win_rate() .win_rate()
.map_or("N/A".to_string(), |r| format!("{r:.1}%")); .map_or("N/A".to_string(), |r| format!("{r:.1}%"));
@@ -150,7 +158,7 @@ fn spawn_stats_screen(commands: &mut Commands, stats: &StatsSnapshot) {
format_duration(stats.avg_time_seconds) format_duration(stats.avg_time_seconds)
}; };
let lines = [ let mut lines: Vec<String> = vec![
"=== Statistics ===".to_string(), "=== Statistics ===".to_string(),
format!("Games Played: {}", stats.games_played), format!("Games Played: {}", stats.games_played),
format!("Games Won: {}", stats.games_won), format!("Games Won: {}", stats.games_won),
@@ -162,10 +170,35 @@ fn spawn_stats_screen(commands: &mut Commands, stats: &StatsSnapshot) {
format!("Best Score: {}", stats.best_single_score), format!("Best Score: {}", stats.best_single_score),
format!("Fastest Win: {fastest}"), format!("Fastest Win: {fastest}"),
format!("Avg Win Time: {avg}"), format!("Avg Win Time: {avg}"),
String::new(),
"Press S to close".to_string(),
]; ];
if let Some(p) = progress {
lines.push(String::new());
lines.push("=== Progression ===".to_string());
lines.push(format!("Level: {}", p.level));
lines.push(format!("Total XP: {}", p.total_xp));
lines.push(format!(
"Daily Streak: {}",
p.daily_challenge_streak
));
lines.push(String::new());
lines.push("-- Weekly Goals --".to_string());
for goal in WEEKLY_GOALS {
let progress_value = p
.weekly_goal_progress
.get(goal.id)
.copied()
.unwrap_or(0);
lines.push(format!(
" {}: {}/{}",
goal.description, progress_value, goal.target
));
}
}
lines.push(String::new());
lines.push("Press S to close".to_string());
commands commands
.spawn(( .spawn((
StatsScreen, StatsScreen,
@@ -219,6 +252,9 @@ mod tests {
// MinimalPlugins doesn't register keyboard input — add it so the // MinimalPlugins doesn't register keyboard input — add it so the
// toggle system can read ButtonInput<KeyCode> in tests. // toggle system can read ButtonInput<KeyCode> in tests.
app.init_resource::<ButtonInput<KeyCode>>(); app.init_resource::<ButtonInput<KeyCode>>();
// ProgressResource is an optional dependency for the stats screen;
// include it so toggle tests exercise the progression panel.
app.add_plugins(crate::progress_plugin::ProgressPlugin::headless());
app.update(); app.update();
app app
} }