wowlab_engine_combat/builder/combat_builder/build/
finish.rs1use wowlab_types::sim::Rotation;
2
3use super::{
4 ActionOverrideRegistration, AuraDefinitionDraft, AuraSubtypeKind, BuiltCombatSystem,
5 CombatBuildError, CombatSystemBuilder, CooldownBypass, CostBypass, EngineError,
6 HeroTalentTreeDesc, ImpactProcRegistration, InitialBufferConfig, RotationCompileInput,
7 SpellGate, SpellIdx, SpellOverride, TriggerProgramRegistration, compile_rotation,
8 ensure_aura_slots, expand_spell_learn_chains, expand_spell_override_chains,
9 lower_builder_state, populate_initial_buffer,
10};
11
12pub(in crate::builder::combat_builder) struct CombatBuildResult(
13 Result<BuiltCombatSystem, CombatBuildError>,
14);
15
16impl CombatBuildResult {
17 pub(in crate::builder::combat_builder) fn into_result(
18 self,
19 ) -> Result<BuiltCombatSystem, CombatBuildError> {
20 self.0
21 }
22}
23
24pub(super) fn apply_hero_tree_spell_gating(
25 trees: &[HeroTalentTreeDesc],
26 selected_tree_names: &[Box<str>],
27 talent_spell_ids: &mut Vec<u32>,
28 selected_talent_spell_ids: &mut Vec<u32>,
29) {
30 for tree in trees {
31 let tree_selected = selected_tree_names
32 .iter()
33 .any(|name| name.eq_ignore_ascii_case(tree.name))
34 || tree
35 .spells
36 .iter()
37 .chain(tree.auras.iter())
38 .any(|&(_, id)| selected_talent_spell_ids.contains(&id));
39
40 for &(_, id) in tree.spells {
41 if !talent_spell_ids.contains(&id) {
42 talent_spell_ids.push(id);
43 }
44
45 if tree_selected && !selected_talent_spell_ids.contains(&id) {
46 selected_talent_spell_ids.push(id);
47 }
48 }
49 }
50}
51
52impl CombatSystemBuilder {
53 pub(in crate::builder::combat_builder) fn finish_build(
56 mut self,
57 rotation: &Rotation,
58 ) -> CombatBuildResult {
59 let result = (|| -> Result<BuiltCombatSystem, CombatBuildError> {
60 if let Some(err) = self.pending_error.take() {
61 return Err(err.into());
62 }
63
64 let encounter = self.encounter.take().ok_or_else(|| {
65 EngineError::spec_construction("combat construction requires a resolved encounter")
66 })?;
67
68 let updates: Vec<(String, u32)> = self
69 .spell_ids
70 .iter()
71 .filter_map(|(name, &id)| {
72 self.game_data
73 .spell_override(SpellIdx::from_raw(id))
74 .map(|replacement| (name.clone(), replacement.as_u32()))
75 })
76 .collect();
77
78 for (name, replacement) in updates {
79 self.spell_ids.insert(name, replacement);
80 }
81
82 expand_spell_learn_chains(&mut self.talent_spell_ids, &self.game_data);
83 expand_spell_learn_chains(&mut self.selected_talent_spell_ids, &self.game_data);
84
85 if self.spec_id.is_some() {
86 apply_hero_tree_spell_gating(
87 self.hero_talent_trees,
88 &self.selected_hero_trees,
89 &mut self.talent_spell_ids,
90 &mut self.selected_talent_spell_ids,
91 );
92
93 for &(name, id) in self
94 .hero_talent_trees
95 .iter()
96 .flat_map(|tree| tree.spells.iter())
97 {
98 self.spell_ids.entry(name.to_string()).or_insert(id);
100 }
101
102 for &(name, id) in self
103 .hero_talent_trees
104 .iter()
105 .flat_map(|tree| tree.auras.iter())
106 {
107 self.aura_ids.entry(name.to_string()).or_insert(id);
109 }
110 }
111
112 ActionOverrideRegistration {
113 definition: &mut self,
114 }
115 .apply_dbc_action_overrides();
116 self.register_class_target_debuffs();
117 self.apply_selected_triggered_aura_values();
118 TriggerProgramRegistration {
119 definition: &mut self,
120 }
121 .auto_register_dbc_trigger_programs();
122 ImpactProcRegistration {
123 definition: &mut self,
124 }
125 .auto_register_dbc_impact_procs();
126
127 let gating: Vec<SpellGate> = self
128 .spells
129 .iter()
130 .filter(|s| s.gating_aura_id != 0)
131 .map(|s| SpellGate {
132 spell_id: s.spell_id,
133 aura_id: s.gating_aura_id,
134 min_stacks: s.gating_aura_min_stacks,
135 })
136 .collect();
137
138 let cost_bypass: Vec<CostBypass> = self
139 .spells
140 .iter()
141 .filter(|s| s.cost_bypass_aura_id != 0)
142 .map(|s| CostBypass {
143 spell_id: s.spell_id,
144 aura_id: s.cost_bypass_aura_id,
145 })
146 .collect();
147
148 let cooldown_bypass: Vec<CooldownBypass> = self
149 .spells
150 .iter()
151 .filter(|s| s.cooldown_bypass_aura_id != 0)
152 .map(|s| CooldownBypass {
153 spell_id: s.spell_id,
154 aura_id: s.cooldown_bypass_aura_id,
155 })
156 .collect();
157
158 let spell_overrides: Vec<SpellOverride> = self
159 .spells
160 .iter()
161 .filter(|spell| spell.override_aura_id != 0 && spell.override_spell_id != 0)
162 .map(|spell| SpellOverride {
163 spell_id: spell.spell_id,
164 replacement_spell_id: spell.override_spell_id,
165 aura_id: spell.override_aura_id,
166 shares_base_cooldown: spell.override_shares_base_cooldown,
167 })
168 .collect();
169
170 self.hints.primary_resource =
171 (!self.resource_name.is_empty()).then(|| self.resource_name.clone());
172
173 let (engine, mut buffer) = compile_rotation(&RotationCompileInput {
174 rotation,
175 spell_ids: &self.spell_ids,
176 aura_ids: &self.aura_ids,
177 secondary_resource_name: self.secondary_resource_name.as_deref(),
178 hints: &self.hints,
179 gating: &gating,
180 cost_bypass: &cost_bypass,
181 cooldown_bypass: &cooldown_bypass,
182 overrides: &spell_overrides,
183 })?;
184
185 let player_aa_count = self.auto_attacks.iter().filter(|aa| !aa.is_pet).count();
189
190 if player_aa_count == 1 && self.game_data.off_hand().is_some_and(|oh| oh.speed_ms > 0) {
191 if let Some(mh_def) = self.auto_attacks.iter().find(|aa| !aa.is_pet).cloned() {
192 self.auto_attacks.push(mh_def);
193 }
194 }
195
196 let mut names: Vec<Box<str>> = Vec::new();
197 let parts = lower_builder_state(
198 &self.spells,
199 &self.aura_defs,
200 &self.auto_attacks,
201 &self.impact_effect_procs,
202 &mut names,
203 );
204 let resource_max = self.resource_max;
205
206 let talent_ranks = std::mem::take(&mut self.hints.talent_ranks);
207 let mut talent_spell_ids = std::mem::take(&mut self.talent_spell_ids);
208 let mut selected_talent_spell_ids = std::mem::take(&mut self.selected_talent_spell_ids);
209 let selected_replaced_spell_ids = std::mem::take(&mut self.selected_replaced_spell_ids);
210
211 expand_spell_override_chains(&mut talent_spell_ids, &self.game_data);
212 expand_spell_override_chains(&mut selected_talent_spell_ids, &self.game_data);
213
214 let state = self
215 .assemble_state(parts, &selected_talent_spell_ids, encounter)
216 .into_result()?;
217 for aura in &state.defs.auras {
223 ensure_aura_slots(&state, &mut buffer, aura.aura_id, aura.on);
224 }
225
226 populate_initial_buffer(
227 &state,
228 &mut buffer,
229 &InitialBufferConfig {
230 resource_max,
231 talent_ranks: &talent_ranks,
232 talent_spell_ids: &talent_spell_ids,
233 selected_talent_spell_ids: &selected_talent_spell_ids,
234 selected_replaced_spell_ids: &selected_replaced_spell_ids,
235 },
236 );
237
238 Ok(BuiltCombatSystem {
239 state,
240 rotation: engine,
241 buffer,
242 names,
243 })
244 })();
245
246 CombatBuildResult(result)
247 }
248
249 fn register_class_target_debuffs(&mut self) {
255 let debuffs: Vec<SpellIdx> = self.game_data.class_target_debuffs().to_vec();
256
257 for debuff in debuffs {
258 if self
259 .aura_defs
260 .iter()
261 .any(|aura| aura.aura_id == debuff.as_u32())
262 {
263 continue;
264 }
265
266 let name = format!("class debuff {}", debuff.as_u32());
268 let draft = match AuraDefinitionDraft::new(&name, debuff.as_u32())
269 .apply_base_from_data(&self.game_data, debuff.as_u32())
270 {
271 Ok(draft) => draft.on_target(),
272 Err(error) => {
273 self.pending_error = Some(error);
274
275 return;
276 }
277 };
278 let Ok(index) = u8::try_from(self.aura_defs.len()) else {
279 return;
280 };
281
282 tracing::debug!(
284 aura_id = debuff.as_u32(),
285 local_index = index,
286 "registered self-provided class damage-taken debuff"
287 );
288 self.aura_ids.insert(name, debuff.as_u32());
289 self.aura_defs.push(draft.finalize());
290 self.precombat_auras
291 .push(crate::state::LocalAuraIdx::new(index));
292 }
293 }
294
295 fn apply_selected_triggered_aura_values(&mut self) {
296 let mut forwarded = std::collections::BTreeMap::<u32, Option<(u32, f64)>>::new();
297
298 for &driver_spell_id in &self.selected_talent_spell_ids {
299 let driver = SpellIdx::from_raw(driver_spell_id);
300
301 for effect_index in 1..=self.game_data.max_effect_index(driver) {
302 if self.game_data.effect_aura(driver, effect_index)
303 != AuraSubtypeKind::TriggerSpellWithValue as i32
304 {
305 continue;
306 }
307
308 let Some(triggered) = self.game_data.trigger_spell(driver, effect_index) else {
309 continue;
310 };
311 let value = self.game_data.base_points(driver, effect_index);
312 let entry = forwarded
313 .entry(triggered.as_u32())
314 .or_insert(Some((driver_spell_id, value)));
315
316 if entry.is_some_and(|(_, existing)| existing.to_bits() != value.to_bits()) {
317 *entry = None;
318 }
319 }
320 }
321
322 for (aura_id, source) in forwarded {
323 let Some((driver_spell_id, value)) = source else {
324 tracing::debug!(
326 aura_id,
327 "DBC_TRIGGER_VALUE_CONFLICT: selected drivers forward different values"
328 );
329 continue;
330 };
331 let Some(aura) = self
332 .aura_defs
333 .iter_mut()
334 .find(|aura| aura.aura_id == aura_id)
335 else {
336 continue;
337 };
338 let patched = crate::builder::aura_builder::apply_forwarded_effect_value(
339 aura.aura_id,
340 &mut aura.effects,
341 &self.game_data,
342 value,
343 );
344
345 if patched > 0 {
346 tracing::debug!(
348 driver_spell_id,
349 aura_id,
350 value,
351 patched,
352 "forwarded selected trigger value into registered aura effects"
353 );
354 }
355 }
356 }
357}