From 3d5f34a65011b95351f7fb29aa87b8bb22a45fdb Mon Sep 17 00:00:00 2001 From: root Date: Mon, 27 Apr 2026 02:14:54 +0000 Subject: [PATCH] feat(engine): stats overlay shows XP progress to next level Adds "Next Level: N XP (P%)" line so players can see how far they are through the current level without doing the arithmetic themselves. Tested with three unit tests covering level 0, mid-level, and level 10. Co-Authored-By: Claude Sonnet 4.6 --- solitaire_engine/src/stats_plugin.rs | 40 ++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/solitaire_engine/src/stats_plugin.rs b/solitaire_engine/src/stats_plugin.rs index 976e973..88114a5 100644 --- a/solitaire_engine/src/stats_plugin.rs +++ b/solitaire_engine/src/stats_plugin.rs @@ -191,6 +191,7 @@ fn spawn_stats_screen( lines.push("=== Progression ===".to_string()); lines.push(format!("Level: {}", p.level)); lines.push(format!("Total XP: {}", p.total_xp)); + lines.push(format!("Next Level: {}", xp_to_next_level_label(p.total_xp, p.level))); lines.push(format!( "Daily Streak: {}", p.daily_challenge_streak @@ -270,6 +271,25 @@ fn spawn_stats_screen( }); } +/// Returns XP remaining until next level, formatted as "N XP (P%)". +fn xp_to_next_level_label(total_xp: u64, level: u32) -> String { + let xp_current = if level < 10 { + level as u64 * 500 + } else { + 5_000 + (level as u64 - 10) * 1_000 + }; + let xp_next = if level < 10 { + (level as u64 + 1) * 500 + } else { + 5_000 + (level as u64 - 9) * 1_000 + }; + let span = xp_next - xp_current; + let done = total_xp.saturating_sub(xp_current).min(span); + let pct = if span == 0 { 100 } else { done.saturating_mul(100).checked_div(span).unwrap_or(100) }; + let remaining = span - done; + format!("{remaining} XP ({pct}%)") +} + fn format_duration(secs: u64) -> String { let m = secs / 60; let s = secs % 60; @@ -434,4 +454,24 @@ mod tests { fn format_id_list_sorts_dedups_and_prefixes() { assert_eq!(format_id_list(&[3, 1, 1, 2]), "#1, #2, #3"); } + + #[test] + fn xp_to_next_level_label_at_zero_xp() { + // Level 0, 0 XP: 500 needed, 0% done. + assert_eq!(xp_to_next_level_label(0, 0), "500 XP (0%)"); + } + + #[test] + fn xp_to_next_level_label_halfway_through_level_1() { + // Level 1 starts at 500 XP, level 2 at 1000 XP. + // At 750 XP: 250 done of 500, 50%, 250 remaining. + assert_eq!(xp_to_next_level_label(750, 1), "250 XP (50%)"); + } + + #[test] + fn xp_to_next_level_label_at_level_10_boundary() { + // Level 10 starts at 5000 XP, level 11 at 6000 XP. + // At 5000 XP: 0 done, 0%, 1000 remaining. + assert_eq!(xp_to_next_level_label(5_000, 10), "1000 XP (0%)"); + } }