Skip to main content

wowlab_engine_domain/rotation/
resolver.rs

1//! Maps spell/aura names to game IDs for rotation parsing.
2
3use wowlab_types::{
4    combat::ResourceType,
5    sim::{AuraIdx, FastMap, FastSet, SpellIdx},
6};
7
8use super::error::{Error, Result};
9
10mod policy;
11
12pub use policy::{SpellAuraPolicy, SpellAuraPolicyKind};
13
14/// Resolved talent state for rotation compilation.
15#[derive(Clone, Copy, Debug)]
16pub struct TalentInfo {
17    pub enabled: bool,
18    pub rank: i32,
19    pub max_rank: i32,
20}
21
22impl TalentInfo {
23    #[must_use]
24    pub fn new(enabled: bool) -> Self {
25        Self {
26            enabled,
27            rank: i32::from(enabled),
28            max_rank: 1,
29        }
30    }
31
32    #[must_use]
33    pub const fn enabled() -> Self {
34        Self {
35            enabled: true,
36            rank: 1,
37            max_rank: 1,
38        }
39    }
40
41    #[must_use]
42    pub const fn disabled() -> Self {
43        Self {
44            enabled: false,
45            rank: 0,
46            max_rank: 1,
47        }
48    }
49
50    #[must_use]
51    pub fn ranked(rank: i32, max_rank: i32) -> Self {
52        Self {
53            enabled: rank > 0,
54            rank,
55            max_rank,
56        }
57    }
58}
59
60/// Maps rotation-JSON string names (spells, auras, talents, resources) to their resolved numeric IDs for a single spec.
61#[derive(Clone, Debug)]
62pub struct SpecResolver {
63    pub name: String,
64    resource_type_str: Option<String>,
65    secondary_resource_type_str: Option<String>,
66    resources: FastMap<String, ResourceType>,
67    spells: FastMap<String, SpellIdx>,
68    auras: FastMap<String, AuraIdx>,
69    dots: FastMap<String, AuraIdx>,
70    talents: FastMap<String, TalentInfo>,
71    charged_cooldowns: FastSet<String>,
72    spell_aura_policies: Vec<SpellAuraPolicy>,
73}
74
75impl SpecResolver {
76    pub fn new(name: impl Into<String>) -> Self {
77        Self {
78            name: name.into(),
79            resource_type_str: None,
80            secondary_resource_type_str: None,
81            resources: FastMap::default(),
82            spells: FastMap::default(),
83            auras: FastMap::default(),
84            dots: FastMap::default(),
85            talents: FastMap::default(),
86            charged_cooldowns: FastSet::default(),
87            spell_aura_policies: Vec::new(),
88        }
89    }
90
91    #[must_use]
92    pub fn resource(mut self, resource_name: impl Into<String>) -> Self {
93        let name = resource_name.into();
94
95        self.resource_type_str = Some(name.clone());
96
97        if let Ok(res_type) = name.parse() {
98            self.resources.insert(name, res_type);
99        }
100
101        self
102    }
103
104    #[must_use]
105    pub fn secondary_resource(mut self, resource_name: impl Into<String>) -> Self {
106        let name = resource_name.into();
107
108        self.secondary_resource_type_str = Some(name.clone());
109
110        if let Ok(res_type) = name.parse() {
111            self.resources.insert(name, res_type);
112        }
113
114        self
115    }
116
117    #[must_use]
118    pub fn resource_type(mut self, name: impl Into<String>, res_type: ResourceType) -> Self {
119        self.resources.insert(name.into(), res_type);
120
121        self
122    }
123
124    #[must_use]
125    pub fn spell(mut self, name: impl Into<String>, id: u32) -> Self {
126        self.spells.insert(name.into(), SpellIdx(id));
127
128        self
129    }
130
131    #[must_use]
132    pub fn aura(mut self, name: impl Into<String>, id: u32) -> Self {
133        self.auras.insert(name.into(), AuraIdx(id));
134
135        self
136    }
137
138    /// Registers a damage-over-time aura; it lives only in the `dots` map and aura accessors fall back to it.
139    #[must_use]
140    pub fn dot(mut self, name: impl Into<String>, id: u32) -> Self {
141        self.dots.insert(name.into(), AuraIdx(id));
142
143        self
144    }
145
146    #[must_use]
147    pub fn charged_cooldown(mut self, name: impl Into<String>) -> Self {
148        self.charged_cooldowns.insert(name.into());
149
150        self
151    }
152
153    #[must_use]
154    pub fn talent(mut self, name: impl Into<String>, enabled: bool) -> Self {
155        self.talents.insert(name.into(), TalentInfo::new(enabled));
156
157        self
158    }
159
160    #[must_use]
161    pub fn talent_ranked(mut self, name: impl Into<String>, rank: i32, max_rank: i32) -> Self {
162        self.talents
163            .insert(name.into(), TalentInfo::ranked(rank, max_rank));
164
165        self
166    }
167
168    /// Resolves a named spell.
169    ///
170    /// # Errors
171    /// Returns an error when the name is not registered.
172    pub fn resolve_spell(&self, name: &str) -> Result<SpellIdx> {
173        self.spells
174            .get(name)
175            .copied()
176            .ok_or_else(|| Error::unknown_spell(name))
177    }
178
179    /// Resolves a named aura.
180    ///
181    /// # Errors
182    /// Returns an error when the name is not registered as an aura or damage-over-time effect.
183    pub fn resolve_aura(&self, name: &str) -> Result<AuraIdx> {
184        self.auras
185            .get(name)
186            .or_else(|| self.dots.get(name))
187            .copied()
188            .ok_or_else(|| Error::unknown_aura(name))
189    }
190
191    /// Resolves a named damage-over-time aura.
192    ///
193    /// # Errors
194    /// Returns an error when the name is not registered.
195    pub fn resolve_dot(&self, name: &str) -> Result<AuraIdx> {
196        self.dots
197            .get(name)
198            .copied()
199            .ok_or_else(|| Error::unknown_aura(name))
200    }
201
202    /// Resolves whether a named talent is active.
203    ///
204    /// # Errors
205    /// Returns an error when the talent name is not registered.
206    pub fn resolve_talent(&self, name: &str) -> Result<bool> {
207        self.talents
208            .get(name)
209            .map(|info| info.enabled)
210            .ok_or_else(|| Error::unknown_talent(name))
211    }
212
213    /// Resolves rank metadata for a named talent.
214    ///
215    /// # Errors
216    /// Returns an error when the talent name is not registered.
217    pub fn resolve_talent_info(&self, name: &str) -> Result<TalentInfo> {
218        self.talents
219            .get(name)
220            .copied()
221            .ok_or_else(|| Error::unknown_talent(name))
222    }
223
224    #[must_use]
225    pub fn is_charged(&self, name: &str) -> bool {
226        self.charged_cooldowns.contains(name)
227    }
228
229    #[must_use]
230    pub fn primary_resource(&self) -> Option<&str> {
231        self.resource_type_str.as_deref()
232    }
233
234    #[must_use]
235    pub fn primary_resource_type(&self) -> Option<ResourceType> {
236        self.resource_type_str.as_deref()?.parse().ok()
237    }
238
239    #[must_use]
240    pub fn secondary_resource_type(&self) -> Option<ResourceType> {
241        self.secondary_resource_type_str.as_deref()?.parse().ok()
242    }
243
244    /// Resolves a named resource type.
245    ///
246    /// # Errors
247    /// Returns an error when the resource name is not registered.
248    pub fn resolve_resource(&self, name: &str) -> Result<ResourceType> {
249        if let Some(&res_type) = self.resources.get(name) {
250            return Ok(res_type);
251        }
252
253        if let Ok(res_type) = name.parse() {
254            return Ok(res_type);
255        }
256
257        Err(Error::unknown_resource(name))
258    }
259
260    #[must_use]
261    pub fn has_spell(&self, name: &str) -> bool {
262        self.spells.contains_key(name)
263    }
264
265    #[must_use]
266    pub fn has_aura(&self, name: &str) -> bool {
267        self.auras.contains_key(name) || self.dots.contains_key(name)
268    }
269
270    #[must_use]
271    pub fn has_dot(&self, name: &str) -> bool {
272        self.dots.contains_key(name)
273    }
274
275    #[must_use]
276    pub fn has_talent(&self, name: &str) -> bool {
277        self.talents.get(name).is_some_and(|info| info.enabled)
278    }
279
280    #[must_use]
281    pub fn knows_talent(&self, name: &str) -> bool {
282        self.talents.contains_key(name)
283    }
284
285    #[must_use]
286    pub fn registered_resource_types(&self) -> FastSet<ResourceType> {
287        self.resources.values().copied().collect()
288    }
289}