Skip to main content

wowlab_engine_combat/builder/combat_builder/
talents.rs

1use wowlab_engine_domain::rotation::to_simc_key;
2use wowlab_engine_ports::TalentSelection;
3use wowlab_types::sim::IntMap;
4
5use super::{BuilderAuraDef, CombatSystemBuilder};
6use crate::{AuraDefinitionDraft, BuffEffect, BuilderError, LocalAuraIdx};
7
8fn resolved_aura_effects(
9    definitions: &[BuilderAuraDef],
10    source_spell_id: u32,
11    effect_index: u8,
12) -> Vec<BuffEffect> {
13    let mut effects = Vec::new();
14
15    if let Some(source) = definitions
16        .iter()
17        .find(|definition| definition.aura_id == source_spell_id)
18    {
19        for &effect in source.effects.iter().flatten() {
20            if effect.dbc_source() == Some((source_spell_id, effect_index)) {
21                effects.push(effect);
22            }
23        }
24    }
25
26    effects
27}
28
29fn remove_resolved_effect(definition: &mut BuilderAuraDef, source_spell_id: u32, effect_index: u8) {
30    for slot in &mut definition.effects {
31        if slot
32            .as_ref()
33            .and_then(BuffEffect::dbc_source)
34            .is_some_and(|source| source == (source_spell_id, effect_index))
35        {
36            *slot = None;
37        }
38    }
39}
40
41fn aura_effects_full(definition: &BuilderAuraDef) -> BuilderError {
42    crate::builder::BuilderErrorKind::AuraEffectsFull {
43        aura_name: definition.name.clone(),
44        aura_id: definition.aura_id,
45        slot_count: crate::state::MAX_AURA_BUFF_EFFECTS,
46    }
47    .into()
48}
49
50fn copy_resolved_effects(
51    definition: &mut BuilderAuraDef,
52    effects: &[BuffEffect],
53) -> Result<(), BuilderError> {
54    let available = definition
55        .effects
56        .iter()
57        .filter(|slot| slot.is_none())
58        .count();
59
60    if effects.len() > available {
61        return Err(aura_effects_full(definition));
62    }
63
64    for &effect in effects {
65        if crate::state::try_push_effect(&mut definition.effects, effect).is_err() {
66            return Err(aura_effects_full(definition));
67        }
68    }
69
70    Ok(())
71}
72
73impl CombatSystemBuilder {
74    pub fn talent_selections(mut self, talents: &[TalentSelection]) -> Self {
75        self.selected_hero_trees.extend(
76            talents
77                .iter()
78                .filter(|talent| talent.ranks > 0)
79                .filter_map(|talent| talent.hero_tree.clone())
80                .map(String::into_boxed_str),
81        );
82        self.selected_hero_trees.sort_unstable();
83        self.selected_hero_trees.dedup();
84
85        let aura_id_to_name: IntMap<u32, String> = self
86            .aura_ids
87            .iter()
88            .map(|(name, &id)| (id, name.clone()))
89            .collect();
90
91        let spell_id_to_name: IntMap<u32, String> = self
92            .spell_ids
93            .iter()
94            .map(|(name, &id)| (id, name.clone()))
95            .collect();
96
97        for talent in talents {
98            if talent.ranks == 0 {
99                continue;
100            }
101
102            self.register_selected_replacement(talent);
103
104            for id in std::iter::once(talent.spell_id).chain(talent.override_spell()) {
105                self.selected_talent_spell_ids.push(id);
106                self.selected_talent_ranks
107                    .entry(id)
108                    .and_modify(|rank| *rank = (*rank).max(talent.ranks))
109                    .or_insert(talent.ranks);
110
111                if let Some(name) = self.talent_names_by_id.get(&id) {
112                    // #t(rust_clone_in_loop) hints own names independently of the builder's lookup map.
113                    self.hints.talent_ranks.push((name.clone(), talent.ranks));
114                } else if let Some(name) = spell_id_to_name.get(&id) {
115                    self.hints
116                        .talent_ranks
117                        .push((to_simc_key(name), talent.ranks));
118                } else if let Some(name) = aura_id_to_name.get(&id) {
119                    self.hints
120                        .talent_ranks
121                        .push((to_simc_key(name), talent.ranks));
122                }
123            }
124
125            if !talent.precombat_aura {
126                continue;
127            }
128
129            if let Some(local_idx) = self.register_selected_passive_aura(talent.spell_id) {
130                self.precombat_auras.push(local_idx);
131                self.talent_aura_stacks.push((local_idx, talent.ranks));
132            }
133        }
134
135        self
136    }
137
138    pub fn talent_spell_ids(mut self, talents: &[(&str, u32)]) -> Self {
139        for &(name, id) in talents {
140            let key = to_simc_key(name);
141
142            self.talent_spell_ids.push(id);
143            // #t(rust_clone_in_loop) the lookup map and exported hints both own the normalized key.
144            self.talent_names_by_id.insert(id, key.clone());
145            self.hints.talent_ranks.push((key, 0));
146        }
147
148        self
149    }
150
151    pub fn talent_companion_aura(mut self, talent_spell_id: u32, aura: LocalAuraIdx) -> Self {
152        if self.selected_talent_spell_ids.contains(&talent_spell_id)
153            && !self.precombat_auras.contains(&aura)
154        {
155            self.precombat_auras.push(aura);
156        }
157
158        self
159    }
160
161    /// Retains or copies one data-derived aura effect only when `talent_spell_id` is selected.
162    ///
163    /// Call this after [`Self::talent_selections`].
164    /// The source identifies an effect already lowered from resolved game data.
165    /// A different `aura` receives copies that preserve the DBC source coordinate.
166    /// Content does not provide a replacement payload.
167    pub fn talent_gated_aura_effect(
168        mut self,
169        talent_spell_id: u32,
170        aura: LocalAuraIdx,
171        source_spell_id: u32,
172        effect_index: u8,
173    ) -> Self {
174        if self.pending_error.is_some() {
175            return self;
176        }
177
178        let selected = self.selected_talent_spell_ids.contains(&talent_spell_id);
179        let source_effects = resolved_aura_effects(&self.aura_defs, source_spell_id, effect_index);
180        let Some(definition) = self.aura_defs.get_mut(aura.as_usize()) else {
181            self.pending_error = Some(
182                crate::builder::BuilderErrorKind::GameData(
183                    wowlab_engine_ports::EngineError::spec_construction(format!(
184                        "talent-gated DBC effect references missing local aura {}",
185                        aura.raw()
186                    )),
187                )
188                .into(),
189            );
190
191            return self;
192        };
193        let source_is_target = definition.aura_id == source_spell_id;
194
195        if source_is_target {
196            if !selected {
197                remove_resolved_effect(definition, source_spell_id, effect_index);
198            }
199        } else if selected {
200            if let Err(error) = copy_resolved_effects(definition, &source_effects) {
201                self.pending_error = Some(error);
202
203                return self;
204            }
205        }
206
207        if source_effects.is_empty() {
208            let source = wowlab_types::sim::SpellIdx::from_raw(source_spell_id);
209            let max_effect_index = self.game_data.max_effect_index(source);
210
211            if max_effect_index > 0 {
212                self.pending_error = Some(
213                    crate::builder::BuilderErrorKind::GameData(
214                        wowlab_engine_ports::EngineError::spec_construction(format!(
215                            "source aura {source_spell_id} did not lower DBC effect {effect_index} \
216                             for target {} (max effect {max_effect_index}, aura subtype {})",
217                            definition.name,
218                            self.game_data.effect_aura(source, effect_index),
219                        )),
220                    )
221                    .into(),
222                );
223            }
224        }
225
226        self
227    }
228
229    pub fn set_bonus_auras(mut self, spell_ids: &[u32]) -> Self {
230        let aura_id_to_local = self.aura_id_to_local();
231
232        for &spell_id in spell_ids {
233            if let Some(&local_idx) = aura_id_to_local.get(&spell_id) {
234                self.precombat_auras.push(local_idx);
235            }
236        }
237
238        self
239    }
240
241    fn register_selected_replacement(&mut self, talent: &TalentSelection) {
242        let Some(replaced_spell_id) = talent.replaced_spell() else {
243            return;
244        };
245
246        if self
247            .selected_replaced_spell_ids
248            .contains(&replaced_spell_id)
249        {
250            return;
251        }
252
253        self.selected_replaced_spell_ids.push(replaced_spell_id);
254
255        if talent.precombat_aura {
256            return;
257        }
258
259        for spell_id in self.spell_ids.values_mut() {
260            if *spell_id == replaced_spell_id {
261                *spell_id = talent.spell_id;
262            }
263        }
264    }
265
266    fn register_selected_passive_aura(&mut self, spell_id: u32) -> Option<LocalAuraIdx> {
267        if let Some(index) = self
268            .aura_defs
269            .iter()
270            .position(|definition| definition.aura_id == spell_id)
271        {
272            return Some(LocalAuraIdx::new(
273                u8::try_from(index).expect("validated aura count fits in u8"),
274            ));
275        }
276
277        if self
278            .game_data
279            .is_folded_passive(wowlab_types::sim::SpellIdx::from_raw(spell_id))
280        {
281            return None;
282        }
283
284        let name = self
285            .talent_names_by_id
286            .get(&spell_id)
287            .cloned()
288            .unwrap_or_else(|| format!("talent_{spell_id}"));
289        let builder = match AuraDefinitionDraft::new(&name, spell_id)
290            .on_player()
291            .apply_base_from_data(&self.game_data, spell_id)
292        {
293            Ok(builder) => builder,
294            Err(error) => {
295                self.pending_error = Some(error);
296
297                return None;
298            }
299        };
300        let local = LocalAuraIdx::new(
301            u8::try_from(self.aura_defs.len()).expect("validated aura count fits in u8"),
302        );
303
304        self.aura_ids.insert(name, spell_id);
305        self.aura_defs.push(builder.finalize());
306
307        Some(local)
308    }
309
310    fn aura_id_to_local(&self) -> IntMap<u32, LocalAuraIdx> {
311        let mut locals = IntMap::default();
312
313        for (index, definition) in self.aura_defs.iter().enumerate() {
314            let index = u8::try_from(index).expect("validated aura count fits in u8");
315
316            locals.insert(definition.aura_id, LocalAuraIdx::new(index));
317        }
318
319        locals
320    }
321}