wowlab_engine_application/game_data/passives/
stats.rs1use 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
10pub(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 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 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
108pub(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}