Skip to main content

wowlab_engine_application/game_data/
passives.rs

1//! Talent, specialization, class, race, and set-bonus passive resolution.
2
3mod crit;
4mod masking;
5mod sources;
6mod stats;
7
8use wowlab_engine_domain::dbc::PowerModifiers;
9use wowlab_engine_gamedata::ResolvedGameDataBuilder;
10use wowlab_engine_ports::{
11    DataResolver, DynDataResolver, EngineError, SpecDescriptor, SpellId, TalentSelection,
12};
13use wowlab_types::{
14    game::RaceId,
15    sim::{FastSet, SpellIdx},
16};
17
18use self::{
19    crit::apply_crit_chance_scaled_player_axis,
20    masking::mask_declared_passive_effects,
21    sources::{
22        resolve_baseline_passives, resolve_race_passives, resolve_set_bonus_passives,
23        resolve_spell_overrides, resolve_talent_passives,
24    },
25    stats::{apply_passive_globals, rescale_folded_effect_precision},
26};
27
28#[rustfmt::skip]
29pub(super) use crit::resolve_crit_chance_scaled_crit_damage;
30#[rustfmt::skip]
31#[cfg(test)]
32pub(super) use masking::should_fold_sheet_stat_passive;
33#[rustfmt::skip]
34pub(super) use masking::{
35    EquippedWeapons, apply_declared_passive_effect_overrides,
36    mask_spec_conditional_passive_effects, retain_statically_folded_passives,
37};
38#[rustfmt::skip]
39#[cfg(test)]
40pub(super) use sources::apply_spell_overrides;
41#[rustfmt::skip]
42pub(crate) use sources::resolve_race_passive_spells;
43#[rustfmt::skip]
44#[cfg(test)]
45pub(super) use stats::apply_effect_precision_scales;
46#[rustfmt::skip]
47pub(super) use stats::apply_talent_effect_overrides;
48
49#[rustfmt::skip]
50pub(crate) use stats::resolve_stat_passives;
51
52#[cfg(test)]
53#[path = "passives/sources/tests.rs"]
54mod learned_passive_tests;
55
56pub(super) struct PassiveData {
57    pub passives: Vec<wowlab_engine_domain::dbc::RankedPassive>,
58    /// Declared passive effect overrides, resolved to `((spell_id, 1-based effect), value)`.
59    pub effect_overrides: Vec<((u32, u8), f64)>,
60    /// Crit-damage terms the passives state as a share of the caster's own critical strike chance.
61    pub crit_chance_scaled: Vec<wowlab_engine_domain::dbc::CritChanceScaledCritDamage>,
62    pub active_aura_spell_ids: FastSet<i32>,
63    pub overrides: Vec<(SpellId, SpellId)>,
64    pub resolvable_spec_spells: Vec<u32>,
65    pub class_target_debuffs: Vec<u32>,
66}
67
68/// Everything passive resolution reads about the character it is resolving for.
69pub(super) struct PassiveInputs<'a> {
70    pub descriptor: &'a SpecDescriptor,
71    pub talents: &'a [TalentSelection],
72    pub set_bonus_auras: &'a [u32],
73    pub weapons: EquippedWeapons<'a>,
74    pub race: Option<RaceId>,
75}
76
77// #t(fn: rust_builder_param) this mutates the resolved-data accumulator; it is not a factory.
78pub(super) async fn resolve_passives(
79    builder: &mut ResolvedGameDataBuilder,
80    resolver: &DynDataResolver<'_>,
81    inputs: PassiveInputs<'_>,
82    power_modifiers: &PowerModifiers,
83) -> Result<PassiveData, EngineError> {
84    let PassiveInputs {
85        descriptor,
86        talents,
87        set_bonus_auras,
88        weapons,
89        race,
90    } = inputs;
91    let mut passive_data = PassiveData {
92        passives: Vec::new(),
93        effect_overrides: Vec::new(),
94        crit_chance_scaled: Vec::new(),
95        active_aura_spell_ids: FastSet::default(),
96        overrides: Vec::new(),
97        resolvable_spec_spells: Vec::new(),
98        class_target_debuffs: Vec::new(),
99    };
100    let specialization_order = resolver
101        .get_spec(wowlab_types::numeric::u32_to_i32_saturating(
102            descriptor.spec_id.wow_spec_id(),
103        ))
104        .await
105        .map_err(|error| {
106            EngineError::spec_construction(format!(
107                "failed to resolve specialization metadata: {error}"
108            ))
109        })?
110        .order_index;
111
112    resolve_talent_passives(
113        builder,
114        resolver,
115        talents,
116        power_modifiers,
117        specialization_order,
118        &mut passive_data,
119    )
120    .await?;
121    resolve_set_bonus_passives(
122        resolver,
123        set_bonus_auras,
124        &mut passive_data.passives,
125        &mut passive_data.active_aura_spell_ids,
126    )
127    .await;
128    resolve_race_passives(
129        resolver,
130        race,
131        descriptor.spec_id.class(),
132        &mut passive_data,
133    )
134    .await;
135    passive_data.overrides = resolve_spell_overrides(builder, descriptor, resolver).await?;
136    passive_data.resolvable_spec_spells = resolve_baseline_passives(
137        builder,
138        descriptor,
139        resolver,
140        power_modifiers,
141        specialization_order,
142        &mut passive_data,
143    )
144    .await?;
145
146    rescale_folded_effect_precision(&mut passive_data.passives);
147    retain_statically_folded_passives(&mut passive_data.passives, weapons);
148    mask_spec_conditional_passive_effects(&mut passive_data.passives, specialization_order);
149    mask_declared_passive_effects(&mut passive_data.passives, descriptor);
150    passive_data.effect_overrides =
151        apply_declared_passive_effect_overrides(&mut passive_data.passives, descriptor);
152    passive_data.crit_chance_scaled =
153        resolve_crit_chance_scaled_crit_damage(&passive_data.passives);
154    apply_crit_chance_scaled_player_axis(builder, &passive_data.crit_chance_scaled);
155    apply_passive_globals(
156        builder,
157        &passive_data.passives,
158        &passive_data.resolvable_spec_spells,
159    );
160    builder.set_folded_passive_spells(
161        passive_data
162            .passives
163            .iter()
164            .map(|(spell, _)| {
165                SpellIdx::from_raw(wowlab_types::numeric::i32_to_u32_nonnegative(spell.id))
166            })
167            .collect::<Vec<_>>(),
168    );
169
170    Ok(passive_data)
171}