Skip to main content

wowlab_engine_combat/systems/
aoe.rs

1//! `AoE` target-count resolution and per-target damage multipliers.
2
3use wowlab_engine_domain::{dbc::ModifierPropertyKind, rotation::DenseBuffer};
4
5use super::buffs::for_each_active_aura_effect;
6use crate::state::{BuffEffect, CombatState, DamageEffectRef, DamageReplication, DamageSource};
7
8#[derive(Clone, Copy, Debug, Default)]
9pub(crate) struct AoeConfig {
10    pub(crate) is_aoe: bool,
11    pub(crate) split: bool,
12    pub(crate) chain_targeting: bool,
13    pub(crate) chain_multiplier: f64,
14    pub(crate) base_mult: f64,
15    pub(crate) reduced_targets: u8,
16    pub(crate) full_targets: u8,
17    pub(crate) max_targets: u8,
18}
19
20pub(crate) fn read_spell_aoe_config(
21    state: &CombatState,
22    buf: &DenseBuffer,
23    source: wowlab_types::sim::ActorId,
24    profile_spell_id: u32,
25    effect: DamageEffectRef,
26) -> AoeConfig {
27    let derived = effect_aoe_config(state, buf, source, effect);
28
29    if let Some((_, spell)) = state.spell_data(profile_spell_id) {
30        return AoeConfig {
31            is_aoe: spell.targeting.is_aoe || derived.is_aoe,
32            split: spell.targeting.split,
33            chain_targeting: spell.targeting.chain_targeting || derived.chain_targeting,
34            chain_multiplier: if spell.targeting.chain_targeting {
35                spell.targeting.chain_multiplier
36            } else {
37                derived.chain_multiplier
38            },
39            base_mult: spell.targeting.aoe_base_mult,
40            reduced_targets: spell.targeting.aoe_reduced_targets,
41            full_targets: spell.targeting.aoe_full_targets,
42            max_targets: if spell.targeting.aoe_max_targets > 0 {
43                spell.targeting.aoe_max_targets
44            } else {
45                derived.max_targets
46            },
47        };
48    }
49
50    derived
51}
52
53fn effect_aoe_config(
54    state: &CombatState,
55    buf: &DenseBuffer,
56    source: wowlab_types::sim::ActorId,
57    effect: DamageEffectRef,
58) -> AoeConfig {
59    let spell = wowlab_types::sim::SpellIdx::from_raw(effect.spell_id);
60    let data = &state.config.game_data;
61    let target_plan = wowlab_engine_domain::targeting::compile_target_plan(
62        wowlab_engine_domain::targeting::TargetPlanInput {
63            spell_id: effect.spell_id,
64            effect_index: effect.effect_index,
65            effect_type: data.effect_type(spell, effect.effect_index),
66            target_a: data.implicit_target_a(spell, effect.effect_index),
67            target_b: data.implicit_target_b(spell, effect.effect_index),
68        },
69    );
70    let chain_targets = data
71        .chain_targets(spell, effect.effect_index)
72        .clamp(0, i32::from(u8::MAX));
73    let chain_targets = wowlab_types::numeric::i32_to_u8_saturating(chain_targets);
74    let spell_cap = super::buffs::active_targeting_value(
75        crate::context::ActorView::new(state, buf, source),
76        spell,
77        ModifierPropertyKind::MaxTargets,
78        f64::from(data.max_affected_targets(spell).unwrap_or(0)),
79    )
80    .round()
81    .clamp(0.0, f64::from(u8::MAX));
82    let spell_cap = wowlab_types::numeric::f64_to_u8_saturating_trunc(spell_cap);
83    let max_targets = match (chain_targets, spell_cap) {
84        (0, cap) | (cap, 0) => cap,
85        (chain, cap) => chain.min(cap),
86    };
87
88    AoeConfig {
89        is_aoe: data.treat_as_area_effect(spell).unwrap_or(false)
90            || target_plan
91                .as_ref()
92                .is_ok_and(wowlab_engine_domain::targeting::target_plan_is_multi_target),
93        chain_targeting: chain_targets > 0,
94        chain_multiplier: data.chain_multiplier(spell, effect.effect_index),
95        base_mult: 1.0,
96        max_targets,
97        ..AoeConfig::default()
98    }
99}
100
101pub(crate) fn resolve_hit_count(candidates: usize, aoe: &AoeConfig) -> u8 {
102    let candidates_u8 = u8::try_from(candidates.min(usize::from(u8::MAX)))
103        .expect("candidate count is clamped to u8::MAX");
104
105    if aoe.max_targets == 0 {
106        candidates_u8
107    } else {
108        aoe.max_targets.min(candidates_u8)
109    }
110}
111
112pub(crate) fn aoe_falloff_multiplier(k: u8, hits: u8, aoe: &AoeConfig) -> f64 {
113    if aoe.split {
114        return if hits == 0 {
115            aoe.base_mult
116        } else {
117            aoe.base_mult / f64::from(hits)
118        };
119    }
120
121    if aoe.chain_targeting {
122        return aoe.base_mult * aoe.chain_multiplier.powi(i32::from(k));
123    }
124
125    let full_targets = usize::from(aoe.full_targets);
126    let reduced_targets = usize::from(aoe.reduced_targets);
127    let reduction = if reduced_targets == 0
128        || usize::from(k) < full_targets
129        || usize::from(hits) <= reduced_targets
130    {
131        1.0
132    } else {
133        (f64::from(aoe.reduced_targets) / f64::from(hits)).sqrt()
134    };
135
136    aoe.base_mult * reduction
137}
138
139pub(crate) fn active_damage_replication(
140    state: &CombatState,
141    buf: &DenseBuffer,
142    source: DamageSource,
143) -> Option<DamageReplication> {
144    let mut replication = None;
145
146    for_each_active_aura_effect(state, buf, |_stacks, effect| {
147        if let BuffEffect::DamageReplication(candidate) = effect {
148            if candidate.source == source {
149                replication.get_or_insert(*candidate);
150            }
151        }
152    });
153
154    replication
155}