Skip to main content

wowlab_engine_combat/systems/auras/
expire.rs

1// #t(file: rust_duplicate_strings) Parallel expiry branches intentionally use the same concrete-binding invariant diagnostic.
2
3use 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
10/// Expires an aura by local index.
11pub 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    // #t(rust_unchecked_indexing) local is validated at build time
29    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        // Reset/despawn lifecycle intentionally visits every concrete slot for the actor.
276        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}