Skip to main content

wowlab_types/types/
spell_desc.rs

1use serde::{Deserialize, Serialize};
2#[cfg(feature = "wasm")]
3use tsify::Tsify;
4
5#[derive(Clone, Debug, Default, Deserialize, Serialize)]
6#[cfg_attr(feature = "wasm", derive(Tsify))]
7#[cfg_attr(feature = "wasm", tsify(into_wasm_abi))]
8#[serde(rename_all = "camelCase")]
9pub struct SpellDescDependencies {
10    pub spell_ids: Vec<u32>,
11    pub effects: Vec<EffectDependency>,
12    pub spell_values: Vec<SpellValueDependency>,
13    pub player_stats: Vec<String>,
14    pub custom_vars: Vec<String>,
15    pub spell_known_checks: Vec<u32>,
16    pub aura_checks: Vec<u32>,
17    pub specialization_checks: Vec<u8>,
18    pub needs_gender: bool,
19    pub embedded_spell_ids: Vec<u32>,
20}
21
22fn push_unique<T>(vec: &mut Vec<T>, value: T)
23where
24    T: PartialEq,
25{
26    if !vec.contains(&value) {
27        vec.push(value);
28    }
29}
30
31impl SpellDescDependencies {
32    #[must_use]
33    pub fn new() -> Self {
34        Self::default()
35    }
36
37    pub fn add_spell_id(&mut self, spell_id: u32) {
38        push_unique(&mut self.spell_ids, spell_id);
39    }
40
41    pub fn add_effect(&mut self, spell_id: u32, effect_index: u8, var_type: String) {
42        self.add_spell_id(spell_id);
43        push_unique(
44            &mut self.effects,
45            EffectDependency {
46                spell_id,
47                effect_index,
48                var_type,
49            },
50        );
51    }
52
53    pub fn add_spell_value(&mut self, spell_id: u32, var_type: String) {
54        self.add_spell_id(spell_id);
55        push_unique(
56            &mut self.spell_values,
57            SpellValueDependency { spell_id, var_type },
58        );
59    }
60
61    pub fn add_player_stat(&mut self, stat: String) {
62        push_unique(&mut self.player_stats, stat);
63    }
64
65    pub fn add_custom_var(&mut self, name: String) {
66        push_unique(&mut self.custom_vars, name);
67    }
68
69    pub fn add_spell_known_check(&mut self, spell_id: u32) {
70        push_unique(&mut self.spell_known_checks, spell_id);
71    }
72
73    pub fn add_aura_check(&mut self, aura_id: u32) {
74        push_unique(&mut self.aura_checks, aura_id);
75    }
76
77    pub fn add_specialization_check(&mut self, spec_index: u8) {
78        push_unique(&mut self.specialization_checks, spec_index);
79    }
80
81    pub fn add_embedded_spell(&mut self, spell_id: u32) {
82        self.add_spell_id(spell_id);
83        push_unique(&mut self.embedded_spell_ids, spell_id);
84    }
85
86    // #t(fn: rust_clone_in_loop) clones are unavoidable when merging borrowed data with uniqueness checks
87    pub fn merge(&mut self, other: &SpellDescDependencies) {
88        for id in &other.spell_ids {
89            self.add_spell_id(*id);
90        }
91
92        for dep in &other.effects {
93            self.add_effect(dep.spell_id, dep.effect_index, dep.var_type.clone());
94        }
95
96        for dep in &other.spell_values {
97            self.add_spell_value(dep.spell_id, dep.var_type.clone());
98        }
99
100        for stat in &other.player_stats {
101            self.add_player_stat(stat.clone());
102        }
103
104        for var in &other.custom_vars {
105            self.add_custom_var(var.clone());
106        }
107
108        for id in &other.spell_known_checks {
109            self.add_spell_known_check(*id);
110        }
111
112        for id in &other.aura_checks {
113            self.add_aura_check(*id);
114        }
115
116        for spec_index in &other.specialization_checks {
117            self.add_specialization_check(*spec_index);
118        }
119
120        if other.needs_gender {
121            self.needs_gender = true;
122        }
123
124        for id in &other.embedded_spell_ids {
125            self.add_embedded_spell(*id);
126        }
127    }
128}
129
130#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
131#[cfg_attr(feature = "wasm", derive(Tsify))]
132#[serde(rename_all = "camelCase")]
133pub struct EffectDependency {
134    pub spell_id: u32,
135    pub effect_index: u8,
136    pub var_type: String,
137}
138
139#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
140#[cfg_attr(feature = "wasm", derive(Tsify))]
141#[serde(rename_all = "camelCase")]
142pub struct SpellValueDependency {
143    pub spell_id: u32,
144    pub var_type: String,
145}
146
147#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
148#[cfg_attr(feature = "wasm", derive(Tsify))]
149#[serde(tag = "kind", rename_all = "camelCase")]
150#[non_exhaustive]
151pub enum SpellDescFragment {
152    Text {
153        value: String,
154    },
155    Value {
156        value: String,
157        raw: f64,
158    },
159    Duration {
160        value: String,
161        raw_ms: f64,
162    },
163    ColorStart {
164        color: String,
165    },
166    ColorEnd,
167    Icon {
168        spell_id: u32,
169        path: String,
170    },
171    SpellName {
172        spell_id: u32,
173        name: String,
174    },
175    Embedded {
176        spell_id: u32,
177        fragments: Vec<SpellDescFragment>,
178    },
179    Unresolved {
180        token: String,
181    },
182    RawToken {
183        value: String,
184    },
185}
186
187#[derive(Clone, Debug, Default, Deserialize, Serialize)]
188#[cfg_attr(feature = "wasm", derive(Tsify))]
189#[cfg_attr(feature = "wasm", tsify(into_wasm_abi))]
190#[serde(rename_all = "camelCase")]
191pub struct SpellDescRenderResult {
192    pub fragments: Vec<SpellDescFragment>,
193    pub parse_errors: Vec<String>,
194    pub warnings: Vec<String>,
195}
196
197impl SpellDescRenderResult {
198    #[must_use]
199    pub fn new() -> Self {
200        Self::default()
201    }
202
203    #[must_use]
204    pub fn with_fragments(fragments: Vec<SpellDescFragment>) -> Self {
205        Self {
206            fragments,
207            parse_errors: Vec::new(),
208            warnings: Vec::new(),
209        }
210    }
211
212    pub fn add_parse_error(&mut self, error: String) {
213        self.parse_errors.push(error);
214    }
215
216    pub fn add_warning(&mut self, warning: String) {
217        self.warnings.push(warning);
218    }
219
220    #[must_use]
221    pub fn has_errors(&self) -> bool {
222        !self.parse_errors.is_empty()
223    }
224
225    #[must_use]
226    pub fn to_plain_text(&self) -> String {
227        self.fragments
228            .iter()
229            .map(|f| match f {
230                SpellDescFragment::Text { value }
231                | SpellDescFragment::Value { value, .. }
232                | SpellDescFragment::Duration { value, .. }
233                | SpellDescFragment::RawToken { value } => value.clone(),
234                SpellDescFragment::ColorStart { .. }
235                | SpellDescFragment::ColorEnd
236                | SpellDescFragment::Icon { .. } => String::new(),
237                SpellDescFragment::SpellName { name, .. } => name.clone(),
238                SpellDescFragment::Embedded { fragments, .. } => {
239                    SpellDescRenderResult::with_fragments(fragments.clone()).to_plain_text()
240                }
241                SpellDescFragment::Unresolved { token } => token.clone(),
242            })
243            .collect()
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use googletest::prelude::*;
250    use rstest::rstest;
251
252    use super::*;
253
254    #[gtest]
255    fn add_spell_id_dedups() -> Result<()> {
256        let mut deps = SpellDescDependencies::new();
257
258        deps.add_spell_id(5);
259        deps.add_spell_id(5);
260        deps.add_spell_id(7);
261
262        verify_that!(deps.spell_ids, eq(&vec![5, 7]))
263    }
264
265    #[gtest]
266    fn add_player_stat_dedups() -> Result<()> {
267        let mut deps = SpellDescDependencies::new();
268
269        deps.add_player_stat("ap".to_string());
270        deps.add_player_stat("ap".to_string());
271
272        verify_that!(deps.player_stats, eq(&vec!["ap".to_string()]))
273    }
274
275    #[gtest]
276    fn add_aura_check_and_custom_var_dedup() -> Result<()> {
277        let mut deps = SpellDescDependencies::new();
278
279        deps.add_aura_check(3);
280        deps.add_aura_check(3);
281        deps.add_custom_var("v".to_string());
282        deps.add_custom_var("v".to_string());
283        verify_that!(deps.aura_checks, eq(&vec![3]))?;
284
285        verify_that!(deps.custom_vars, eq(&vec!["v".to_string()]))
286    }
287
288    #[gtest]
289    fn add_effect_pushes_spell_id_and_dedups() -> Result<()> {
290        let mut deps = SpellDescDependencies::new();
291
292        deps.add_effect(9, 0, "s".to_string());
293        deps.add_effect(9, 0, "s".to_string());
294        verify_that!(deps.spell_ids, contains(eq(&9u32)))?;
295
296        verify_that!(deps.effects, len(eq(1)))
297    }
298
299    #[gtest]
300    fn add_spell_value_pushes_spell_id_and_dedups() -> Result<()> {
301        let mut deps = SpellDescDependencies::new();
302
303        deps.add_spell_value(4, "d".to_string());
304        deps.add_spell_value(4, "d".to_string());
305        verify_that!(deps.spell_ids, contains(eq(&4u32)))?;
306
307        verify_that!(deps.spell_values, len(eq(1)))
308    }
309
310    #[gtest]
311    fn add_embedded_spell_pushes_both_and_dedups() -> Result<()> {
312        let mut deps = SpellDescDependencies::new();
313
314        deps.add_embedded_spell(11);
315        deps.add_embedded_spell(11);
316        verify_that!(deps.spell_ids, eq(&vec![11]))?;
317
318        verify_that!(deps.embedded_spell_ids, eq(&vec![11]))
319    }
320
321    #[gtest]
322    fn merge_dedups_and_sets_needs_gender() -> Result<()> {
323        let mut a = SpellDescDependencies::new();
324
325        a.add_spell_id(1);
326        a.add_effect(2, 0, "s".to_string());
327
328        let mut b = SpellDescDependencies::new();
329
330        b.add_spell_id(2);
331        b.add_spell_id(3);
332        b.add_effect(2, 0, "s".to_string());
333        b.needs_gender = true;
334
335        a.merge(&b);
336        verify_that!(a.spell_ids, eq(&vec![1, 2, 3]))?;
337        verify_that!(a.effects, len(eq(1)))?;
338
339        verify_that!(a.needs_gender, eq(true))
340    }
341
342    #[gtest]
343    fn merge_does_not_clear_needs_gender() -> Result<()> {
344        let mut a = SpellDescDependencies::new();
345
346        a.needs_gender = true;
347        let b = SpellDescDependencies::new();
348
349        a.merge(&b);
350
351        verify_that!(a.needs_gender, eq(true))
352    }
353
354    #[gtest]
355    #[rstest]
356    #[case::text(SpellDescFragment::Text { value: "hi".to_string() }, "hi")]
357    #[case::value(SpellDescFragment::Value { value: "5".to_string(), raw: 5.0 }, "5")]
358    #[case::duration(SpellDescFragment::Duration { value: "2s".to_string(), raw_ms: 2000.0 }, "2s")]
359    #[case::spell_name(SpellDescFragment::SpellName { spell_id: 1, name: "Kick".to_string() }, "Kick")]
360    #[case::unresolved(SpellDescFragment::Unresolved { token: "$x".to_string() }, "$x")]
361    #[case::raw_token(SpellDescFragment::RawToken { value: "{y}".to_string() }, "{y}")]
362    #[case::color_start(SpellDescFragment::ColorStart { color: "ff0000".to_string() }, "")]
363    #[case::color_end(SpellDescFragment::ColorEnd, "")]
364    #[case::icon(SpellDescFragment::Icon { spell_id: 1, path: "p".to_string() }, "")]
365    fn to_plain_text_per_fragment(
366        #[case] fragment: SpellDescFragment,
367        #[case] expected: &str,
368    ) -> Result<()> {
369        let result = SpellDescRenderResult::with_fragments(vec![fragment]);
370
371        verify_that!(result.to_plain_text(), eq(expected))
372    }
373
374    #[gtest]
375    fn to_plain_text_recursive_embedded() -> Result<()> {
376        let embedded = SpellDescFragment::Embedded {
377            spell_id: 1,
378            fragments: vec![
379                SpellDescFragment::Text {
380                    value: "a".to_string(),
381                },
382                SpellDescFragment::SpellName {
383                    spell_id: 2,
384                    name: "B".to_string(),
385                },
386            ],
387        };
388        let result = SpellDescRenderResult::with_fragments(vec![embedded]);
389
390        verify_that!(result.to_plain_text(), eq("aB"))
391    }
392
393    #[gtest]
394    fn to_plain_text_full_sequence() -> Result<()> {
395        let result = SpellDescRenderResult::with_fragments(vec![
396            SpellDescFragment::Text {
397                value: "deal ".to_string(),
398            },
399            SpellDescFragment::Value {
400                value: "5".to_string(),
401                raw: 5.0,
402            },
403            SpellDescFragment::Text {
404                value: " dmg".to_string(),
405            },
406        ]);
407
408        verify_that!(result.to_plain_text(), eq("deal 5 dmg"))
409    }
410
411    #[gtest]
412    fn has_errors_and_with_fragments() -> Result<()> {
413        let mut result = SpellDescRenderResult::new();
414
415        verify_that!(result.has_errors(), eq(false))?;
416        result.add_parse_error("x".to_string());
417        verify_that!(result.has_errors(), eq(true))?;
418
419        let clean = SpellDescRenderResult::with_fragments(vec![SpellDescFragment::Text {
420            value: "z".to_string(),
421        }]);
422
423        verify_that!(clean.has_errors(), eq(false))?;
424        verify_that!(clean.parse_errors, len(eq(0)))?;
425
426        verify_that!(clean.warnings, len(eq(0)))
427    }
428}