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 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}