From bd488139008931899bd5fc0fbcc83fe6c3e46be7 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 27 Apr 2026 01:16:11 +0000 Subject: [PATCH] fix(engine): fire LevelUpEvent when weekly bonus XP crosses a level threshold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit evaluate_weekly_goals discarded the return value of add_xp(bonus_xp), so a level-up triggered by a weekly goal completion never fired LevelUpEvent — the level-up toast and mode-unlock at L5 were silently skipped. Now captures prev_level, checks leveled_up_from(), and sends LevelUpEvent matching the pattern used by progress_plugin. Co-Authored-By: Claude Sonnet 4.6 --- solitaire_engine/src/weekly_goals_plugin.rs | 41 ++++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/solitaire_engine/src/weekly_goals_plugin.rs b/solitaire_engine/src/weekly_goals_plugin.rs index 7c2eb29..2cc8a78 100644 --- a/solitaire_engine/src/weekly_goals_plugin.rs +++ b/solitaire_engine/src/weekly_goals_plugin.rs @@ -11,7 +11,7 @@ use solitaire_data::{ use crate::events::GameWonEvent; use crate::game_plugin::GameMutation; -use crate::progress_plugin::{ProgressResource, ProgressStoragePath, ProgressUpdate}; +use crate::progress_plugin::{LevelUpEvent, ProgressResource, ProgressStoragePath, ProgressUpdate}; use crate::resources::GameStateResource; /// Fired when the player has just completed a weekly goal. @@ -61,6 +61,7 @@ fn evaluate_weekly_goals( mut progress: ResMut, path: Res, mut completions: EventWriter, + mut levelups: EventWriter, ) { let mut events: Vec<&GameWonEvent> = wins.read().collect(); if events.is_empty() { @@ -97,7 +98,14 @@ fn evaluate_weekly_goals( } if bonus_xp > 0 { - progress.0.add_xp(bonus_xp); + let prev_level = progress.0.add_xp(bonus_xp); + if progress.0.leveled_up_from(prev_level) { + levelups.send(LevelUpEvent { + previous_level: prev_level, + new_level: progress.0.level, + total_xp: progress.0.total_xp, + }); + } } if any_change { @@ -246,6 +254,35 @@ mod tests { ); } + #[test] + fn weekly_bonus_xp_fires_levelup_when_threshold_crossed() { + let mut app = headless_app(); + // Set XP just below the first level boundary (500) so the 75-XP bonus crosses it. + app.world_mut().resource_mut::().0.total_xp = 430; + // Pre-set goal to 2/3 so the next fast win completes it. + app.world_mut() + .resource_mut::() + .0 + .weekly_goal_progress + .insert("weekly_3_fast".to_string(), 2); + let key = current_iso_week_key(Local::now().date_naive()); + app.world_mut() + .resource_mut::() + .0 + .weekly_goal_week_iso = Some(key); + + app.world_mut().send_event(GameWonEvent { + score: 500, + time_seconds: 60, + }); + app.update(); + + let events = app.world().resource::>(); + let mut cursor = events.get_cursor(); + let fired: Vec<_> = cursor.read(events).copied().collect(); + assert!(!fired.is_empty(), "LevelUpEvent must fire when weekly bonus pushes past a level threshold"); + } + #[test] fn weekly_goal_description_resolves_known_and_unknown() { assert_eq!(