Skip to main content

wowlab_parsers/parsers/spell_desc/renderer/
evaluate.rs

1use super::{
2    BINARY_ARGS, FLOAT_EPSILON, RenderContext, TERNARY_ARGS, build_misc_key, build_spell_level_key,
3};
4use crate::parsers::{
5    formatting::{NearIntegerStrategy, format_decimal_trimmed},
6    spell_desc::{
7        resolver::SpellDescResolver,
8        types::{
9            BinaryOperator, ConditionalPredicateNode, ExpressionNode, SingleConditionNode,
10            UnaryOperator, VariableNode,
11        },
12    },
13};
14
15const DEFAULT_DECIMAL_PLACES: u8 = 2;
16
17impl<R> RenderContext<'_, R>
18where
19    R: SpellDescResolver,
20{
21    pub(super) fn evaluate_predicate(&self, predicate: &ConditionalPredicateNode) -> bool {
22        predicate
23            .conditions
24            .iter()
25            .any(|c| self.evaluate_condition(c))
26    }
27
28    // #t(fn: rust_floating_point_eq) exact zero-check is intentional division-by-zero guard
29    pub(super) fn evaluate_expression(&self, expr: &ExpressionNode) -> f64 {
30        match expr {
31            ExpressionNode::Number(num) => num.value(),
32
33            ExpressionNode::Variable(var) => self.evaluate_variable(var),
34
35            ExpressionNode::Binary(binary) => {
36                let left = self.evaluate_expression(&binary.left);
37                let right = self.evaluate_expression(&binary.right);
38
39                match binary.operator {
40                    BinaryOperator::Add => left + right,
41                    BinaryOperator::Sub => left - right,
42                    BinaryOperator::Mul => left * right,
43                    BinaryOperator::Div => {
44                        if right == 0.0 {
45                            0.0
46                        } else {
47                            left / right
48                        }
49                    }
50                }
51            }
52
53            ExpressionNode::Unary(unary) => {
54                let operand = self.evaluate_expression(&unary.operand);
55
56                match unary.operator {
57                    UnaryOperator::Neg => -operand,
58                }
59            }
60
61            ExpressionNode::FunctionCall(call) => {
62                self.evaluate_function(&call.func_name, &call.args)
63            }
64
65            ExpressionNode::Paren(paren) => self.evaluate_expression(&paren.expression),
66        }
67    }
68
69    fn evaluate_condition(&self, condition: &SingleConditionNode) -> bool {
70        match condition {
71            SingleConditionNode::SpellKnown(spell_known) => {
72                self.resolver.knows_spell(spell_known.spell_id())
73            }
74
75            SingleConditionNode::Aura(aura) => self.resolver.has_aura(aura.aura_id()),
76
77            SingleConditionNode::Specialization(spec) => {
78                self.resolver.is_specialization(spec.spec_index())
79            }
80
81            // WHY: `$?pc<id>` PlayerCondition has no data source, so it is a no-op (else-branch wins).
82            SingleConditionNode::PlayerCondition(_) => false,
83
84            SingleConditionNode::Expression(expr) => {
85                self.evaluate_condition_expression(&expr.func_name, &expr.args)
86            }
87        }
88    }
89
90    // #t(fn: rust_floating_point_eq) exact zero-check is intentional for condition evaluation semantics
91    fn evaluate_condition_expression(&self, func_name: &str, args: &[ExpressionNode]) -> bool {
92        let values: Vec<f64> = args
93            .iter()
94            .map(|arg| self.evaluate_expression(arg))
95            .collect();
96
97        match func_name {
98            "gt" => values.len() >= BINARY_ARGS && values[0] > values[1],
99            "gte" => values.len() >= BINARY_ARGS && values[0] >= values[1],
100            "lt" => values.len() >= BINARY_ARGS && values[0] < values[1],
101            "lte" => values.len() >= BINARY_ARGS && values[0] <= values[1],
102            "eq" => values.len() >= BINARY_ARGS && (values[0] - values[1]).abs() < f64::EPSILON,
103            "cond" => !values.is_empty() && values[0] != 0.0,
104            _ => false,
105        }
106    }
107
108    fn evaluate_variable(&self, var: &VariableNode) -> f64 {
109        match var {
110            VariableNode::Effect(effect) => self
111                .resolver
112                .get_effect_value(self.self_spell_id, effect.effect_index, &effect.var_type)
113                .unwrap_or(0.0),
114
115            VariableNode::SpellLevel(spell_level) => {
116                let key = build_spell_level_key(&spell_level.var_type, spell_level.index);
117
118                self.resolver
119                    .get_spell_value(self.self_spell_id, &key)
120                    .and_then(|s| s.split_whitespace().next()?.parse().ok())
121                    .unwrap_or(0.0)
122            }
123
124            VariableNode::Player(player) => self
125                .resolver
126                .get_player_stat(player.var_name())
127                .unwrap_or(0.0),
128
129            VariableNode::CrossSpell(cross) => {
130                if let Some(idx) = cross.effect_index {
131                    self.resolver
132                        .get_effect_value(cross.spell_id, idx, &cross.var_type)
133                        .unwrap_or(0.0)
134                } else {
135                    self.resolver
136                        .get_spell_value(cross.spell_id, &cross.var_type)
137                        .and_then(|s| s.split_whitespace().next()?.parse().ok())
138                        .unwrap_or(0.0)
139                }
140            }
141
142            VariableNode::Custom(custom) => self
143                .resolver
144                .get_custom_var(custom.var_name())
145                .unwrap_or(0.0),
146
147            VariableNode::At(at) => self.resolver.get_player_stat(&at.var_type).unwrap_or(0.0),
148
149            VariableNode::Enchant(enchant) => self
150                .resolver
151                .get_spell_value(self.self_spell_id, enchant.var_type())
152                .and_then(|s| s.split_whitespace().next()?.parse().ok())
153                .or_else(|| self.resolver.get_player_stat(enchant.var_type()))
154                .unwrap_or(0.0),
155
156            VariableNode::Misc(misc) => {
157                let key = build_misc_key(&misc.var_name, misc.id);
158
159                self.resolver
160                    .get_spell_value(self.self_spell_id, &key)
161                    .and_then(|s| s.split_whitespace().next()?.parse().ok())
162                    .or_else(|| self.resolver.get_player_stat(&key))
163                    .unwrap_or(0.0)
164            }
165        }
166    }
167
168    // #t(fn: rust_floating_point_eq) exact zero-check is intentional for condition evaluation semantics
169    // #t(rust_cyclomatic_complexity) math function dispatch: max/min/floor/ceil/cond/gt/gte/lt/lte
170    fn evaluate_function(&self, func_name: &str, args: &[ExpressionNode]) -> f64 {
171        let values: Vec<f64> = args
172            .iter()
173            .map(|arg| self.evaluate_expression(arg))
174            .collect();
175
176        match func_name {
177            "max" => values.iter().copied().fold(f64::NEG_INFINITY, f64::max),
178            "min" => values.iter().copied().fold(f64::INFINITY, f64::min),
179            "floor" => values.first().map_or(0.0, |v| v.floor()),
180            "ceil" => values.first().map_or(0.0, |v| v.ceil()),
181            "abs" => values.first().map_or(0.0, |v| v.abs()),
182            // #t(block: rust_magic_numbers) argument indices for ternary functions
183            "clamp" => {
184                if values.len() >= TERNARY_ARGS {
185                    values[0].clamp(values[1], values[2])
186                } else {
187                    values.first().copied().unwrap_or(0.0)
188                }
189            }
190            // #t(block: rust_magic_numbers) argument indices for ternary cond function
191            "cond" => {
192                if values.len() >= TERNARY_ARGS {
193                    if values[0] == 0.0 {
194                        values[2]
195                    } else {
196                        values[1]
197                    }
198                } else {
199                    0.0
200                }
201            }
202            "gt" if values.len() >= BINARY_ARGS && values[0] > values[1] => 1.0,
203            "gte" if values.len() >= BINARY_ARGS && values[0] >= values[1] => 1.0,
204            "lt" if values.len() >= BINARY_ARGS && values[0] < values[1] => 1.0,
205            "lte" if values.len() >= BINARY_ARGS && values[0] <= values[1] => 1.0,
206            _ => 0.0,
207        }
208    }
209}
210
211pub(super) fn format_rendered_number(value: f64, decimal_places: Option<u8>) -> String {
212    match decimal_places {
213        Some(places) => format!("{:.1$}", value, places as usize),
214        None => format_decimal_trimmed(
215            value,
216            DEFAULT_DECIMAL_PLACES,
217            NearIntegerStrategy::NearestSaturating {
218                epsilon: FLOAT_EPSILON,
219            },
220        ),
221    }
222}
223
224pub(super) fn format_effect_value(value: f64, var_type: &str) -> String {
225    match var_type {
226        "t" => format!("{value}"),
227        "bc" => format!("{}%", format_rendered_number(value * 100.0, Some(1))),
228        _ => format_rendered_number(value, None),
229    }
230}