pub const REGENERATION_SECONDS: u64 = 432_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Manabar {
pub current_mana: i64,
pub last_update_time: u64,
}
impl Manabar {
pub fn current(&self, max_mana: i64, now: u64) -> i64 {
if max_mana <= 0 {
return 0;
}
let elapsed = now.saturating_sub(self.last_update_time);
let regenerated = (i128::from(elapsed.min(REGENERATION_SECONDS)) * i128::from(max_mana)
/ i128::from(REGENERATION_SECONDS)) as i64;
self.current_mana
.saturating_add(regenerated)
.clamp(0, max_mana)
}
pub fn percentage(&self, max_mana: i64, now: u64) -> f64 {
if max_mana <= 0 {
return 0.0;
}
let current = i128::from(self.current(max_mana, now));
(current * 100) as f64 / max_mana as f64
}
pub fn seconds_until_full(&self, max_mana: i64, now: u64) -> Option<u64> {
let current = self.current(max_mana, now);
if current >= max_mana || max_mana <= 0 {
return None;
}
let missing = i128::from(max_mana - current);
Some((missing * i128::from(REGENERATION_SECONDS) / i128::from(max_mana)) as u64)
}
pub fn seconds_until(&self, max_mana: i64, now: u64, target_percent: f64) -> Option<u64> {
if max_mana <= 0 || !(0.0..=100.0).contains(&target_percent) {
return None;
}
let target = (max_mana as f64 * target_percent / 100.0) as i64;
let current = self.current(max_mana, now);
if current >= target {
return None;
}
let missing = i128::from(target - current);
Some((missing * i128::from(REGENERATION_SECONDS) / i128::from(max_mana)) as u64)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_full_bar_stays_full() {
let bar = Manabar {
current_mana: 1000,
last_update_time: 0,
};
assert_eq!(bar.current(1000, 0), 1000);
assert_eq!(bar.current(1000, REGENERATION_SECONDS * 10), 1000);
assert_eq!(bar.percentage(1000, 0), 100.0);
assert_eq!(bar.seconds_until_full(1000, 0), None);
}
#[test]
fn an_empty_bar_refills_linearly_over_five_days() {
let bar = Manabar {
current_mana: 0,
last_update_time: 0,
};
assert_eq!(bar.current(1000, 0), 0);
assert_eq!(bar.current(1000, REGENERATION_SECONDS / 2), 500);
assert_eq!(bar.current(1000, REGENERATION_SECONDS), 1000);
assert_eq!(bar.seconds_until_full(1000, 0), Some(REGENERATION_SECONDS));
}
#[test]
fn refilling_never_exceeds_the_maximum() {
let bar = Manabar {
current_mana: 900,
last_update_time: 0,
};
assert_eq!(bar.current(1000, REGENERATION_SECONDS), 1000);
assert_eq!(bar.current(1000, REGENERATION_SECONDS * 100), 1000);
}
#[test]
fn does_not_overflow_on_a_realistic_vests_balance() {
let max: i64 = 314_566_314_850_000;
let bar = Manabar {
current_mana: max / 2,
last_update_time: 0,
};
let pct = bar.percentage(max, 0);
assert!((pct - 50.0).abs() < 0.001, "got {pct}");
assert_eq!(bar.current(max, 0), max / 2);
let huge = i64::MAX / 2;
let bar = Manabar {
current_mana: huge / 2,
last_update_time: 0,
};
let pct = bar.percentage(huge, 0);
assert!((pct - 50.0).abs() < 0.001, "got {pct}");
}
#[test]
fn a_clock_that_goes_backwards_does_not_drain_the_bar() {
let bar = Manabar {
current_mana: 500,
last_update_time: 1000,
};
assert_eq!(bar.current(1000, 0), 500, "no negative regeneration");
}
#[test]
fn seconds_until_a_target_percentage() {
let bar = Manabar {
current_mana: 0,
last_update_time: 0,
};
assert_eq!(
bar.seconds_until(1000, 0, 50.0),
Some(REGENERATION_SECONDS / 2)
);
assert_eq!(
bar.seconds_until(1000, 0, 100.0),
Some(REGENERATION_SECONDS)
);
let full = Manabar {
current_mana: 1000,
last_update_time: 0,
};
assert_eq!(full.seconds_until(1000, 0, 50.0), None);
assert_eq!(bar.seconds_until(1000, 0, 101.0), None);
assert_eq!(bar.seconds_until(1000, 0, -1.0), None);
}
#[test]
fn a_zero_maximum_is_handled_rather_than_dividing_by_zero() {
let bar = Manabar {
current_mana: 0,
last_update_time: 0,
};
assert_eq!(bar.current(0, 100), 0);
assert_eq!(bar.percentage(0, 100), 0.0);
assert_eq!(bar.seconds_until_full(0, 100), None);
}
#[test]
fn parses_the_shape_the_api_sends() {
let bar: Manabar = serde_json::from_str(
r#"{"current_mana": 314566314850, "last_update_time": 1754586540}"#,
)
.unwrap();
assert_eq!(bar.current_mana, 314_566_314_850);
assert_eq!(bar.last_update_time, 1_754_586_540);
}
}