Skip to main content

wowlab_engine_combat/systems/auras/
stacks.rs

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