wowlab_engine_combat/builder/combat_builder/build/
finish.rs1use 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 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 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 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 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 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 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 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 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 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}