Skip to main content

wowlab_engine_content/hooks/windwalker_monk/
handler.rs

1use wowlab_engine_combat::ResourceOps as _;
2use wowlab_engine_combat::{
3    CombatHandler, LocalAuraIdx, ResourceGainSource, apply_aura, expire_aura,
4};
5use wowlab_engine_ports::{EngineError, HandlerParams, SimContext, SpecAction};
6
7
8use crate::generated::specs::windwalker_monk::{
9    AURA_COMBAT_WISDOM, AURA_COMBO_STRIKES, AURA_COMBO_STRIKES_BLACKOUT_KICK,
10    AURA_COMBO_STRIKES_CRACKLING_JADE_LIGHTNING, AURA_COMBO_STRIKES_FISTS_OF_FURY,
11    AURA_COMBO_STRIKES_RISING_SUN_KICK, AURA_COMBO_STRIKES_SPINNING_CRANE_KICK,
12    AURA_COMBO_STRIKES_STRIKE_OF_THE_WINDLORD, AURA_COMBO_STRIKES_TIGER_PALM,
13    AURA_COMBO_STRIKES_WHIRLING_DRAGON_PUNCH, AURA_HIT_COMBO, EFFECT, SPELL,
14};
15
16use super::config::{WindwalkerConfig, WindwalkerRuntime, register_windwalker};
17
18#[rustfmt::skip]
19const COMBO_CAPABLE: &[u32] = &[
20    SPELL::TIGER_PALM,
21    SPELL::BLACKOUT_KICK,
22    SPELL::RISING_SUN_KICK,
23    SPELL::FISTS_OF_FURY,
24    SPELL::SPINNING_CRANE_KICK,
25    SPELL::WHIRLING_DRAGON_PUNCH,
26    SPELL::STRIKE_OF_THE_WINDLORD,
27    SPELL::TOUCH_OF_DEATH,
28    SPELL::CRACKLING_JADE_LIGHTNING,
29    SPELL::FLYING_SERPENT_KICK,
30    SPELL::CHI_BURST,
31    SPELL::ZENITH,
32    SPELL::CELESTIAL_CONDUIT,
33];
34
35fn helper_aura(spell_id: u32) -> Option<LocalAuraIdx> {
36    match spell_id {
37        SPELL::TIGER_PALM => Some(AURA_COMBO_STRIKES_TIGER_PALM),
38        SPELL::BLACKOUT_KICK => Some(AURA_COMBO_STRIKES_BLACKOUT_KICK),
39        SPELL::RISING_SUN_KICK => Some(AURA_COMBO_STRIKES_RISING_SUN_KICK),
40        SPELL::FISTS_OF_FURY => Some(AURA_COMBO_STRIKES_FISTS_OF_FURY),
41        SPELL::SPINNING_CRANE_KICK => Some(AURA_COMBO_STRIKES_SPINNING_CRANE_KICK),
42        SPELL::WHIRLING_DRAGON_PUNCH => Some(AURA_COMBO_STRIKES_WHIRLING_DRAGON_PUNCH),
43        SPELL::STRIKE_OF_THE_WINDLORD => Some(AURA_COMBO_STRIKES_STRIKE_OF_THE_WINDLORD),
44        SPELL::CRACKLING_JADE_LIGHTNING => Some(AURA_COMBO_STRIKES_CRACKLING_JADE_LIGHTNING),
45        _ => None,
46    }
47}
48
49/// Tracks the previous combo-capable cast for Combo Strikes.
50#[derive(Debug)]
51pub(crate) struct WindwalkerHandler {
52    inner: CombatHandler,
53}
54
55impl WindwalkerHandler {
56    pub(crate) fn try_new(
57        parts: crate::composition::ContentHandlerParts,
58        params: &HandlerParams<'_>,
59    ) -> Result<Self, EngineError> {
60        let mut built = parts.into_built();
61
62        register_windwalker(&mut built, params)?;
63
64        Ok(Self {
65            inner: CombatHandler::from_built(built),
66        })
67    }
68
69    // Runs before cast processing so the hit sees the new mastery state.
70    fn combo_strikes_trigger(&mut self, spell_id: u32, ctx: &mut SimContext) {
71        if !COMBO_CAPABLE.contains(&spell_id) {
72            return;
73        }
74
75        let now = ctx.state.current_time;
76        let hit_combo = self
77            .inner
78            .state()
79            .spec_config::<WindwalkerConfig>()
80            .is_some_and(|cfg| cfg.hit_combo);
81        let previous = self
82            .inner
83            .state()
84            .spec_runtime::<WindwalkerRuntime>()
85            .map_or(0, |runtime| runtime.last_combo_spell);
86        let is_combo = previous != spell_id;
87
88        self.inner
89            .with_hook_ctx(ctx.telemetry, wowlab_engine_combat::HookCtxRequest::targetless(now), |aura_ctx| {
90                if is_combo {
91                    apply_aura(aura_ctx, AURA_COMBO_STRIKES);
92
93                    if hit_combo {
94                        apply_aura(aura_ctx, AURA_HIT_COMBO);
95                    }
96                } else {
97                    expire_aura(aura_ctx, AURA_COMBO_STRIKES);
98                    expire_aura(aura_ctx, AURA_HIT_COMBO);
99                }
100
101                if previous != spell_id {
102                    if let Some(prev) = helper_aura(previous) {
103                        expire_aura(aura_ctx, prev);
104                    }
105                }
106
107                if let Some(cur) = helper_aura(spell_id) {
108                    apply_aura(aura_ctx, cur);
109                }
110
111                if let Some(runtime) = aura_ctx.spec_runtime_mut::<WindwalkerRuntime>() {
112                    runtime.last_combo_spell = spell_id;
113                }
114            });
115    }
116}
117
118crate::hooks::define_spec_handler! {
119impl WindwalkerHandler(self, inner) {
120    fn on_cast_complete(&mut self, event: wowlab_engine_ports::Event, ctx: &mut SimContext) {
121        let wowlab_engine_ports::Event::CastComplete { spell_id, .. } = event else {
122            panic!("Windwalker on_cast_complete received a non-cast-complete event");
123        };
124        self.combo_strikes_trigger(spell_id.as_u32(), ctx);
125        self.inner.on_cast_complete(event, ctx);
126    }
127
128    fn on_spell_impact(&mut self, impact_id: u32, ctx: &mut SimContext) {
129        self.inner.on_spell_impact(impact_id, ctx);
130    }
131
132    fn on_sim_start(&mut self, ctx: &mut SimContext) {
133        self.inner.on_sim_start(ctx);
134        let (combat_wisdom, initial_chi) = self
135            .inner
136            .state()
137            .spec_config::<WindwalkerConfig>()
138            .map_or((false, 0.0), |cfg| (cfg.combat_wisdom, cfg.initial_chi));
139        if combat_wisdom {
140            self.inner.with_hook_ctx(ctx.telemetry, wowlab_engine_combat::HookCtxRequest::targetless(wowlab_types::sim::SimTime::ZERO), |hook| {
141                hook.gain_secondary_resource_from(
142                    initial_chi,
143                    ResourceGainSource::SpellEffect {
144                        spell_id: EFFECT::COMBAT_WISDOM_CHI.0,
145                        effect_index: EFFECT::COMBAT_WISDOM_CHI.1,
146                    },
147                );
148                apply_aura(hook, AURA_COMBAT_WISDOM);
149            });
150        }
151    }
152
153}
154delegate {
155            fn on_player_ready(&mut self, ctx: &mut SimContext) -> Option<SpecAction>;
156            fn on_aura_tick(&mut self, event: wowlab_engine_ports::AuraEventRef, ctx: &mut SimContext);
157            fn on_aura_expire(&mut self, event: wowlab_engine_ports::AuraEventRef, ctx: &mut SimContext);
158            fn on_auto_attack(&mut self, source: wowlab_types::sim::ActorId, target: wowlab_types::sim::EnemyIdx, ctx: &mut SimContext);
159            fn on_cooldown_ready(&mut self, cooldown_key: wowlab_types::sim::SpellIdx, ctx: &mut SimContext);
160            fn flush_scheduled(&mut self, push: &mut dyn FnMut(wowlab_engine_ports::Event));
161            fn reset(&mut self);
162            fn total_damage(&self) -> f64;
163            fn cast_time_ms(&self, spell_id: wowlab_types::sim::SpellIdx, empower_rank: u8) -> u32;
164            fn introspect(&self) -> wowlab_types::game::SpecIntrospection;
165            fn paperdoll(&self) -> Option<wowlab_engine_ports::Paperdoll>;
166            fn attach_decision_trace(
167                &mut self,
168                sink: std::sync::Arc<dyn wowlab_engine_ports::DecisionTraceSink>,
169            );
170}
171}