wowlab_engine_combat/systems/auras/
stacks.rs1use super::{
2 ActorId, CombatState, DenseBuffer, EnemyIdx, Event, HookCtx, LocalAuraIdx,
3 PERMANENT_AURA_EXPIRES_AT_S, SimTime, apply_aura, aura_query_key_for, context_aura_key,
4 emit_aura_refresh_event, enqueue_aura_application_proc, expire_aura, refresh_aura_projections,
5 refreshed_stacks, revive_tick_chain, synchronize_aura_dependents,
6};
7
8pub(crate) fn add_aura_stack(ctx: &mut HookCtx<'_>, local: LocalAuraIdx) {
10 let aura = ctx.state.defs.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 .buf
19 .aura(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 stacks = if let Some(slot) = ctx.buf.aura_mut(key) {
28 slot.stacks = refreshed_stacks(&aura, slot.stacks);
29
30 wowlab_types::numeric::i32_to_u8_saturating(slot.stacks)
31 } else {
32 return;
33 };
34
35 emit_aura_refresh_event(ctx.state, ctx.sink, key, aura.aura_id, stacks, ctx.now);
36 synchronize_aura_dependents(ctx);
37 enqueue_aura_application_proc(ctx, &aura, key);
38}
39
40pub(crate) fn consume_aura_stack_by_id(ctx: &mut HookCtx<'_>, aura_id: u32) -> bool {
41 let Some(&local) = ctx.state.index.aura_by_id.get(&aura_id) else {
42 return false;
43 };
44
45 consume_aura_stack(ctx, local)
46}
47
48pub fn extend_aura(ctx: &mut HookCtx<'_>, local: LocalAuraIdx, extend_ms: u32) {
50 let aura = ctx.state.defs.auras[local.as_usize()];
52 let Some(key) = context_aura_key(ctx, local) else {
53 tracing::trace!(
54 aura_id = aura.aura_id,
55 source = ?ctx.source,
56 target = ?ctx.target,
57 extend_ms,
58 "AURA_EXTEND_NO_CONTEXT_KEY"
59 );
60
61 return;
62 };
63
64 let (old_expiry, new_expiry, stacks) = {
65 let Some(a) = ctx.buf.aura_mut(key) else {
66 tracing::trace!(
67 aura_id = aura.aura_id,
68 key = ?key,
69 extend_ms,
70 "AURA_EXTEND_MISSING_INSTANCE"
71 );
72
73 return;
74 };
75
76 if !a.is_occupied() || a.expires_at >= PERMANENT_AURA_EXPIRES_AT_S || extend_ms == 0 {
77 tracing::trace!(
78 aura_id = aura.aura_id,
79 key = ?key,
80 occupied = a.is_occupied(),
81 expires_at = a.expires_at,
82 extend_ms,
83 "AURA_EXTEND_INELIGIBLE"
84 );
85
86 return;
87 }
88
89 let old_expiry = a.expires_at;
90 let new_expiry =
91 SimTime::from_secs_f64(a.expires_at).saturating_add(SimTime::from_millis(extend_ms));
92
93 a.expires_at = new_expiry.as_secs_f64();
94
95 (
96 old_expiry,
97 new_expiry,
98 wowlab_types::numeric::i32_to_u8_saturating(a.stacks),
99 )
100 };
101
102 tracing::trace!(
103 aura_id = aura.aura_id,
104 key = ?key,
105 old_expiry,
106 new_expiry = new_expiry.as_secs_f64(),
107 extend_ms,
108 "AURA_EXTENDED"
109 );
110
111 revive_tick_chain(ctx, &aura, key, Some(old_expiry));
112 refresh_aura_projections(ctx.buf, ctx.state.current_target());
113
114 emit_aura_refresh_event(ctx.state, ctx.sink, key, aura.aura_id, stacks, ctx.now);
115 ctx.state.schedule(Event::AuraExpire {
116 t: new_expiry,
117 key,
118 target: ctx.target,
119 });
120}
121
122pub(crate) fn reduce_aura(ctx: &mut HookCtx<'_>, local: LocalAuraIdx, reduce_ms: u32) {
123 let Some(key) = context_aura_key(ctx, local) else {
124 return;
125 };
126 let Some(active) = ctx.buf.aura(key) else {
127 return;
128 };
129
130 if !active.is_occupied() || active.expires_at >= PERMANENT_AURA_EXPIRES_AT_S || reduce_ms == 0 {
131 return;
132 }
133
134 let expiry = SimTime::from_secs_f64(active.expires_at);
135 let reduction = SimTime::from_millis(reduce_ms);
136
137 if expiry <= ctx.now.saturating_add(reduction) {
138 expire_aura(ctx, local);
139
140 return;
141 }
142
143 let new_expiry = expiry.saturating_sub(reduction);
144 let stacks = {
145 let active = ctx.buf.aura_mut(key).expect("active aura disappeared");
146
147 active.expires_at = new_expiry.as_secs_f64();
148
149 wowlab_types::numeric::i32_to_u8_saturating(active.stacks)
150 };
151
152 refresh_aura_projections(ctx.buf, ctx.state.current_target());
153 emit_aura_refresh_event(
154 ctx.state,
155 ctx.sink,
156 key,
157 ctx.state.aura(local).aura_id,
158 stacks,
159 ctx.now,
160 );
161 ctx.state.schedule(Event::AuraExpire {
162 t: new_expiry,
163 key,
164 target: ctx.target,
165 });
166}
167
168#[must_use]
170pub fn is_aura_active(
171 view: &crate::context::CombatView<'_>,
172 local: LocalAuraIdx,
173 source: ActorId,
174 target: Option<EnemyIdx>,
175) -> bool {
176 aura_query_key_for(view.state, local, source, target)
177 .and_then(|key| view.buf.aura(key))
178 .is_some_and(wowlab_engine_domain::rotation::AuraSlot::is_occupied)
179}
180
181pub(crate) fn aura_stacks(
182 state: &CombatState,
183 buf: &DenseBuffer,
184 local: LocalAuraIdx,
185 source: ActorId,
186 target: Option<EnemyIdx>,
187) -> i32 {
188 aura_query_key_for(state, local, source, target)
189 .and_then(|key| buf.aura(key))
190 .map_or(0, |a| if a.is_occupied() { a.stacks } else { 0 })
191}
192
193pub(crate) fn aura_stacks_by_id(
194 state: &CombatState,
195 buf: &DenseBuffer,
196 aura_id: u32,
197 source: ActorId,
198 target: Option<EnemyIdx>,
199) -> i32 {
200 state
201 .aura_local(aura_id)
202 .map_or(0, |local| aura_stacks(state, buf, local, source, target))
203}
204
205#[inline]
206pub(crate) fn bypass_aura_active(
207 state: &CombatState,
208 buf: &DenseBuffer,
209 aura_id: u32,
210 source: ActorId,
211 target: Option<EnemyIdx>,
212) -> bool {
213 aura_id != 0 && aura_stacks_by_id(state, buf, aura_id, source, target) > 0
214}
215
216pub(crate) fn consume_aura(ctx: &mut HookCtx<'_>, local: LocalAuraIdx) -> bool {
217 let Some(key) = context_aura_key(ctx, local) else {
218 return false;
219 };
220 let stacks = ctx
221 .buf
222 .aura(key)
223 .map_or(0, |aura| if aura.is_occupied() { aura.stacks } else { 0 });
224
225 if stacks <= 0 {
226 return false;
227 }
228 let aura = ctx.state.defs.auras[local.as_usize()];
231
232 if aura.on_consume.len > 0 {
233 let repeat = wowlab_types::numeric::i32_to_u8_saturating(stacks);
234
235 ctx.state
236 .runtime
237 .deferred_work
238 .push_effect(crate::state::PendingEffectProgram {
239 effects: aura.on_consume,
240 profile_spell_id: aura.aura_id,
241 repeat,
242 source: ctx.source,
243 target: ctx.target,
244 effect_target: ctx.effect_target,
245 flags: crate::DamageFlags::empty(),
246 originating_proc_driver_spell_id: 0,
247 });
248 }
249
250 expire_aura(ctx, local);
251
252 true
253}
254
255pub(crate) fn consume_aura_stack(ctx: &mut HookCtx<'_>, local: LocalAuraIdx) -> bool {
256 let Some(key) = context_aura_key(ctx, local) else {
257 return false;
258 };
259 let Some(active) = ctx.buf.aura(key) else {
260 return false;
261 };
262
263 if !active.is_occupied() {
264 return false;
265 }
266
267 let expires = active.stacks <= 1;
268 let aura = ctx.state.defs.auras[local.as_usize()];
270
271 if aura.on_consume.len > 0 {
272 ctx.state
273 .runtime
274 .deferred_work
275 .push_effect(crate::state::PendingEffectProgram {
276 effects: aura.on_consume,
277 profile_spell_id: aura.aura_id,
278 repeat: 1,
279 source: ctx.source,
280 target: ctx.target,
281 effect_target: ctx.effect_target,
282 flags: crate::DamageFlags::empty(),
283 originating_proc_driver_spell_id: 0,
284 });
285 }
286
287 if expires {
288 expire_aura(ctx, local);
289
290 return true;
291 }
292
293 if aura.async_stacks {
294 ctx.state
295 .runtime
296 .pools
297 .async_stack_expiries
298 .remove_oldest(&key);
299 }
300
301 if let Some(active) = ctx.buf.aura_mut(key) {
302 active.stacks -= 1;
303 }
304
305 synchronize_aura_dependents(ctx);
306
307 true
308}
309
310pub(crate) fn consume_aura_stacks(ctx: &mut HookCtx<'_>, local: LocalAuraIdx, count: i32) -> i32 {
312 let to_consume =
313 aura_stacks(ctx.state, ctx.buf, local, ctx.source, ctx.target).min(count.max(0));
314 let mut consumed = 0;
315
316 for _ in 0..to_consume {
317 if !consume_aura_stack(ctx, local) {
318 break;
319 }
320
321 consumed += 1;
322 }
323
324 consumed
325}
326
327pub(crate) fn take_aura_stacks(ctx: &mut HookCtx<'_>, local: LocalAuraIdx) -> i32 {
329 let aura = ctx.state.defs.auras[local.as_usize()];
331 let Some(key) = context_aura_key(ctx, local) else {
332 return 0;
333 };
334 let Some(slot) = ctx.buf.aura_mut(key) else {
335 return 0;
336 };
337
338 if !slot.is_occupied() {
339 return 0;
340 }
341
342 let stacks = std::mem::take(&mut slot.stacks).max(0);
343
344 emit_aura_refresh_event(ctx.state, ctx.sink, key, aura.aura_id, 0, ctx.now);
345 synchronize_aura_dependents(ctx);
346
347 stacks
348}