Skip to main content

wowlab_engine_content/hooks/destruction_warlock/
handler.rs

1use wowlab_engine_combat::GuardianOps as _;
2use wowlab_engine_combat::{
3    CombatHandler, DamageFlags, DriverSpellModifier, DriverSpellModifierResult,
4    GroundEffectDefinition, GroundEffectId, GuardianAbility, GuardianSpec,
5};
6use wowlab_engine_ports::HandlerParams;
7use wowlab_types::constants::MS_PER_SECOND;
8
9use crate::generated::specs::destruction_warlock::{
10    DAMAGE_EFFECT, EFFECT, REPORTED_SPELL, SET_BONUS, SPELL,
11};
12use wowlab_engine_domain::dbc::ResolvedGameDataEffectExt as _;
13use wowlab_types::constants::HUNDRED;
14
15static MID2_INCINERATE_SPELLS: [u32; 1] = [SPELL::INCINERATE];
16pub(super) const BASE_ECHO_CHANCE: f64 = 0.10;
17
18use super::hooks::{SAYAAD_ACTION_MS, sayaad_action};
19
20#[derive(Clone, Copy, Debug)]
21pub(super) struct DestructionConfig {
22    pub(super) rain_of_fire_ground: Option<GroundEffectId>,
23    pub(super) echo_chance: f64,
24    pub(super) mid2_4pc: bool,
25    pub(super) incinerate_mult: f64,
26}
27
28#[derive(Debug)]
29// #t(rust_similar_structs) specialization handlers own independent hook behavior despite matching runtime state fields
30pub(crate) struct DestructionHandler {
31    inner: CombatHandler,
32    fight_duration_ms: u32,
33}
34
35impl DestructionHandler {
36    /// Finishes the specialization handler from the shared-content composition stage.
37    pub(crate) fn new(
38        parts: crate::composition::ContentHandlerParts,
39        params: &HandlerParams<'_>,
40    ) -> Result<Self, wowlab_engine_ports::EngineError> {
41        let mut parts = parts;
42        let built = parts.draft_mut();
43
44        let has_mid2_2pc = params
45            .set_bonus_auras
46            .contains(&SET_BONUS::MID2_DESTRUCTION_2PC);
47        let mid2_4pc = params
48            .set_bonus_auras
49            .contains(&SET_BONUS::MID2_DESTRUCTION_4PC);
50
51        let incinerate_mult = if has_mid2_2pc {
52            1.0 + params
53                .game_data
54                .effect_base_points(EFFECT::MID2_DESTRUCTION_2PC_INCINERATE)
55                / HUNDRED
56        } else {
57            1.0
58        };
59        let echo_chance = BASE_ECHO_CHANCE
60            + if has_mid2_2pc {
61                params
62                    .game_data
63                    .effect_base_points(EFFECT::MID2_DESTRUCTION_2PC_ECHO_CHANCE)
64                    / HUNDRED
65            } else {
66                0.0
67            };
68
69        if has_mid2_2pc {
70            built.register_driver_spell_modifier(DriverSpellModifier::new(
71                SET_BONUS::MID2_DESTRUCTION_2PC,
72                &MID2_INCINERATE_SPELLS,
73                mid2_incinerate_modifier,
74            ));
75        }
76
77        if let Some(local) = built.spell_local(SPELL::CHANNEL_DEMONFIRE) {
78            built.patch_spell(local, |spell| {
79                // Talent selection owns availability; 228312 is not exported in 12.1.
80                spell.requirements.caster_aura_spell = 0;
81            });
82        }
83
84        if let Some(local) = built.spell_local(SPELL::CORRUPTION) {
85            built.patch_spell(local, |spell| {
86                // Metamorphosis is unavailable to Destruction; its legacy exclusion is inert.
87                spell.requirements.exclude_caster_aura_spell = 0;
88            });
89        }
90
91        let rain_of_fire_ground = (!params.game_data.is_empty())
92            .then(|| {
93                built.register_ground_effect(
94                    GroundEffectDefinition::from_driver(
95                        DAMAGE_EFFECT::RAIN_OF_FIRE_TIMING.action(),
96                        DAMAGE_EFFECT::RAIN_OF_FIRE_TIMING,
97                        DAMAGE_EFFECT::RAIN_OF_FIRE_DAMAGE,
98                        DAMAGE_EFFECT::RAIN_OF_FIRE_DAMAGE,
99                    )
100                    .flags(DamageFlags::PERIODIC),
101                )
102            })
103            .transpose()?;
104
105        built.set_spec_config(DestructionConfig {
106            rain_of_fire_ground,
107            echo_chance,
108            mid2_4pc,
109            incinerate_mult,
110        });
111
112        let built = parts.finalize()?;
113
114        Ok(Self {
115            inner: CombatHandler::from_built(built),
116            fight_duration_ms: wowlab_types::numeric::f64_to_u32_saturating_trunc(
117                params.fight_duration_secs * MS_PER_SECOND,
118            ),
119        })
120    }
121}
122
123fn mid2_incinerate_modifier(view: wowlab_engine_combat::HookView<'_>) -> DriverSpellModifierResult {
124    DriverSpellModifierResult::damage_mult(
125        view.program()
126            .spec_config::<DestructionConfig>()
127            .map_or(1.0, |cfg| cfg.incinerate_mult),
128    )
129}
130
131crate::hooks::define_spec_handler! {
132impl DestructionHandler(self, inner) {
133    fn on_sim_start(&mut self, ctx: &mut wowlab_engine_ports::SimContext) {
134        self.inner.on_sim_start(ctx);
135        self.inner
136            .with_hook_ctx(&mut *ctx.telemetry, wowlab_engine_combat::HookCtxRequest::targetless(wowlab_types::sim::SimTime::ZERO), |precombat| {
137                let _ = precombat.summon_guardian(
138                    GuardianSpec::new_inheriting_owner(
139                        REPORTED_SPELL::LASH_OF_PAIN,
140                        self.fight_duration_ms.saturating_add(1),
141                    )
142                    .ability(
143                        GuardianAbility::new(SAYAAD_ACTION_MS, sayaad_action)
144                            .first_action_delay(SAYAAD_ACTION_MS),
145                    ),
146                );
147            });
148    }
149
150}
151}