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