Skip to main content

wowlab_parsers/parsers/spell_desc/
spec_effects.rs

1//! Effect pairs a description selects between by the player's specialization.
2
3use super::types::{ConditionalNode, SingleConditionNode, SpellDescriptionNode, VariableNode};
4
5/// One `$?cN[$sI][$sJ]` statement: which effect each specialization reads.
6#[derive(Clone, Copy, Debug, PartialEq)]
7pub struct SpecConditionalEffects {
8    /// 1-based specialization index the predicate names, `ChrSpecialization.OrderIndex + 1`.
9    pub spec_index: u8,
10    /// 1-based effect index the named specialization reads.
11    pub matching_effect: u8,
12    /// 1-based effect index every other specialization reads.
13    pub fallback_effect: u8,
14}
15
16impl SpecConditionalEffects {
17    /// The effect index a specialization at `order_index` never reads.
18    #[must_use]
19    pub const fn inert_effect(&self, order_index: u8) -> u8 {
20        if self.spec_index == order_index.saturating_add(1) {
21            self.fallback_effect
22        } else {
23            self.matching_effect
24        }
25    }
26}
27
28/// Reads the effect pairs a description selects between by specialization.
29///
30/// A spell's DBC effects cover every specialization that can learn it.
31///   One passive therefore ships both values, and its description states which one each reads.
32///   No Mercy 472660 is `"Your Bleed effects deal $?c1[$s1][$s2]% increased damage"`.
33///   Overwhelming Shadows 1266883 is `"$?c3[Void Torrent deals $s1%][Mind Blast deals $s2%]"`.
34///   Only an unambiguous statement counts: exactly one specialization predicate, exactly one `$sN`
35///   per branch, and two different effects.
36///   Anything else states nothing usable.
37#[must_use]
38pub fn spec_conditional_effects(description: &str) -> Vec<SpecConditionalEffects> {
39    let parsed = super::parser::parse(description);
40    let mut found = Vec::new();
41
42    collect_from_nodes(&parsed.ast.nodes, &mut found);
43    found.dedup();
44
45    // Two statements naming one effect contradict each other and state nothing usable.
46    let contradicted: Vec<u8> = found
47        .iter()
48        .flat_map(|statement| [statement.matching_effect, statement.fallback_effect])
49        .filter(|effect| {
50            found
51                .iter()
52                .filter(|statement| {
53                    statement.matching_effect == *effect || statement.fallback_effect == *effect
54                })
55                .count()
56                > 1
57        })
58        .collect();
59
60    found.retain(|statement| {
61        !contradicted.contains(&statement.matching_effect)
62            && !contradicted.contains(&statement.fallback_effect)
63    });
64
65    found
66}
67
68// #t(fn: rust_recursive_fn) bounded AST walk over parsed spell description nodes
69fn collect_from_nodes(nodes: &[SpellDescriptionNode], found: &mut Vec<SpecConditionalEffects>) {
70    for node in nodes {
71        let SpellDescriptionNode::Conditional(conditional) = node else {
72            continue;
73        };
74
75        if let Some(statement) = spec_conditional_effects_of(conditional) {
76            found.push(statement);
77        }
78
79        for branch in &conditional.conditions {
80            collect_from_nodes(&branch.content, found);
81        }
82
83        if let Some(else_branch) = &conditional.else_branch {
84            collect_from_nodes(else_branch, found);
85        }
86    }
87}
88
89fn spec_conditional_effects_of(conditional: &ConditionalNode) -> Option<SpecConditionalEffects> {
90    let [branch] = conditional.conditions.as_slice() else {
91        return None;
92    };
93    let [SingleConditionNode::Specialization(spec)] = branch.predicate.conditions.as_slice() else {
94        return None;
95    };
96    let matching_effect = sole_base_value_effect(&branch.content)?;
97    let fallback_effect = sole_base_value_effect(conditional.else_branch.as_deref()?)?;
98
99    if matching_effect == fallback_effect {
100        return None;
101    }
102
103    Some(SpecConditionalEffects {
104        spec_index: spec.spec_index(),
105        matching_effect,
106        fallback_effect,
107    })
108}
109
110/// The single `$sN` a branch renders, if that is the only effect value it names.
111fn sole_base_value_effect(nodes: &[SpellDescriptionNode]) -> Option<u8> {
112    let mut effects = nodes.iter().filter_map(|node| {
113        let SpellDescriptionNode::Variable(VariableNode::Effect(effect)) = node else {
114            return None;
115        };
116
117        effect
118            .var_type
119            .eq_ignore_ascii_case("s")
120            .then_some(effect.effect_index)
121    });
122    let only = effects.next()?;
123
124    effects.next().is_none().then_some(only)
125}
126
127#[cfg(test)]
128mod tests;