Skip to main content

wowlab_engine_content/hooks/marksmanship_hunter/
handler.rs

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