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
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
52// #t(fn: rust_builder_param) this mutates the resolved-data accumulator; it is not a factory.
53pub(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    // #t(block: rust_async_loop_no_yield) folds already-resolved rows; every await happened above
159    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
198// #t(fn: rust_builder_param) this mutates the resolved-data accumulator; it is not a factory.
199pub(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
218// #t(fn: rust_builder_param) this mutates the resolved-data accumulator; it is not a factory.
219pub(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
239// #t(fn: rust_builder_param) this mutates the resolved-data accumulator; it is not a factory.
240pub(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}