Skip to main content

wowlab_engine_combat/pipeline/
empower.rs

1use 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}