wowlab_engine_combat/systems/auras/
expire.rs1use super::{
2 ActiveAbsorb, ActorId, AuraKey, BuffEffect, HUNDRED, HookCtx, LocalAuraIdx, SimTime, SpellIdx,
3 application_state, context_aura_key, emit_aura_expire_event, is_rolling_periodic,
4 refresh_pet_actions_after_aura_change, restore_active_shapeshift_form,
5 synchronize_aura_dependents,
6};
7
8pub fn expire_aura(ctx: &mut HookCtx<'_>, local: LocalAuraIdx) {
10 let Some(key) = context_aura_key(ctx, local) else {
11 return;
12 };
13
14 expire_aura_instance(ctx, local, key);
15}
16
17pub(super) fn expire_aura_instance(ctx: &mut HookCtx<'_>, local: LocalAuraIdx, key: AuraKey) {
18 let aura = ctx.state.defs.auras[local.as_usize()];
20 let was_active = ctx.buf.aura_mut(key).is_some_and(|a| {
21 let occupied = a.is_occupied();
22
23 a.remove();
24
25 occupied
26 });
27
28 if matches!(
29 aura.periodic.as_ref().map(|periodic| periodic.effect),
30 Some(crate::state::PeriodicKind::ResidualDamage)
31 ) {
32 ctx.state.runtime.pools.residual_damage_pools.remove(&key);
33 }
34
35 if is_rolling_periodic(&aura) {
36 ctx.state.runtime.pools.rolling_tick_mult.remove(&key);
37 }
38
39 if aura.async_stacks {
40 ctx.state.runtime.pools.async_stack_expiries.remove(&key);
41 }
42
43 ctx.state
44 .runtime
45 .health
46 .active_absorbs
47 .retain(|absorb| absorb.key != key);
48
49 if ctx.state.defs.stealth_aura_id == Some(aura.aura_id) {
50 ctx.buf.player_mut().stealthed = 0;
51 }
52
53 if aura.shapeshift_form > 0 && ctx.buf.player().shapeshift_form == aura.shapeshift_form {
54 restore_active_shapeshift_form(ctx);
55 }
56
57 if let Some(control) = aura.crowd_control {
58 synchronize_control_after_aura_expiry(ctx, key, control);
59 }
60
61 synchronize_aura_dependents(ctx);
62
63 if was_active {
64 refresh_pet_actions_after_aura_change(ctx.state, ctx.buf, ctx.now);
65 }
66
67 emit_aura_expire_event(ctx.state, ctx.sink, key, aura.aura_id, ctx.now);
68
69 if was_active && aura.on_expire.is_some() {
70 ctx.state
71 .runtime
72 .deferred_work
73 .push_aura_expire(crate::state::PendingAuraExpireHook {
74 key,
75 aura: local,
76 target: ctx.target,
77 });
78 }
79}
80
81pub(super) fn synchronize_control_after_aura_expiry(
82 ctx: &mut HookCtx<'_>,
83 expired_key: AuraKey,
84 control: wowlab_engine_domain::dbc::CrowdControlSemantic,
85) {
86 let actor = expired_key.affected();
87 let mut kind_deadline = None::<crate::state::ControlDeadline>;
88 let mut group_deadline = None::<crate::state::ControlDeadline>;
89
90 for key in ctx.buf.aura_keys().filter(|key| key.affected() == actor) {
91 let Some(slot) = ctx
92 .buf
93 .aura(key)
94 .filter(|slot| slot.is_active(ctx.now.as_secs_f64()))
95 else {
96 continue;
97 };
98 let Some(local) = ctx.state.aura_local_for_key(key) else {
99 continue;
100 };
101 let Some((active, is_permanent)) =
102 ctx.state.defs.auras.get(local.as_usize()).and_then(|aura| {
103 aura.crowd_control
104 .map(|control| (control, aura.base_duration_ms == 0))
105 })
106 else {
107 continue;
108 };
109 let deadline = if is_permanent {
110 crate::state::ControlDeadline::Permanent
111 } else {
112 crate::state::ControlDeadline::At(SimTime::from_secs_f64(slot.expires_at))
113 };
114
115 if active.kind == control.kind {
116 kind_deadline = Some(kind_deadline.map_or(deadline, |current| current.later(deadline)));
117 }
118
119 if active.diminishing == control.diminishing {
120 group_deadline =
121 Some(group_deadline.map_or(deadline, |current| current.later(deadline)));
122 }
123 }
124
125 let control_key = (actor, control.kind);
126
127 if let Some(deadline) = kind_deadline {
128 ctx.state
129 .runtime
130 .control
131 .crowd_control
132 .insert(control_key, deadline);
133 } else {
134 ctx.state.runtime.control.crowd_control.remove(&control_key);
135 }
136
137 if control.diminishing != wowlab_engine_domain::dbc::DiminishingGroup::None
138 && let Some(dr) = ctx
139 .state
140 .runtime
141 .control
142 .diminishing_returns
143 .get_mut(&(actor, control.diminishing))
144 {
145 dr.last_control_ends = match group_deadline {
146 Some(deadline) => deadline,
147 None => crate::state::ControlDeadline::At(ctx.now),
148 };
149 }
150}
151
152pub(super) fn refresh_absorbs(
153 ctx: &mut HookCtx<'_>,
154 request: application_state::AuraSlotRequest<'_>,
155) {
156 let application_state::AuraSlotRequest { aura, key, .. } = request;
157
158 ctx.state
159 .runtime
160 .health
161 .active_absorbs
162 .retain(|absorb| absorb.key != key);
163
164 let stacks = ctx
165 .buf
166 .aura(key)
167 .map_or(1.0, |slot| f64::from(slot.stacks.max(1)));
168 let source = key.source();
169 let target = key.affected();
170 let mut done_percent = 0.0;
171 let mut taken_percent = 0.0;
172
173 crate::systems::buffs::for_each_active_actor_aura_effect(
174 ctx.view().for_actor(source),
175 |effect_stacks, effect| {
176 if let BuffEffect::AbsorbDonePercent(percent) = effect {
177 done_percent += percent * effect_stacks;
178 }
179 },
180 );
181 crate::systems::buffs::for_each_active_actor_aura_effect(
182 ctx.view().for_actor(target),
183 |effect_stacks, effect| {
184 if let BuffEffect::AbsorbTakenPercent(percent) = effect {
185 taken_percent += percent * effect_stacks;
186 }
187 },
188 );
189 let multiplier =
190 (1.0 + done_percent / HUNDRED).max(0.0) * (1.0 + taken_percent / HUNDRED).max(0.0);
191
192 for effect in aura.effects.iter().flatten() {
193 let BuffEffect::Absorb(effect) = effect else {
194 continue;
195 };
196 let application_order = ctx.state.runtime.health.next_absorb_application_order;
197 let amount = crate::systems::buffs::active_effect_points(
198 ctx.view().for_actor(source),
199 SpellIdx::from_raw(aura.aura_id),
200 effect.effect_index,
201 effect.amount,
202 );
203
204 ctx.state.runtime.health.next_absorb_application_order =
205 application_order.saturating_add(1);
206 ctx.state.runtime.health.active_absorbs.push(ActiveAbsorb {
207 key,
208 kind: effect.kind,
209 schools: effect.schools,
210 remaining: amount.max(0.0) * stacks * multiplier,
211 high_priority: effect.high_priority,
212 overkill_script: effect.overkill_script,
213 priority: effect.priority,
214 application_order,
215 });
216 }
217}
218
219pub fn expire_aura_by_id(ctx: &mut HookCtx<'_>, aura_id: u32) {
221 let Some(&local) = ctx.state.index.aura_by_id.get(&aura_id) else {
222 return;
223 };
224
225 expire_aura(ctx, local);
226}
227
228pub(crate) fn expire_auras_on_actor(ctx: &mut HookCtx<'_>, actor: ActorId) {
229 let keys: Vec<_> = ctx
230 .buf
231 .aura_keys()
232 .filter(|key| key.affected() == actor)
233 .filter(|key| {
234 ctx.buf
235 .aura(*key)
236 .is_some_and(wowlab_engine_domain::rotation::AuraSlot::is_occupied)
237 })
238 .collect();
239
240 for key in keys {
241 let Some(local) = ctx.state.aura_local_for_key(key) else {
242 continue;
243 };
244
245 expire_aura_instance(ctx, local, key);
246 }
247}