Skip to main content

wowlab_engine_application/game_data/passives/
stats.rs

1use wowlab_engine_domain::dbc::PowerType;
2use wowlab_engine_gamedata::ResolvedGameDataBuilder;
3use wowlab_engine_ports::{
4    DataResolver, DynDataResolver, EngineError, SpecDescriptor, SpellId, TalentSelection,
5};
6use wowlab_types::{data::SpellDataFlat, game::RaceId, sim::SpellIdx};
7
8use super::{masking::should_fold_sheet_stat_passive, sources::resolve_race_passive_spells};
9
10/// Folds class, spec, race, and selected-talent passives into sheet stats before gear conversion.
11pub(crate) async fn resolve_stat_passives(
12    descriptor: &SpecDescriptor,
13    resolver: &DynDataResolver<'_>,
14    talents: &[TalentSelection],
15    race: Option<RaceId>,
16) -> Result<wowlab_engine_domain::dbc::PassiveStatMods, EngineError> {
17    let mut stat_passives: Vec<wowlab_engine_domain::dbc::RankedPassive> = Vec::new();
18
19    for talent in talents {
20        let Ok(mut spell) = resolver
21            .get_spell(SpellId::new(wowlab_types::numeric::u32_to_i32_saturating(
22                talent.spell_id,
23            )))
24            .await
25        else {
26            continue;
27        };
28
29        apply_talent_effect_overrides(&mut spell, talent, true);
30        apply_effect_precision_scales(&mut spell);
31
32        if should_fold_sheet_stat_passive(&spell) {
33            stat_passives.push((spell, f64::from(talent.ranks)));
34        }
35    }
36
37    let spec_spell_ids = resolver
38        .get_specialization_spells(wowlab_types::numeric::u32_to_i32_saturating(
39            descriptor.spec_id.wow_spec_id(),
40        ))
41        .await
42        .map_err(|error| {
43            EngineError::spec_construction(format!(
44                "failed to resolve specialization spells: {error}"
45            ))
46        })?;
47    let baseline_ids = spec_spell_ids.iter().map(SpellId::as_i32).chain(
48        wowlab_engine_domain::dbc::class_passive_spell_ids(descriptor.spec_id.class())
49            .map(wowlab_types::numeric::u32_to_i32_saturating),
50    );
51
52    for baseline_id in baseline_ids {
53        let Ok(mut spell) = resolver.get_spell(SpellId::new(baseline_id)).await else {
54            continue;
55        };
56
57        apply_effect_precision_scales(&mut spell);
58
59        if should_fold_sheet_stat_passive(&spell) {
60            stat_passives.push((spell, 1.0));
61        }
62    }
63
64    // #t(block: rust_async_loop_no_yield) folds already-resolved rows; every await happened above
65    for mut spell in resolve_race_passive_spells(resolver, race, descriptor.spec_id.class()).await {
66        apply_effect_precision_scales(&mut spell);
67
68        if should_fold_sheet_stat_passive(&spell) {
69            stat_passives.push((spell, 1.0));
70        }
71    }
72
73    Ok(wowlab_engine_domain::dbc::passive_stat_mods(&stat_passives))
74}
75
76pub(super) fn rescale_folded_effect_precision(
77    passives: &mut [wowlab_engine_domain::dbc::RankedPassive],
78) {
79    for (spell, _) in passives.iter_mut() {
80        apply_effect_precision_scales(spell);
81    }
82}
83
84pub(crate) fn apply_effect_precision_scales(spell: &mut SpellDataFlat) {
85    for scale in wowlab_parsers::spell_desc_effect_precision_scales(&spell.description) {
86        let dbc_index = i32::from(scale.effect_index) - 1;
87
88        for effect in spell
89            .effects
90            .iter_mut()
91            .filter(|effect| effect.index == dbc_index)
92        {
93            // #t(rust_log_in_loop) one line per rescaled effect is the only record of the rewrite
94            tracing::debug!(
95                spell_id = spell.id,
96                spell_name = spell.name.as_str(),
97                effect_index = scale.effect_index,
98                divisor = scale.divisor,
99                base_points = effect.base_points,
100                "rescaled a passive effect the description states at reduced magnitude"
101            );
102
103            effect.base_points /= scale.divisor;
104        }
105    }
106}
107
108// #t(fn: rust_builder_param) this mutates the resolved-data accumulator; it is not a factory.
109pub(super) fn apply_passive_globals(
110    builder: &mut ResolvedGameDataBuilder,
111    passives: &[wowlab_engine_domain::dbc::RankedPassive],
112    resolvable_spec_spells: &[u32],
113) {
114    if let Some(mana) = builder.power_type_mut(PowerType::Mana as i32) {
115        mana.regen_percent_of_max *=
116            wowlab_engine_domain::dbc::passive_mana_regen_multiplier(passives);
117    }
118
119    builder.set_auto_attack_damage_mult(wowlab_engine_domain::dbc::passive_auto_attack_mult(
120        passives,
121    ));
122    let pet_damage_mult = wowlab_engine_domain::dbc::passive_pet_damage_mult(passives);
123
124    tracing::debug!(
125        pet_damage_mult,
126        "resolved global passive pet damage multiplier"
127    );
128    builder.set_pet_damage_mult(pet_damage_mult);
129    let guardian_damage_mult = wowlab_engine_domain::dbc::passive_guardian_damage_mult(passives);
130
131    tracing::debug!(
132        guardian_damage_mult,
133        "resolved global passive guardian damage multiplier"
134    );
135    builder.set_guardian_damage_mult(guardian_damage_mult);
136
137    for modifier in wowlab_engine_domain::dbc::passive_pet_stat_mods(passives) {
138        builder.insert_pet_stat_inheritance_mult(
139            modifier.npc_id,
140            modifier.attack_power_mult,
141            modifier.spell_power_mult,
142        );
143    }
144
145    builder.set_specialization_spells(
146        resolvable_spec_spells
147            .iter()
148            .copied()
149            .map(SpellIdx::from_raw)
150            .collect(),
151    );
152}
153
154pub(crate) fn apply_talent_effect_overrides(
155    spell: &mut SpellDataFlat,
156    talent: &TalentSelection,
157    normalize_for_rank_scaling: bool,
158) {
159    if talent.ranks == 0 {
160        return;
161    }
162
163    let divisor = if normalize_for_rank_scaling {
164        f64::from(talent.ranks)
165    } else {
166        1.0
167    };
168
169    for effect in &mut spell.effects {
170        if let Some(adjustment) = talent
171            .effect_overrides
172            .iter()
173            .find(|adjustment| adjustment.effect_index == effect.index)
174        {
175            effect.base_points = adjustment
176                .operation
177                .apply(effect.base_points, adjustment.value)
178                / divisor;
179        }
180    }
181}