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