wowlab_engine_content/hooks/windwalker_monk/
hooks.rs1use wowlab_engine_combat::DamageOps as _;
2use wowlab_engine_combat::CooldownOps as _;
3use wowlab_engine_combat::ResourceOps as _;
4use wowlab_engine_combat::ProcOps as _;
5use wowlab_engine_combat::AuraOps as _;
6use wowlab_types::constants::HUNDRED;
7
8use wowlab_engine_combat::{DamageFlags, HookCtx, MasteryCtx, SwingEvent, SwingHand, SwingReplacement};
9use wowlab_engine_rng::proc_chance;
10use wowlab_types::sim::SpellIdx;
11
12use crate::generated::specs::windwalker_monk::{
13 AURA_BLACKOUT_KICK, AURA_COMBAT_WISDOM, AURA_COMBO_STRIKES, AURA_DANCE_OF_CHI_JI,
14 AURA_FLURRY_CHARGE, AURA_MOMENTUM_BOOST, AURA_STAND_READY, AURA_TEACHINGS_OF_THE_MONASTERY,
15 AURA_WHIRLING_DRAGON_PUNCH, AURA_ZENITH, EFFECT, HERO, REPORTED_SPELL, SPELL_FISTS_OF_FURY,
16 SPELL_RISING_SUN_KICK, TALENT,
17};
18
19use super::config::{WindwalkerConfig, WindwalkerRuntime, windwalker_config};
20
21const TIGER_PALM_CHI: f64 = 2.0;
23
24const WDP_AOE_PULSES: u32 = 3;
26
27const WDP_GRACE_PERIOD_MS: f64 = 1500.0;
29
30#[rustfmt::skip]
32const NO_WW_MASTERY: &[u32] = &[
33 REPORTED_SPELL::TEACHINGS_BLACKOUT_KICK,
34 HERO::SHADO_PAN::SPELL::FLURRY_STRIKE,
35 HERO::SHADO_PAN::SPELL::SHADO_OVER_THE_BATTLEFIELD,
36 EFFECT::EH_DMG.0,
37];
38
39fn haste_frac(ctx: &HookCtx<'_>) -> f64 {
41 ctx.buf().player().haste / HUNDRED
42}
43
44#[must_use]
46pub(crate) fn mastery(ctx: &MasteryCtx<'_>) -> f64 {
47 if ctx.spell_id == REPORTED_SPELL::MELEE || NO_WW_MASTERY.contains(&ctx.spell_id) {
48 return 1.0;
49 }
50
51 if !ctx.is_aura_active(AURA_COMBO_STRIKES) {
52 return 1.0;
53 }
54
55 ctx.bonus(1)
56}
57
58fn ap(ctx: &HookCtx<'_>, effect: (u32, u8)) -> f64 {
59 ctx.game_data()
60 .ap_coef(SpellIdx::from_raw(effect.0), effect.1)
61}
62
63fn base(ctx: &HookCtx<'_>, effect: (u32, u8)) -> f64 {
64 ctx.game_data()
65 .base_points(SpellIdx::from_raw(effect.0), effect.1)
66}
67
68pub(super) fn dual_threat_replacement(
70 ctx: &mut HookCtx<'_>,
71 _hand: SwingHand,
72) -> Option<SwingReplacement> {
73 let cfg = windwalker_config(ctx);
74 let mut runtime = ctx.spec_runtime::<WindwalkerRuntime>().copied()?;
75 let should_replace =
76 dual_threat_should_replace(cfg.dual_threat_chance, &mut runtime, ctx.rng());
77
78 *ctx.spec_runtime_mut::<WindwalkerRuntime>()? = runtime;
79
80 if !should_replace {
81 return None;
82 }
83
84 Some(SwingReplacement {
85 spell_id: REPORTED_SPELL::DUAL_THREAT,
86 damage_multiplier: cfg.dual_threat_damage_mult,
87 })
88}
89
90fn dual_threat_should_replace(
91 chance: f64,
92 runtime: &mut WindwalkerRuntime,
93 rng: &mut dyn FnMut() -> f64,
94) -> bool {
95 if chance <= 0.0 {
96 return false;
97 }
98
99 if !runtime.dual_threat_ready {
100 runtime.dual_threat_ready = true;
101
102 return false;
103 }
104
105 if !proc_chance(rng, chance) {
106 return false;
107 }
108
109 runtime.dual_threat_ready = false;
110
111 true
112}
113
114fn sp(ctx: &HookCtx<'_>, effect: (u32, u8)) -> f64 {
115 ctx.game_data()
116 .sp_coef(SpellIdx::from_raw(effect.0), effect.1)
117}
118
119fn roll_dance_of_chi_ji(
120 ctx: &mut HookCtx<'_>,
121 _cfg: &WindwalkerConfig,
122) {
123 if ctx.roll_system_rppm(TALENT::DANCE_OF_CHI_JI) {
124 ctx.apply_aura(AURA_DANCE_OF_CHI_JI.raw());
125 }
126}
127
128fn flurry_volley(
129 ctx: &mut HookCtx<'_>,
130 cfg: &WindwalkerConfig,
131 count: i32,
132 strike_mult: f64,
133) {
134 for _ in 0..count {
135 ctx.deal_damage_ap(
136 HERO::SHADO_PAN::SPELL::FLURRY_STRIKE,
137 cfg.flurry_coef * strike_mult,
138 DamageFlags::PHYSICAL,
139 );
140 ctx.deal_damage_ap(
141 HERO::SHADO_PAN::SPELL::SHADO_OVER_THE_BATTLEFIELD,
142 cfg.shado_coef,
143 DamageFlags::empty(),
144 );
145 }
146}
147
148fn release_flurry_charges(
149 ctx: &mut HookCtx<'_>,
150 cfg: &WindwalkerConfig,
151) {
152 if !cfg.flurry_strikes {
153 return;
154 }
155
156 let count = ctx.aura_stacks(AURA_FLURRY_CHARGE.raw());
157
158 if count <= 0 {
159 return;
160 }
161
162 ctx.expire_aura(AURA_FLURRY_CHARGE.raw());
163 flurry_volley(ctx, cfg, count, 1.0);
164}
165
166fn release_wisdom_of_the_wall(
167 ctx: &mut HookCtx<'_>,
168 cfg: &WindwalkerConfig,
169) {
170 if !cfg.wisdom_of_the_wall
171 || !ctx.is_aura_active(AURA_ZENITH.raw())
172 || !ctx.proc_category_ready(TALENT::WISDOM_OF_THE_WALL)
173 {
174 return;
175 }
176
177 ctx.start_proc_category_cooldown(TALENT::WISDOM_OF_THE_WALL);
178 flurry_volley(ctx, cfg, cfg.wotw_count, 1.0);
179}
180
181fn release_stand_ready(
182 ctx: &mut HookCtx<'_>,
183 cfg: &WindwalkerConfig,
184) {
185 if !cfg.stand_ready || !ctx.is_aura_active(AURA_STAND_READY.raw()) {
186 return;
187 }
188
189 ctx.expire_aura(AURA_STAND_READY.raw());
190 flurry_volley(ctx, cfg, cfg.stand_ready_count, cfg.stand_ready_mult);
191}
192
193pub(crate) fn tiger_palm_hook(ctx: &mut HookCtx<'_>) {
194 ctx.gain_secondary_resource(TIGER_PALM_CHI);
195
196 let cfg = windwalker_config(ctx);
197
198 if cfg.teachings {
199 ctx.apply_aura(AURA_TEACHINGS_OF_THE_MONASTERY.raw());
200 }
201
202 if cfg.combo_breaker_chance > 0.0 && proc_chance(ctx.rng(), cfg.combo_breaker_chance) {
203 ctx.apply_aura(AURA_BLACKOUT_KICK.raw());
204 }
205
206 if cfg.combat_wisdom && ctx.is_aura_active(AURA_COMBAT_WISDOM.raw()) {
207 ctx.expire_aura(AURA_COMBAT_WISDOM.raw());
208 let heal_mult = ctx
209 .game_data()
210 .passive_damage_mult(SpellIdx::from_raw(EFFECT::EH_HEAL_SP.0), false);
211 let coef =
212 sp(ctx, EFFECT::EH_HEAL_SP) * heal_mult * base(ctx, EFFECT::EH_DMG_PCT) / HUNDRED;
213
214 ctx.deal_damage_sp(EFFECT::EH_DMG.0, coef, DamageFlags::empty());
215 }
216
217 release_stand_ready(ctx, &cfg);
218}
219
220pub(crate) fn blackout_kick_hook(ctx: &mut HookCtx<'_>) {
221 let cfg = windwalker_config(ctx);
222
223 ctx.consume_aura_stack(AURA_BLACKOUT_KICK.raw());
224
225 let mut reset_rolls = 1;
226
227 if cfg.teachings {
228 let stacks = ctx.aura_stacks(AURA_TEACHINGS_OF_THE_MONASTERY.raw());
229
230 if stacks > 0 {
231 ctx.expire_aura(AURA_TEACHINGS_OF_THE_MONASTERY.raw());
232 let coef = ap(ctx, EFFECT::TOTM_BOK_DMG);
233
234 for _ in 0..stacks {
235 ctx.deal_damage_ap(EFFECT::TOTM_BOK_DMG.0, coef, DamageFlags::PHYSICAL);
236 }
237
238 reset_rolls += stacks;
239 }
240 }
241
242 for _ in 0..reset_rolls {
243 if cfg.totm_reset_chance > 0.0 && proc_chance(ctx.rng(), cfg.totm_reset_chance) {
244 ctx.reset_cooldown(SPELL_RISING_SUN_KICK.raw());
245 }
246 }
247
248 roll_dance_of_chi_ji(ctx, &cfg);
249 release_stand_ready(ctx, &cfg);
250}
251
252fn open_whirling_dragon_punch_window(ctx: &mut HookCtx<'_>) {
260 let gate_ms = ctx
261 .spell_cooldown_remaining_ms(SPELL_RISING_SUN_KICK.raw())
262 .min(ctx.spell_cooldown_remaining_ms(SPELL_FISTS_OF_FURY.raw()));
263
264 if gate_ms == 0 {
265 return;
266 }
267
268 let grace_ms = WDP_GRACE_PERIOD_MS / (1.0 + haste_frac(ctx));
269
270 ctx.apply_aura_with_duration(
271 AURA_WHIRLING_DRAGON_PUNCH.raw(),
272 gate_ms + wowlab_types::numeric::f64_to_u32_saturating_round(grace_ms),
273 );
274}
275
276pub(crate) fn rising_sun_kick_hook(ctx: &mut HookCtx<'_>) {
277 let cfg = windwalker_config(ctx);
278
279 open_whirling_dragon_punch_window(ctx);
280
281 if cfg.glory_of_the_dawn {
282 let chance = base(ctx, EFFECT::GOTD_CHANCE) / HUNDRED * haste_frac(ctx);
283
284 if proc_chance(ctx.rng(), chance) {
285 let coef = ap(ctx, EFFECT::GOTD_DMG);
286
287 ctx.deal_damage_ap(EFFECT::GOTD_DMG.0, coef, DamageFlags::PHYSICAL);
288 ctx.gain_secondary_resource(base(ctx, EFFECT::GOTD_CHI));
289 }
290 }
291
292 release_wisdom_of_the_wall(ctx, &cfg);
293 roll_dance_of_chi_ji(ctx, &cfg);
294 release_stand_ready(ctx, &cfg);
295}
296
297pub(crate) fn fists_of_fury_hook(ctx: &mut HookCtx<'_>) {
298 let cfg = windwalker_config(ctx);
299
300 open_whirling_dragon_punch_window(ctx);
301 release_flurry_charges(ctx, &cfg);
302 roll_dance_of_chi_ji(ctx, &cfg);
303 release_stand_ready(ctx, &cfg);
304}
305
306pub(crate) fn spinning_crane_kick_hook(ctx: &mut HookCtx<'_>) {
307 let cfg = windwalker_config(ctx);
308
309 if ctx.consume_aura_stack(AURA_DANCE_OF_CHI_JI.raw())
310 && cfg.sequenced_strikes_chance > 0.0
311 && proc_chance(ctx.rng(), cfg.sequenced_strikes_chance)
312 {
313 ctx.apply_aura(AURA_BLACKOUT_KICK.raw());
314 }
315
316 release_wisdom_of_the_wall(ctx, &cfg);
317 roll_dance_of_chi_ji(ctx, &cfg);
318 release_stand_ready(ctx, &cfg);
319}
320
321pub(crate) fn whirling_dragon_punch_hook(ctx: &mut HookCtx<'_>) {
323 let (spell_id, effect) = EFFECT::WDP_AOE_TICK;
324 let coef = ctx
325 .game_data()
326 .ap_coef(SpellIdx::from_raw(spell_id), effect);
327
328 for _ in 0..WDP_AOE_PULSES {
329 ctx.deal_damage_ap(spell_id, coef, DamageFlags::PHYSICAL);
330 }
331}
332
333pub(crate) fn zenith_hook(ctx: &mut HookCtx<'_>) {
334 let cfg = windwalker_config(ctx);
335
336 ctx.reset_cooldown(SPELL_RISING_SUN_KICK.raw());
337
338 if cfg.stand_ready {
339 ctx.apply_aura(AURA_STAND_READY.raw());
340 }
341
342 if cfg.zenith_stomp {
343 let coef = ap(ctx, EFFECT::ZENITH_STOMP_DMG);
344
345 ctx.deal_damage_ap(EFFECT::ZENITH_STOMP_DMG.0, coef, DamageFlags::empty());
346 ctx.gain_secondary_resource(base(ctx, EFFECT::ZENITH_STOMP_CHI));
347 }
348}
349
350pub(crate) fn fists_of_fury_tick_hook(ctx: &mut HookCtx<'_>) {
351 let cfg = windwalker_config(ctx);
352
353 let mut mult = 1.0;
354
355 if cfg.momentum_boost {
360 mult *= 1.0 + haste_frac(ctx) * base(ctx, EFFECT::MOMENTUM_HASTE) / HUNDRED;
361 }
362
363 let coef = ap(ctx, EFFECT::FOF_TICK);
364
365 ctx.deal_damage_ap(EFFECT::FOF_TICK.0, coef * mult, DamageFlags::PHYSICAL);
366
367 if cfg.momentum_boost {
368 ctx.apply_aura(AURA_MOMENTUM_BOOST.raw());
369 }
370
371 if !ctx.is_last_channel_tick() {
372 return;
373 }
374
375 ctx.expire_aura(AURA_MOMENTUM_BOOST.raw());
376
377 if cfg.jadefire_stomp {
378 let (spell_id, effect) = EFFECT::JADEFIRE_STOMP_DMG;
379 let coef = ctx
380 .game_data()
381 .ap_coef(SpellIdx::from_raw(spell_id), effect);
382
383 ctx.deal_damage_ap(spell_id, coef, DamageFlags::empty());
384 }
385}
386
387pub(crate) fn swing_hook(
388 ctx: &mut HookCtx<'_>,
389 event: SwingEvent,
390) {
391 let is_crit = event.is_crit;
392
393 let cfg = windwalker_config(ctx);
394
395 if !cfg.flurry_strikes {
396 return;
397 }
398
399 let mut charges = cfg.flurry_charges_per_swing;
400
401 if is_crit {
402 charges *= cfg.flurry_crit_charge_mult;
403 }
404
405 ctx.apply_aura_n(AURA_FLURRY_CHARGE.raw(), charges);
406}
407
408#[cfg(test)]
409mod tests {
410 use googletest::prelude::*;
411
412 use super::*;
413
414 #[gtest]
415 fn dual_threat_requires_a_normal_swing_between_replacements() -> Result<()> {
416 let mut runtime = WindwalkerRuntime::default();
417 let mut unexpected_rolls = 0;
418
419 verify_that!(
420 dual_threat_should_replace(0.3, &mut runtime, &mut || {
421 unexpected_rolls += 1;
422
423 0.0
424 }),
425 eq(false)
426 )?;
427 verify_that!(unexpected_rolls, eq(0))?;
428 verify_that!(runtime.dual_threat_ready, eq(true))?;
429 verify_that!(
430 dual_threat_should_replace(0.3, &mut runtime, &mut || 0.1),
431 eq(true)
432 )?;
433 verify_that!(runtime.dual_threat_ready, eq(false))?;
434 verify_that!(
435 dual_threat_should_replace(0.3, &mut runtime, &mut || {
436 unexpected_rolls += 1;
437
438 0.0
439 }),
440 eq(false)
441 )?;
442 verify_that!(unexpected_rolls, eq(0))?;
443 verify_that!(
444 dual_threat_should_replace(0.3, &mut runtime, &mut || 0.9),
445 eq(false)
446 )?;
447 verify_that!(runtime.dual_threat_ready, eq(true))?;
448
449 verify_that!(
450 dual_threat_should_replace(0.3, &mut runtime, &mut || 0.1),
451 eq(true)
452 )
453 }
454}