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