Skip to main content

wowlab_engine_content/hooks/shared/
warrior.rs

1use wowlab_engine_combat::{
2    AuraOps as _, CombatHandler, CooldownOps as _, DamageOps as _, HookCtx, LocalAuraIdx,
3    LocalSpellIdx, ProcOps as _, ResourceGainSource, ResourceOps as _, is_aura_active,
4    set_spell_gate,
5};
6use wowlab_engine_domain::pool::SpellGate;
7use wowlab_engine_rng::proc_chance;
8use wowlab_types::constants::MS_PER_SECOND;
9
10/// Baseline Execute health threshold shared by Arms and Fury.
11pub(crate) const EXECUTE_BASE_PCT: f64 = 20.0;
12/// Improved Overpower and Improved Raging Blow each grant a second charge.
13pub(crate) const IMPROVED_ATTACK_CHARGES: u8 = 2;
14/// Frothing Berserker proc chance from `SpellAuraOptions`.
15pub(crate) const FROTHING_CHANCE: f64 = 0.20;
16/// `SimC` PRD constant for a 15% nominal proc rate.
17pub(crate) const PRD_C_15: f64 = 0.032_220_914;
18
19#[derive(Clone, Copy, Debug)]
20pub(crate) struct RageSpendConfig {
21    pub anger_management_div: f64,
22    pub frothing_chance: f64,
23    pub frothing_refund_pct: f64,
24    pub frothing_refund_source: (u32, u8),
25    pub avatar: LocalSpellIdx,
26    pub paired_cooldown: LocalSpellIdx,
27    pub tactician: Option<(f64, LocalSpellIdx)>,
28}
29
30#[derive(Clone, Copy, Debug)]
31pub(crate) struct SpecialAttackConfig {
32    pub rage: RageSpendConfig,
33    pub sudden_death_driver: u32,
34    pub sudden_death_aura: LocalAuraIdx,
35    pub execute: LocalSpellIdx,
36    pub slayers_dominance: bool,
37    pub slayers_dominance_driver: u32,
38    pub prd_c: f64,
39    pub slayers_strike: u32,
40    pub slayers_strike_coef: f64,
41    pub executioner: LocalAuraIdx,
42    pub imminent_demise_driver: u32,
43    pub imminent_demise_every: i32,
44}
45
46pub(crate) fn grant_sudden_death(
47    ctx: &mut HookCtx<'_>,
48    aura: LocalAuraIdx,
49    execute: LocalSpellIdx,
50) {
51    ctx.apply_aura(aura.raw());
52    let cooldown = ctx.spell_cooldown_ms(execute.raw());
53
54    ctx.reduce_cooldown(execute.raw(), cooldown);
55}
56
57pub(crate) fn on_rage_spent(ctx: &mut HookCtx<'_>, cfg: RageSpendConfig, rage: f64) {
58    if rage <= 0.0 {
59        return;
60    }
61
62    if let Some((chance, spell)) = cfg.tactician {
63        if chance > 0.0 && proc_chance(ctx.rng(), chance) {
64            ctx.reduce_cooldown(spell.raw(), ctx.spell_cooldown_ms(spell.raw()));
65        }
66    }
67
68    if cfg.anger_management_div > 0.0 {
69        let cdr_ms = wowlab_types::numeric::f64_to_u32_saturating_trunc(
70            rage / cfg.anger_management_div * MS_PER_SECOND,
71        );
72
73        ctx.reduce_cooldown(cfg.avatar.raw(), cdr_ms);
74        ctx.reduce_cooldown(cfg.paired_cooldown.raw(), cdr_ms);
75    }
76
77    if cfg.frothing_chance > 0.0 && proc_chance(ctx.rng(), cfg.frothing_chance) {
78        ctx.gain_unmodified_resource_from(
79            rage * cfg.frothing_refund_pct,
80            ResourceGainSource::SpellEffect {
81                spell_id: cfg.frothing_refund_source.0,
82                effect_index: cfg.frothing_refund_source.1,
83            },
84        );
85    }
86}
87
88pub(crate) fn on_special_attack(ctx: &mut HookCtx<'_>, cfg: SpecialAttackConfig, rage_spent: f64) {
89    on_rage_spent(ctx, cfg.rage, rage_spent);
90
91    if ctx.roll_system_rppm(cfg.sudden_death_driver) {
92        grant_sudden_death(ctx, cfg.sudden_death_aura, cfg.execute);
93    }
94
95    if cfg.slayers_dominance
96        && ctx.proc_category_ready(cfg.slayers_dominance_driver)
97        && ctx.roll_accumulating_proc(cfg.slayers_dominance_driver, cfg.prd_c)
98    {
99        ctx.start_proc_category_cooldown(cfg.slayers_dominance_driver);
100        ctx.deal_physical_damage_ap_if_positive(cfg.slayers_strike, cfg.slayers_strike_coef);
101        ctx.apply_aura(cfg.executioner.raw());
102
103        if ctx.advance_proc_counter(
104            cfg.imminent_demise_driver,
105            wowlab_types::numeric::i32_to_u32_nonnegative(cfg.imminent_demise_every),
106        ) {
107            grant_sudden_death(ctx, cfg.sudden_death_aura, cfg.execute);
108        }
109    }
110}
111
112/// Consume Imminent Demise and resolve this Bladestorm activation's lifetime.
113///
114/// Each stack adds one data-resolved tick while extending only the real Bladestorm aura.
115/// Mechanic callbacks use the returned lifetime.
116pub(crate) fn consume_bladestorm_extension(
117    ctx: &mut HookCtx<'_>,
118    imminent_demise: LocalAuraIdx,
119    bladestorm: LocalAuraIdx,
120    base_duration_ms: u32,
121    tick_ms: u32,
122) -> Option<std::num::NonZeroU32> {
123    let stacks =
124        wowlab_types::numeric::i32_to_u32_nonnegative(ctx.aura_stacks(imminent_demise.raw()));
125    let extension_ms = tick_ms.saturating_mul(stacks);
126
127    if extension_ms > 0 {
128        ctx.extend_aura(bladestorm.raw(), extension_ms);
129    }
130
131    ctx.consume_aura(imminent_demise.raw());
132
133    std::num::NonZeroU32::new(base_duration_ms.saturating_add(extension_ms))
134}
135
136pub(crate) fn sync_execute_gate(
137    handler: &mut CombatHandler,
138    threshold: f64,
139    sudden_death: LocalAuraIdx,
140    execute_spell: LocalSpellIdx,
141    now: wowlab_types::sim::SimTime,
142) {
143    let (state, buf) = handler.state_and_buf_mut();
144    let sudden_death = is_aura_active(
145        &wowlab_engine_combat::CombatView::new(state, buf),
146        sudden_death,
147        wowlab_types::sim::ActorId::Player,
148        state.current_target(),
149    );
150    let health_fraction = state
151        .current_target()
152        .and_then(|target| state.enemy_health_fraction(target, now))
153        .unwrap_or(1.0);
154
155    set_spell_gate(
156        state,
157        buf,
158        execute_spell,
159        SpellGate::TargetHealth,
160        sudden_death || health_fraction < threshold,
161    );
162}