use crate::{GameResult, gameerror::ValueError};
mod sealed {
pub trait Sealed {}
macro_rules! impl_sealed {
($($ty:ty),* $(,)?) => {
$(impl Sealed for $ty {})*
};
}
impl_sealed!(u8, u16, u32, u64, u128, usize);
}
pub trait MeteredValue: sealed::Sealed + Copy + Ord {
#[must_use]
fn saturating_add(self, rhs: Self) -> Self;
#[must_use]
fn saturating_sub(self, rhs: Self) -> Self;
fn as_f64(self) -> f64;
}
macro_rules! impl_metered_value {
($($ty:ty),* $(,)?) => {
$(
impl MeteredValue for $ty {
fn saturating_add(self, rhs: Self) -> Self {
self.saturating_add(rhs)
}
fn saturating_sub(self, rhs: Self) -> Self {
self.saturating_sub(rhs)
}
#[allow(clippy::cast_lossless)]
#[allow(clippy::cast_precision_loss)]
fn as_f64(self) -> f64 {
self as f64
}
}
)*
};
}
impl_metered_value!(u8, u16, u32, u64, u128, usize);
#[derive(Debug, Clone, PartialEq)]
pub struct MeteredResource<T> {
unit: String,
min: T,
max: T,
current: T,
}
impl<T> MeteredResource<T> {
pub fn unit(&self) -> &str {
&self.unit
}
}
impl<T: Copy> MeteredResource<T> {
pub fn min(&self) -> T {
self.min
}
pub fn max(&self) -> T {
self.max
}
pub fn current(&self) -> T {
self.current
}
pub fn deplete(&mut self) {
self.current = self.min;
}
pub fn refill(&mut self) {
self.current = self.max;
}
}
impl<T: MeteredValue> MeteredResource<T> {
#[allow(clippy::needless_pass_by_value)]
pub fn new(unit: impl ToString, min: T, max: T, current: T) -> GameResult<Self> {
if min >= max {
return Err(ValueError::MinOverMax.into());
}
if current < min || current > max {
return Err(ValueError::OutOfRange.into());
}
Ok(Self {
unit: unit.to_string(),
min,
max,
current,
})
}
pub fn with_new_bounds(&self, min: T, max: T) -> GameResult<Self> {
if min >= max {
return Err(ValueError::MinOverMax.into());
}
let current = match (self.current >= min, self.current <= max) {
(true, true) => self.current,
(true, false) => max,
(false, true) => min,
(false, false) => unreachable!("can't be both < min and > max"),
};
Ok(Self {
min,
max,
current,
unit: self.unit.clone(),
})
}
pub fn is_depleted(&self) -> bool {
self.current <= self.min
}
pub fn is_full(&self) -> bool {
self.current >= self.max
}
pub fn fraction_left(&self) -> f64 {
self.current.saturating_sub(self.min).as_f64() / self.max.saturating_sub(self.min).as_f64()
}
pub fn reduce_by(&mut self, amount: T) {
self.current = self.current.saturating_sub(amount).max(self.min);
}
pub fn increase_by(&mut self, amount: T) {
self.current = self.current.saturating_add(amount).min(self.max);
}
}
#[cfg(test)]
mod tests {
use super::MeteredResource;
use crate::{GameError, gameerror::ValueError};
#[test]
fn new_rejects_non_increasing_bounds() {
assert_eq!(
MeteredResource::new("hp", 10_u8, 10, 10),
Err(GameError::ValueError(ValueError::MinOverMax))
);
assert_eq!(
MeteredResource::new("hp", 11_u8, 10, 10),
Err(GameError::ValueError(ValueError::MinOverMax))
);
}
#[test]
fn new_rejects_current_values_outside_bounds() {
assert_eq!(
MeteredResource::new("hp", 10_u8, 20, 9),
Err(GameError::ValueError(ValueError::OutOfRange))
);
assert_eq!(
MeteredResource::new("hp", 10_u8, 20, 21),
Err(GameError::ValueError(ValueError::OutOfRange))
);
}
#[test]
fn with_new_bounds_clamps_without_changing_original() {
let resource = MeteredResource::new("hp", 0_u8, 100, 80).unwrap();
let lower_max = resource.with_new_bounds(0, 50).unwrap();
let higher_min = resource.with_new_bounds(90, 120).unwrap();
assert_eq!(resource.current(), 80);
assert_eq!(lower_max.current(), 50);
assert_eq!(higher_min.current(), 90);
}
#[test]
fn with_new_bounds_rejects_non_increasing_bounds() {
let resource = MeteredResource::new("hp", 0_u8, 100, 80).unwrap();
assert_eq!(
resource.with_new_bounds(50, 50),
Err(GameError::ValueError(ValueError::MinOverMax))
);
}
#[test]
fn deplete_refill_and_state_queries_use_configured_bounds() {
let mut resource = MeteredResource::new("stamina", 10_u8, 20, 15).unwrap();
resource.deplete();
assert!(resource.is_depleted());
assert!(!resource.is_full());
resource.refill();
assert!(resource.is_full());
assert!(!resource.is_depleted());
}
#[test]
fn reduce_by_clamps_at_minimum_without_underflowing() {
let mut resource = MeteredResource::new("hp", 10_u8, 100, 25).unwrap();
resource.reduce_by(30);
assert_eq!(resource.current(), 10);
}
#[test]
fn increase_by_clamps_at_maximum_without_overflowing() {
let mut resource = MeteredResource::new("mp", 0_u8, 250, 240).unwrap();
resource.increase_by(30);
assert_eq!(resource.current(), 250);
}
#[test]
fn fraction_full_uses_the_configured_range() {
let resource = MeteredResource::new("stamina", 20_u32, 120, 70).unwrap();
assert_eq!(resource.fraction_left(), 0.5);
}
}