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