Skip to main content

wowlab_engine_combat/builder/combat_builder/build/
finish.rs

1use super::{
2    ActionOverrideRegistration, AuraDefinitionDraft, AuraSubtypeKind, CombatSystemBuilder,
3    CombatSystemDraft, CooldownBypass, CostBypass, EngineError, HeroTalentTreeDesc,
4    ImpactProcRegistration, SpellGate, SpellIdx, SpellOverride, TriggerProgramRegistration,
5    expand_spell_learn_chains, expand_spell_override_chains, lower_builder_state,
6};
7
8pub(in crate::builder::combat_builder) struct DraftBuildResult(
9    Result<CombatSystemDraft, EngineError>,
10);
11
12impl DraftBuildResult {
13    pub(in crate::builder::combat_builder) fn into_result(
14        self,
15    ) -> Result<CombatSystemDraft, EngineError> {
16        self.0
17    }
18}
19
20pub(super) fn apply_hero_tree_spell_gating(
21    trees: &[HeroTalentTreeDesc],
22    selected_tree_names: &[Box<str>],
23    talent_spell_ids: &mut Vec<u32>,
24    selected_talent_spell_ids: &mut Vec<u32>,
25) {
26    for tree in trees {
27        let tree_selected = selected_tree_names
28            .iter()
29            .any(|name| name.eq_ignore_ascii_case(tree.name))
30            || tree
31                .spells
32                .iter()
33                .chain(tree.auras.iter())
34                .any(|&(_, id)| selected_talent_spell_ids.contains(&id));
35
36        for &(_, id) in tree.spells {
37            if !talent_spell_ids.contains(&id) {
38                talent_spell_ids.push(id);
39            }
40
41            if tree_selected && !selected_talent_spell_ids.contains(&id) {
42                selected_talent_spell_ids.push(id);
43            }
44        }
45    }
46}
47
48impl CombatSystemBuilder {
49    // #t(fn: rust_cyclomatic_complexity) sequential build steps have independent validation and registration paths
50    // #t(fn: rust_max_fn_lines) the build method keeps its ordered construction transaction together
51    pub(in crate::builder::combat_builder) fn finish_draft(mut self) -> DraftBuildResult {
52        let result = (|| -> Result<CombatSystemDraft, EngineError> {
53            if let Some(err) = self.pending_error.take() {
54                return Err(err.into());
55            }
56
57            let encounter = self.encounter.take().ok_or_else(|| {
58                EngineError::spec_construction("combat construction requires a resolved encounter")
59            })?;
60
61            let updates: Vec<(String, u32)> = self
62                .spell_ids
63                .iter()
64                .filter_map(|(name, &id)| {
65                    self.game_data
66                        .spell_override(SpellIdx::from_raw(id))
67                        .map(|replacement| (name.clone(), replacement.as_u32()))
68                })
69                .collect();
70
71            for (name, replacement) in updates {
72                self.spell_ids.insert(name, replacement);
73            }
74
75            expand_spell_learn_chains(&mut self.talent_spell_ids, &self.game_data);
76            expand_spell_learn_chains(&mut self.selected_talent_spell_ids, &self.game_data);
77
78            if self.spec_id.is_some() {
79                apply_hero_tree_spell_gating(
80                    self.hero_talent_trees,
81                    &self.selected_hero_trees,
82                    &mut self.talent_spell_ids,
83                    &mut self.selected_talent_spell_ids,
84                );
85
86                for &(name, id) in self
87                    .hero_talent_trees
88                    .iter()
89                    .flat_map(|tree| tree.spells.iter())
90                {
91                    // #t(rust_alloc_in_loop) required for map key
92                    self.spell_ids.entry(name.to_string()).or_insert(id);
93                }
94
95                for &(name, id) in self
96                    .hero_talent_trees
97                    .iter()
98                    .flat_map(|tree| tree.auras.iter())
99                {
100                    // #t(rust_alloc_in_loop) required for map key
101                    self.aura_ids.entry(name.to_string()).or_insert(id);
102                }
103            }
104
105            ActionOverrideRegistration {
106                definition: &mut self,
107            }
108            .apply_dbc_action_overrides();
109            self.register_class_target_debuffs();
110            self.apply_selected_triggered_aura_values();
111            TriggerProgramRegistration {
112                definition: &mut self,
113            }
114            .auto_register_dbc_trigger_programs();
115            ImpactProcRegistration {
116                definition: &mut self,
117            }
118            .auto_register_dbc_impact_procs();
119
120            let gating: Vec<SpellGate> = self
121                .spells
122                .iter()
123                .filter(|s| s.gating_aura_id != 0)
124                .map(|s| SpellGate {
125                    spell_id: s.spell_id,
126                    aura_id: s.gating_aura_id,
127                    min_stacks: s.gating_aura_min_stacks,
128                })
129                .collect();
130
131            let cost_bypass: Vec<CostBypass> = self
132                .spells
133                .iter()
134                .filter(|s| s.cost_bypass_aura_id != 0)
135                .map(|s| CostBypass {
136                    spell_id: s.spell_id,
137                    aura_id: s.cost_bypass_aura_id,
138                })
139                .collect();
140
141            let cooldown_bypass: Vec<CooldownBypass> = self
142                .spells
143                .iter()
144                .filter(|s| s.cooldown_bypass_aura_id != 0)
145                .map(|s| CooldownBypass {
146                    spell_id: s.spell_id,
147                    aura_id: s.cooldown_bypass_aura_id,
148                })
149                .collect();
150
151            let spell_overrides: Vec<SpellOverride> = self
152                .spells
153                .iter()
154                .filter(|spell| spell.override_aura_id != 0 && spell.override_spell_id != 0)
155                .map(|spell| SpellOverride {
156                    spell_id: spell.spell_id,
157                    replacement_spell_id: spell.override_spell_id,
158                    aura_id: spell.override_aura_id,
159                    shares_base_cooldown: spell.override_shares_base_cooldown,
160                })
161                .collect();
162
163            self.hints.primary_resource =
164                (!self.resource_name.is_empty()).then(|| self.resource_name.clone());
165
166            // Dual wield: an equipped off-hand weapon swings on its own timer. The swing loop
167            // maps non-pet defs to hands in order (first = main hand, second = off hand), so a
168            // single-player-def spec gets its off-hand swing synthesized from the main-hand def.
169            let player_aa_count = self.auto_attacks.iter().filter(|aa| !aa.is_pet).count();
170
171            if player_aa_count == 1
172                && self.game_data.off_hand().is_some_and(|oh| oh.speed_ms > 0)
173                && let Some(mh_def) = self.auto_attacks.iter().find(|aa| !aa.is_pet).cloned()
174            {
175                self.auto_attacks.push(mh_def);
176            }
177
178            let mut names: Vec<Box<str>> = Vec::new();
179            let parts = lower_builder_state(
180                &self.spells,
181                &self.aura_defs,
182                &self.auto_attacks,
183                &self.impact_effect_procs,
184                &mut names,
185            );
186            let resource_max = self.resource_max;
187
188            let talent_ranks = std::mem::take(&mut self.hints.talent_ranks);
189            let mut talent_spell_ids = std::mem::take(&mut self.talent_spell_ids);
190            let mut selected_talent_spell_ids = std::mem::take(&mut self.selected_talent_spell_ids);
191            let selected_replaced_spell_ids = std::mem::take(&mut self.selected_replaced_spell_ids);
192
193            expand_spell_override_chains(&mut talent_spell_ids, &self.game_data);
194            expand_spell_override_chains(&mut selected_talent_spell_ids, &self.game_data);
195
196            let (definitions, indexes, runtime, config) = self
197                .assemble_draft(parts, &selected_talent_spell_ids, encounter)
198                .into_result()?;
199
200            Ok(CombatSystemDraft {
201                spec_identity_brand: self.spec_identity_brand,
202                definitions,
203                indexes,
204                runtime,
205                config,
206                names,
207                spell_ids: std::mem::take(&mut self.spell_ids),
208                damage_spell_ids: std::mem::take(&mut self.damage_spell_ids),
209                effect_spell_ids: std::mem::take(&mut self.effect_spell_ids),
210                generated_damage_identities: std::mem::take(&mut self.generated_damage_identities),
211                aura_ids: std::mem::take(&mut self.aura_ids),
212                hints: std::mem::take(&mut self.hints),
213                gating,
214                cost_bypass,
215                cooldown_bypass,
216                overrides: spell_overrides,
217                resource_max,
218                talent_ranks,
219                talent_spell_ids,
220                selected_talent_spell_ids,
221                selected_replaced_spell_ids,
222                disabled_spell_ids: Vec::new(),
223                initial_weapon_imbues: Vec::new(),
224                ground_effects: Vec::new(),
225                hook_flat_damage: wowlab_types::sim::FastMap::default(),
226                hook_scalars: wowlab_types::sim::FastMap::default(),
227            })
228        })();
229
230        DraftBuildResult(result)
231    }
232
233    /// Registers the class's self-provided damage-taken debuffs as permanent target auras.
234    ///
235    /// A lone monk still applies Mystic Touch and a lone demon hunter still brands its target.
236    /// External raid buffs do not suppress these self-provided debuffs.
237    /// The debuff's DBC duration is infinite, so a precombat application is its whole lifecycle.
238    fn register_class_target_debuffs(&mut self) {
239        let debuffs: Vec<SpellIdx> = self.game_data.class_target_debuffs().to_vec();
240
241        for debuff in debuffs {
242            if self
243                .aura_defs
244                .iter()
245                .any(|aura| aura.aura_id == debuff.as_u32())
246            {
247                continue;
248            }
249
250            // #t(rust_alloc_in_loop) each registered aura requires its own stable map key
251            let name = format!("class debuff {}", debuff.as_u32());
252            let draft = match AuraDefinitionDraft::new(&name, debuff.as_u32())
253                .apply_base_from_data(&self.game_data, debuff.as_u32())
254            {
255                Ok(draft) => draft.on_target(),
256                Err(error) => {
257                    self.pending_error = Some(error);
258
259                    return;
260                }
261            };
262            let Ok(index) = u8::try_from(self.aura_defs.len()) else {
263                return;
264            };
265
266            // #t(rust_log_in_loop) each construction-time registration identifies the exact class aura
267            tracing::debug!(
268                aura_id = debuff.as_u32(),
269                local_index = index,
270                "registered self-provided class damage-taken debuff"
271            );
272            self.aura_ids.insert(name, debuff.as_u32());
273            self.aura_defs.push(draft.finalize());
274            self.precombat_auras
275                .push(crate::state::LocalAuraIdx::new(index));
276        }
277    }
278
279    fn apply_selected_triggered_aura_values(&mut self) {
280        let mut forwarded = std::collections::BTreeMap::<u32, Option<(u32, f64)>>::new();
281
282        for &driver_spell_id in &self.selected_talent_spell_ids {
283            let driver = SpellIdx::from_raw(driver_spell_id);
284
285            for effect_index in 1..=self.game_data.max_effect_index(driver) {
286                if self.game_data.effect_aura(driver, effect_index)
287                    != AuraSubtypeKind::TriggerSpellWithValue as i32
288                {
289                    continue;
290                }
291
292                let Some(triggered) = self.game_data.trigger_spell(driver, effect_index) else {
293                    continue;
294                };
295                let value = self.game_data.base_points(driver, effect_index);
296                let entry = forwarded
297                    .entry(triggered.as_u32())
298                    .or_insert(Some((driver_spell_id, value)));
299
300                if entry.is_some_and(|(_, existing)| existing.to_bits() != value.to_bits()) {
301                    *entry = None;
302                }
303            }
304        }
305
306        for (aura_id, source) in forwarded {
307            let Some((driver_spell_id, value)) = source else {
308                // #t(rust_log_in_loop) each conflict identifies the exact forwarded aura value
309                tracing::debug!(
310                    aura_id,
311                    "DBC_TRIGGER_VALUE_CONFLICT: selected drivers forward different values"
312                );
313                continue;
314            };
315            let Some(aura) = self
316                .aura_defs
317                .iter_mut()
318                .find(|aura| aura.aura_id == aura_id)
319            else {
320                continue;
321            };
322            let patched = crate::builder::aura_builder::apply_forwarded_effect_value(
323                aura.aura_id,
324                &mut aura.effects,
325                &self.game_data,
326                value,
327            );
328
329            if patched > 0 {
330                // #t(rust_log_in_loop) each patch identifies the exact driver-to-aura rewrite
331                tracing::debug!(
332                    driver_spell_id,
333                    aura_id,
334                    value,
335                    patched,
336                    "forwarded selected trigger value into registered aura effects"
337                );
338            }
339        }
340    }
341}