hypixel-sdk 0.2.1

Async client for the Hypixel Public API with typed models and SkyBlock helpers
Documentation
use serde::Serialize;

use crate::models::resources::{Skill, SkillLevel};

/// The resolved level and in-level progress for a given amount of experience.
#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
pub struct LevelProgress {
    /// The whole level reached.
    pub level: u32,
    /// Total experience supplied.
    pub total_xp: f64,
    /// Experience accumulated past the start of the current level.
    pub xp_into_level: f64,
    /// Experience required to advance from the current level to the next, if any.
    pub xp_to_next: Option<f64>,
    /// The level expressed as a fraction (e.g. `12.5` is halfway through level 12).
    pub fractional_level: f64,
}

/// Resolve experience into a [`LevelProgress`] against a table of cumulative
/// experience thresholds.
///
/// `cumulative` must be sorted ascending and hold the *total* experience
/// required to reach each successive level (the value at index `i` is the cost
/// of reaching level `i + 1`).
pub fn level_from_cumulative(total_xp: f64, cumulative: &[f64]) -> LevelProgress {
    let mut level = 0u32;
    let mut level_start = 0.0;
    for &threshold in cumulative {
        if total_xp < threshold {
            let span = threshold - level_start;
            let into = total_xp - level_start;
            let fractional = if span > 0.0 { into / span } else { 0.0 };
            return LevelProgress {
                level,
                total_xp,
                xp_into_level: into,
                xp_to_next: Some(threshold - total_xp),
                fractional_level: level as f64 + fractional,
            };
        }
        level += 1;
        level_start = threshold;
    }
    LevelProgress {
        level,
        total_xp,
        xp_into_level: total_xp - level_start,
        xp_to_next: None,
        fractional_level: level as f64,
    }
}

/// Build a sorted cumulative threshold table from a skill's level definitions.
pub fn cumulative_from_levels(levels: &[SkillLevel]) -> Vec<f64> {
    let mut thresholds: Vec<f64> = levels.iter().map(|l| l.total_exp_required).collect();
    thresholds.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
    thresholds
}

/// Resolve experience into a [`LevelProgress`] using a [`Skill`] definition from
/// the `resources/skyblock/skills` endpoint.
pub fn skill_level(total_xp: f64, skill: &Skill) -> LevelProgress {
    let cumulative = cumulative_from_levels(&skill.levels);
    level_from_cumulative(total_xp, &cumulative)
}

#[cfg(test)]
mod tests {
    use super::*;

    fn table() -> Vec<f64> {
        vec![100.0, 250.0, 500.0]
    }

    #[test]
    fn resolves_mid_level() {
        let p = level_from_cumulative(175.0, &table());
        assert_eq!(p.level, 1);
        assert_eq!(p.xp_into_level, 75.0);
        assert_eq!(p.xp_to_next, Some(75.0));
        assert_eq!(p.fractional_level, 1.5);
    }

    #[test]
    fn caps_at_max_level() {
        let p = level_from_cumulative(900.0, &table());
        assert_eq!(p.level, 3);
        assert_eq!(p.xp_to_next, None);
        assert_eq!(p.fractional_level, 3.0);
    }

    #[test]
    fn handles_zero() {
        let p = level_from_cumulative(0.0, &table());
        assert_eq!(p.level, 0);
        assert_eq!(p.fractional_level, 0.0);
    }
}