Skip to main content

wowlab_engine_combat/systems/
async_stacks.rs

1//! Asynchronous per-stack buff expiry.
2
3#[cfg(test)]
4use wowlab_engine_domain::rotation::DenseBuffer;
5use wowlab_engine_ports::Event;
6#[cfg(test)]
7use wowlab_engine_telemetry::TelemetrySink;
8use wowlab_types::{
9    constants::MS_PER_SECOND,
10    sim::{AuraKey, SimTime},
11};
12
13use super::auras::{emit_aura_apply_event, emit_aura_refresh_event, expire_aura};
14#[cfg(test)]
15use crate::state::CombatState;
16use crate::{
17    context::{CombatCtx, HookCtx},
18    state::LocalAuraIdx,
19};
20
21fn apply_async_stack(
22    ctx: &mut CombatCtx<'_>,
23    local: LocalAuraIdx,
24    key: AuraKey,
25    duration_ms: u32,
26    event_target: Option<wowlab_types::sim::EnemyIdx>,
27) {
28    let mut hook = ctx.hook_ctx();
29
30    apply_async_stack_with_hook(&mut hook, local, key, duration_ms, event_target);
31}
32
33fn apply_async_stack_with_hook(
34    ctx: &mut HookCtx<'_>,
35    local: LocalAuraIdx,
36    key: AuraKey,
37    duration_ms: u32,
38    event_target: Option<wowlab_types::sim::EnemyIdx>,
39) {
40    debug_assert_eq!(
41        ctx.target, event_target,
42        "async-stack hook target {:?} does not match event target {event_target:?}",
43        ctx.target
44    );
45
46    // #t(rust_unchecked_indexing) local is validated at build time
47    let aura = ctx.state.defs.auras[local.as_usize()];
48    let base_dur = SimTime::from_millis(duration_ms);
49    let expiry = ctx.now.saturating_add(base_dur);
50    let expiry_s = expiry.as_secs_f64();
51
52    let (stacks, was_fresh) = ctx.state.runtime.pools.async_stack_expiries.push_capped(
53        key,
54        expiry,
55        usize::from(aura.max_stacks.max(1)),
56    );
57
58    {
59        let Some(a) = ctx.buf.aura_mut(key) else {
60            return;
61        };
62
63        a.stacks = i32::try_from(stacks).expect("live stacks are capped at max_stacks (u8)");
64        a.expires_at = expiry_s;
65        a.base_duration = f64::from(duration_ms) / MS_PER_SECOND;
66        a.max_stacks = i32::from(aura.max_stacks);
67    };
68
69    super::refresh_aura_projections(ctx.buf, ctx.state.current_target());
70    let stacks_u8 = u8::try_from(stacks).expect("live stacks are capped at max_stacks (u8)");
71
72    if was_fresh {
73        emit_aura_apply_event(ctx.state, ctx.sink, key, aura.aura_id, stacks_u8, ctx.now);
74    } else {
75        emit_aura_refresh_event(ctx.state, ctx.sink, key, aura.aura_id, stacks_u8, ctx.now);
76    }
77
78    super::sync_spell_costs(ctx.state, ctx.buf);
79    super::sync_recharge_rates(ctx.state, ctx.buf, ctx.now);
80
81    ctx.state.schedule(Event::AuraExpire {
82        t: expiry,
83        key,
84        target: event_target,
85    });
86}
87
88pub(crate) fn apply_async_stacks(
89    ctx: &mut HookCtx<'_>,
90    local: LocalAuraIdx,
91    key: AuraKey,
92    duration_ms: u32,
93    doses: u8,
94) -> bool {
95    let was_fresh = ctx.state.runtime.pools.async_stack_expiries.is_empty(&key);
96    let event_target = ctx.target;
97
98    let applied = CombatCtx::for_optional_target(ctx, event_target, |combat| {
99        for _ in 0..doses {
100            apply_async_stack(combat, local, key, duration_ms, event_target);
101        }
102    });
103
104    if applied.is_none() {
105        for _ in 0..doses {
106            apply_async_stack_with_hook(ctx, local, key, duration_ms, event_target);
107        }
108    }
109
110    was_fresh
111}
112
113pub(crate) fn expire_async_stacks_due(ctx: &mut HookCtx<'_>, local: LocalAuraIdx, key: AuraKey) {
114    // #t(rust_unchecked_indexing) local is validated at build time
115    let aura = ctx.state.defs.auras[local.as_usize()];
116    let Some((popped, remaining)) = ctx
117        .state
118        .runtime
119        .pools
120        .async_stack_expiries
121        .expire_due(&key, ctx.now)
122    else {
123        return;
124    };
125
126    if popped == 0 {
127        return;
128    }
129
130    if remaining == 0 {
131        expire_aura(ctx, local);
132
133        return;
134    }
135
136    let remaining = u8::try_from(remaining).unwrap_or(u8::MAX);
137
138    if let Some(a) = ctx.buf.aura_mut(key) {
139        a.stacks = i32::from(remaining);
140    }
141
142    super::refresh_aura_projections(ctx.buf, ctx.state.current_target());
143    super::sync_spell_costs(ctx.state, ctx.buf);
144    super::sync_recharge_rates(ctx.state, ctx.buf, ctx.now);
145    emit_aura_refresh_event(ctx.state, ctx.sink, key, aura.aura_id, remaining, ctx.now);
146}
147
148#[cfg(test)]
149#[path = "async_stacks/tests.rs"]
150mod tests;