wowlab_engine_application/game_data/passives/
sources.rs1use std::collections::VecDeque;
2
3use wowlab_engine_domain::dbc::PowerModifiers;
4use wowlab_engine_gamedata::ResolvedGameDataBuilder;
5use wowlab_engine_ports::{
6 DataResolver, DynDataResolver, EngineError, SpecDescriptor, SpellId, TalentSelection,
7};
8use wowlab_types::{
9 data::SpellDataFlat,
10 game::{ClassId, RaceId},
11 sim::{FastSet, SpellIdx},
12};
13
14use super::{PassiveData, stats::apply_talent_effect_overrides};
15use crate::game_data::powers::apply_resource_passives;
16
17pub(crate) async fn resolve_race_passive_spells(
18 resolver: &DynDataResolver<'_>,
19 race: Option<RaceId>,
20 class: ClassId,
21) -> Vec<SpellDataFlat> {
22 let Some(race) = race else {
23 return Vec::new();
24 };
25 let Ok(spell_ids) = resolver
26 .get_racial_spells(race.wow_race_id(), i32::from(u8::from(class)))
27 .await
28 else {
29 tracing::warn!(
30 ?race,
31 ?class,
32 "racial spells not found; skipping race passives"
33 );
34
35 return Vec::new();
36 };
37 let mut spells = Vec::with_capacity(spell_ids.len());
38
39 for spell_id in spell_ids {
40 let Ok(spell) = resolver.get_spell(spell_id).await else {
41 continue;
42 };
43
44 if spell.is_passive {
45 spells.push(spell);
46 }
47 }
48
49 spells
50}
51
52pub(super) async fn resolve_talent_passives(
54 builder: &mut ResolvedGameDataBuilder,
55 resolver: &DynDataResolver<'_>,
56 talents: &[TalentSelection],
57 power_modifiers: &PowerModifiers,
58 specialization_order: i32,
59 passive_data: &mut PassiveData,
60) -> Result<(), EngineError> {
61 let mut resolved_learned_spells = FastSet::default();
62 let mut pending_learned_spells = VecDeque::new();
63
64 for talent in talents {
65 tokio::task::yield_now().await;
66 pending_learned_spells.clear();
67 passive_data
68 .active_aura_spell_ids
69 .insert(wowlab_types::numeric::u32_to_i32_saturating(
70 talent.spell_id,
71 ));
72 let Ok(mut spell) = resolver
73 .get_spell(SpellId::new(wowlab_types::numeric::u32_to_i32_saturating(
74 talent.spell_id,
75 )))
76 .await
77 else {
78 continue;
79 };
80
81 pending_learned_spells.extend(
82 spell
83 .learn_spells
84 .iter()
85 .filter(|learned| learned.learn_spell_id > 0)
86 .map(|learned| (spell.id, learned.learn_spell_id)),
87 );
88
89 apply_talent_effect_overrides(&mut spell, talent, true);
90 apply_resource_passives(
91 builder,
92 power_modifiers,
93 &spell,
94 talent.ranks,
95 Some(specialization_order),
96 );
97
98 if spell.is_passive {
99 passive_data.passives.push((spell, f64::from(talent.ranks)));
100 }
101
102 while let Some((source_spell_id, learned_spell_id)) = pending_learned_spells.pop_front() {
103 tokio::task::yield_now().await;
104
105 if !resolved_learned_spells.insert(learned_spell_id) {
106 continue;
107 }
108
109 passive_data.active_aura_spell_ids.insert(learned_spell_id);
110 let learned_spell = resolver
111 .get_spell(SpellId::new(learned_spell_id))
112 .await
113 .map_err(|error| {
114 learned_spell_resolution_error(source_spell_id, learned_spell_id, &error)
115 })?;
116
117 pending_learned_spells.extend(
118 learned_spell
119 .learn_spells
120 .iter()
121 .filter(|nested| nested.learn_spell_id > 0)
122 .map(|nested| (learned_spell.id, nested.learn_spell_id)),
123 );
124
125 apply_resource_passives(
126 builder,
127 power_modifiers,
128 &learned_spell,
129 1,
130 Some(specialization_order),
131 );
132
133 if learned_spell.is_passive {
134 passive_data.passives.push((learned_spell, 1.0));
135 }
136 }
137 }
138
139 Ok(())
140}
141
142fn learned_spell_resolution_error(
143 source_spell_id: i32,
144 learned_spell_id: i32,
145 error: &impl std::fmt::Display,
146) -> EngineError {
147 EngineError::spec_construction(format!(
148 "failed to resolve spell {learned_spell_id} learned from selected talent spell {source_spell_id}: {error}"
149 ))
150}
151
152pub(super) async fn resolve_race_passives(
153 resolver: &DynDataResolver<'_>,
154 race: Option<RaceId>,
155 class: ClassId,
156 passive_data: &mut PassiveData,
157) {
158 for spell in resolve_race_passive_spells(resolver, race, class).await {
160 passive_data.active_aura_spell_ids.insert(spell.id);
161 passive_data.passives.push((spell, 1.0));
162 }
163}
164
165pub(super) async fn resolve_set_bonus_passives(
166 resolver: &DynDataResolver<'_>,
167 set_bonus_auras: &[u32],
168 passives: &mut Vec<wowlab_engine_domain::dbc::RankedPassive>,
169 active_aura_spell_ids: &mut FastSet<i32>,
170) {
171 let mut missing_spell_ids = Vec::with_capacity(set_bonus_auras.len());
172
173 for &set_bonus_id in set_bonus_auras {
174 active_aura_spell_ids.insert(wowlab_types::numeric::u32_to_i32_saturating(set_bonus_id));
175 let Ok(spell) = resolver
176 .get_spell(SpellId::new(wowlab_types::numeric::u32_to_i32_saturating(
177 set_bonus_id,
178 )))
179 .await
180 else {
181 missing_spell_ids.push(set_bonus_id);
182 continue;
183 };
184
185 if spell.is_passive {
186 passives.push((spell, 1.0));
187 }
188 }
189
190 if !missing_spell_ids.is_empty() {
191 tracing::warn!(
192 ?missing_spell_ids,
193 "set bonus spells not found; skipping passive damage modifiers"
194 );
195 }
196}
197
198pub(super) async fn resolve_spell_overrides(
200 builder: &mut ResolvedGameDataBuilder,
201 descriptor: &SpecDescriptor,
202 resolver: &DynDataResolver<'_>,
203) -> Result<Vec<(SpellId, SpellId)>, EngineError> {
204 let overrides = resolver
205 .get_spell_overrides(wowlab_types::numeric::u32_to_i32_saturating(
206 descriptor.spec_id.wow_spec_id(),
207 ))
208 .await
209 .map_err(|error| {
210 EngineError::spec_construction(format!("failed to resolve spell overrides: {error}"))
211 })?;
212
213 apply_spell_overrides(builder, &overrides)?;
214
215 Ok(overrides)
216}
217
218pub(crate) fn apply_spell_overrides(
220 builder: &mut ResolvedGameDataBuilder,
221 overrides: &[(SpellId, SpellId)],
222) -> Result<(), EngineError> {
223 for (overridden_id, replacement_id) in overrides {
224 let overridden = override_spell_idx(*overridden_id, "source")?;
225 let replacement = override_spell_idx(*replacement_id, "replacement")?;
226
227 builder.insert_spell_override(overridden, replacement);
228 }
229
230 Ok(())
231}
232
233fn override_spell_idx(spell_id: SpellId, role: &str) -> Result<SpellIdx, EngineError> {
234 SpellIdx::try_from(spell_id).map_err(|error| {
235 EngineError::spec_construction(format!("invalid override {role} id: {error}"))
236 })
237}
238
239pub(super) async fn resolve_baseline_passives(
241 builder: &mut ResolvedGameDataBuilder,
242 descriptor: &SpecDescriptor,
243 resolver: &DynDataResolver<'_>,
244 power_modifiers: &PowerModifiers,
245 specialization_order: i32,
246 passive_data: &mut PassiveData,
247) -> Result<Vec<u32>, EngineError> {
248 let spec_spell_ids = resolver
249 .get_specialization_spells(wowlab_types::numeric::u32_to_i32_saturating(
250 descriptor.spec_id.wow_spec_id(),
251 ))
252 .await
253 .map_err(|error| {
254 EngineError::spec_construction(format!(
255 "failed to resolve specialization spells: {error}"
256 ))
257 })?;
258 let mut resolvable = Vec::with_capacity(spec_spell_ids.len());
259 let mut missing_spell_ids = Vec::with_capacity(spec_spell_ids.len());
260
261 for &spec_spell_id in &spec_spell_ids {
262 let Ok(spell) = resolver.get_spell(spec_spell_id).await else {
263 missing_spell_ids.push(spec_spell_id.as_i32());
264 continue;
265 };
266
267 passive_data
268 .active_aura_spell_ids
269 .insert(spec_spell_id.as_i32());
270 resolvable.push(wowlab_types::numeric::i32_to_u32_nonnegative(
271 spec_spell_id.as_i32(),
272 ));
273 apply_resource_passives(
274 builder,
275 power_modifiers,
276 &spell,
277 1,
278 Some(specialization_order),
279 );
280
281 if spell.is_passive {
282 passive_data.passives.push((spell, 1.0));
283 }
284 }
285
286 if !missing_spell_ids.is_empty() {
287 tracing::warn!(
288 ?missing_spell_ids,
289 "specialization spells not found; skipping passive damage modifiers"
290 );
291 }
292
293 for class_passive_id in
294 wowlab_engine_domain::dbc::class_passive_spell_ids(descriptor.spec_id.class())
295 {
296 passive_data
297 .active_aura_spell_ids
298 .insert(wowlab_types::numeric::u32_to_i32_saturating(
299 class_passive_id,
300 ));
301 let Ok(spell) = resolver
302 .get_spell(SpellId::new(wowlab_types::numeric::u32_to_i32_saturating(
303 class_passive_id,
304 )))
305 .await
306 else {
307 continue;
308 };
309
310 apply_resource_passives(
311 builder,
312 power_modifiers,
313 &spell,
314 1,
315 Some(specialization_order),
316 );
317 passive_data
318 .class_target_debuffs
319 .extend(resolve_class_target_debuffs(resolver, &spell, &mut resolvable).await);
320
321 if spell.is_passive {
322 passive_data.passives.push((spell, 1.0));
323 }
324 }
325
326 builder.set_class_target_debuffs(
327 passive_data
328 .class_target_debuffs
329 .iter()
330 .copied()
331 .map(SpellIdx::from_raw)
332 .collect(),
333 );
334
335 Ok(resolvable)
336}
337
338async fn resolve_class_target_debuffs(
339 resolver: &DynDataResolver<'_>,
340 passive: &SpellDataFlat,
341 resolvable: &mut Vec<u32>,
342) -> Vec<u32> {
343 let ast = wowlab_parsers::parse_spell_desc(&passive.description).ast;
344 let referenced =
345 wowlab_parsers::analyze_spell_desc_dependencies(&ast, u32_from_spell_id(passive.id));
346 let mut debuffs = Vec::new();
347
348 for spell_id in referenced.spell_ids {
349 if spell_id == u32_from_spell_id(passive.id) {
350 continue;
351 }
352
353 let Ok(referenced_spell) = resolver
354 .get_spell(SpellId::new(wowlab_types::numeric::u32_to_i32_saturating(
355 spell_id,
356 )))
357 .await
358 else {
359 continue;
360 };
361
362 if !wowlab_engine_domain::dbc::is_target_damage_taken_debuff(&referenced_spell) {
363 continue;
364 }
365
366 resolvable.push(spell_id);
367 debuffs.push(spell_id);
368 }
369
370 tracing::debug!(
371 class_passive_id = passive.id,
372 ?debuffs,
373 "resolved self-provided class damage-taken debuffs"
374 );
375
376 debuffs
377}
378
379fn u32_from_spell_id(spell_id: i32) -> u32 {
380 wowlab_types::numeric::i32_to_u32_nonnegative(spell_id)
381}