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