Skip to main content

hypixel/util/
leveling.rs

1use serde::Serialize;
2
3use crate::models::resources::{Skill, SkillLevel};
4
5/// The resolved level and in-level progress for a given amount of experience.
6#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
7pub struct LevelProgress {
8    /// The whole level reached.
9    pub level: u32,
10    /// Total experience supplied.
11    pub total_xp: f64,
12    /// Experience accumulated past the start of the current level.
13    pub xp_into_level: f64,
14    /// Experience required to advance from the current level to the next, if any.
15    pub xp_to_next: Option<f64>,
16    /// The level expressed as a fraction (e.g. `12.5` is halfway through level 12).
17    pub fractional_level: f64,
18}
19
20/// Resolve experience into a [`LevelProgress`] against a table of cumulative
21/// experience thresholds.
22///
23/// `cumulative` must be sorted ascending and hold the *total* experience
24/// required to reach each successive level (the value at index `i` is the cost
25/// of reaching level `i + 1`).
26pub fn level_from_cumulative(total_xp: f64, cumulative: &[f64]) -> LevelProgress {
27    let mut level = 0u32;
28    let mut level_start = 0.0;
29    for &threshold in cumulative {
30        if total_xp < threshold {
31            let span = threshold - level_start;
32            let into = total_xp - level_start;
33            let fractional = if span > 0.0 { into / span } else { 0.0 };
34            return LevelProgress {
35                level,
36                total_xp,
37                xp_into_level: into,
38                xp_to_next: Some(threshold - total_xp),
39                fractional_level: level as f64 + fractional,
40            };
41        }
42        level += 1;
43        level_start = threshold;
44    }
45    LevelProgress {
46        level,
47        total_xp,
48        xp_into_level: total_xp - level_start,
49        xp_to_next: None,
50        fractional_level: level as f64,
51    }
52}
53
54/// Build a sorted cumulative threshold table from a skill's level definitions.
55pub fn cumulative_from_levels(levels: &[SkillLevel]) -> Vec<f64> {
56    let mut thresholds: Vec<f64> = levels.iter().map(|l| l.total_exp_required).collect();
57    thresholds.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
58    thresholds
59}
60
61/// Resolve experience into a [`LevelProgress`] using a [`Skill`] definition from
62/// the `resources/skyblock/skills` endpoint.
63pub fn skill_level(total_xp: f64, skill: &Skill) -> LevelProgress {
64    let cumulative = cumulative_from_levels(&skill.levels);
65    level_from_cumulative(total_xp, &cumulative)
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    fn table() -> Vec<f64> {
73        vec![100.0, 250.0, 500.0]
74    }
75
76    #[test]
77    fn resolves_mid_level() {
78        let p = level_from_cumulative(175.0, &table());
79        assert_eq!(p.level, 1);
80        assert_eq!(p.xp_into_level, 75.0);
81        assert_eq!(p.xp_to_next, Some(75.0));
82        assert_eq!(p.fractional_level, 1.5);
83    }
84
85    #[test]
86    fn caps_at_max_level() {
87        let p = level_from_cumulative(900.0, &table());
88        assert_eq!(p.level, 3);
89        assert_eq!(p.xp_to_next, None);
90        assert_eq!(p.fractional_level, 3.0);
91    }
92
93    #[test]
94    fn handles_zero() {
95        let p = level_from_cumulative(0.0, &table());
96        assert_eq!(p.level, 0);
97        assert_eq!(p.fractional_level, 0.0);
98    }
99}