wowlab_engine_combat/systems/
async_stacks.rs1#[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 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 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;