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