wowlab_engine_combat/systems/auras/
copy.rs1use 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#[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 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}