Skip to main content

wowlab_parsers/parsers/spell_desc/
effect_copies.rs

1//! Effect values a description copies from another spell at reduced magnitude.
2
3use super::{
4    precision::{power_of_ten, renders_percentage},
5    types::{
6        BinaryOperator, ExpressionBlockNode, ExpressionNode, SpellDescriptionNode, VariableNode,
7    },
8};
9
10/// One percentage a description states as another spell's stored effect value, divided down.
11#[derive(Clone, Copy, Debug, PartialEq)]
12pub struct CrossSpellEffectCopy {
13    /// Spell whose stored effect value the percentage comes from.
14    pub source_spell_id: u32,
15    /// 1-based effect index on the source spell matching the `$<id>sN` reference.
16    pub source_effect: u8,
17    /// Power of ten the stored value carries beyond the applied percentage.
18    pub divisor: f64,
19}
20
21/// Reads the percentages a description states as another spell's effect value, in prose order.
22///
23/// Master of Warfare 1269394 renders `"${$1269306s1/10}.1%"`.
24///   That names both the spell storing the value and the divisor turning the stored integer back
25///   into a percentage.
26///   Its own effects ship at base zero because the client resolves them from this statement.
27///   Only a power-of-ten divisor rendered as a percentage qualifies, for the same reason the
28///   self-referencing form is limited that way.
29///   `${$<id>s1/2}%` is gameplay arithmetic, and `${$<id>s1/1000}` over a duration is a
30///   millisecond conversion the engine already handles in its own units.
31#[must_use]
32pub fn cross_spell_precision_copies(description: &str) -> Vec<CrossSpellEffectCopy> {
33    let parsed = super::parser::parse(description);
34    let mut copies = Vec::new();
35
36    collect_copies(&parsed.ast.nodes, &mut copies);
37
38    copies
39}
40
41// #t(fn: rust_recursive_fn) bounded AST walk over parsed spell description nodes
42fn collect_copies(nodes: &[SpellDescriptionNode], copies: &mut Vec<CrossSpellEffectCopy>) {
43    for (position, node) in nodes.iter().enumerate() {
44        match node {
45            SpellDescriptionNode::ExpressionBlock(block) => {
46                if !renders_percentage(nodes.get(position + 1)) {
47                    continue;
48                }
49
50                if let Some(copy) = cross_spell_copy(block) {
51                    copies.push(copy);
52                }
53            }
54
55            SpellDescriptionNode::Conditional(conditional) => {
56                for branch in &conditional.conditions {
57                    collect_copies(&branch.content, copies);
58                }
59
60                if let Some(else_branch) = &conditional.else_branch {
61                    collect_copies(else_branch, copies);
62                }
63            }
64
65            SpellDescriptionNode::Text(_)
66            | SpellDescriptionNode::Variable(_)
67            | SpellDescriptionNode::Pluralization(_)
68            | SpellDescriptionNode::Gender(_)
69            | SpellDescriptionNode::ColorCode(_) => {}
70        }
71    }
72}
73
74/// Reads the spell's own effect indexes an aura description renders as percentages, in prose order.
75///
76/// The aura description writes the same sentence from the buff's side.
77///   That names the effects the spell description leaves unnamed.
78///   Master of Warfare 1269394 renders `"increased by ${$W2}.1% and ... by ${$W3}.1%"` for the two
79///   values its spell description copies from Master of Warfare 1269306.
80///   Pairing the two orders is what makes the copy target data-stated rather than guessed.
81#[must_use]
82pub fn precision_rendered_effect_indexes(aura_description: &str) -> Vec<u8> {
83    let parsed = super::parser::parse(aura_description);
84    let mut indexes = Vec::new();
85
86    collect_rendered_effects(&parsed.ast.nodes, &mut indexes);
87
88    indexes
89}
90
91// #t(fn: rust_recursive_fn) bounded AST walk over parsed spell description nodes
92fn collect_rendered_effects(nodes: &[SpellDescriptionNode], indexes: &mut Vec<u8>) {
93    for (position, node) in nodes.iter().enumerate() {
94        match node {
95            SpellDescriptionNode::ExpressionBlock(block) => {
96                if !renders_percentage(nodes.get(position + 1)) {
97                    continue;
98                }
99
100                if let Some(index) = own_effect_index(&block.expression) {
101                    indexes.push(index);
102                }
103            }
104
105            SpellDescriptionNode::Conditional(conditional) => {
106                for branch in &conditional.conditions {
107                    collect_rendered_effects(&branch.content, indexes);
108                }
109
110                if let Some(else_branch) = &conditional.else_branch {
111                    collect_rendered_effects(else_branch, indexes);
112                }
113            }
114
115            SpellDescriptionNode::Text(_)
116            | SpellDescriptionNode::Variable(_)
117            | SpellDescriptionNode::Pluralization(_)
118            | SpellDescriptionNode::Gender(_)
119            | SpellDescriptionNode::ColorCode(_) => {}
120        }
121    }
122}
123
124fn cross_spell_copy(block: &ExpressionBlockNode) -> Option<CrossSpellEffectCopy> {
125    let ExpressionNode::Binary(binary) = &block.expression else {
126        return None;
127    };
128
129    if binary.operator != BinaryOperator::Div {
130        return None;
131    }
132
133    let ExpressionNode::Variable(variable) = binary.left.as_ref() else {
134        return None;
135    };
136    let VariableNode::CrossSpell(reference) = variable.as_ref() else {
137        return None;
138    };
139
140    if !reference.var_type.eq_ignore_ascii_case("s") {
141        return None;
142    }
143
144    Some(CrossSpellEffectCopy {
145        source_spell_id: reference.spell_id,
146        source_effect: reference.effect_index?,
147        divisor: power_of_ten(&binary.right)?,
148    })
149}
150
151/// The spell's own 1-based effect index a bare `${$WN}` or `${$sN}` block renders.
152fn own_effect_index(expression: &ExpressionNode) -> Option<u8> {
153    let ExpressionNode::Variable(variable) = expression else {
154        return None;
155    };
156    let VariableNode::Effect(effect) = variable.as_ref() else {
157        return None;
158    };
159
160    effect
161        .var_type
162        .eq_ignore_ascii_case("w")
163        .then_some(effect.effect_index)
164}
165
166#[cfg(test)]
167mod tests;