Skip to main content

wowlab_engine_combat/systems/auras/
copy.rs

1use super::{
2    ActorId, CombatState, DenseBuffer, EnemyIdx, LocalAuraIdx, PERMANENT_AURA_EXPIRES_AT_S,
3    SimTime, aura_key_for,
4};
5#[cfg(test)]
6use super::{
7    DotCopyBehavior, Event, HookCtx, emit_aura_apply_event, enqueue_aura_application_proc,
8    schedule_aura_expiry, synchronize_aura_dependents,
9};
10
11/// Copy a target aura from `source_target` to `destination_target`.
12///
13/// The source caster is the hook context's source actor.
14/// Returns `false` for an inactive source or an aura with another scope.
15#[cfg(test)]
16pub(crate) fn copy_target_aura(
17    ctx: &mut HookCtx<'_>,
18    local: LocalAuraIdx,
19    source_target: EnemyIdx,
20    destination_target: EnemyIdx,
21    behavior: DotCopyBehavior,
22) -> bool {
23    // BOUNDS: public local aura indices are allocated from this definition table.
24    let aura = ctx.state.defs.auras[local.as_usize()];
25
26    if aura.on != wowlab_types::sim::AuraOn::Target {
27        return false;
28    }
29
30    let Some(source_key) = aura_key_for(ctx.state, local, ctx.source, Some(source_target)) else {
31        return false;
32    };
33    let Some(source_slot) = ctx.buf.aura(source_key).copied() else {
34        return false;
35    };
36
37    if !source_slot.is_active(ctx.now.as_secs_f64()) {
38        return false;
39    }
40
41    let Some(destination_key) =
42        aura_key_for(ctx.state, local, ctx.source, Some(destination_target))
43    else {
44        return false;
45    };
46
47    ctx.buf.ensure_aura_slot(destination_key);
48
49    let now_s = ctx.now.as_secs_f64();
50    let source_remaining = source_slot.expires_at - now_s;
51    let cloned_duration = match behavior {
52        DotCopyBehavior::Start | DotCopyBehavior::CloneNoRefresh => source_remaining,
53    };
54
55    let mut destination_slot = source_slot;
56
57    destination_slot.expires_at = now_s + cloned_duration;
58
59    if behavior == DotCopyBehavior::Start && destination_slot.tick_interval > 0.0 {
60        destination_slot.next_tick = now_s + destination_slot.tick_interval;
61        destination_slot.remaining_ticks = wowlab_types::numeric::f64_to_i32_saturating_ceil(
62            cloned_duration / destination_slot.tick_interval,
63        );
64    }
65
66    if let Some(slot) = ctx.buf.aura_mut(destination_key) {
67        *slot = destination_slot;
68    }
69
70    let stacks = wowlab_types::numeric::i32_to_u8_saturating(destination_slot.stacks);
71
72    emit_aura_apply_event(
73        ctx.state,
74        ctx.sink,
75        destination_key,
76        aura.aura_id,
77        stacks,
78        ctx.now,
79    );
80    schedule_aura_expiry(ctx, destination_key);
81
82    if destination_slot.tick_interval > 0.0
83        && destination_slot.next_tick <= destination_slot.expires_at
84    {
85        ctx.state.schedule(Event::AuraTick {
86            t: SimTime::from_secs_f64(destination_slot.next_tick),
87            key: destination_key,
88            target: Some(destination_target),
89        });
90    }
91
92    synchronize_aura_dependents(ctx);
93    enqueue_aura_application_proc(ctx, &aura, destination_key);
94
95    true
96}
97
98pub(crate) fn aura_remaining_ms(
99    state: &CombatState,
100    buf: &DenseBuffer,
101    local: LocalAuraIdx,
102    source: ActorId,
103    target: Option<EnemyIdx>,
104    now: SimTime,
105) -> Option<u32> {
106    let key = aura_key_for(state, local, source, target)?;
107    let slot = buf.aura(key)?;
108
109    if !slot.is_active(now.as_secs_f64()) {
110        return None;
111    }
112
113    if slot.expires_at >= PERMANENT_AURA_EXPIRES_AT_S {
114        return Some(u32::MAX);
115    }
116
117    Some(wowlab_types::numeric::f64_to_u32_saturating_ceil(
118        (slot.expires_at - now.as_secs_f64()) * wowlab_types::constants::MS_PER_SECOND,
119    ))
120}