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 && let Err(error) = copy_resolved_effects(definition, &source_effects) {
200 self.pending_error = Some(error);
201
202 return self;
203 }
204
205 if source_effects.is_empty() {
206 let source = wowlab_types::sim::SpellIdx::from_raw(source_spell_id);
207 let max_effect_index = self.game_data.max_effect_index(source);
208
209 if max_effect_index > 0 {
210 self.pending_error = Some(
211 crate::builder::BuilderErrorKind::GameData(
212 wowlab_engine_ports::EngineError::spec_construction(format!(
213 "source aura {source_spell_id} did not lower DBC effect {effect_index} \
214 for target {} (max effect {max_effect_index}, aura subtype {})",
215 definition.name,
216 self.game_data.effect_aura(source, effect_index),
217 )),
218 )
219 .into(),
220 );
221 }
222 }
223
224 self
225 }
226
227 pub fn set_bonus_auras(mut self, spell_ids: &[u32]) -> Self {
228 let aura_id_to_local = self.aura_id_to_local();
229
230 for &spell_id in spell_ids {
231 if let Some(&local_idx) = aura_id_to_local.get(&spell_id) {
232 self.precombat_auras.push(local_idx);
233 }
234 }
235
236 self
237 }
238
239 fn register_selected_replacement(&mut self, talent: &TalentSelection) {
240 let Some(replaced_spell_id) = talent.replaced_spell() else {
241 return;
242 };
243
244 if self
245 .selected_replaced_spell_ids
246 .contains(&replaced_spell_id)
247 {
248 return;
249 }
250
251 self.selected_replaced_spell_ids.push(replaced_spell_id);
252
253 if talent.precombat_aura {
254 return;
255 }
256
257 for spell_id in self.spell_ids.values_mut() {
258 if *spell_id == replaced_spell_id {
259 *spell_id = talent.spell_id;
260 }
261 }
262 }
263
264 fn register_selected_passive_aura(&mut self, spell_id: u32) -> Option<LocalAuraIdx> {
265 if let Some(index) = self
266 .aura_defs
267 .iter()
268 .position(|definition| definition.aura_id == spell_id)
269 {
270 return Some(LocalAuraIdx::new(
271 u8::try_from(index).expect("validated aura count fits in u8"),
272 ));
273 }
274
275 if self
276 .game_data
277 .is_folded_passive(wowlab_types::sim::SpellIdx::from_raw(spell_id))
278 {
279 return None;
280 }
281
282 let name = self
283 .talent_names_by_id
284 .get(&spell_id)
285 .cloned()
286 .unwrap_or_else(|| format!("talent_{spell_id}"));
287 let builder = match AuraDefinitionDraft::new(&name, spell_id)
288 .on_player()
289 .apply_base_from_data(&self.game_data, spell_id)
290 {
291 Ok(builder) => builder,
292 Err(error) => {
293 self.pending_error = Some(error);
294
295 return None;
296 }
297 };
298 let local = LocalAuraIdx::new(
299 u8::try_from(self.aura_defs.len()).expect("validated aura count fits in u8"),
300 );
301
302 self.aura_ids.insert(name, spell_id);
303 self.aura_defs.push(builder.finalize());
304
305 Some(local)
306 }
307
308 fn aura_id_to_local(&self) -> IntMap<u32, LocalAuraIdx> {
309 let mut locals = IntMap::default();
310
311 for (index, definition) in self.aura_defs.iter().enumerate() {
312 let index = u8::try_from(index).expect("validated aura count fits in u8");
313
314 locals.insert(definition.aura_id, LocalAuraIdx::new(index));
315 }
316
317 locals
318 }
319}