wowlab_engine_content/hooks/marksmanship_hunter/
handler.rs1use 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 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#[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
65fn 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 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 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}