wowlab_engine_combat/systems/
async_stacks.rs1use 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 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 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;