wowlab_engine_combat/systems/
aoe.rs1use 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}