wowlab_engine_content/hooks/unholy_death_knight/
handler.rs1use wowlab_engine_combat::DamageOps as _;
2use wowlab_engine_combat::AuraOps as _;
3use wowlab_engine_combat::{
4 ImpactActorFilter, ImpactProc,
5 CombatHandler,
6};
7use wowlab_engine_ports::{HandlerParams, SimContext, SpecAction};
8
9
10use super::{
11 config::{RiderRuntime, UnholyConfig, UnholyRuntime, unholy_config},
12 hooks::{
13 forbidden_knowledge_fire, sudden_doom_fire, PERMANENT_GHOUL_ACTION_BAR,
14 PERMANENT_GHOUL_NPC_ID,
15 },
16 riders::resource_spend,
17};
18use crate::generated::specs::unholy_death_knight::{
19 AURA, AURA_APOCALYPSE_DEATH, AURA_APOCALYPSE_FAMINE, AURA_APOCALYPSE_PESTILENCE,
20 AURA_APOCALYPSE_WAR, AURA_COIL_OF_DEVASTATION, EFFECT, HERO,
21};
22
23const fn dread_plague_filter(spell_id: u32) -> bool {
24 spell_id == AURA::DREAD_PLAGUE
25}
26
27const fn coil_of_devastation_filter(spell_id: u32) -> bool {
28 spell_id == EFFECT::DC_DAMAGE.0
29 || matches!(
30 spell_id,
31 HERO::UNHOLY_PETS::SPELL::NECROTIC_COIL
32 | HERO::UNHOLY_PETS::SPELL::NECROTIC_COIL_SHADOWSTRIKE
33 )
34}
35
36fn trigger_coil_of_devastation(
37 ctx: &mut wowlab_engine_combat::HookCtx<'_>,
38 event: wowlab_engine_combat::ImpactEvent,
39) {
40 let fraction = ctx.effect_percent(crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::COIL_OF_DEVASTATION_PCT);
41
42 ctx.add_residual_damage(AURA_COIL_OF_DEVASTATION, event.amount * fraction);
43}
44
45const fn apocalypse_impact_filter(spell_id: u32) -> bool {
46 matches!(
47 spell_id,
48 HERO::UNHOLY_PETS::SPELL::CLAW
49 | HERO::UNHOLY_PETS::SPELL::SWEEPING_CLAWS
50 | HERO::UNHOLY_PETS::SPELL::LESSER_CLAW
51 | HERO::UNHOLY_PETS::SPELL::LESSER_SWEEPING_CLAWS
52 | HERO::UNHOLY_PETS::SPELL::RUPTURED_VISCERA
53 | HERO::UNHOLY_PETS::SPELL::PUTREFY_AOE
54 )
55}
56
57fn trigger_rune_of_the_apocalypse(
58 ctx: &mut wowlab_engine_combat::HookCtx<'_>,
59 _event: wowlab_engine_combat::ImpactEvent,
60) {
61 const DEATH_THRESHOLD: f64 = 0.25;
62 const FAMINE_THRESHOLD: f64 = 0.5;
63 const PESTILENCE_THRESHOLD: f64 = 0.75;
64
65 if !ctx
66 .spec_config::<UnholyConfig>()
67 .is_some_and(|config| config.apocalypse_runeforge)
68 {
69 return;
70 }
71
72 let roll = ctx.rng()();
73
74 if roll < DEATH_THRESHOLD {
75 ctx.apply_owner_aura(AURA_APOCALYPSE_DEATH);
76 } else if roll < FAMINE_THRESHOLD {
77 ctx.apply_owner_aura(AURA_APOCALYPSE_FAMINE);
78 } else if roll < PESTILENCE_THRESHOLD {
79 ctx.apply_owner_aura(AURA_APOCALYPSE_PESTILENCE);
80 } else {
81 ctx.apply_owner_aura(AURA_APOCALYPSE_WAR);
82 }
83}
84
85#[derive(Debug)]
86pub(crate) struct UnholyDkHandler {
87 inner: CombatHandler,
88}
89
90impl UnholyDkHandler {
91 pub(crate) fn try_new(
92 parts: crate::composition::ContentHandlerParts,
93 params: &HandlerParams<'_>,
94 ) -> Result<Self, wowlab_engine_ports::EngineError> {
95 let mut parts = parts;
96 let built = parts.draft_mut();
97
98 if !built.set_pet_action_bar(PERMANENT_GHOUL_NPC_ID, PERMANENT_GHOUL_ACTION_BAR) {
99 return Err(wowlab_engine_ports::EngineError::spec_construction(
100 "unholy permanent-ghoul auto attack is missing",
101 ));
102 }
103
104 built.register_player_cast_hook(resource_spend);
105
106 built.register_impact_proc(
107 ImpactProc::new(sudden_doom_fire)
108 .with_spell_filter(dread_plague_filter)
109 .periodic_only(),
110 );
111 built.register_impact_proc(
112 ImpactProc::new(forbidden_knowledge_fire)
113 .with_spell_filter(dread_plague_filter)
114 .periodic_only(),
115 );
116 built.register_impact_proc(
117 ImpactProc::new(trigger_coil_of_devastation)
118 .with_spell_filter(coil_of_devastation_filter)
119 .skip_periodic(),
120 );
121 built.register_impact_proc(
122 ImpactProc::new(trigger_rune_of_the_apocalypse)
123 .with_actor_filter(ImpactActorFilter::Any)
124 .with_spell_filter(apocalypse_impact_filter)
125 .skip_periodic(),
126 );
127 let config = unholy_config(params)?;
128 let runtime = UnholyRuntime {
129 riders: config.riders.map(|_| RiderRuntime::default()),
130 active_army_ghouls: 0,
131 };
132
133 built.set_spec_config(config);
134 built.set_spec_runtime(runtime);
135
136 let built = parts.finalize()?;
137
138 Ok(Self {
139 inner: CombatHandler::from_built(built),
140 })
141 }
142}
143
144crate::hooks::define_spec_handler! {
145impl UnholyDkHandler(self, inner) {
146 fn on_sim_start(&mut self, ctx: &mut SimContext) {
147 self.inner.on_sim_start(ctx);
148 }
149
150 fn on_player_ready(&mut self, ctx: &mut SimContext) -> Option<SpecAction> {
151 self.inner.on_player_ready(ctx)
152 }
153
154 fn on_cast_complete(&mut self, event: wowlab_engine_ports::Event, ctx: &mut SimContext) {
155 self.inner.on_cast_complete(event, ctx);
156 }
157
158 fn on_spell_impact(&mut self, impact_id: wowlab_engine_ports::PendingImpactBinding, ctx: &mut SimContext) {
159 self.inner.on_spell_impact(impact_id, ctx);
160 }
161
162}
163delegate {
164 fn on_aura_tick(&mut self, event: wowlab_engine_ports::AuraEventRef, ctx: &mut SimContext);
165 fn on_aura_expire(&mut self, event: wowlab_engine_ports::AuraEventRef, ctx: &mut SimContext);
166 fn on_auto_attack(&mut self, source: wowlab_types::sim::ActorId, target: wowlab_types::sim::EnemyIdx, ctx: &mut SimContext);
167 fn on_cooldown_ready(&mut self, spell: wowlab_engine_ports::ScheduledSpellBinding, ctx: &mut SimContext);
168 fn flush_scheduled(&mut self, push: &mut dyn FnMut(wowlab_engine_ports::Event));
169 fn reset(&mut self);
170 fn total_damage(&self) -> f64;
171 fn cast_time_ms(&self, spell: wowlab_engine_ports::ScheduledSpellBinding, empower_rank: u8) -> u32;
172 fn introspect(&self) -> wowlab_types::game::SpecIntrospection;
173 fn paperdoll(&self) -> Option<wowlab_engine_ports::Paperdoll>;
174 fn attach_decision_trace(
175 &mut self,
176 sink: std::sync::Arc<dyn wowlab_engine_ports::DecisionTraceSink>,
177 );
178}
179}