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