wowlab_engine_combat/systems/auras/
expire.rs1use super::{
4 ActiveAbsorb, ActorId, AuraKey, BuffEffect, HUNDRED, HookCtx, LocalAuraIdx, SimTime,
5 application_state, context_aura_key, emit_aura_expire_event, is_rolling_periodic,
6 restore_active_shapeshift_form,
7};
8use crate::state::AuraMutation;
9
10pub fn expire_aura(ctx: &mut HookCtx<'_>, local: LocalAuraIdx) {
12 let Some(key) = context_aura_key(ctx, local) else {
13 return;
14 };
15
16 expire_aura_instance(ctx, local, key);
17}
18
19pub(super) fn expire_aura_instance(ctx: &mut HookCtx<'_>, local: LocalAuraIdx, key: AuraKey) {
20 let Some(binding) = ctx.program.aura_binding(local, key) else {
21 return;
22 };
23
24 if !ctx.instance.buffer.aura_at(binding.handle).is_occupied() {
25 return;
26 }
27
28 let aura = ctx.program.auras[local.as_usize()];
30 let changed = ctx
31 .program
32 .mutate_aura(ctx.instance, binding, AuraMutation::Expire, ctx.now);
33 let pool = binding.index().as_usize();
34
35 if matches!(
36 aura.periodic.as_ref().map(|periodic| periodic.effect),
37 Some(crate::state::PeriodicKind::ResidualDamage)
38 ) {
39 ctx.instance
40 .runtime
41 .pools
42 .residual_damage_pools
43 .remove(pool);
44 }
45
46 if is_rolling_periodic(&aura) {
47 ctx.instance.runtime.pools.rolling_tick_mult.remove(pool);
48 }
49
50 if aura.async_stacks {
51 ctx.instance.runtime.pools.async_stack_expiries.remove(pool);
52 }
53
54 ctx.instance
55 .runtime
56 .health
57 .active_absorbs
58 .retain(|absorb| absorb.key != key);
59
60 if ctx.program.stealth_aura == Some(local) {
61 ctx.instance.buffer.player_mut().stealthed = 0;
62 }
63
64 if aura.shapeshift_form > 0
65 && ctx.instance.buffer.player().shapeshift_form == aura.shapeshift_form
66 {
67 restore_active_shapeshift_form(ctx);
68 }
69
70 if let Some(control) = aura.crowd_control {
71 synchronize_control_after_aura_expiry(ctx, local, key, control);
72 }
73
74 emit_aura_expire_event(
75 ctx.program,
76 ctx.instance,
77 ctx.sink,
78 key,
79 aura.aura_id,
80 ctx.now,
81 );
82
83 if changed && aura.on_expire.is_some() {
84 ctx.instance
85 .runtime
86 .deferred_work
87 .push_aura_expire(crate::state::PendingAuraExpireHook {
88 key,
89 aura: local,
90 target: ctx.target,
91 });
92 }
93}
94
95pub(super) fn synchronize_control_after_aura_expiry(
96 ctx: &mut HookCtx<'_>,
97 expired_local: LocalAuraIdx,
98 expired_key: AuraKey,
99 control: wowlab_engine_domain::dbc::CrowdControlSemantic,
100) {
101 let actor = expired_key.affected();
102 let mut kind_deadline = None::<crate::state::ControlDeadline>;
103 let mut group_deadline = None::<crate::state::ControlDeadline>;
104
105 for peer in ctx
106 .program
107 .aura_dependencies
108 .control_peers(expired_local, actor)
109 {
110 let slot = ctx.instance.buffer.aura_at(peer.handle);
111
112 if !slot.is_active(ctx.now.as_secs_f64()) {
113 continue;
114 }
115
116 let deadline = if peer.permanent {
117 crate::state::ControlDeadline::Permanent
118 } else {
119 crate::state::ControlDeadline::At(SimTime::from_secs_f64(slot.expires_at))
120 };
121
122 if peer.matches_kind {
123 kind_deadline = Some(kind_deadline.map_or(deadline, |current| current.later(deadline)));
124 }
125
126 if peer.matches_group {
127 group_deadline =
128 Some(group_deadline.map_or(deadline, |current| current.later(deadline)));
129 }
130 }
131
132 let control_key = (actor, control.kind);
133
134 if let Some(deadline) = kind_deadline {
135 ctx.instance
136 .runtime
137 .control
138 .crowd_control
139 .insert(control_key, deadline);
140 } else {
141 ctx.instance
142 .runtime
143 .control
144 .crowd_control
145 .remove(control_key);
146 }
147
148 if control.diminishing != wowlab_engine_domain::dbc::DiminishingGroup::None
149 && let Some(dr) = ctx
150 .instance
151 .runtime
152 .control
153 .diminishing_returns
154 .get_mut((actor, control.diminishing))
155 {
156 dr.last_control_ends = match group_deadline {
157 Some(deadline) => deadline,
158 None => crate::state::ControlDeadline::At(ctx.now),
159 };
160 }
161}
162
163pub(super) fn refresh_absorbs(
164 ctx: &mut HookCtx<'_>,
165 request: application_state::AuraSlotRequest<'_>,
166) {
167 let application_state::AuraSlotRequest {
168 aura, local, key, ..
169 } = request;
170
171 #[cfg(feature = "rotation-optimizations")]
172 if ctx
173 .program
174 .effects
175 .family_aura_plan(crate::state::EffectFamily::ABSORB)
176 .mask(local)
177 == 0
178 {
179 return;
180 }
181
182 ctx.instance
183 .runtime
184 .health
185 .active_absorbs
186 .retain(|absorb| absorb.key != key);
187
188 let stacks = ctx
189 .program
190 .buffer_aura(ctx.instance, local, key)
191 .map_or(1.0, |slot| f64::from(slot.stacks.max(1)));
192 let source = key.source();
193 let target = key.affected();
194 let mut done_percent = 0.0;
195 let mut taken_percent = 0.0;
196
197 crate::systems::buffs::for_each_planned_actor_effect(
198 ctx.view().for_actor(source),
199 crate::state::EffectFamily::ABSORB,
200 |effect_stacks, effect| {
201 if let BuffEffect::AbsorbDonePercent(percent) = effect {
202 done_percent += percent * effect_stacks;
203 }
204 },
205 );
206 crate::systems::buffs::for_each_planned_actor_effect(
207 ctx.view().for_actor(target),
208 crate::state::EffectFamily::ABSORB,
209 |effect_stacks, effect| {
210 if let BuffEffect::AbsorbTakenPercent(percent) = effect {
211 taken_percent += percent * effect_stacks;
212 }
213 },
214 );
215 let multiplier =
216 (1.0 + done_percent / HUNDRED).max(0.0) * (1.0 + taken_percent / HUNDRED).max(0.0);
217 let pool = ctx
218 .program
219 .aura_binding(local, key)
220 .expect("active aura has a finalized concrete binding")
221 .index()
222 .as_usize();
223
224 for effect in aura.effects.iter().flatten() {
225 let BuffEffect::Absorb(effect) = effect else {
226 continue;
227 };
228 let application_order = ctx.instance.runtime.health.next_absorb_application_order;
229 let aura_action = ctx.program.damage.aura_action(local);
230 let effect_binding = ctx
231 .program
232 .damage
233 .action_effect(aura_action, effect.effect_index)
234 .expect("finalized absorb effect coordinate exists");
235 let amount = crate::systems::buffs::active_effect_points(
236 ctx.view().for_actor(source),
237 effect_binding,
238 effect.amount,
239 );
240
241 ctx.instance.runtime.health.next_absorb_application_order =
242 application_order.saturating_add(1);
243 ctx.instance
244 .runtime
245 .health
246 .active_absorbs
247 .push(ActiveAbsorb {
248 key,
249 pool,
250 kind: effect.kind,
251 schools: effect.schools,
252 remaining: amount.max(0.0) * stacks * multiplier,
253 high_priority: effect.high_priority,
254 overkill_script: effect.overkill_script,
255 priority: effect.priority,
256 application_order,
257 });
258 }
259}
260
261pub(crate) fn expire_auras_on_actor(ctx: &mut HookCtx<'_>, actor: ActorId) {
262 #[cfg(feature = "rotation-optimizations")]
263 while let Some(binding) = ctx.instance.pop_first_active_aura_binding(actor) {
264 ctx.instance
265 .buffer
266 .aura_at(binding.handle)
267 .is_occupied()
268 .then_some(())
269 .expect("active aura registry contains only occupied concrete bindings");
270 expire_aura_instance(ctx, binding.local, binding.key);
271 }
272
273 #[cfg(not(feature = "rotation-optimizations"))]
274 {
275 let aura_count = ctx.program.effects.lifecycle_actor_auras(actor).len();
277
278 for index in 0..aura_count {
279 let entry = *ctx
280 .program
281 .effects
282 .lifecycle_actor_auras(actor)
283 .get(index)
284 .expect("bounded actor aura-effect index");
285
286 if !ctx.instance.buffer.aura_at(entry.handle).is_occupied() {
287 continue;
288 }
289
290 expire_aura_instance(ctx, entry.local, entry.key);
291 }
292 }
293}