Skip to main content

wowlab_engine_combat/systems/buffs/
totals.rs

1use super::{
2    ActorId, ActorView, BuffEffect, BuffTotals, CombatInstance, EnemyIdx, PlayerScalingBuffs,
3};
4
5/// Sum all active aura buff contributions, scaled by stack count.
6#[must_use]
7pub fn buff_totals(
8    program: &crate::state::CombatProgram,
9    state: &CombatInstance,
10    target: Option<EnemyIdx>,
11) -> BuffTotals {
12    actor_buff_totals(ActorView::new(program, state, ActorId::Player), target)
13}
14
15pub(crate) fn actor_buff_totals(view: ActorView<'_>, target: Option<EnemyIdx>) -> BuffTotals {
16    let mut totals = cached_static_actor_totals(view, false);
17
18    fold_dynamic_actor_totals(
19        view,
20        target,
21        false,
22        wowlab_types::sim::SimTime::ZERO,
23        &mut totals,
24    );
25    trace_actor_buff_totals(view, target, false);
26
27    totals
28}
29
30pub(crate) fn actor_only_buff_totals(view: ActorView<'_>, target: Option<EnemyIdx>) -> BuffTotals {
31    let mut totals = cached_static_actor_totals(view, true);
32
33    fold_dynamic_actor_totals(
34        view,
35        target,
36        true,
37        wowlab_types::sim::SimTime::ZERO,
38        &mut totals,
39    );
40    trace_actor_buff_totals(view, target, true);
41
42    totals
43}
44
45pub(crate) fn actor_static_buff_totals(view: ActorView<'_>, actor_only: bool) -> BuffTotals {
46    cached_static_actor_totals(view, actor_only)
47}
48
49pub(crate) fn player_scaling_buffs(view: &crate::context::CombatView<'_>) -> PlayerScalingBuffs {
50    let revision = view.instance.player_scaling_revision();
51
52    if let Some(buffs) = view.instance.cached_player_scaling_buffs(revision) {
53        return buffs;
54    }
55
56    let buffs = PlayerScalingBuffs::from(cached_static_actor_totals(
57        view.for_actor(ActorId::Player),
58        false,
59    ));
60
61    view.instance.cache_player_scaling_buffs(revision, buffs);
62
63    buffs
64}
65
66pub(crate) fn actor_dynamic_damage_mult(
67    view: ActorView<'_>,
68    target: Option<EnemyIdx>,
69    actor_only: bool,
70    now: wowlab_types::sim::SimTime,
71) -> f64 {
72    let mut totals = BuffTotals::default();
73
74    fold_dynamic_actor_totals(view, target, actor_only, now, &mut totals);
75
76    totals.damage_mult
77}
78
79fn cached_static_actor_totals(view: ActorView<'_>, actor_only: bool) -> BuffTotals {
80    let revision = view.instance.actor_aura_revision(view.actor).unwrap_or(0);
81    let owner_revision = view
82        .instance
83        .actor_aura_revision(ActorId::Player)
84        .unwrap_or(0);
85    let cached = if actor_only {
86        view.instance
87            .cached_actor_only_buff_totals(view.actor, revision, owner_revision)
88    } else {
89        view.instance.cached_actor_buff_totals(view.actor, revision)
90    };
91
92    if let Some(totals) = cached {
93        return totals;
94    }
95
96    let mut totals = BuffTotals::default();
97
98    if view.actor == ActorId::Player {
99        for &index in view.program.effects.static_total_passive_driver_indexes() {
100            // BOUNDS: effect-plan indexes are finalized from this same passive-driver array.
101            let effect = &view.program.passive_driver_effects[index].effect;
102
103            if !matches!(
104                effect,
105                BuffEffect::DynamicDamageMult(..) | BuffEffect::DynamicRegenPercent(..)
106            ) {
107                totals.add_scaled(effect, 1.0);
108            }
109        }
110    }
111
112    #[cfg(not(feature = "rotation-optimizations"))]
113    for entry in view.program.effects.static_total_actor_effects(view.actor) {
114        wowlab_types::bench_metrics::record_aura_slot_visit();
115        let slot = view.instance.buffer.aura_at(entry.handle);
116
117        if !slot.is_occupied() || (actor_only && player_has_active_aura(view, entry.local)) {
118            continue;
119        }
120
121        fold_masked_totals(
122            view.program.aura(entry.local),
123            entry.static_totals_mask,
124            f64::from(slot.stacks),
125            &mut totals,
126        );
127    }
128
129    #[cfg(feature = "rotation-optimizations")]
130    for_each_active_static_total_aura(view, |local, mask, stacks| {
131        if !actor_only || !player_has_active_aura(view, local) {
132            fold_masked_totals(view.program.aura(local), mask, stacks, &mut totals);
133        }
134    });
135
136    if actor_only {
137        view.instance
138            .cache_actor_only_buff_totals(view.actor, revision, owner_revision, &totals);
139    } else {
140        view.instance
141            .cache_actor_buff_totals(view.actor, revision, &totals);
142    }
143
144    totals
145}
146
147fn fold_dynamic_actor_totals(
148    view: ActorView<'_>,
149    target: Option<EnemyIdx>,
150    actor_only: bool,
151    now: wowlab_types::sim::SimTime,
152    totals: &mut BuffTotals,
153) {
154    if view.actor == ActorId::Player {
155        for &index in view.program.effects.dynamic_total_passive_driver_indexes() {
156            // BOUNDS: effect-plan indexes are finalized from this same passive-driver array.
157            let effect = &view.program.passive_driver_effects[index].effect;
158
159            fold_dynamic_effect(view, target, 1.0, effect, now, totals);
160        }
161    }
162
163    #[cfg(not(feature = "rotation-optimizations"))]
164    for entry in view.program.effects.dynamic_total_actor_effects(view.actor) {
165        wowlab_types::bench_metrics::record_aura_slot_visit();
166        let slot = view.instance.buffer.aura_at(entry.handle);
167
168        if !slot.is_occupied() || (actor_only && player_has_active_aura(view, entry.local)) {
169            continue;
170        }
171
172        let aura = view.program.aura(entry.local);
173        let mut mask = entry.dynamic_totals_mask;
174
175        while mask != 0 {
176            let position = mask.trailing_zeros() as usize;
177
178            mask &= mask - 1;
179
180            // BOUNDS: dynamic_totals_mask is derived from this aura's fixed effect array.
181
182            if let Some(effect) = aura.effects.get(position).and_then(Option::as_ref) {
183                fold_dynamic_effect(view, target, f64::from(slot.stacks), effect, now, totals);
184            }
185        }
186    }
187
188    #[cfg(feature = "rotation-optimizations")]
189    for_each_active_total_aura(
190        view,
191        view.program.effects.dynamic_total_aura_plan(),
192        |local, mut mask, stacks| {
193            if actor_only && player_has_active_aura(view, local) {
194                return;
195            }
196
197            let aura = view.program.aura(local);
198
199            while mask != 0 {
200                let position = mask.trailing_zeros() as usize;
201
202                mask &= mask - 1;
203
204                if let Some(effect) = aura.effects.get(position).and_then(Option::as_ref) {
205                    fold_dynamic_effect(view, target, stacks, effect, now, totals);
206                }
207            }
208        },
209    );
210}
211
212#[cfg(feature = "rotation-optimizations")]
213fn for_each_active_static_total_aura(
214    view: ActorView<'_>,
215    mut visit: impl FnMut(crate::state::LocalAuraIdx, u16, f64),
216) {
217    view.instance.for_each_active_static_total_aura(
218        view.actor,
219        view.program
220            .effects
221            .static_total_aura_plan()
222            .relevant_locals(),
223        |local, binding, mask| {
224            wowlab_types::bench_metrics::record_aura_slot_visit();
225            let slot = view.instance.buffer.aura_at(binding.handle);
226
227            debug_assert!(
228                slot.is_occupied(),
229                "active static-total aura intersection contains only occupied concrete bindings"
230            );
231
232            visit(local, mask, f64::from(slot.stacks));
233        },
234    );
235}
236
237#[cfg(feature = "rotation-optimizations")]
238fn for_each_active_total_aura(
239    view: ActorView<'_>,
240    plan: &crate::state::program::ActiveAuraEffectPlan,
241    mut visit: impl FnMut(crate::state::LocalAuraIdx, u16, f64),
242) {
243    view.instance.for_each_active_aura_intersection(
244        view.actor,
245        (plan.relevant_locals(), &[]),
246        |local, binding| {
247            wowlab_types::bench_metrics::record_aura_slot_visit();
248            let slot = view.instance.buffer.aura_at(binding.handle);
249
250            debug_assert!(
251                slot.is_occupied(),
252                "active total-aura intersection contains only occupied concrete bindings"
253            );
254
255            visit(local, plan.mask(local), f64::from(slot.stacks));
256        },
257    );
258}
259
260fn fold_masked_totals(
261    aura: &crate::state::AuraData,
262    mut mask: u16,
263    stacks: f64,
264    totals: &mut BuffTotals,
265) {
266    while mask != 0 {
267        let position = mask.trailing_zeros() as usize;
268
269        mask &= mask - 1;
270
271        // BOUNDS: the supplied mask is derived from this aura's fixed effect array.
272
273        if let Some(effect) = aura.effects.get(position).and_then(Option::as_ref) {
274            totals.add_scaled(effect, stacks);
275        }
276    }
277}
278
279fn fold_dynamic_effect(
280    view: ActorView<'_>,
281    target: Option<EnemyIdx>,
282    stacks: f64,
283    effect: &BuffEffect,
284    now: wowlab_types::sim::SimTime,
285    totals: &mut BuffTotals,
286) {
287    match effect {
288        BuffEffect::DynamicDamageMult(resolve) => {
289            totals.damage_mult *= resolve(view.program, view.instance, target, now).powf(stacks);
290        }
291        BuffEffect::DynamicRegenPercent(resolve) => {
292            totals.regen_pct += resolve(view.program, view.instance) * stacks;
293        }
294        _ => {}
295    }
296}
297
298fn player_has_active_aura(view: ActorView<'_>, local: crate::state::LocalAuraIdx) -> bool {
299    view.program
300        .effects
301        .owner_duplicate_handles(local)
302        .iter()
303        .any(|handle| view.instance.buffer.aura_at(*handle).is_occupied())
304}
305
306/// Complete per-effect diagnostics deliberately separate from the cached ordinary path.
307fn trace_actor_buff_totals(view: ActorView<'_>, target: Option<EnemyIdx>, actor_only: bool) {
308    if !tracing::enabled!(tracing::Level::TRACE) {
309        return;
310    }
311
312    if view.actor == ActorId::Player {
313        for driver in &view.program.passive_driver_effects {
314            // #t(rust_log_in_loop) trace mode intentionally emits one complete diagnostic per contributing passive driver.
315            tracing::trace!(
316                driver_spell_id = driver.driver_spell_id,
317                ?target,
318                effect = ?driver.effect,
319                "planned passive buff contribution"
320            );
321        }
322    }
323
324    super::for_each_planned_actor_aura_effect(
325        view,
326        crate::state::EffectFamily::GENERAL_TOTALS,
327        |key, local, stacks, effect| {
328            if actor_only && player_has_active_aura(view, local) {
329                return;
330            }
331
332            // #t(rust_log_in_loop) trace mode intentionally emits one complete diagnostic per active aura effect.
333            tracing::trace!(
334                aura_id = key.aura().as_u32(),
335                ?key,
336                stacks,
337                ?target,
338                scaled = ?effect.scaled(stacks),
339                "planned buff contribution"
340            );
341        },
342    );
343}
344
345/// Independent uncached recomputation oracle for cache differential tests.
346#[cfg(test)]
347pub(crate) fn reference_uncached_actor_buff_totals(
348    view: ActorView<'_>,
349    target: Option<EnemyIdx>,
350) -> BuffTotals {
351    let mut totals = BuffTotals::default();
352
353    if view.actor == ActorId::Player {
354        for driver in &view.program.passive_driver_effects {
355            fold_dynamic_effect(
356                view,
357                target,
358                1.0,
359                &driver.effect,
360                wowlab_types::sim::SimTime::ZERO,
361                &mut totals,
362            );
363            totals.add_scaled(&driver.effect, 1.0);
364        }
365    }
366
367    for entry in view.program.effects.lifecycle_actor_auras(view.actor) {
368        let slot = view.instance.buffer.aura_at(entry.handle);
369
370        if !slot.is_occupied() {
371            continue;
372        }
373
374        for effect in view.program.aura(entry.local).effects.iter().flatten() {
375            fold_dynamic_effect(
376                view,
377                target,
378                f64::from(slot.stacks),
379                effect,
380                wowlab_types::sim::SimTime::ZERO,
381                &mut totals,
382            );
383            totals.add_scaled(effect, f64::from(slot.stacks));
384        }
385    }
386
387    totals
388}