Skip to main content

wowlab_engine_combat/systems/
async_stacks.rs

1//! Asynchronous per-stack buff expiry.
2
3use wowlab_engine_ports::Event;
4#[cfg(test)]
5use wowlab_engine_telemetry::TelemetrySink;
6use wowlab_types::{
7    constants::MS_PER_SECOND,
8    sim::{AuraKey, SimTime},
9};
10
11use super::auras::{emit_aura_apply_event, emit_aura_refresh_event, expire_aura};
12use crate::{
13    context::{CombatCtx, HookCtx},
14    state::{AuraActivation, AuraMutation, AuraRefresh, LocalAuraIdx},
15};
16
17fn apply_async_stack(
18    ctx: &mut CombatCtx<'_>,
19    local: LocalAuraIdx,
20    key: AuraKey,
21    duration_ms: u32,
22    event_target: Option<wowlab_types::sim::EnemyIdx>,
23) {
24    let mut hook = ctx.hook_ctx();
25
26    apply_async_stack_with_hook(&mut hook, local, key, duration_ms, event_target);
27}
28
29fn apply_async_stack_with_hook(
30    ctx: &mut HookCtx<'_>,
31    local: LocalAuraIdx,
32    key: AuraKey,
33    duration_ms: u32,
34    event_target: Option<wowlab_types::sim::EnemyIdx>,
35) {
36    debug_assert_eq!(
37        ctx.target, event_target,
38        "async-stack hook target {:?} does not match event target {event_target:?}",
39        ctx.target
40    );
41
42    // #t(rust_unchecked_indexing) local is validated at build time
43    let aura = ctx.program.auras[local.as_usize()];
44    let base_dur = SimTime::from_millis(duration_ms);
45    let expiry = ctx.now.saturating_add(base_dur);
46    let expiry_s = expiry.as_secs_f64();
47    let Some(pool) = ctx.program.aura_pool_index(local, key) else {
48        return;
49    };
50
51    let (stacks, was_fresh) = ctx.instance.runtime.pools.async_stack_expiries.push_capped(
52        pool,
53        expiry,
54        usize::from(aura.max_stacks.max(1)),
55    );
56
57    let stacks_i32 = i32::try_from(stacks).expect("live stacks are capped at max_stacks (u8)");
58
59    let Some(binding) = ctx.program.aura_binding(local, key) else {
60        return;
61    };
62    let mutation = if was_fresh {
63        AuraMutation::Activate(AuraActivation {
64            expires_at: expiry_s,
65            base_duration: f64::from(duration_ms) / MS_PER_SECOND,
66            stacks: stacks_i32,
67            max_stacks: i32::from(aura.max_stacks),
68        })
69    } else {
70        AuraMutation::Refresh(AuraRefresh {
71            expires_at: Some(expiry_s),
72            stacks: stacks_i32,
73        })
74    };
75
76    ctx.program
77        .mutate_aura(ctx.instance, binding, mutation, ctx.now);
78    let stacks_u8 = u8::try_from(stacks).expect("live stacks are capped at max_stacks (u8)");
79
80    if was_fresh {
81        emit_aura_apply_event(
82            ctx.program,
83            ctx.instance,
84            ctx.sink,
85            key,
86            aura.aura_id,
87            stacks_u8,
88            ctx.now,
89        );
90    } else {
91        emit_aura_refresh_event(
92            ctx.program,
93            ctx.instance,
94            ctx.sink,
95            key,
96            aura.aura_id,
97            stacks_u8,
98            ctx.now,
99        );
100    }
101
102    ctx.instance.schedule(Event::AuraExpire {
103        t: expiry,
104        aura: ctx
105            .program
106            .aura_event(local, key, event_target)
107            .expect("async aura has a finalized event binding"),
108    });
109}
110
111pub(crate) fn apply_async_stacks(
112    ctx: &mut HookCtx<'_>,
113    local: LocalAuraIdx,
114    key: AuraKey,
115    duration_ms: u32,
116    doses: u8,
117) -> bool {
118    let Some(pool) = ctx.program.aura_pool_index(local, key) else {
119        return false;
120    };
121    let was_fresh = ctx
122        .instance
123        .runtime
124        .pools
125        .async_stack_expiries
126        .is_empty(pool);
127    let event_target = ctx.target;
128
129    let applied = CombatCtx::for_optional_target(ctx, event_target, |combat| {
130        for _ in 0..doses {
131            apply_async_stack(combat, local, key, duration_ms, event_target);
132        }
133    });
134
135    if applied.is_none() {
136        for _ in 0..doses {
137            apply_async_stack_with_hook(ctx, local, key, duration_ms, event_target);
138        }
139    }
140
141    was_fresh
142}
143
144pub(crate) fn expire_async_stacks_due(ctx: &mut HookCtx<'_>, local: LocalAuraIdx, key: AuraKey) {
145    // #t(rust_unchecked_indexing) local is validated at build time
146    let aura = ctx.program.auras[local.as_usize()];
147    let Some(pool) = ctx.program.aura_pool_index(local, key) else {
148        return;
149    };
150    let Some((popped, remaining)) = ctx
151        .instance
152        .runtime
153        .pools
154        .async_stack_expiries
155        .expire_due(pool, ctx.now)
156    else {
157        return;
158    };
159
160    if popped == 0 {
161        return;
162    }
163
164    if remaining == 0 {
165        expire_aura(ctx, local);
166
167        return;
168    }
169
170    let remaining = u8::try_from(remaining).unwrap_or(u8::MAX);
171
172    if let Some(binding) = ctx.program.aura_binding(local, key) {
173        ctx.program.mutate_aura(
174            ctx.instance,
175            binding,
176            AuraMutation::SetStacks(i32::from(remaining)),
177            ctx.now,
178        );
179    }
180
181    emit_aura_refresh_event(
182        ctx.program,
183        ctx.instance,
184        ctx.sink,
185        key,
186        aura.aura_id,
187        remaining,
188        ctx.now,
189    );
190}
191
192#[cfg(test)]
193#[path = "async_stacks/tests.rs"]
194mod tests;