Skip to main content

wowlab_engine_content/hooks/marksmanship_hunter/
handler.rs

1use wowlab_engine_combat::AuraOps as _;
2use wowlab_engine_combat::{
3    CombatHandler, ImpactEvent, ImpactProc, is_aura_active, set_spell_gate,
4};
5use wowlab_engine_domain::pool::SpellGate;
6use wowlab_engine_ports::{EngineError, HandlerParams, SimContext, SpecAction};
7
8use wowlab_types::sim::SpellIdx;
9
10use crate::generated::specs::marksmanship_hunter::{
11    AURA_DEATHBLOW, AURA_MASTER_MARKSMAN, HERO, SPELL, SPELL_KILL_SHOT,
12};
13
14use super::config::{MarksmanshipConfig, derive_config, marksmanship_config};
15
16const fn is_master_marksman_trigger(spell_id: u32) -> bool {
17    matches!(
18        spell_id,
19        SPELL::AIMED_SHOT
20            | SPELL::RAPID_FIRE_TICK
21            | SPELL::ARCANE_SHOT
22            | SPELL::KILL_SHOT
23            | SPELL::MULTI_SHOT
24            | SPELL::STEADY_SHOT
25            | SPELL::VOLLEY
26            | HERO::DARK_RANGER::SPELL::BLACK_ARROW
27            | HERO::DARK_RANGER::SPELL::BLACK_ARROW_WITHERING_FIRE
28    )
29}
30
31fn trigger_master_marksman(ctx: &mut wowlab_engine_combat::HookCtx<'_>, event: ImpactEvent) {
32    let fraction = marksmanship_config(ctx).master_marksman_fraction;
33
34    ctx.add_residual_damage(AURA_MASTER_MARKSMAN.raw(), event.amount * fraction);
35}
36
37fn register_master_marksman(
38    built: &mut wowlab_engine_combat::BuiltCombatSystem,
39    cfg: &MarksmanshipConfig,
40) {
41    if !cfg.master_marksman {
42        return;
43    }
44
45    // Rapid Fire eligibility comes from the explicit tick-spell filter.
46
47    built.register_impact_proc(
48        ImpactProc::new(trigger_master_marksman)
49            .with_spell_filter(is_master_marksman_trigger)
50            .crit_only(),
51    );
52}
53
54/// Maintains Kill Shot and Black Arrow's health and Deathblow gate.
55#[derive(Debug)]
56pub(crate) struct MarksmanshipHandler {
57    inner: CombatHandler,
58    cfg: MarksmanshipConfig,
59}
60
61const fn is_black_arrow_dot(spell_id: u32) -> bool {
62    spell_id == HERO::DARK_RANGER::SPELL::BLACK_ARROW_DOT_SPELL
63}
64
65/// Corpsecaller's RPPM driver (`sc_hunter.cpp:8414` `get_rppm( "Corpsecaller", talents.corpsecaller )`).
66fn register_corpsecaller(
67    built: &mut wowlab_engine_combat::BuiltCombatSystem,
68    cfg: &MarksmanshipConfig,
69) -> Result<(), EngineError> {
70    if !cfg.corpsecaller {
71        return Ok(());
72    }
73
74    let _ = built.register_system_rppm_from_data(HERO::DARK_RANGER::SPELL::CORPSECALLER_TALENT)?;
75
76    built.register_impact_proc(
77        ImpactProc::new(super::hooks::corpsecaller_tick)
78            .with_spell_filter(is_black_arrow_dot)
79            .periodic_only(),
80    );
81
82    Ok(())
83}
84
85fn apply_talent_registrations(
86    built: &mut wowlab_engine_combat::BuiltCombatSystem,
87    cfg: &MarksmanshipConfig,
88) -> Result<(), EngineError> {
89    register_master_marksman(built, cfg);
90    register_corpsecaller(built, cfg)?;
91
92    if cfg.bleak_arrows {
93        built.patch_auto_attacks(|attacks| if let Some(auto) = attacks.first_mut() {
94            auto.spell_id = HERO::DARK_RANGER::SPELL::BLEAK_ARROWS;
95        });
96    }
97
98    Ok(())
99}
100
101impl MarksmanshipHandler {
102    /// Finishes the specialization handler from the shared-content composition stage.
103    pub(crate) fn try_new(
104        parts: crate::composition::ContentHandlerParts,
105        params: &HandlerParams<'_>,
106    ) -> Result<Self, EngineError> {
107        let mut built = parts.into_built();
108        let cfg = derive_config(&mut built, params);
109
110        apply_talent_registrations(&mut built, &cfg)?;
111        built.state.set_spec_config(cfg);
112
113        Ok(Self {
114            inner: CombatHandler::from_built(built),
115            cfg,
116        })
117    }
118
119    // Target health is at most one event stale.
120    fn sync_kill_shot_gate(&mut self, now: wowlab_types::sim::SimTime) {
121        let cfg = self.cfg;
122        let (state, buf) = self.inner.state_and_buf_mut();
123        let deathblow = is_aura_active(
124            &wowlab_engine_combat::CombatView::new(state, buf),
125            AURA_DEATHBLOW,
126            wowlab_types::sim::ActorId::Player,
127            state.current_target(),
128        );
129        let health_pct = state
130            .current_target()
131            .and_then(|target| state.enemy_health_fraction(target, now))
132            .unwrap_or(1.0);
133        let enabled = if cfg.black_arrow {
134            deathblow || health_pct <= cfg.ba_lower_pct || health_pct >= cfg.ba_upper_pct
135        } else {
136            deathblow || health_pct <= cfg.ks_threshold
137        };
138
139        set_spell_gate(
140            state,
141            buf,
142            SPELL_KILL_SHOT,
143            SpellGate::TargetHealth,
144            enabled,
145        );
146    }
147}
148
149crate::hooks::define_spec_handler! {
150impl MarksmanshipHandler(self, inner) {
151    fn on_sim_start(&mut self, ctx: &mut SimContext) {
152        self.inner.on_sim_start(ctx);
153        self.sync_kill_shot_gate(ctx.state.current_time);
154    }
155
156    fn on_player_ready(&mut self, ctx: &mut SimContext) -> Option<SpecAction> {
157        self.sync_kill_shot_gate(ctx.state.current_time);
158        self.inner.on_player_ready(ctx)
159    }
160
161    fn on_cast_complete(&mut self, event: wowlab_engine_ports::Event, ctx: &mut SimContext) {
162        self.inner.on_cast_complete(event, ctx);
163        self.sync_kill_shot_gate(ctx.state.current_time);
164    }
165
166    fn on_spell_impact(&mut self, impact_id: u32, ctx: &mut SimContext) {
167        self.inner.on_spell_impact(impact_id, ctx);
168    }
169
170}
171delegate {
172            fn on_aura_tick(&mut self, event: wowlab_engine_ports::AuraEventRef, ctx: &mut SimContext);
173            fn on_aura_expire(&mut self, event: wowlab_engine_ports::AuraEventRef, ctx: &mut SimContext);
174            fn on_auto_attack(&mut self, source: wowlab_types::sim::ActorId, target: wowlab_types::sim::EnemyIdx, ctx: &mut SimContext);
175            fn on_cooldown_ready(&mut self, cooldown_key: SpellIdx, ctx: &mut SimContext);
176            fn flush_scheduled(&mut self, push: &mut dyn FnMut(wowlab_engine_ports::Event));
177            fn reset(&mut self);
178            fn total_damage(&self) -> f64;
179            fn cast_time_ms(&self, spell_id: SpellIdx, empower_rank: u8) -> u32;
180            fn introspect(&self) -> wowlab_types::game::SpecIntrospection;
181            fn paperdoll(&self) -> Option<wowlab_engine_ports::Paperdoll>;
182            fn attach_decision_trace(
183                &mut self,
184                sink: std::sync::Arc<dyn wowlab_engine_ports::DecisionTraceSink>,
185            );
186}
187}