wowlab_engine_combat/systems/buffs/
modifiers.rs1use super::{
2 ActorId, ActorView, BuffEffect, CombatInstance, CombatView, EffectFamily, EnemyIdx, HUNDRED,
3 IncomingSpellDefense, ModifierOperation, SimTime, dbc_effect_points,
4 for_each_active_spell_modifier_for_action, for_each_planned_actor_effect_damage_binding,
5 for_each_planned_target_effect,
6};
7
8fn spell_can_be_reflected(
9 program: &crate::state::CombatProgram,
10 _state: &CombatInstance,
11 spell: crate::state::program::LocalDamageActionIdx,
12) -> bool {
13 program.damage.action_at(spell).reflection_eligible
14}
15
16pub(crate) fn resolve_incoming_spell_defense(
17 program: &crate::state::CombatProgram,
18 state: &CombatInstance,
19 target: EnemyIdx,
20 profile_action: crate::state::program::LocalDamageActionIdx,
21 school: wowlab_types::combat::DamageSchool,
22 rng: &mut dyn FnMut() -> f64,
23) -> IncomingSpellDefense {
24 if !spell_can_be_reflected(program, state, profile_action) {
25 return IncomingSpellDefense::None;
26 }
27
28 let school_mask = wowlab_engine_domain::dbc::SpellSchoolMask::from(school);
29 let mut reflected = false;
30 let mut deflect_chance = 0.0;
31
32 for_each_planned_target_effect(
33 program,
34 state,
35 target,
36 EffectFamily::REFLECTION | EffectFamily::DEFLECTION,
37 |stacks, effect| match effect {
38 BuffEffect::SpellReflection(schools) => {
39 reflected |= schools.intersects(school_mask);
40 }
41 BuffEffect::SpellDeflectChance(percent) => {
42 deflect_chance += percent * stacks;
43 }
44 _ => {}
45 },
46 );
47
48 if reflected {
49 IncomingSpellDefense::Reflected
50 } else if wowlab_engine_rng::proc_chance(rng, deflect_chance / HUNDRED) {
51 IncomingSpellDefense::Deflected
52 } else {
53 IncomingSpellDefense::None
54 }
55}
56
57pub(crate) fn active_auto_attack_replacement(
59 program: &crate::state::CombatProgram,
60 state: &CombatInstance,
61) -> Option<crate::state::program::DamageRequestBinding> {
62 let mut replacement = None;
63
64 for_each_planned_actor_effect_damage_binding(
65 ActorView::new(program, state, ActorId::Player),
66 EffectFamily::AUTO_ATTACK_REPLACEMENT,
67 |_stacks, effect, binding| {
68 if replacement.is_none() && matches!(effect, BuffEffect::AutoAttackReplacement(_)) {
69 replacement = binding.replacement_request;
70 }
71 },
72 );
73
74 replacement
75}
76
77pub(crate) fn active_spell_school_override(
79 view: &CombatView<'_>,
80 profile: crate::state::program::LocalDamageActionIdx,
81 effect_action: crate::state::program::LocalDamageActionIdx,
82) -> Option<wowlab_types::combat::DamageSchool> {
83 let mut school = None;
84
85 for_each_planned_actor_effect_damage_binding(
86 ActorView::new(view.program, view.instance, ActorId::Player),
87 EffectFamily::SCHOOL_OVERRIDE,
88 |_stacks, buff, binding| {
89 let BuffEffect::SpellSchoolFromEffect {
90 school: candidate, ..
91 } = buff
92 else {
93 return;
94 };
95
96 if school.is_none()
97 && binding.source_effect.is_some_and(|source| {
98 [profile, effect_action]
99 .into_iter()
100 .any(|target| view.program.damage.effect_affects_action(source, target))
101 })
102 {
103 school = Some(*candidate);
104 }
105 },
106 );
107
108 school
109}
110
111pub(crate) fn active_form_combat_round_time_ms(
113 program: &crate::state::CombatProgram,
114 state: &CombatInstance,
115) -> Option<u32> {
116 let form = state.buffer.player().shapeshift_form;
117
118 if form == 0 {
119 return None;
120 }
121
122 let active_form = program
123 .effects
124 .shapeshift_auras()
125 .iter()
126 .find_map(|entry| {
127 let aura = program.aura(entry.local);
128
129 (aura.shapeshift_form == form).then_some(aura)
130 })?;
131
132 (active_form.shapeshift_combat_round_time_ms > 0)
133 .then_some(active_form.shapeshift_combat_round_time_ms)
134}
135
136pub(crate) fn target_aura_mechanic_mask(
137 program: &crate::state::CombatProgram,
138 state: &CombatInstance,
139 target: EnemyIdx,
140 now: SimTime,
141) -> wowlab_engine_domain::dbc::MechanicMask {
142 let mut mask = wowlab_engine_domain::dbc::MechanicMask::empty();
143
144 for entry in program.effects.mechanic_auras(ActorId::Enemy(target)) {
145 if state
146 .buffer
147 .aura_at(entry.handle)
148 .is_active(now.as_secs_f64())
149 {
150 mask |= entry.mechanic_mask;
151 }
152 }
153
154 mask
155}
156
157#[must_use]
161pub(crate) const fn spell_modifier_owner(source: ActorId) -> Option<ActorId> {
162 match source {
163 ActorId::Player | ActorId::Pet(_) => Some(ActorId::Player),
164 ActorId::External | ActorId::Enemy(_) => None,
165 }
166}
167
168pub(crate) fn active_spell_modifier_value(
170 view: ActorView<'_>,
171 spell: crate::state::program::LocalDamageActionIdx,
172 property: wowlab_engine_domain::dbc::ModifierPropertyKind,
173 initial: f64,
174) -> f64 {
175 let ActorView {
176 program,
177 instance: state,
178 actor: source,
179 } = view;
180 let action = program.damage.action_at(spell);
181 let initial = match property {
182 wowlab_engine_domain::dbc::ModifierPropertyKind::HitChance => {
183 (initial + action.hit_chance_flat) * action.hit_chance_multiplier
184 }
185 wowlab_engine_domain::dbc::ModifierPropertyKind::TargetResistance => {
186 (initial + action.target_resistance_flat) * action.target_resistance_multiplier
187 }
188 _ => initial,
189 };
190 let mut flat = 0.0;
191 let mut percent = 1.0;
192
193 if let Some(owner) = spell_modifier_owner(source) {
194 for_each_active_spell_modifier_for_action(
195 ActorView::new(program, state, owner),
196 spell,
197 property,
198 |stacks, effect, binding| {
199 let BuffEffect::SpellModifierFromEffect {
200 value,
201 property: effect_property,
202 operation,
203 ..
204 } = effect
205 else {
206 return;
207 };
208
209 let affected = binding
210 .source_effect
211 .is_some_and(|source| program.damage.effect_affects_action(source, spell));
212
213 if *effect_property != property || !affected {
214 return;
215 }
216
217 let points = dbc_effect_points(
218 program,
219 state,
220 binding
221 .source_effect
222 .expect("matched source binding exists"),
223 stacks,
224 );
225
226 match operation {
227 ModifierOperation::Flat => flat += value * points,
228 ModifierOperation::Percent => percent *= 1.0 + value * points / HUNDRED,
229 _ => {}
230 }
231 },
232 );
233 }
234
235 (initial + flat) * percent
236}
237
238pub(crate) fn active_effect_points(
240 view: ActorView<'_>,
241 effect: crate::state::program::LocalDamageEffectIdx,
242 initial: f64,
243) -> f64 {
244 let planned = view.program.damage.effect_at(effect);
245 let points = active_spell_modifier_value(
246 view,
247 planned.action,
248 wowlab_engine_domain::dbc::ModifierPropertyKind::Points,
249 initial,
250 );
251
252 wowlab_engine_domain::dbc::modifier_property_for_effect(planned.effect_index)
253 .map_or(points, |property| {
254 active_spell_modifier_value(view, planned.action, property, points)
255 })
256}
257
258pub(crate) fn active_effect_amplitude(
259 view: ActorView<'_>,
260 spell: crate::state::program::LocalDamageActionIdx,
261 initial: f64,
262) -> f64 {
263 active_spell_modifier_value(
264 view,
265 spell,
266 wowlab_engine_domain::dbc::ModifierPropertyKind::Amplitude,
267 initial,
268 )
269}
270
271pub(crate) fn active_targeting_value(
273 view: ActorView<'_>,
274 spell: crate::state::program::LocalDamageActionIdx,
275 property: wowlab_engine_domain::dbc::ModifierPropertyKind,
276 initial: f64,
277) -> f64 {
278 let ActorView {
279 program,
280 instance: state,
281 actor: source,
282 } = view;
283 let mut flat = 0.0;
284 let mut percent = 1.0;
285
286 let Some(owner) = spell_modifier_owner(source) else {
287 return initial;
288 };
289
290 for_each_active_spell_modifier_for_action(
291 ActorView::new(program, state, owner),
292 spell,
293 property,
294 |stacks, effect, binding| {
295 let BuffEffect::SpellTargetingFromEffect {
296 value,
297 property: effect_property,
298 operation,
299 ..
300 } = effect
301 else {
302 return;
303 };
304
305 let affected = binding
306 .source_effect
307 .is_some_and(|source| program.damage.effect_affects_action(source, spell));
308
309 if *effect_property != property || !affected {
310 return;
311 }
312
313 let count = dbc_effect_points(
314 program,
315 state,
316 binding
317 .source_effect
318 .expect("matched source binding exists"),
319 stacks,
320 );
321
322 match operation {
323 ModifierOperation::Flat => flat += value * count,
324 ModifierOperation::Percent => percent *= (1.0 + value / HUNDRED).powf(count),
325 _ => {}
326 }
327 },
328 );
329
330 ((initial + flat) * percent).max(0.0)
331}