wowlab_engine_combat/systems/buffs/
modifiers.rs1use super::*;
2
3fn spell_can_be_reflected(state: &CombatState, spell: wowlab_types::sim::SpellIdx) -> bool {
4 let attributes = state
5 .config
6 .game_data
7 .spell_attributes(spell)
8 .unwrap_or_default();
9 let has = |kind| wowlab_engine_domain::dbc::spell_attribute_is(attributes, kind);
10 let explicitly_allowed =
11 has(wowlab_engine_domain::dbc::SpellAttributeKind::AllowSpellReflection);
12 let eligible_class = (state
13 .config
14 .game_data
15 .defense_type(spell)
16 .and_then(|raw| wowlab_engine_domain::dbc::DefenseType::try_from(raw).ok())
17 == Some(wowlab_engine_domain::dbc::DefenseType::Magic)
18 && !has(wowlab_engine_domain::dbc::SpellAttributeKind::Ability))
19 || explicitly_allowed;
20
21 eligible_class
22 && !has(wowlab_engine_domain::dbc::SpellAttributeKind::NoReflection)
23 && !has(wowlab_engine_domain::dbc::SpellAttributeKind::NoImmunities)
24 && !has(wowlab_engine_domain::dbc::SpellAttributeKind::Passive)
25}
26
27pub(crate) fn resolve_incoming_spell_defense(
28 state: &CombatState,
29 buf: &DenseBuffer,
30 target: EnemyIdx,
31 profile_spell_id: u32,
32 school: wowlab_types::combat::DamageSchool,
33 rng: &mut dyn FnMut() -> f64,
34) -> IncomingSpellDefense {
35 let spell = wowlab_types::sim::SpellIdx::from_raw(profile_spell_id);
36
37 if !spell_can_be_reflected(state, spell) {
38 return IncomingSpellDefense::None;
39 }
40
41 let school_mask = wowlab_engine_domain::dbc::SpellSchoolMask::from(school);
42 let mut reflected = false;
43 let mut deflect_chance = 0.0;
44
45 for_each_active_target_aura_effect(state, buf, target, |stacks, effect| match effect {
46 BuffEffect::SpellReflection(schools) => {
47 reflected |= schools.intersects(school_mask);
48 }
49 BuffEffect::SpellDeflectChance(percent) => {
50 deflect_chance += percent * stacks;
51 }
52 _ => {}
53 });
54
55 if reflected {
56 IncomingSpellDefense::Reflected
57 } else if wowlab_engine_rng::proc_chance(rng, deflect_chance / HUNDRED) {
58 IncomingSpellDefense::Deflected
59 } else {
60 IncomingSpellDefense::None
61 }
62}
63
64pub(crate) fn active_auto_attack_replacement(
66 state: &CombatState,
67 buf: &DenseBuffer,
68) -> Option<u32> {
69 let mut replacement = None;
70
71 for_each_active_aura_effect(state, buf, |_stacks, effect| {
72 if replacement.is_none() {
73 if let BuffEffect::AutoAttackReplacement(spell_id) = effect {
74 replacement = Some(*spell_id);
75 }
76 }
77 });
78
79 replacement
80}
81
82pub(crate) fn active_spell_school_override(
84 view: &CombatView<'_>,
85 profile_spell_id: u32,
86 effect_spell_id: u32,
87) -> Option<wowlab_types::combat::DamageSchool> {
88 let mut school = None;
89
90 for_each_active_aura_effect(view.state, view.buf, |_stacks, effect| {
91 let BuffEffect::SpellSchoolFromEffect {
92 school: candidate,
93 source_spell_id,
94 effect_index,
95 } = effect
96 else {
97 return;
98 };
99
100 if school.is_none()
101 && [profile_spell_id, effect_spell_id]
102 .into_iter()
103 .any(|spell_id| {
104 view.state.config.game_data.effect_affects_spell(
105 wowlab_types::sim::SpellIdx::from_raw(*source_spell_id),
106 *effect_index,
107 wowlab_types::sim::SpellIdx::from_raw(spell_id),
108 )
109 })
110 {
111 school = Some(*candidate);
112 }
113 });
114
115 school
116}
117
118pub(crate) fn active_stance_mask_exception(view: &CombatView<'_>, spell_id: u32) -> bool {
120 let mut allowed = false;
121
122 for_each_active_aura_effect(view.state, view.buf, |_stacks, effect| {
123 let BuffEffect::SpellStanceMaskFromEffect {
124 source_spell_id,
125 effect_index,
126 } = effect
127 else {
128 return;
129 };
130
131 allowed |= view.state.config.game_data.effect_affects_spell(
132 wowlab_types::sim::SpellIdx::from_raw(*source_spell_id),
133 *effect_index,
134 wowlab_types::sim::SpellIdx::from_raw(spell_id),
135 );
136 });
137
138 allowed
139}
140
141pub(crate) fn active_form_combat_round_time_ms(
143 state: &CombatState,
144 buf: &DenseBuffer,
145) -> Option<u32> {
146 let form = buf.player().shapeshift_form;
147
148 if form == 0 {
149 return None;
150 }
151
152 let active_form = state
153 .defs
154 .auras
155 .iter()
156 .find(|aura| aura.shapeshift_form == form)?;
157
158 (active_form.shapeshift_combat_round_time_ms > 0)
159 .then_some(active_form.shapeshift_combat_round_time_ms)
160}
161
162pub(crate) fn target_aura_mechanic_mask(
163 state: &CombatState,
164 buf: &DenseBuffer,
165 target: EnemyIdx,
166 now: SimTime,
167) -> wowlab_engine_domain::dbc::MechanicMask {
168 use wowlab_engine_domain::dbc::ResolvedGameDataSemanticExt as _;
169
170 let mut mask = wowlab_engine_domain::dbc::MechanicMask::empty();
171
172 for key in buf.aura_keys() {
173 if key.affected() != ActorId::Enemy(target)
174 || buf
175 .aura(key)
176 .is_none_or(|slot| !slot.is_active(now.as_secs_f64()))
177 {
178 continue;
179 }
180
181 let Some(local) = state.aura_local_for_key(key) else {
182 continue;
183 };
184 let aura_id = state.aura(local).aura_id;
185
186 mask |= state
187 .config
188 .game_data
189 .all_effect_mechanic_mask(wowlab_types::sim::SpellIdx::from_raw(aura_id));
190 }
191
192 mask
193}
194
195#[must_use]
199pub(crate) const fn spell_modifier_owner(source: ActorId) -> Option<ActorId> {
200 match source {
201 ActorId::Player | ActorId::Pet(_) => Some(ActorId::Player),
202 ActorId::External | ActorId::Enemy(_) => None,
203 }
204}
205
206pub(crate) fn active_spell_modifier_value(
208 view: ActorView<'_>,
209 spell: wowlab_types::sim::SpellIdx,
210 property: wowlab_engine_domain::dbc::ModifierPropertyKind,
211 initial: f64,
212) -> f64 {
213 let ActorView {
214 state,
215 buf,
216 actor: source,
217 } = view;
218 let initial = match property {
219 wowlab_engine_domain::dbc::ModifierPropertyKind::HitChance => {
220 state.config.game_data.hit_chance_modifier(spell, initial)
221 }
222 wowlab_engine_domain::dbc::ModifierPropertyKind::TargetResistance => state
223 .config
224 .game_data
225 .target_resistance_modifier(spell, initial),
226 wowlab_engine_domain::dbc::ModifierPropertyKind::DispelResistance => state
227 .config
228 .game_data
229 .dispel_resistance_modifier(spell, initial),
230 _ => initial,
231 };
232 let mut flat = 0.0;
233 let mut percent = 1.0;
234
235 if let Some(owner) = spell_modifier_owner(source) {
236 for_each_active_actor_aura_effect(ActorView::new(state, buf, owner), |stacks, effect| {
237 let BuffEffect::SpellModifierFromEffect {
238 value,
239 source_spell_id,
240 effect_index,
241 property: effect_property,
242 operation,
243 } = effect
244 else {
245 return;
246 };
247
248 if *effect_property != property
249 || !state.config.game_data.effect_affects_spell(
250 wowlab_types::sim::SpellIdx::from_raw(*source_spell_id),
251 *effect_index,
252 spell,
253 )
254 {
255 return;
256 }
257
258 let points = dbc_effect_points(state, *source_spell_id, *effect_index, stacks);
259
260 match operation {
261 ModifierOperation::Flat => flat += value * points,
262 ModifierOperation::Percent => percent *= 1.0 + value * points / HUNDRED,
263 _ => {}
264 }
265 });
266 }
267
268 (initial + flat) * percent
269}
270
271pub(crate) fn active_effect_points(
273 view: ActorView<'_>,
274 spell: wowlab_types::sim::SpellIdx,
275 effect_index: u8,
276 initial: f64,
277) -> f64 {
278 let points = active_spell_modifier_value(
279 view,
280 spell,
281 wowlab_engine_domain::dbc::ModifierPropertyKind::Points,
282 initial,
283 );
284
285 wowlab_engine_domain::dbc::modifier_property_for_effect(effect_index)
286 .map_or(points, |property| {
287 active_spell_modifier_value(view, spell, property, points)
288 })
289}
290
291pub(crate) fn active_effect_amplitude(
292 view: ActorView<'_>,
293 spell: wowlab_types::sim::SpellIdx,
294 initial: f64,
295) -> f64 {
296 active_spell_modifier_value(
297 view,
298 spell,
299 wowlab_engine_domain::dbc::ModifierPropertyKind::Amplitude,
300 initial,
301 )
302}
303
304pub(crate) fn active_targeting_value(
306 view: ActorView<'_>,
307 spell: wowlab_types::sim::SpellIdx,
308 property: wowlab_engine_domain::dbc::ModifierPropertyKind,
309 initial: f64,
310) -> f64 {
311 let ActorView {
312 state,
313 buf,
314 actor: source,
315 } = view;
316 let mut flat = 0.0;
317 let mut percent = 1.0;
318
319 let Some(owner) = spell_modifier_owner(source) else {
320 return initial;
321 };
322
323 for_each_active_actor_aura_effect(ActorView::new(state, buf, owner), |stacks, effect| {
324 let BuffEffect::SpellTargetingFromEffect {
325 value,
326 source_spell_id,
327 effect_index,
328 property: effect_property,
329 operation,
330 } = effect
331 else {
332 return;
333 };
334
335 if *effect_property != property
336 || !state.config.game_data.effect_affects_spell(
337 wowlab_types::sim::SpellIdx::from_raw(*source_spell_id),
338 *effect_index,
339 spell,
340 )
341 {
342 return;
343 }
344
345 let count = dbc_effect_points(state, *source_spell_id, *effect_index, stacks);
346
347 match operation {
348 ModifierOperation::Flat => flat += value * count,
349 ModifierOperation::Percent => percent *= (1.0 + value / HUNDRED).powf(count),
350 _ => {}
351 }
352 });
353
354 ((initial + flat) * percent).max(0.0)
355}