Skip to main content

wowlab_engine_combat/systems/auras/
expire.rs

1use 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
8/// Expires an aura by local index.
9pub 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    // #t(rust_unchecked_indexing) local is validated at build time
19    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
219/// Expires an aura by game ID.
220pub 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}