Skip to main content

wowlab_parsers/parsers/spell_desc/
crit_scaling.rs

1//! Crit damage a description states as a share of the caster's own critical strike chance.
2
3use super::types::{SpellDescriptionNode, VariableNode};
4
5/// The prose that follows the percentage variable when the value is a share of critical strike chance.
6const CRIT_CHANCE_SHARE: &str = "% of your critical strike chance";
7
8/// Reads the effect index whose value is a percentage of the caster's own critical strike chance.
9///
10/// The shape is `"... critical strike damage ... $sN% of your critical strike chance"`.
11///   The effect the percentage lands on is not named by the description; it is the spell's own
12///   crit-damage effect, which ships with a base value of zero precisely because the client
13///   computes the real one from this statement.
14///   Only `$sN` qualifies: a cross-spell `$<id>sN` states another spell's value, and `$mN`/`$oN`
15///   are not the base value the game scales.
16///   `None` means the description makes no such statement.
17#[must_use]
18pub fn crit_chance_scaled_effect_index(description: &str) -> Option<u8> {
19    let parsed = super::parser::parse(description);
20    let mut found = Vec::new();
21
22    collect_from_nodes(&parsed.ast.nodes, &mut found);
23    // A description that names two different effects for one statement states nothing usable.
24    found.dedup();
25
26    match found.as_slice() {
27        [only] => Some(*only),
28        _ => None,
29    }
30}
31
32// #t(fn: rust_recursive_fn) bounded AST walk over parsed spell description nodes
33fn collect_from_nodes(nodes: &[SpellDescriptionNode], found: &mut Vec<u8>) {
34    for (position, node) in nodes.iter().enumerate() {
35        match node {
36            SpellDescriptionNode::Variable(variable) => {
37                let VariableNode::Effect(effect) = variable else {
38                    continue;
39                };
40
41                if !effect.var_type.eq_ignore_ascii_case("s")
42                    || !states_crit_chance_share(nodes.get(position + 1))
43                {
44                    continue;
45                }
46
47                found.push(effect.effect_index);
48            }
49
50            SpellDescriptionNode::Conditional(conditional) => {
51                for branch in &conditional.conditions {
52                    collect_from_nodes(&branch.content, found);
53                }
54
55                if let Some(else_branch) = &conditional.else_branch {
56                    collect_from_nodes(else_branch, found);
57                }
58            }
59
60            SpellDescriptionNode::Text(_)
61            | SpellDescriptionNode::ExpressionBlock(_)
62            | SpellDescriptionNode::Pluralization(_)
63            | SpellDescriptionNode::Gender(_)
64            | SpellDescriptionNode::ColorCode(_) => {}
65        }
66    }
67}
68
69fn states_crit_chance_share(node: Option<&SpellDescriptionNode>) -> bool {
70    matches!(node, Some(SpellDescriptionNode::Text(text))
71        if text.value().to_ascii_lowercase().starts_with(CRIT_CHANCE_SHARE))
72}
73
74#[cfg(test)]
75mod tests;