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(EFFECT::COIL_OF_DEVASTATION_PCT);
41
42 ctx.add_residual_damage(AURA_COIL_OF_DEVASTATION.raw(), 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.raw());
76 } else if roll < FAMINE_THRESHOLD {
77 ctx.apply_owner_aura(AURA_APOCALYPSE_FAMINE.raw());
78 } else if roll < PESTILENCE_THRESHOLD {
79 ctx.apply_owner_aura(AURA_APOCALYPSE_PESTILENCE.raw());
80 } else {
81 ctx.apply_owner_aura(AURA_APOCALYPSE_WAR.raw());
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 built = parts.into_built();
96
97 if !built.set_pet_action_bar(PERMANENT_GHOUL_NPC_ID, PERMANENT_GHOUL_ACTION_BAR) {
98 return Err(wowlab_engine_ports::EngineError::spec_construction(
99 "unholy permanent-ghoul auto attack is missing",
100 ));
101 }
102
103 built.register_player_cast_hook(resource_spend);
104
105 built.register_impact_proc(
106 ImpactProc::new(sudden_doom_fire)
107 .with_spell_filter(dread_plague_filter)
108 .periodic_only(),
109 );
110 built.register_impact_proc(
111 ImpactProc::new(forbidden_knowledge_fire)
112 .with_spell_filter(dread_plague_filter)
113 .periodic_only(),
114 );
115 built.register_impact_proc(
116 ImpactProc::new(trigger_coil_of_devastation)
117 .with_spell_filter(coil_of_devastation_filter)
118 .skip_periodic(),
119 );
120 built.register_impact_proc(
121 ImpactProc::new(trigger_rune_of_the_apocalypse)
122 .with_actor_filter(ImpactActorFilter::Any)
123 .with_spell_filter(apocalypse_impact_filter)
124 .skip_periodic(),
125 );
126 let config = unholy_config(params)?;
127 let runtime = UnholyRuntime {
128 riders: config.riders.map(|_| RiderRuntime::default()),
129 };
130
131 built.state.set_spec_config(config);
132 built.state.set_spec_runtime(runtime);
133
134 Ok(Self {
135 inner: CombatHandler::from_built(built),
136 })
137 }
138}
139
140crate::hooks::define_spec_handler! {
141impl UnholyDkHandler(self, inner) {
142 fn on_sim_start(&mut self, ctx: &mut SimContext) {
143 self.inner.on_sim_start(ctx);
144 }
145
146 fn on_player_ready(&mut self, ctx: &mut SimContext) -> Option<SpecAction> {
147 self.inner.on_player_ready(ctx)
148 }
149
150 fn on_cast_complete(&mut self, event: wowlab_engine_ports::Event, ctx: &mut SimContext) {
151 self.inner.on_cast_complete(event, ctx);
152 }
153
154 fn on_spell_impact(&mut self, impact_id: u32, ctx: &mut SimContext) {
155 self.inner.on_spell_impact(impact_id, ctx);
156 }
157
158}
159delegate {
160 fn on_aura_tick(&mut self, event: wowlab_engine_ports::AuraEventRef, ctx: &mut SimContext);
161 fn on_aura_expire(&mut self, event: wowlab_engine_ports::AuraEventRef, ctx: &mut SimContext);
162 fn on_auto_attack(&mut self, source: wowlab_types::sim::ActorId, target: wowlab_types::sim::EnemyIdx, ctx: &mut SimContext);
163 fn on_cooldown_ready(&mut self, cooldown_key: wowlab_types::sim::SpellIdx, ctx: &mut SimContext);
164 fn flush_scheduled(&mut self, push: &mut dyn FnMut(wowlab_engine_ports::Event));
165 fn reset(&mut self);
166 fn total_damage(&self) -> f64;
167 fn cast_time_ms(&self, spell_id: wowlab_types::sim::SpellIdx, empower_rank: u8) -> u32;
168 fn introspect(&self) -> wowlab_types::game::SpecIntrospection;
169 fn paperdoll(&self) -> Option<wowlab_engine_ports::Paperdoll>;
170 fn attach_decision_trace(
171 &mut self,
172 sink: std::sync::Arc<dyn wowlab_engine_ports::DecisionTraceSink>,
173 );
174}
175}