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