Skip to main content

edda_types/
resources.rs

1use serde::{Deserialize, Serialize};
2
3/// A count of limited uses: spell slots, Action Surge, Ki points, etc.
4#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
5#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
6#[serde(default)]
7pub struct Pool {
8    pub max: u16,
9    pub used: u16,
10}
11
12impl Pool {
13    /// A pool of `max` uses, none spent.
14    pub fn new(max: u16) -> Self {
15        Self { max, used: 0 }
16    }
17
18    /// How many uses are left.
19    pub fn remaining(&self) -> u16 {
20        self.max.saturating_sub(self.used)
21    }
22
23    /// Spends `amount` uses, or does nothing and returns `false` if there
24    /// aren't enough remaining.
25    pub fn expend(&mut self, amount: u16) -> bool {
26        if amount > self.remaining() {
27            return false;
28        }
29        self.used += amount;
30        true
31    }
32
33    /// Regains up to `amount` uses (never beyond `max`).
34    pub fn restore(&mut self, amount: u16) {
35        self.used = self.used.saturating_sub(amount);
36    }
37
38    /// Regains every use.
39    pub fn reset(&mut self) {
40        self.used = 0;
41    }
42
43    /// Whether no more are spent than there are.
44    pub fn is_valid(&self) -> bool {
45        self.used <= self.max
46    }
47}
48
49/// When a resource comes back on its own.
50#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
51#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
52#[serde(rename_all = "snake_case")]
53pub enum Recharge {
54    /// Recovers on a short or long rest (Action Surge, Ki, Channel Divinity).
55    ShortRest,
56    /// Recovers on a long rest only (Rage, Arcane Recovery).
57    #[default]
58    LongRest,
59    /// Never recovers automatically (item charges, homebrew).
60    Manual,
61}
62
63/// A short or a long rest.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
65#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
66#[serde(rename_all = "snake_case")]
67pub enum RestKind {
68    Short,
69    Long,
70}
71
72impl Recharge {
73    /// Whether a resource with this recharge comes back on `rest`.
74    pub fn recovers_on(self, rest: RestKind) -> bool {
75        match self {
76            Recharge::ShortRest => true,
77            Recharge::LongRest => rest == RestKind::Long,
78            Recharge::Manual => false,
79        }
80    }
81}
82
83/// A named, limited-use class feature or ability, e.g. Action Surge.
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
86pub struct Resource {
87    pub name: String,
88    #[serde(flatten)]
89    pub uses: Pool,
90    #[serde(default)]
91    pub recharge: Recharge,
92    #[serde(default, skip_serializing_if = "String::is_empty")]
93    pub notes: String,
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn pool_never_overspends_or_overrestores() {
102        let mut p = Pool::new(2);
103        assert!(p.expend(2));
104        assert!(!p.expend(1));
105        assert_eq!(p.used, 2);
106        p.restore(5);
107        assert_eq!(p.used, 0);
108    }
109
110    #[test]
111    fn recharge_rules() {
112        assert!(Recharge::ShortRest.recovers_on(RestKind::Short));
113        assert!(Recharge::ShortRest.recovers_on(RestKind::Long));
114        assert!(!Recharge::LongRest.recovers_on(RestKind::Short));
115        assert!(!Recharge::Manual.recovers_on(RestKind::Long));
116    }
117}