Skip to main content

wowlab_engine_combat/systems/auras/
stacks.rs

1use super::{
2    ActorId, CombatState, DenseBuffer, EnemyIdx, Event, HookCtx, LocalAuraIdx,
3    PERMANENT_AURA_EXPIRES_AT_S, SimTime, apply_aura, aura_query_key_for, context_aura_key,
4    emit_aura_refresh_event, enqueue_aura_application_proc, expire_aura, refresh_aura_projections,
5    refreshed_stacks, revive_tick_chain, synchronize_aura_dependents,
6};
7
8/// Adds a stack without refreshing the expiry deadline.
9pub(crate) fn add_aura_stack(ctx: &mut HookCtx<'_>, local: LocalAuraIdx) {
10    // #t(rust_unchecked_indexing) local is validated at build time
11    let aura = ctx.state.defs.auras[local.as_usize()];
12    let Some(key) = context_aura_key(ctx, local) else {
13        return;
14    };
15
16    if aura.async_stacks
17        || !ctx
18            .buf
19            .aura(key)
20            .is_some_and(wowlab_engine_domain::rotation::AuraSlot::is_occupied)
21    {
22        apply_aura(ctx, local);
23
24        return;
25    }
26
27    let stacks = if let Some(slot) = ctx.buf.aura_mut(key) {
28        slot.stacks = refreshed_stacks(&aura, slot.stacks);
29
30        wowlab_types::numeric::i32_to_u8_saturating(slot.stacks)
31    } else {
32        return;
33    };
34
35    emit_aura_refresh_event(ctx.state, ctx.sink, key, aura.aura_id, stacks, ctx.now);
36    synchronize_aura_dependents(ctx);
37    enqueue_aura_application_proc(ctx, &aura, key);
38}
39
40pub(crate) fn consume_aura_stack_by_id(ctx: &mut HookCtx<'_>, aura_id: u32) -> bool {
41    let Some(&local) = ctx.state.index.aura_by_id.get(&aura_id) else {
42        return false;
43    };
44
45    consume_aura_stack(ctx, local)
46}
47
48/// Extends an active non-permanent aura without pandemic recomputation.
49pub fn extend_aura(ctx: &mut HookCtx<'_>, local: LocalAuraIdx, extend_ms: u32) {
50    // #t(rust_unchecked_indexing) local is validated at build time
51    let aura = ctx.state.defs.auras[local.as_usize()];
52    let Some(key) = context_aura_key(ctx, local) else {
53        tracing::trace!(
54            aura_id = aura.aura_id,
55            source = ?ctx.source,
56            target = ?ctx.target,
57            extend_ms,
58            "AURA_EXTEND_NO_CONTEXT_KEY"
59        );
60
61        return;
62    };
63
64    let (old_expiry, new_expiry, stacks) = {
65        let Some(a) = ctx.buf.aura_mut(key) else {
66            tracing::trace!(
67                aura_id = aura.aura_id,
68                key = ?key,
69                extend_ms,
70                "AURA_EXTEND_MISSING_INSTANCE"
71            );
72
73            return;
74        };
75
76        if !a.is_occupied() || a.expires_at >= PERMANENT_AURA_EXPIRES_AT_S || extend_ms == 0 {
77            tracing::trace!(
78                aura_id = aura.aura_id,
79                key = ?key,
80                occupied = a.is_occupied(),
81                expires_at = a.expires_at,
82                extend_ms,
83                "AURA_EXTEND_INELIGIBLE"
84            );
85
86            return;
87        }
88
89        let old_expiry = a.expires_at;
90        let new_expiry =
91            SimTime::from_secs_f64(a.expires_at).saturating_add(SimTime::from_millis(extend_ms));
92
93        a.expires_at = new_expiry.as_secs_f64();
94
95        (
96            old_expiry,
97            new_expiry,
98            wowlab_types::numeric::i32_to_u8_saturating(a.stacks),
99        )
100    };
101
102    tracing::trace!(
103        aura_id = aura.aura_id,
104        key = ?key,
105        old_expiry,
106        new_expiry = new_expiry.as_secs_f64(),
107        extend_ms,
108        "AURA_EXTENDED"
109    );
110
111    revive_tick_chain(ctx, &aura, key, Some(old_expiry));
112    refresh_aura_projections(ctx.buf, ctx.state.current_target());
113
114    emit_aura_refresh_event(ctx.state, ctx.sink, key, aura.aura_id, stacks, ctx.now);
115    ctx.state.schedule(Event::AuraExpire {
116        t: new_expiry,
117        key,
118        target: ctx.target,
119    });
120}
121
122pub(crate) fn reduce_aura(ctx: &mut HookCtx<'_>, local: LocalAuraIdx, reduce_ms: u32) {
123    let Some(key) = context_aura_key(ctx, local) else {
124        return;
125    };
126    let Some(active) = ctx.buf.aura(key) else {
127        return;
128    };
129
130    if !active.is_occupied() || active.expires_at >= PERMANENT_AURA_EXPIRES_AT_S || reduce_ms == 0 {
131        return;
132    }
133
134    let expiry = SimTime::from_secs_f64(active.expires_at);
135    let reduction = SimTime::from_millis(reduce_ms);
136
137    if expiry <= ctx.now.saturating_add(reduction) {
138        expire_aura(ctx, local);
139
140        return;
141    }
142
143    let new_expiry = expiry.saturating_sub(reduction);
144    let stacks = {
145        let active = ctx.buf.aura_mut(key).expect("active aura disappeared");
146
147        active.expires_at = new_expiry.as_secs_f64();
148
149        wowlab_types::numeric::i32_to_u8_saturating(active.stacks)
150    };
151
152    refresh_aura_projections(ctx.buf, ctx.state.current_target());
153    emit_aura_refresh_event(
154        ctx.state,
155        ctx.sink,
156        key,
157        ctx.state.aura(local).aura_id,
158        stacks,
159        ctx.now,
160    );
161    ctx.state.schedule(Event::AuraExpire {
162        t: new_expiry,
163        key,
164        target: ctx.target,
165    });
166}
167
168/// Returns whether an aura is active.
169#[must_use]
170pub fn is_aura_active(
171    view: &crate::context::CombatView<'_>,
172    local: LocalAuraIdx,
173    source: ActorId,
174    target: Option<EnemyIdx>,
175) -> bool {
176    aura_query_key_for(view.state, local, source, target)
177        .and_then(|key| view.buf.aura(key))
178        .is_some_and(wowlab_engine_domain::rotation::AuraSlot::is_occupied)
179}
180
181pub(crate) fn aura_stacks(
182    state: &CombatState,
183    buf: &DenseBuffer,
184    local: LocalAuraIdx,
185    source: ActorId,
186    target: Option<EnemyIdx>,
187) -> i32 {
188    aura_query_key_for(state, local, source, target)
189        .and_then(|key| buf.aura(key))
190        .map_or(0, |a| if a.is_occupied() { a.stacks } else { 0 })
191}
192
193pub(crate) fn aura_stacks_by_id(
194    state: &CombatState,
195    buf: &DenseBuffer,
196    aura_id: u32,
197    source: ActorId,
198    target: Option<EnemyIdx>,
199) -> i32 {
200    state
201        .aura_local(aura_id)
202        .map_or(0, |local| aura_stacks(state, buf, local, source, target))
203}
204
205#[inline]
206pub(crate) fn bypass_aura_active(
207    state: &CombatState,
208    buf: &DenseBuffer,
209    aura_id: u32,
210    source: ActorId,
211    target: Option<EnemyIdx>,
212) -> bool {
213    aura_id != 0 && aura_stacks_by_id(state, buf, aura_id, source, target) > 0
214}
215
216pub(crate) fn consume_aura(ctx: &mut HookCtx<'_>, local: LocalAuraIdx) -> bool {
217    let Some(key) = context_aura_key(ctx, local) else {
218        return false;
219    };
220    let stacks = ctx
221        .buf
222        .aura(key)
223        .map_or(0, |aura| if aura.is_occupied() { aura.stacks } else { 0 });
224
225    if stacks <= 0 {
226        return false;
227    }
228    // BOUNDS: local indices are created from the registered aura definition table.
229
230    let aura = ctx.state.defs.auras[local.as_usize()];
231
232    if aura.on_consume.len > 0 {
233        let repeat = wowlab_types::numeric::i32_to_u8_saturating(stacks);
234
235        ctx.state
236            .runtime
237            .deferred_work
238            .push_effect(crate::state::PendingEffectProgram {
239                effects: aura.on_consume,
240                profile_spell_id: aura.aura_id,
241                repeat,
242                source: ctx.source,
243                target: ctx.target,
244                effect_target: ctx.effect_target,
245                flags: crate::DamageFlags::empty(),
246                originating_proc_driver_spell_id: 0,
247            });
248    }
249
250    expire_aura(ctx, local);
251
252    true
253}
254
255pub(crate) fn consume_aura_stack(ctx: &mut HookCtx<'_>, local: LocalAuraIdx) -> bool {
256    let Some(key) = context_aura_key(ctx, local) else {
257        return false;
258    };
259    let Some(active) = ctx.buf.aura(key) else {
260        return false;
261    };
262
263    if !active.is_occupied() {
264        return false;
265    }
266
267    let expires = active.stacks <= 1;
268    // BOUNDS: local indices are created from the registered aura definition table.
269    let aura = ctx.state.defs.auras[local.as_usize()];
270
271    if aura.on_consume.len > 0 {
272        ctx.state
273            .runtime
274            .deferred_work
275            .push_effect(crate::state::PendingEffectProgram {
276                effects: aura.on_consume,
277                profile_spell_id: aura.aura_id,
278                repeat: 1,
279                source: ctx.source,
280                target: ctx.target,
281                effect_target: ctx.effect_target,
282                flags: crate::DamageFlags::empty(),
283                originating_proc_driver_spell_id: 0,
284            });
285    }
286
287    if expires {
288        expire_aura(ctx, local);
289
290        return true;
291    }
292
293    if aura.async_stacks {
294        ctx.state
295            .runtime
296            .pools
297            .async_stack_expiries
298            .remove_oldest(&key);
299    }
300
301    if let Some(active) = ctx.buf.aura_mut(key) {
302        active.stacks -= 1;
303    }
304
305    synchronize_aura_dependents(ctx);
306
307    true
308}
309
310/// Consumes up to `count` stacks without refreshing the remaining duration.
311pub(crate) fn consume_aura_stacks(ctx: &mut HookCtx<'_>, local: LocalAuraIdx, count: i32) -> i32 {
312    let to_consume =
313        aura_stacks(ctx.state, ctx.buf, local, ctx.source, ctx.target).min(count.max(0));
314    let mut consumed = 0;
315
316    for _ in 0..to_consume {
317        if !consume_aura_stack(ctx, local) {
318            break;
319        }
320
321        consumed += 1;
322    }
323
324    consumed
325}
326
327/// Drains every stack without expiring the aura or stopping its periodic tick chain.
328pub(crate) fn take_aura_stacks(ctx: &mut HookCtx<'_>, local: LocalAuraIdx) -> i32 {
329    // #t(rust_unchecked_indexing) local is validated at build time.
330    let aura = ctx.state.defs.auras[local.as_usize()];
331    let Some(key) = context_aura_key(ctx, local) else {
332        return 0;
333    };
334    let Some(slot) = ctx.buf.aura_mut(key) else {
335        return 0;
336    };
337
338    if !slot.is_occupied() {
339        return 0;
340    }
341
342    let stacks = std::mem::take(&mut slot.stacks).max(0);
343
344    emit_aura_refresh_event(ctx.state, ctx.sink, key, aura.aura_id, 0, ctx.now);
345    synchronize_aura_dependents(ctx);
346
347    stacks
348}