wowlab_engine_domain/rotation/
spec_catalog.rs1use wowlab_types::{
4 game::{SpecId, SpecIntrospection},
5 sim::{AuraIdx, FastMap, IntMap, SpellIdx},
6};
7
8use crate::rotation::{SpecResolver, TalentInfo};
9
10#[derive(Clone, Debug, Default)]
12pub struct CatalogHints {
13 pub primary_resource: Option<String>,
14 pub cooldown_spells: Vec<String>,
15 pub charged_spells: Vec<String>,
16 pub dot_auras: Vec<String>,
17 pub talents: Vec<String>,
18 pub talent_ranks: Vec<(String, u8)>,
19}
20
21#[derive(Debug)]
23pub struct SpecCatalog {
24 spells_by_name: FastMap<String, SpellIdx>,
25 spells_by_id: IntMap<u32, SpellIdx>,
26 auras_by_name: FastMap<String, AuraIdx>,
27 dots_by_name: FastMap<String, AuraIdx>,
28 talents_by_name: FastMap<String, TalentInfo>,
29 charged_cooldowns: FastMap<String, SpellIdx>,
30 cooldowns_by_name: FastMap<String, SpellIdx>,
31 primary_resource: Option<String>,
32 secondary_resource: Option<String>,
33}
34
35impl SpecCatalog {
36 #[must_use]
39 pub fn new(
40 spell_ids: &FastMap<String, u32>,
41 aura_ids: &FastMap<String, u32>,
42 hints: &CatalogHints,
43 ) -> Self {
44 let mut catalog = Self {
45 spells_by_name: FastMap::default(),
46 spells_by_id: IntMap::default(),
47 auras_by_name: FastMap::default(),
48 dots_by_name: FastMap::default(),
49 talents_by_name: FastMap::default(),
50 charged_cooldowns: FastMap::default(),
51 cooldowns_by_name: FastMap::default(),
52 primary_resource: hints.primary_resource.clone(),
53 secondary_resource: None,
54 };
55
56 for (display_name, &id) in spell_ids {
57 let key = to_simc_key(display_name);
58 let idx = SpellIdx(id);
59
60 catalog.spells_by_name.insert(key.clone(), idx);
61 catalog.spells_by_id.insert(id, idx);
62 }
63
64 for (display_name, &id) in aura_ids {
65 let key = to_simc_key(display_name);
66
67 catalog.auras_by_name.insert(key.clone(), AuraIdx(id));
68 }
69
70 for name in &hints.cooldown_spells {
71 if let Some(&idx) = catalog.spells_by_name.get(name.as_str()) {
72 catalog.cooldowns_by_name.insert(name.clone(), idx);
73 }
74 }
75
76 for name in &hints.charged_spells {
77 if let Some(&idx) = catalog.spells_by_name.get(name.as_str()) {
78 catalog.charged_cooldowns.insert(name.clone(), idx);
79 }
80 }
81
82 for name in &hints.dot_auras {
83 if let Some(&idx) = catalog.auras_by_name.get(name.as_str()) {
84 catalog.dots_by_name.insert(name.clone(), idx);
85 }
86 }
87
88 for name in &hints.talents {
89 catalog
90 .talents_by_name
91 .entry(name.clone())
92 .or_insert(TalentInfo::enabled());
93 }
94
95 for (name, rank) in &hints.talent_ranks {
96 catalog.talents_by_name.insert(
97 name.clone(),
98 TalentInfo::ranked(i32::from(*rank), i32::from((*rank).max(1))),
99 );
100 }
101
102 catalog
103 }
104
105 #[must_use]
106 pub fn spell_by_name(&self, name: &str) -> Option<SpellIdx> {
107 self.spells_by_name.get(name).copied()
108 }
109
110 #[must_use]
111 pub fn spell_by_id(&self, id: u32) -> Option<SpellIdx> {
112 self.spells_by_id.get(&id).copied()
113 }
114
115 #[must_use]
116 pub fn has_spell(&self, name: &str) -> bool {
117 self.spells_by_name.contains_key(name)
118 }
119
120 #[must_use]
121 pub fn aura_by_name(&self, name: &str) -> Option<AuraIdx> {
122 self.auras_by_name.get(name).copied()
123 }
124
125 #[must_use]
126 pub fn has_aura(&self, name: &str) -> bool {
127 self.auras_by_name.contains_key(name)
128 }
129
130 #[must_use]
131 pub fn dot_by_name(&self, name: &str) -> Option<AuraIdx> {
132 self.dots_by_name.get(name).copied()
133 }
134
135 #[must_use]
136 pub fn has_dot(&self, name: &str) -> bool {
137 self.dots_by_name.contains_key(name)
138 }
139
140 #[must_use]
141 pub fn talent(&self, name: &str) -> Option<TalentInfo> {
142 self.talents_by_name.get(name).copied()
143 }
144
145 #[must_use]
146 pub fn has_talent(&self, name: &str) -> bool {
147 self.talents_by_name.get(name).is_some_and(|t| t.enabled)
148 }
149
150 #[must_use]
151 pub fn knows_talent(&self, name: &str) -> bool {
152 self.talents_by_name.contains_key(name)
153 }
154
155 pub fn set_talent(&mut self, name: &str, rank: TalentInfo) {
156 self.talents_by_name.insert(name.to_string(), rank);
157 }
158
159 #[must_use]
160 pub fn is_charged(&self, name: &str) -> bool {
161 self.charged_cooldowns.contains_key(name)
162 }
163
164 #[must_use]
165 pub fn cooldown_by_name(&self, name: &str) -> Option<SpellIdx> {
166 self.cooldowns_by_name.get(name).copied()
167 }
168
169 #[must_use]
170 pub fn primary_resource(&self) -> Option<&str> {
171 self.primary_resource.as_deref()
172 }
173
174 pub fn register_secondary_resource(&mut self, name: &str) {
175 self.secondary_resource = Some(name.to_string());
176 }
177
178 #[must_use]
179 pub fn secondary_resource(&self) -> Option<&str> {
180 self.secondary_resource.as_deref()
181 }
182
183 #[must_use]
184 pub fn to_resolver(&self, name: &str) -> SpecResolver {
185 let mut resolver = SpecResolver::new(name);
186
187 if let Some(resource) = self.primary_resource() {
188 resolver = resolver.resource(resource);
189 }
190
191 if let Some(sec_resource) = self.secondary_resource() {
192 resolver = resolver.secondary_resource(sec_resource);
193 }
194
195 for (spell_name, spell_idx) in self.spells() {
196 resolver = resolver.spell(spell_name, spell_idx.0);
197 }
198
199 for (aura_name, aura_idx) in self.auras() {
200 resolver = resolver.aura(aura_name, aura_idx.0);
201 }
202
203 for (dot_name, dot_idx) in self.dots() {
204 resolver = resolver.dot(dot_name, dot_idx.0);
205 }
206
207 for (talent_name, talent_rank) in self.talents() {
208 if talent_rank.max_rank > 1 {
209 resolver =
210 resolver.talent_ranked(talent_name, talent_rank.rank, talent_rank.max_rank);
211 } else {
212 resolver = resolver.talent(talent_name, talent_rank.enabled);
213 }
214 }
215
216 for cd_name in self.charged_cooldowns() {
217 resolver = resolver.charged_cooldown(cd_name);
218 }
219
220 resolver
221 }
222
223 pub(crate) fn spells(&self) -> impl Iterator<Item = (&str, SpellIdx)> {
224 self.spells_by_name.iter().map(|(k, v)| (k.as_str(), *v))
225 }
226
227 pub(crate) fn auras(&self) -> impl Iterator<Item = (&str, AuraIdx)> {
229 self.auras_by_name
230 .iter()
231 .filter(|(k, _)| !self.dots_by_name.contains_key(k.as_str()))
232 .map(|(k, v)| (k.as_str(), *v))
233 }
234
235 pub(crate) fn dots(&self) -> impl Iterator<Item = (&str, AuraIdx)> {
236 self.dots_by_name.iter().map(|(k, v)| (k.as_str(), *v))
237 }
238
239 pub(crate) fn talents(&self) -> impl Iterator<Item = (&str, TalentInfo)> {
240 self.talents_by_name.iter().map(|(k, v)| (k.as_str(), *v))
241 }
242
243 pub(crate) fn charged_cooldowns(&self) -> impl Iterator<Item = &str> {
244 self.charged_cooldowns.keys().map(String::as_str)
245 }
246}
247
248#[must_use]
250pub fn to_simc_key(name: &str) -> String {
251 let mut result = String::with_capacity(name.len());
252 let name = name.trim_start_matches(['_', '+']);
253
254 for ch in name.chars() {
255 match ch {
256 ' ' => result.push('_'),
257 '_' | '+' | '.' | '%' | '0'..='9' | 'a'..='z' => result.push(ch),
258 'A'..='Z' => result.push(ch.to_ascii_lowercase()),
259 _ => {}
260 }
261 }
262
263 result
264}
265
266#[must_use]
268pub fn build_spec_resolver(
269 intro: &SpecIntrospection,
270 spec_id: SpecId,
271 talent_names: &[&str],
272) -> SpecResolver {
273 let spell_ids: FastMap<String, u32> = intro
274 .spells
275 .iter()
276 .map(|s| (s.slug.clone(), s.spell_id))
277 .collect();
278 let aura_ids: FastMap<String, u32> = intro
279 .auras
280 .iter()
281 .map(|a| (a.slug.clone(), a.aura_id))
282 .collect();
283 let resource_name = spec_id.primary_resource().to_string().to_lowercase();
284 let hints = CatalogHints {
285 primary_resource: Some(resource_name.clone()),
286 talents: talent_names.iter().map(|t| (*t).to_string()).collect(),
287 ..Default::default()
288 };
289 let catalog = SpecCatalog::new(&spell_ids, &aura_ids, &hints);
290
291 catalog.to_resolver(spec_id.slug())
292}
293
294#[cfg(test)]
295mod tests;