wowlab_engine_combat/pipeline/
empower.rs1use super::{
2 CombatCtx, EmpowerReleaseAdjustment, HookCtx, LocalSpellIdx, RuntimeDamageDef,
3 SpellCastPayload, SpellData, aura_stacks_by_id, secondary_spends_all_points,
4};
5
6pub(super) fn empower_release_adjustment(
7 ctx: &mut CombatCtx<'_>,
8 spell_local: LocalSpellIdx,
9 rank: u8,
10) -> EmpowerReleaseAdjustment {
11 if rank == 0 {
12 return EmpowerReleaseAdjustment::default();
13 }
14
15 let hook = ctx
16 .state
17 .defs
18 .empower_release_hooks
19 .get(spell_local.as_usize())
20 .copied()
21 .flatten();
22 let Some(hook) = hook else {
23 return EmpowerReleaseAdjustment::default();
24 };
25 let hook_ctx = HookCtx::new(
26 crate::context::HookCtxServices {
27 state: ctx.state,
28 buf: ctx.buf,
29 sink: ctx.sink,
30 rng: ctx.rng,
31 },
32 crate::context::HookCtxRequest::for_target(ctx.now, ctx.target).with_source(ctx.source),
33 )
34 .with_empower_rank(rank);
35
36 hook(&hook_ctx, rank)
37}
38
39pub(super) fn adjust_empower_payload(
40 state: &crate::state::CombatState,
41 spell: &SpellData,
42 damage: RuntimeDamageDef,
43 adjustment: EmpowerReleaseAdjustment,
44) -> SpellCastPayload {
45 let unchanged = SpellCastPayload {
46 damage,
47 base_points: spell.base_points,
48 aura_duration_ms: None,
49 };
50 let Some(transfer) = adjustment.periodic_tick_transfer else {
51 return unchanged;
52 };
53
54 let Some((periodic, aura_duration_ms)) = empower_periodic_transfer_data(
55 state,
56 spell,
57 transfer.total_ticks,
58 transfer.transferred_ticks,
59 ) else {
60 return unchanged;
61 };
62 let ticks = f64::from(transfer.transferred_ticks);
63
64 match (damage, periodic) {
65 (
66 RuntimeDamageDef::SpCoefficient { coef, is_physical },
67 crate::state::PeriodicKind::DamageSp {
68 coef: periodic_coef,
69 is_physical: periodic_physical,
70 }
71 | crate::state::PeriodicKind::RollingDamageSp {
72 coef: periodic_coef,
73 is_physical: periodic_physical,
74 },
75 ) if is_physical == periodic_physical => SpellCastPayload {
76 damage: RuntimeDamageDef::SpCoefficient {
77 coef: coef + periodic_coef * ticks,
78 is_physical,
79 },
80 base_points: spell.base_points,
81 aura_duration_ms: Some(aura_duration_ms),
82 },
83 (
84 RuntimeDamageDef::None,
85 crate::state::PeriodicKind::DamageSp { coef, is_physical }
86 | crate::state::PeriodicKind::RollingDamageSp { coef, is_physical },
87 ) => SpellCastPayload {
88 damage: RuntimeDamageDef::SpCoefficient {
89 coef: coef * ticks,
90 is_physical,
91 },
92 base_points: spell.base_points,
93 aura_duration_ms: Some(aura_duration_ms),
94 },
95 (
96 RuntimeDamageDef::ApCoefficient {
97 coef,
98 is_physical,
99 ap_type,
100 },
101 crate::state::PeriodicKind::DamageAp {
102 coef: periodic_coef,
103 is_physical: periodic_physical,
104 }
105 | crate::state::PeriodicKind::RollingDamageAp {
106 coef: periodic_coef,
107 is_physical: periodic_physical,
108 },
109 ) if is_physical == periodic_physical => SpellCastPayload {
110 damage: RuntimeDamageDef::ApCoefficient {
111 coef: coef + periodic_coef * ticks,
112 is_physical,
113 ap_type,
114 },
115 base_points: spell.base_points,
116 aura_duration_ms: Some(aura_duration_ms),
117 },
118 (
119 RuntimeDamageDef::None,
120 crate::state::PeriodicKind::DamageAp { coef, is_physical }
121 | crate::state::PeriodicKind::RollingDamageAp { coef, is_physical },
122 ) => SpellCastPayload {
123 damage: RuntimeDamageDef::ApCoefficient {
124 coef: coef * ticks,
125 is_physical,
126 ap_type: crate::state::WeaponApType::None,
127 },
128 base_points: spell.base_points,
129 aura_duration_ms: Some(aura_duration_ms),
130 },
131 (
132 RuntimeDamageDef::Flat(amount),
133 crate::state::PeriodicKind::FlatDamage {
134 amount: periodic_amount,
135 ..
136 }
137 | crate::state::PeriodicKind::RollingFlatDamage {
138 amount: periodic_amount,
139 ..
140 },
141 ) => SpellCastPayload {
142 damage: RuntimeDamageDef::Flat(amount + periodic_amount * ticks),
143 base_points: spell.base_points,
144 aura_duration_ms: Some(aura_duration_ms),
145 },
146 (
147 RuntimeDamageDef::None,
148 crate::state::PeriodicKind::FlatDamage { amount, .. }
149 | crate::state::PeriodicKind::RollingFlatDamage { amount, .. },
150 ) => SpellCastPayload {
151 damage: RuntimeDamageDef::Flat(amount * ticks),
152 base_points: spell.base_points,
153 aura_duration_ms: Some(aura_duration_ms),
154 },
155 (
156 direct @ (RuntimeDamageDef::SpCoefficient { .. }
157 | RuntimeDamageDef::ApCoefficient { .. }),
158 crate::state::PeriodicKind::FlatDamage { amount, .. }
159 | crate::state::PeriodicKind::RollingFlatDamage { amount, .. },
160 ) => SpellCastPayload {
161 damage: direct,
162 base_points: spell.base_points + amount * ticks,
163 aura_duration_ms: Some(aura_duration_ms),
164 },
165 _ => {
166 tracing::warn!(
167 spell_id = spell.spell_id,
168 "empower periodic transfer has incompatible direct and periodic damage kinds"
169 );
170
171 unchanged
172 }
173 }
174}
175
176pub(super) fn empower_periodic_transfer_data(
177 state: &crate::state::CombatState,
178 spell: &SpellData,
179 total_ticks: u16,
180 transferred_ticks: u16,
181) -> Option<(crate::state::PeriodicKind, u32)> {
182 if transferred_ticks > total_ticks {
183 tracing::warn!(
184 spell_id = spell.spell_id,
185 total_ticks,
186 transferred_ticks,
187 "empower release driver transferred more periodic ticks than exist"
188 );
189
190 return None;
191 }
192
193 let Some(aura_local) = spell.applies_aura else {
194 tracing::warn!(
195 spell_id = spell.spell_id,
196 "empower periodic transfer requires an applied aura"
197 );
198
199 return None;
200 };
201 let Some(aura) = state.defs.auras.get(aura_local.as_usize()).copied() else {
202 tracing::warn!(
203 spell_id = spell.spell_id,
204 "empower periodic transfer references an unknown aura"
205 );
206
207 return None;
208 };
209 let Some(periodic) = aura.periodic else {
210 tracing::warn!(
211 spell_id = spell.spell_id,
212 aura_id = aura.aura_id,
213 "empower periodic transfer requires a periodic aura"
214 );
215
216 return None;
217 };
218 let remaining_ticks = total_ticks - transferred_ticks;
219 let Some(aura_duration_ms) = u32::from(remaining_ticks).checked_mul(periodic.tick_interval_ms)
220 else {
221 tracing::warn!(
222 spell_id = spell.spell_id,
223 aura_id = aura.aura_id,
224 remaining_ticks,
225 tick_interval_ms = periodic.tick_interval_ms,
226 "empower periodic duration overflow"
227 );
228
229 return None;
230 };
231
232 Some((periodic.effect, aura_duration_ms))
233}
234
235pub(super) fn finisher_scaled_damage(ctx: &CombatCtx<'_>, spell: &SpellData) -> RuntimeDamageDef {
236 if spell.cost.secondary_resource_cost > 0.0 && secondary_spends_all_points(ctx.state) {
237 spell
238 .damage
239 .scaled(ctx.state.runtime.resources.last_secondary_spent)
240 } else {
241 spell.damage
242 }
243}
244
245pub(super) fn empower_rank_for_cast(
246 ctx: &CombatCtx<'_>,
247 spell_local: LocalSpellIdx,
248 requested_rank: u8,
249) -> u8 {
250 let Some(spell) = ctx.state.defs.spells.get(spell_local.as_usize()).copied() else {
251 return 0;
252 };
253
254 if spell.empower_rank_count > 0
255 && spell.max_empower_aura_id != 0
256 && aura_stacks_by_id(
257 ctx.state,
258 ctx.buf,
259 spell.max_empower_aura_id,
260 ctx.source,
261 Some(ctx.target),
262 ) > 0
263 {
264 spell.empower_rank_count
265 } else if spell.empower_rank_count > 0 {
266 requested_rank.max(1).min(spell.empower_rank_count)
267 } else {
268 0
269 }
270}