use crate::core::AbilityModifier;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(transparent)]
pub struct AbilityScore(u8);
impl AbilityScore {
pub const MIN: AbilityScore = AbilityScore(1);
pub const MAX: AbilityScore = AbilityScore(30);
pub const DEFAULT: AbilityScore = AbilityScore(10);
#[must_use]
pub const fn new(value: u8) -> Self {
debug_assert!(
!(value < Self::MIN.value() || value > Self::MAX.value()),
"Ability score must be between 1 and 30"
);
Self::new_clamped(value)
}
#[must_use]
pub const fn new_clamped(value: u8) -> Self {
if value < Self::MIN.value() {
Self::MIN
} else if value > Self::MAX.value() {
Self::MAX
} else {
Self(value)
}
}
pub fn try_new(value: u8) -> Result<Self, &'static str> {
if value < Self::MIN.value() {
Err("Ability score cannot be less than 1")
} else if value > Self::MAX.value() {
Err("Ability score cannot be greater than 30")
} else {
Ok(Self(value))
}
}
#[must_use]
pub const fn value(&self) -> u8 {
self.0
}
#[must_use]
#[allow(clippy::cast_possible_wrap)]
pub const fn modifier(&self) -> AbilityModifier {
AbilityModifier::new_clamped((self.value() as i8 - 10) >> 1)
}
}
impl Default for AbilityScore {
fn default() -> Self {
Self::DEFAULT
}
}
impl TryFrom<u8> for AbilityScore {
type Error = &'static str;
fn try_from(value: u8) -> Result<Self, Self::Error> {
Self::try_new(value)
}
}
impl From<AbilityScore> for u8 {
fn from(score: AbilityScore) -> Self {
score.value()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_clamped_min() {
let score = AbilityScore::new_clamped(0);
assert_eq!(score, AbilityScore::MIN);
}
#[test]
fn new_clamped_max() {
let score = AbilityScore::new_clamped(31);
assert_eq!(score, AbilityScore::MAX);
}
#[test]
fn new_clamped_valid() {
let score = AbilityScore::new_clamped(15);
assert_eq!(score, AbilityScore(15));
}
#[test]
fn try_new_min() {
let score = AbilityScore::try_new(0);
assert_eq!(score, Err("Ability score cannot be less than 1"));
}
#[test]
fn try_new_max() {
let score = AbilityScore::try_new(31);
assert_eq!(score, Err("Ability score cannot be greater than 30"));
}
#[test]
fn try_new_valid() {
let score = AbilityScore::try_new(15);
assert_eq!(score, Ok(AbilityScore(15)));
}
#[test]
#[should_panic(expected = "Ability score must be between 1 and 30")]
fn new_panic() {
let _score = AbilityScore::new(0);
}
#[test]
fn new() {
let score = AbilityScore::new(15);
assert_eq!(score, AbilityScore(15));
}
#[test]
fn value() {
let score = AbilityScore(15);
assert_eq!(score.value(), 15);
}
#[test]
fn modifier() {
let expected: [(u8, i8); 30] = [
(1, -5),
(2, -4),
(3, -4),
(4, -3),
(5, -3),
(6, -2),
(7, -2),
(8, -1),
(9, -1),
(10, 0),
(11, 0),
(12, 1),
(13, 1),
(14, 2),
(15, 2),
(16, 3),
(17, 3),
(18, 4),
(19, 4),
(20, 5),
(21, 5),
(22, 6),
(23, 6),
(24, 7),
(25, 7),
(26, 8),
(27, 8),
(28, 9),
(29, 9),
(30, 10),
];
for (score, modifier) in expected {
let ability_score = AbilityScore(score);
assert_eq!(
ability_score.modifier().value(),
modifier,
"Modifier mismatch for score {score}"
);
}
}
#[test]
fn from_u8() {
let score: AbilityScore = 20u8.try_into().unwrap();
assert_eq!(score, AbilityScore(20));
}
#[test]
fn into_u8() {
let score = AbilityScore(25);
let value: u8 = score.into();
assert_eq!(value, 25);
}
#[test]
#[cfg(feature = "serde")]
fn serde_serialize() {
let score = AbilityScore(18);
let serialized = serde_json::to_string(&score).unwrap();
assert_eq!(serialized, "18");
let deserialized: AbilityScore = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized, AbilityScore(18));
}
}