Skip to main content

wowlab_engine_content/hooks/windwalker_monk/
handler.rs

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