Skip to main content

wowlab_engine_combat/builder/combat_builder/build/
dbc_procs.rs

1use super::{
2    AuraSubtypeKind, CombatSystemBuilder, ImpactActorFilter, ImpactEffectProcDefinition,
3    ImpactSource, SpellIdx, TriggerProgramLowering,
4};
5
6pub(super) struct ImpactProcRegistration<'a> {
7    pub(super) definition: &'a mut CombatSystemBuilder,
8}
9
10impl ImpactProcRegistration<'_> {
11    pub(super) fn auto_register_dbc_impact_procs(&mut self) {
12        let mut registrations = Vec::new();
13        let selected_talent_spell_ids = self.definition.selected_talent_spell_ids.clone();
14
15        for driver_spell_id in selected_talent_spell_ids {
16            let driver = SpellIdx::from_raw(driver_spell_id);
17
18            for effect_index in 1..=self.definition.game_data.max_effect_index(driver) {
19                let aura_subtype = self.definition.game_data.effect_aura(driver, effect_index);
20
21                if ![
22                    AuraSubtypeKind::ProcTriggerSpell as i32,
23                    AuraSubtypeKind::TriggerSpellWithValue as i32,
24                ]
25                .contains(&aura_subtype)
26                {
27                    continue;
28                }
29
30                let proc_mask = self
31                    .definition
32                    .game_data
33                    .proc_type_mask(driver)
34                    .unwrap_or(0);
35                let chance = self.definition.game_data.proc_chance(driver).unwrap_or(0.0);
36                let (rppm, rppm_flags) = self.definition.game_data.rppm(driver).unwrap_or((0.0, 0));
37                let rppm_haste_scales = self
38                    .definition
39                    .game_data
40                    .rppm_haste_scales(driver)
41                    .unwrap_or(false);
42                let rppm_crit_scales = self
43                    .definition
44                    .game_data
45                    .rppm_crit_scales(driver)
46                    .unwrap_or(false);
47                let class_mask = self
48                    .definition
49                    .game_data
50                    .effect_class_mask(driver, effect_index);
51                let has_opaque_sibling = self.has_opaque_sibling(driver, effect_index);
52                let Some(triggered) = self
53                    .definition
54                    .game_data
55                    .trigger_spell(driver, effect_index)
56                else {
57                    continue;
58                };
59
60                if !dbc_proc_mask_is_complete(proc_mask)
61                    || !dbc_proc_retains_a_rate(rppm, chance)
62                    || has_opaque_sibling
63                    || class_mask.iter().any(|mask| *mask != 0)
64                    || self.definition.impact_effect_procs.iter().any(|existing| {
65                        existing.driver_spell_id == driver_spell_id
66                            && existing.driver_effect_index == effect_index
67                    })
68                {
69                    continue;
70                }
71
72                let forwarded_value = (aura_subtype
73                    == AuraSubtypeKind::TriggerSpellWithValue as i32)
74                    .then(|| self.definition.game_data.base_points(driver, effect_index));
75                let effects = wowlab_engine_domain::dbc::resolve_trigger_program(
76                    wowlab_engine_domain::dbc::EffectLookup::new(
77                        &self.definition.game_data,
78                        wowlab_types::sim::EffectRef::new(driver, effect_index),
79                    ),
80                    forwarded_value,
81                )
82                .ok()
83                .and_then(|program| self.lowering().lower_trigger_program(&program).ok())
84                .unwrap_or_default();
85
86                if effects.is_empty() {
87                    continue;
88                }
89
90                let mut proc =
91                    ImpactEffectProcDefinition::new(ImpactSource::ProcTypeMask(proc_mask))
92                        .dbc_driver_from_data(wowlab_engine_domain::dbc::EffectLookup::new(
93                            &self.definition.game_data,
94                            wowlab_types::sim::EffectRef::new(driver, effect_index),
95                        ))
96                        .profile_spell(triggered.as_u32())
97                        .actor_filter(ImpactActorFilter::Player)
98                        .chance(chance);
99
100                if self.uses_stacks_for_charges(driver) {
101                    proc = proc.use_stacks_for_charges();
102                }
103
104                if rppm > 0.0 {
105                    let tracker = self.definition.scaled_rppm(
106                        rppm,
107                        super::super::RppmScalingOptions {
108                            haste: rppm_haste_scales,
109                            crit: rppm_crit_scales,
110                            auto_attack_speed: false,
111                        },
112                    );
113
114                    proc = proc.rppm(tracker);
115                }
116
117                proc.effects = effects;
118                // #t(rust_log_in_loop) each construction-time registration identifies its exact DBC driver edge
119                tracing::debug!(
120                    driver_spell_id,
121                    effect_index,
122                    triggered_spell_id = triggered.as_u32(),
123                    proc_mask,
124                    chance,
125                    rppm_flags,
126                    "registered supported DBC AU42 impact proc"
127                );
128                registrations.push(proc);
129            }
130        }
131
132        self.definition.impact_effect_procs.extend(registrations);
133    }
134
135    fn lowering(&self) -> TriggerProgramLowering<'_> {
136        TriggerProgramLowering {
137            definition: &*self.definition,
138        }
139    }
140
141    fn has_opaque_sibling(&self, driver: SpellIdx, effect_index: u8) -> bool {
142        (1..=self.definition.game_data.max_effect_index(driver))
143            .filter(|sibling_index| *sibling_index != effect_index)
144            .any(|sibling_index| {
145                self.definition.game_data.effect_aura(driver, sibling_index)
146                    == AuraSubtypeKind::Dummy as i32
147                    && self
148                        .definition
149                        .game_data
150                        .base_points(driver, sibling_index)
151                        .abs()
152                        > f64::EPSILON
153            })
154    }
155
156    fn uses_stacks_for_charges(&self, driver: SpellIdx) -> bool {
157        self.definition
158            .game_data
159            .proc_attributes(driver)
160            .is_some_and(|attributes| {
161                wowlab_engine_domain::dbc::ProcAttributes::from_dbc(attributes)
162                    .contains(wowlab_engine_domain::dbc::ProcAttributes::USE_STACKS_FOR_CHARGES)
163            })
164    }
165}
166
167pub(super) fn dbc_proc_mask_is_complete(mask: u64) -> bool {
168    let typed = wowlab_engine_domain::dbc::ProcTypeMask::from_dbc(mask);
169
170    !typed.is_empty()
171        && typed
172            .difference(wowlab_engine_domain::dbc::ProcTypeMask::SUPPORTED_EVENT)
173            .is_empty()
174}
175
176/// A retained rate is RPPM or a genuine sub-100% `SpellAuraOptions.ProcChance`.
177/// A 100% header chance instead means the real gate is scripted server-side and absent from DBC.
178/// Gates observed across the 27 specs, each hand-written in the matching `SimC` class module:
179///
180/// - a successful interrupt (Disrupting Fury 183782)
181/// - a critical strike on a combo-point builder (Primal Fury 159286)
182/// - an off-hand-only chance parked in an effect's base value (Fatal Flourish 35551)
183/// - one named parent spell (Momentum Boost 451294)
184///
185/// Without a retained rate the activation belongs to content, matching aura 42's `Partial` row.
186const fn dbc_proc_retains_a_rate(rppm: f64, chance: f64) -> bool {
187    rppm > 0.0 || (chance > 0.0 && chance < 1.0)
188}