Skip to main content

wowlab_parsers/parsers/spell_desc/
precision.rs

1//! Decimal-precision scales a description states for its own effect values.
2
3use super::types::{BinaryOperator, ExpressionNode, SpellDescriptionNode, VariableNode};
4
5/// The smallest divisor that carries a decimal digit; `${$s1/1}` states no extra precision.
6const MIN_PRECISION_DIVISOR: f64 = 10.0;
7/// Guards the power-of-ten walk against a malformed literal.
8const MAX_PRECISION_DIVISOR: f64 = 1_000_000.0;
9
10/// One effect value the description renders at reduced magnitude.
11#[derive(Clone, Copy, Debug, PartialEq)]
12pub struct EffectPrecisionScale {
13    /// 1-based effect index, matching `$sN` and `SimC`'s `effectN`.
14    pub effect_index: u8,
15    /// Power of ten the stored base value carries beyond the applied percentage.
16    pub divisor: f64,
17}
18
19/// Reads the decimal precision a description states for its own effect base values.
20///
21/// A percentage rendered as `${$sN/10}.1%` stores one extra decimal digit.
22///   The game applies `base_points / 10` for it.
23///   Only powers of ten qualify, because `${$s1/2}%` is gameplay arithmetic — a split, a halving —
24///   rather than a statement about stored precision.
25///   The `%` right after the block is what makes the reading unambiguous, since the same
26///   `${$s1/1000}` over a duration is a millisecond-to-second conversion the engine already handles
27///   in its own units.
28#[must_use]
29pub fn effect_precision_scales(description: &str) -> Vec<EffectPrecisionScale> {
30    let parsed = super::parser::parse(description);
31    let mut scales = Vec::new();
32
33    collect_from_nodes(&parsed.ast.nodes, &mut scales);
34    scales.sort_unstable_by_key(|scale| scale.effect_index);
35    scales.dedup();
36
37    // A description that states two different divisors for one effect states nothing usable.
38    let ambiguous: Vec<u8> = scales
39        .iter()
40        .filter(|scale| {
41            scales
42                .iter()
43                .filter(|other| other.effect_index == scale.effect_index)
44                .count()
45                > 1
46        })
47        .map(|scale| scale.effect_index)
48        .collect();
49
50    scales.retain(|scale| !ambiguous.contains(&scale.effect_index));
51
52    scales
53}
54
55// #t(fn: rust_recursive_fn) bounded AST walk over parsed spell description nodes
56fn collect_from_nodes(nodes: &[SpellDescriptionNode], scales: &mut Vec<EffectPrecisionScale>) {
57    for (position, node) in nodes.iter().enumerate() {
58        match node {
59            SpellDescriptionNode::ExpressionBlock(block) => {
60                if !renders_percentage(nodes.get(position + 1)) {
61                    continue;
62                }
63
64                if let Some(scale) = precision_scale(&block.expression) {
65                    scales.push(scale);
66                }
67            }
68
69            SpellDescriptionNode::Conditional(conditional) => {
70                for branch in &conditional.conditions {
71                    collect_from_nodes(&branch.content, scales);
72                }
73
74                if let Some(else_branch) = &conditional.else_branch {
75                    collect_from_nodes(else_branch, scales);
76                }
77            }
78
79            SpellDescriptionNode::Text(_)
80            | SpellDescriptionNode::Variable(_)
81            | SpellDescriptionNode::Pluralization(_)
82            | SpellDescriptionNode::Gender(_)
83            | SpellDescriptionNode::ColorCode(_) => {}
84        }
85    }
86}
87
88/// Whether the text right after an expression block starts the percentage it renders.
89pub(super) fn renders_percentage(node: Option<&SpellDescriptionNode>) -> bool {
90    matches!(node, Some(SpellDescriptionNode::Text(text)) if text.value().starts_with('%'))
91}
92
93fn precision_scale(expression: &ExpressionNode) -> Option<EffectPrecisionScale> {
94    let ExpressionNode::Binary(binary) = expression else {
95        return None;
96    };
97
98    if binary.operator != BinaryOperator::Div {
99        return None;
100    }
101
102    let effect_index = base_value_effect_index(&binary.left)?;
103    let divisor = power_of_ten(&binary.right)?;
104
105    Some(EffectPrecisionScale {
106        effect_index,
107        divisor,
108    })
109}
110
111fn base_value_effect_index(expression: &ExpressionNode) -> Option<u8> {
112    let ExpressionNode::Variable(variable) = expression else {
113        return None;
114    };
115    let VariableNode::Effect(effect) = variable.as_ref() else {
116        return None;
117    };
118
119    effect
120        .var_type
121        .eq_ignore_ascii_case("s")
122        .then_some(effect.effect_index)
123}
124
125/// The power-of-ten divisor a `${… / N}` block states, if `N` is one.
126pub(super) fn power_of_ten(expression: &ExpressionNode) -> Option<f64> {
127    let ExpressionNode::Number(number) = expression else {
128        return None;
129    };
130    let value = number.value();
131    let mut candidate = MIN_PRECISION_DIVISOR;
132
133    while candidate <= MAX_PRECISION_DIVISOR {
134        if (value - candidate).abs() < f64::EPSILON {
135            return Some(candidate);
136        }
137
138        candidate *= MIN_PRECISION_DIVISOR;
139    }
140
141    None
142}
143
144#[cfg(test)]
145mod tests;