Skip to main content

wowlab_engine_application/game_data/passives/
sources.rs

1use 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
17/// Resolves the race's passive abilities, mirroring `SimC`'s racial pass.
18pub(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
53// #t(fn: rust_builder_param) this mutates the resolved-data accumulator; it is not a factory.
54pub(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    // #t(block: rust_async_loop_no_yield) folds already-resolved rows; every await happened above
160    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
199// #t(fn: rust_builder_param) this mutates the resolved-data accumulator; it is not a factory.
200pub(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
219// #t(fn: rust_builder_param) this mutates the resolved-data accumulator; it is not a factory.
220pub(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
240// #t(fn: rust_builder_param) this mutates the resolved-data accumulator; it is not a factory.
241pub(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}