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;
4
5use crate::state::{BuffEffect, CombatInstance, DamageSource};
6
7pub(crate) type AoeConfig = crate::state::program::DamageProfileAoe;
8
9pub(crate) fn read_spell_aoe_config(
10    program: &crate::state::CombatProgram,
11    state: &CombatInstance,
12    source: wowlab_types::sim::ActorId,
13    profile: crate::state::program::LocalDamageActionIdx,
14    effect: crate::state::program::LocalDamageEffectIdx,
15) -> AoeConfig {
16    let derived = effect_aoe_config(program, state, source, effect);
17    let profile = program.damage.action_at(profile).profile_aoe;
18
19    AoeConfig {
20        is_aoe: profile.is_aoe || derived.is_aoe,
21        split: profile.split,
22        chain_targeting: profile.chain_targeting || derived.chain_targeting,
23        chain_multiplier: if profile.chain_targeting {
24            profile.chain_multiplier
25        } else {
26            derived.chain_multiplier
27        },
28        base_mult: profile.base_mult,
29        reduced_targets: profile.reduced_targets,
30        full_targets: profile.full_targets,
31        max_targets: if profile.max_targets > 0 {
32            profile.max_targets
33        } else {
34            derived.max_targets
35        },
36    }
37}
38
39fn effect_aoe_config(
40    program: &crate::state::CombatProgram,
41    state: &CombatInstance,
42    source: wowlab_types::sim::ActorId,
43    effect: crate::state::program::LocalDamageEffectIdx,
44) -> AoeConfig {
45    let effect_plan = program.damage.effect_at(effect);
46    let chain_targets = effect_plan.aoe.chain_targets.clamp(0, i32::from(u8::MAX));
47    let chain_targets = wowlab_types::numeric::i32_to_u8_saturating(chain_targets);
48    let spell_cap = super::buffs::active_targeting_value(
49        crate::context::ActorView::new(program, state, source),
50        effect_plan.action,
51        ModifierPropertyKind::MaxTargets,
52        f64::from(effect_plan.aoe.max_affected_targets),
53    )
54    .round()
55    .clamp(0.0, f64::from(u8::MAX));
56    let spell_cap = wowlab_types::numeric::f64_to_u8_saturating_trunc(spell_cap);
57    let max_targets = match (chain_targets, spell_cap) {
58        (0, cap) | (cap, 0) => cap,
59        (chain, cap) => chain.min(cap),
60    };
61
62    AoeConfig {
63        is_aoe: effect_plan.aoe.treat_as_area_effect
64            || effect_plan
65                .target
66                .is_some_and(|target| target.is_multi_target),
67        chain_targeting: chain_targets > 0,
68        chain_multiplier: effect_plan.aoe.chain_multiplier,
69        base_mult: 1.0,
70        max_targets,
71        ..AoeConfig::default()
72    }
73}
74
75pub(crate) fn resolve_hit_count(candidates: usize, aoe: &AoeConfig) -> u8 {
76    let candidates_u8 = u8::try_from(candidates.min(usize::from(u8::MAX)))
77        .expect("candidate count is clamped to u8::MAX");
78
79    if aoe.max_targets == 0 {
80        candidates_u8
81    } else {
82        aoe.max_targets.min(candidates_u8)
83    }
84}
85
86pub(crate) fn aoe_falloff_multiplier(k: u8, hits: u8, aoe: &AoeConfig) -> f64 {
87    if aoe.split {
88        return if hits == 0 {
89            aoe.base_mult
90        } else {
91            aoe.base_mult / f64::from(hits)
92        };
93    }
94
95    if aoe.chain_targeting {
96        return aoe.base_mult * aoe.chain_multiplier.powi(i32::from(k));
97    }
98
99    let full_targets = usize::from(aoe.full_targets);
100    let reduced_targets = usize::from(aoe.reduced_targets);
101    let reduction = if reduced_targets == 0
102        || usize::from(k) < full_targets
103        || usize::from(hits) <= reduced_targets
104    {
105        1.0
106    } else {
107        (f64::from(aoe.reduced_targets) / f64::from(hits)).sqrt()
108    };
109
110    aoe.base_mult * reduction
111}
112
113pub(crate) fn active_damage_replication<'a>(
114    program: &'a crate::state::CombatProgram,
115    state: &CombatInstance,
116    source: DamageSource,
117) -> Option<&'a crate::state::program::FinalizedDamageReplication> {
118    let mut replication = None;
119
120    for &index in program
121        .effects
122        .passive_family_indexes(crate::state::EffectFamily::DAMAGE_REPLICATION)
123    {
124        if let Some(candidate) = program.damage.passive_replication(index)
125            && candidate.definition.source == source
126        {
127            replication.get_or_insert(candidate);
128        }
129    }
130
131    crate::systems::buffs::for_each_planned_actor_aura_effect_binding(
132        crate::context::ActorView::new(program, state, wowlab_types::sim::ActorId::Player),
133        crate::state::EffectFamily::DAMAGE_REPLICATION,
134        |_key, local, position, _stacks, effect| {
135            if let BuffEffect::DamageReplication(candidate) = effect
136                && candidate.source == source
137                && let Some(candidate) = program.damage.aura_replication(local, position)
138            {
139                replication.get_or_insert(candidate);
140            }
141        },
142    );
143
144    replication
145}