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 <noreply@anthropic.com>
This commit is contained in:
root
2026-04-27 02:14:54 +00:00
parent 314186d6f4
commit 3d5f34a650
+40
View File
@@ -191,6 +191,7 @@ fn spawn_stats_screen(
lines.push("=== Progression ===".to_string()); lines.push("=== Progression ===".to_string());
lines.push(format!("Level: {}", p.level)); lines.push(format!("Level: {}", p.level));
lines.push(format!("Total XP: {}", p.total_xp)); 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!( lines.push(format!(
"Daily Streak: {}", "Daily Streak: {}",
p.daily_challenge_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 { fn format_duration(secs: u64) -> String {
let m = secs / 60; let m = secs / 60;
let s = secs % 60; let s = secs % 60;
@@ -434,4 +454,24 @@ mod tests {
fn format_id_list_sorts_dedups_and_prefixes() { fn format_id_list_sorts_dedups_and_prefixes() {
assert_eq!(format_id_list(&[3, 1, 1, 2]), "#1, #2, #3"); 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%)");
}
} }