Skip to main content

wowlab_parsers/parsers/spell_desc/renderer/
render.rs

1use wowlab_types::spell_desc::SpellDescFragment;
2
3use super::{
4    MAX_EMBED_DEPTH, RenderContext, build_misc_key, build_spell_level_key, capitalize_if_needed,
5    evaluate::{format_effect_value, format_rendered_number},
6    merge_text_fragments, parse_duration_ms,
7};
8use crate::parsers::spell_desc::{
9    parser::parse,
10    resolver::SpellDescResolver,
11    types::{
12        AtVariableNode, ConditionalNode, GenderNode, PluralizationNode, SpellDescriptionNode,
13        VariableNode,
14    },
15};
16
17impl<R> RenderContext<'_, R>
18where
19    R: SpellDescResolver,
20{
21    pub(super) fn render_nodes(
22        &mut self,
23        nodes: &[SpellDescriptionNode],
24    ) -> Vec<SpellDescFragment> {
25        let mut fragments = Vec::new();
26
27        for node in nodes {
28            fragments.extend(self.render_node(node));
29        }
30
31        merge_text_fragments(&mut fragments);
32
33        fragments
34    }
35
36    fn render_node(&mut self, node: &SpellDescriptionNode) -> Vec<SpellDescFragment> {
37        match node {
38            SpellDescriptionNode::Text(text) => {
39                // WHY: strip carriage returns so `\r\n` paragraph breaks collapse to the kept `\n`.
40                vec![SpellDescFragment::Text {
41                    value: text.value().replace('\r', ""),
42                }]
43            }
44
45            SpellDescriptionNode::ColorCode(color) => {
46                if color.code() == "r" {
47                    vec![SpellDescFragment::ColorEnd]
48                } else {
49                    vec![SpellDescFragment::ColorStart {
50                        color: color.code().to_owned(),
51                    }]
52                }
53            }
54
55            SpellDescriptionNode::Variable(var) => self.render_variable(var),
56
57            SpellDescriptionNode::ExpressionBlock(block) => {
58                let value = self.evaluate_expression(&block.expression);
59
60                self.last_number = Some(value);
61
62                vec![SpellDescFragment::Value {
63                    value: format_rendered_number(value, block.decimal_places),
64                    raw: value,
65                }]
66            }
67
68            SpellDescriptionNode::Conditional(cond) => self.render_conditional(cond),
69
70            SpellDescriptionNode::Pluralization(plural) => self.render_pluralization(plural),
71
72            SpellDescriptionNode::Gender(gender) => self.render_gender(gender),
73        }
74    }
75
76    // #t(fn: rust_cyclomatic_complexity, rust_max_fn_lines) exhaustive variable rendering keeps resolve/fallback behavior adjacent per variant
77    fn render_variable(&mut self, var: &VariableNode) -> Vec<SpellDescFragment> {
78        match var {
79            VariableNode::Effect(effect) => {
80                if let Some(value) = self.resolver.get_effect_value(
81                    self.self_spell_id,
82                    effect.effect_index,
83                    &effect.var_type,
84                ) {
85                    self.last_number = Some(value);
86
87                    vec![SpellDescFragment::Value {
88                        value: format_effect_value(value, &effect.var_type),
89                        raw: value,
90                    }]
91                } else {
92                    let token = format!("${}{}", effect.var_type, effect.effect_index);
93
94                    self.warn(format!("Unresolved effect variable: {token}"));
95
96                    vec![SpellDescFragment::Unresolved { token }]
97                }
98            }
99
100            VariableNode::SpellLevel(spell_level) => {
101                let key = build_spell_level_key(&spell_level.var_type, spell_level.index);
102
103                if let Some(value) = self.resolver.get_spell_value(self.self_spell_id, &key) {
104                    if let Ok(num) = value.split_whitespace().next().unwrap_or("").parse::<f64>() {
105                        self.last_number = Some(num);
106                    }
107
108                    if spell_level.var_type == "d" {
109                        if let Some(raw_ms) = parse_duration_ms(&value) {
110                            return vec![SpellDescFragment::Duration { value, raw_ms }];
111                        }
112                    }
113
114                    vec![SpellDescFragment::Text { value }]
115                } else {
116                    let token = format!("${key}");
117
118                    self.warn(format!("Unresolved spell-level variable: {token}"));
119
120                    vec![SpellDescFragment::Unresolved { token }]
121                }
122            }
123
124            VariableNode::Player(player) => {
125                if let Some(value) = self.resolver.get_player_stat(player.var_name()) {
126                    self.last_number = Some(value);
127
128                    vec![SpellDescFragment::Value {
129                        value: format_rendered_number(value, None),
130                        raw: value,
131                    }]
132                } else {
133                    let token = format!("${}", player.var_name());
134
135                    self.warn(format!("Unresolved player stat: {token}"));
136
137                    vec![SpellDescFragment::Unresolved { token }]
138                }
139            }
140
141            VariableNode::CrossSpell(cross) => {
142                if let Some(idx) = cross.effect_index {
143                    if let Some(value) =
144                        self.resolver
145                            .get_effect_value(cross.spell_id, idx, &cross.var_type)
146                    {
147                        self.last_number = Some(value);
148
149                        vec![SpellDescFragment::Value {
150                            value: format_effect_value(value, &cross.var_type),
151                            raw: value,
152                        }]
153                    } else {
154                        let token = format!("${}{}{}", cross.spell_id, cross.var_type, idx);
155
156                        self.warn(format!("Unresolved cross-spell effect: {token}"));
157
158                        vec![SpellDescFragment::Unresolved { token }]
159                    }
160                } else if let Some(value) = self
161                    .resolver
162                    .get_spell_value(cross.spell_id, &cross.var_type)
163                {
164                    vec![SpellDescFragment::Text { value }]
165                } else {
166                    let token = format!("${}{}", cross.spell_id, cross.var_type);
167
168                    self.warn(format!("Unresolved cross-spell value: {token}"));
169
170                    vec![SpellDescFragment::Unresolved { token }]
171                }
172            }
173
174            VariableNode::Custom(custom) => {
175                if let Some(value) = self.resolver.get_custom_var(custom.var_name()) {
176                    self.last_number = Some(value);
177
178                    vec![SpellDescFragment::Value {
179                        value: format_rendered_number(value, None),
180                        raw: value,
181                    }]
182                } else {
183                    let token = format!("$<{}>", custom.var_name());
184
185                    self.warn(format!("Unresolved custom variable: {token}"));
186
187                    vec![SpellDescFragment::Unresolved { token }]
188                }
189            }
190
191            VariableNode::At(at) => self.render_at_variable(at),
192
193            VariableNode::Enchant(enchant) => {
194                if let Some(value) = self
195                    .resolver
196                    .get_spell_value(self.self_spell_id, enchant.var_type())
197                {
198                    vec![SpellDescFragment::Text { value }]
199                } else if let Some(value) = self.resolver.get_player_stat(enchant.var_type()) {
200                    self.last_number = Some(value);
201
202                    vec![SpellDescFragment::Value {
203                        value: format_rendered_number(value, None),
204                        raw: value,
205                    }]
206                } else {
207                    let token = format!("${}", enchant.var_type());
208
209                    self.warn(format!("Unresolved enchant variable: {token}"));
210
211                    vec![SpellDescFragment::Unresolved { token }]
212                }
213            }
214
215            VariableNode::Misc(misc) => {
216                let key = build_misc_key(&misc.var_name, misc.id);
217
218                if let Some(value) = self.resolver.get_spell_value(self.self_spell_id, &key) {
219                    vec![SpellDescFragment::Text { value }]
220                } else if let Some(value) = self.resolver.get_player_stat(&key) {
221                    self.last_number = Some(value);
222
223                    vec![SpellDescFragment::Value {
224                        value: format_rendered_number(value, None),
225                        raw: value,
226                    }]
227                } else {
228                    let token = format!("${key}");
229
230                    self.warn(format!("Unresolved misc variable: {token}"));
231
232                    vec![SpellDescFragment::Unresolved { token }]
233                }
234            }
235        }
236    }
237
238    // #t(rust_cyclomatic_complexity) @ variable dispatch: spelldesc/spellname/spellicon/player stats
239    fn render_at_variable(&mut self, at: &AtVariableNode) -> Vec<SpellDescFragment> {
240        match at.var_type.as_str() {
241            "spelldesc" => {
242                if self.embed_depth >= MAX_EMBED_DEPTH {
243                    self.warn(format!(
244                        "Max embed depth ({MAX_EMBED_DEPTH}) exceeded for @spelldesc"
245                    ));
246
247                    return vec![SpellDescFragment::Text {
248                        value: "[...]".to_string(),
249                    }];
250                }
251
252                if let Some(spell_id) = at.spell_id {
253                    if let Some(desc) = self.resolver.get_spell_description(spell_id) {
254                        let saved_spell_id = self.self_spell_id;
255
256                        self.self_spell_id = spell_id;
257                        self.embed_depth += 1;
258                        let parsed = parse(&desc);
259                        let fragments = self.render_nodes(&parsed.ast.nodes);
260
261                        self.embed_depth -= 1;
262                        self.self_spell_id = saved_spell_id;
263
264                        vec![SpellDescFragment::Embedded {
265                            spell_id,
266                            fragments,
267                        }]
268                    } else {
269                        let token = format!("@spelldesc{spell_id}");
270
271                        self.warn(format!("Could not fetch embedded spell: {spell_id}"));
272
273                        vec![SpellDescFragment::Unresolved { token }]
274                    }
275                } else {
276                    vec![SpellDescFragment::Unresolved {
277                        token: "@spelldesc".to_string(),
278                    }]
279                }
280            }
281
282            "spellname" => {
283                if let Some(spell_id) = at.spell_id {
284                    if let Some(name) = self.resolver.get_spell_name(spell_id) {
285                        vec![SpellDescFragment::SpellName { spell_id, name }]
286                    } else {
287                        let token = format!("@spellname{spell_id}");
288
289                        self.warn(format!("Could not fetch spell name: {spell_id}"));
290
291                        vec![SpellDescFragment::Unresolved { token }]
292                    }
293                } else {
294                    vec![SpellDescFragment::Unresolved {
295                        token: "@spellname".to_string(),
296                    }]
297                }
298            }
299
300            "spellicon" => {
301                if let Some(spell_id) = at.spell_id {
302                    if let Some(path) = self.resolver.get_spell_icon(spell_id) {
303                        vec![SpellDescFragment::Icon { spell_id, path }]
304                    } else {
305                        let token = format!("@spellicon{spell_id}");
306
307                        self.warn(format!("Could not fetch spell icon: {spell_id}"));
308
309                        vec![SpellDescFragment::Unresolved { token }]
310                    }
311                } else {
312                    vec![SpellDescFragment::Unresolved {
313                        token: "@spellicon".to_string(),
314                    }]
315                }
316            }
317
318            "versadmg" | "versaheal" | "versatility" => {
319                if let Some(value) = self.resolver.get_player_stat(&at.var_type) {
320                    self.last_number = Some(value);
321
322                    vec![SpellDescFragment::Value {
323                        value: format!("{}%", format_rendered_number(value, Some(1))),
324                        raw: value,
325                    }]
326                } else {
327                    let token = format!("@{}", at.var_type);
328
329                    self.warn(format!("Unresolved @ variable: {token}"));
330
331                    vec![SpellDescFragment::Unresolved { token }]
332                }
333            }
334
335            _ => {
336                if let Some(value) = self.resolver.get_player_stat(&at.var_type) {
337                    self.last_number = Some(value);
338
339                    vec![SpellDescFragment::Value {
340                        value: format_rendered_number(value, None),
341                        raw: value,
342                    }]
343                } else {
344                    let token = format!("@{}", at.var_type);
345
346                    self.warn(format!("Unresolved @ variable: {token}"));
347
348                    vec![SpellDescFragment::Unresolved { token }]
349                }
350            }
351        }
352    }
353
354    fn render_conditional(&mut self, cond: &ConditionalNode) -> Vec<SpellDescFragment> {
355        for branch in &cond.conditions {
356            if self.evaluate_predicate(&branch.predicate) {
357                return self.render_nodes(&branch.content);
358            }
359        }
360
361        if let Some(else_branch) = &cond.else_branch {
362            self.render_nodes(else_branch)
363        } else {
364            Vec::new()
365        }
366    }
367
368    fn render_pluralization(&self, plural: &PluralizationNode) -> Vec<SpellDescFragment> {
369        let is_singular = self
370            .last_number
371            .is_some_and(|n| (n - 1.0).abs() < f64::EPSILON);
372
373        let result = if is_singular {
374            &plural.singular
375        } else {
376            &plural.plural
377        };
378
379        let value = capitalize_if_needed(result, plural.capitalized);
380
381        vec![SpellDescFragment::Text { value }]
382    }
383
384    fn render_gender(&self, gender: &GenderNode) -> Vec<SpellDescFragment> {
385        let result = if self.resolver.is_male() {
386            &gender.male
387        } else {
388            &gender.female
389        };
390
391        let value = capitalize_if_needed(result, gender.capitalized);
392
393        vec![SpellDescFragment::Text { value }]
394    }
395}