1use super::{
2 ActorId, ActorView, BuffEffect, BuffTotals, CombatInstance, EnemyIdx, PlayerScalingBuffs,
3};
4
5#[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 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 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 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 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
306fn 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 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 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#[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}