Skip to main content

hypixel/util/
leveling.rs

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