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