Skip to main content

wowlab_engine_combat/systems/auras/
copy.rs

1use super::{
2    ActorId, CombatInstance, EnemyIdx, LocalAuraIdx, PERMANENT_AURA_EXPIRES_AT_S, SimTime,
3    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,
9};
10#[cfg(test)]
11use crate::state::AuraMutation;
12
13/// Copy a target aura from `source_target` to `destination_target`.
14///
15/// The source caster is the hook context's source actor.
16/// Returns `false` for an inactive source or an aura with another scope.
17#[cfg(test)]
18pub(crate) fn copy_target_aura(
19    ctx: &mut HookCtx<'_>,
20    local: LocalAuraIdx,
21    source_target: EnemyIdx,
22    destination_target: EnemyIdx,
23    behavior: DotCopyBehavior,
24) -> bool {
25    // BOUNDS: public local aura indices are allocated from this definition table.
26    let aura = ctx.program.auras[local.as_usize()];
27
28    if aura.on != wowlab_types::sim::AuraOn::Target {
29        return false;
30    }
31
32    let Some(source_key) = aura_key_for(
33        ctx.program,
34        ctx.instance,
35        local,
36        ctx.source,
37        Some(source_target),
38    ) else {
39        return false;
40    };
41    let Some(source_slot) = ctx
42        .program
43        .buffer_aura(ctx.instance, local, source_key)
44        .copied()
45    else {
46        return false;
47    };
48
49    if !source_slot.is_active(ctx.now.as_secs_f64()) {
50        return false;
51    }
52
53    let Some(destination_key) = aura_key_for(
54        ctx.program,
55        ctx.instance,
56        local,
57        ctx.source,
58        Some(destination_target),
59    ) else {
60        return false;
61    };
62
63    let now_s = ctx.now.as_secs_f64();
64    let source_remaining = source_slot.expires_at - now_s;
65    let cloned_duration = match behavior {
66        DotCopyBehavior::Start | DotCopyBehavior::CloneNoRefresh => source_remaining,
67    };
68
69    let Some(source_binding) = ctx.program.aura_binding(local, source_key) else {
70        return false;
71    };
72    let Some(mut destination_state): Option<crate::state::AuraTransferState> = ctx
73        .program
74        .capture_aura_transfer(ctx.instance, source_binding)
75    else {
76        return false;
77    };
78
79    destination_state = destination_state.with_expiry(now_s + cloned_duration);
80
81    if behavior == DotCopyBehavior::Start {
82        destination_state = destination_state.restart_ticks(now_s, cloned_duration);
83    }
84
85    let Some(destination_binding) = ctx.program.aura_binding(local, destination_key) else {
86        return false;
87    };
88
89    ctx.program.mutate_aura(
90        ctx.instance,
91        destination_binding,
92        AuraMutation::Transfer(destination_state),
93        ctx.now,
94    );
95
96    let stacks = wowlab_types::numeric::i32_to_u8_saturating(destination_state.stacks());
97
98    emit_aura_apply_event(
99        ctx.program,
100        ctx.instance,
101        ctx.sink,
102        destination_key,
103        aura.aura_id,
104        stacks,
105        ctx.now,
106    );
107    schedule_aura_expiry(ctx, local, destination_key);
108
109    if destination_state.tick_interval() > 0.0
110        && destination_state.next_tick() <= destination_state.expires_at()
111    {
112        ctx.instance.schedule(Event::AuraTick {
113            t: SimTime::from_secs_f64(destination_state.next_tick()),
114            aura: ctx
115                .program
116                .aura_event(local, destination_key, Some(destination_target))
117                .expect("copied aura has a finalized event binding"),
118        });
119    }
120
121    enqueue_aura_application_proc(ctx, local, &aura, destination_key);
122
123    true
124}
125
126pub(crate) fn aura_remaining_ms(
127    program: &crate::state::CombatProgram,
128    state: &CombatInstance,
129    local: LocalAuraIdx,
130    source: ActorId,
131    target: Option<EnemyIdx>,
132    now: SimTime,
133) -> Option<u32> {
134    let key = aura_key_for(program, state, local, source, target)?;
135    let slot = program.buffer_aura(state, local, key)?;
136
137    if !slot.is_active(now.as_secs_f64()) {
138        return None;
139    }
140
141    if slot.expires_at >= PERMANENT_AURA_EXPIRES_AT_S {
142        return Some(u32::MAX);
143    }
144
145    Some(wowlab_types::numeric::f64_to_u32_saturating_ceil(
146        (slot.expires_at - now.as_secs_f64()) * wowlab_types::constants::MS_PER_SECOND,
147    ))
148}