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///   `SimC` writes the same selection by hand per specialization through `deregister_passive_spell`
35///   and its per-spec effect masks.
36///   Only an unambiguous statement counts: exactly one specialization predicate, exactly one `$sN`
37///   per branch, and two different effects.
38///   Anything else states nothing usable.
39#[must_use]
40pub fn spec_conditional_effects(description: &str) -> Vec<SpecConditionalEffects> {
41    let parsed = super::parser::parse(description);
42    let mut found = Vec::new();
43
44    collect_from_nodes(&parsed.ast.nodes, &mut found);
45    found.dedup();
46
47    // Two statements naming one effect contradict each other and state nothing usable.
48    let contradicted: Vec<u8> = found
49        .iter()
50        .flat_map(|statement| [statement.matching_effect, statement.fallback_effect])
51        .filter(|effect| {
52            found
53                .iter()
54                .filter(|statement| {
55                    statement.matching_effect == *effect || statement.fallback_effect == *effect
56                })
57                .count()
58                > 1
59        })
60        .collect();
61
62    found.retain(|statement| {
63        !contradicted.contains(&statement.matching_effect)
64            && !contradicted.contains(&statement.fallback_effect)
65    });
66
67    found
68}
69
70// #t(fn: rust_recursive_fn) bounded AST walk over parsed spell description nodes
71fn collect_from_nodes(nodes: &[SpellDescriptionNode], found: &mut Vec<SpecConditionalEffects>) {
72    for node in nodes {
73        let SpellDescriptionNode::Conditional(conditional) = node else {
74            continue;
75        };
76
77        if let Some(statement) = spec_conditional_effects_of(conditional) {
78            found.push(statement);
79        }
80
81        for branch in &conditional.conditions {
82            collect_from_nodes(&branch.content, found);
83        }
84
85        if let Some(else_branch) = &conditional.else_branch {
86            collect_from_nodes(else_branch, found);
87        }
88    }
89}
90
91fn spec_conditional_effects_of(conditional: &ConditionalNode) -> Option<SpecConditionalEffects> {
92    let [branch] = conditional.conditions.as_slice() else {
93        return None;
94    };
95    let [SingleConditionNode::Specialization(spec)] = branch.predicate.conditions.as_slice() else {
96        return None;
97    };
98    let matching_effect = sole_base_value_effect(&branch.content)?;
99    let fallback_effect = sole_base_value_effect(conditional.else_branch.as_deref()?)?;
100
101    if matching_effect == fallback_effect {
102        return None;
103    }
104
105    Some(SpecConditionalEffects {
106        spec_index: spec.spec_index(),
107        matching_effect,
108        fallback_effect,
109    })
110}
111
112/// The single `$sN` a branch renders, if that is the only effect value it names.
113fn sole_base_value_effect(nodes: &[SpellDescriptionNode]) -> Option<u8> {
114    let mut effects = nodes.iter().filter_map(|node| {
115        let SpellDescriptionNode::Variable(VariableNode::Effect(effect)) = node 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    let only = effects.next()?;
125
126    effects.next().is_none().then_some(only)
127}
128
129#[cfg(test)]
130mod tests;