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 _,
4};
5use wowlab_engine_domain::pool::SpellGate;
6use wowlab_engine_rng::proc_chance;
7use wowlab_types::constants::MS_PER_SECOND;
8
9/// Baseline Execute health threshold shared by Arms and Fury.
10pub(crate) const EXECUTE_BASE_PCT: f64 = 20.0;
11/// Improved Overpower and Improved Raging Blow each grant a second charge.
12pub(crate) const IMPROVED_ATTACK_CHARGES: u8 = 2;
13/// Frothing Berserker proc chance from `SpellAuraOptions`.
14pub(crate) const FROTHING_CHANCE: f64 = 0.20;
15/// PRD constant for a 15% nominal proc rate.
16pub(crate) const PRD_C_15: f64 = 0.032_220_914;
17
18#[derive(Clone, Copy, Debug)]
19pub(crate) struct RageSpendConfig {
20    pub anger_management_div: f64,
21    pub frothing_chance: f64,
22    pub frothing_refund_pct: f64,
23    pub frothing_refund_source: (u32, u8),
24    pub avatar: LocalSpellIdx,
25    pub paired_cooldown: LocalSpellIdx,
26    pub tactician: Option<(f64, LocalSpellIdx)>,
27}
28
29#[derive(Clone, Copy, Debug)]
30pub(crate) struct SpecialAttackConfig {
31    pub rage: RageSpendConfig,
32    pub sudden_death_driver: wowlab_engine_combat::SystemRppmBinding,
33    pub sudden_death_aura: LocalAuraIdx,
34    pub execute: LocalSpellIdx,
35    pub slayers_dominance: bool,
36    pub slayers_dominance_driver: wowlab_engine_combat::ProcCounterBinding,
37    pub prd_c: f64,
38    pub slayers_strike: wowlab_engine_combat::DamageActionId,
39    pub slayers_strike_coef: f64,
40    pub executioner: LocalAuraIdx,
41    pub imminent_demise_driver: wowlab_engine_combat::ProcCounterBinding,
42    pub imminent_demise_every: i32,
43}
44
45pub(crate) fn grant_sudden_death(
46    ctx: &mut HookCtx<'_>,
47    aura: LocalAuraIdx,
48    execute: LocalSpellIdx,
49) {
50    ctx.apply_aura(aura);
51    let cooldown = ctx.spell_cooldown_ms(execute);
52
53    ctx.reduce_cooldown(execute, cooldown);
54}
55
56pub(crate) fn on_rage_spent(ctx: &mut HookCtx<'_>, cfg: RageSpendConfig, rage: f64) {
57    if rage <= 0.0 {
58        return;
59    }
60
61    if let Some((chance, spell)) = cfg.tactician
62        && chance > 0.0
63        && proc_chance(ctx.rng(), chance)
64    {
65        ctx.reduce_cooldown(spell, ctx.spell_cooldown_ms(spell));
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, cdr_ms);
74        ctx.reduce_cooldown(cfg.paired_cooldown, 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(
97            wowlab_engine_combat::generated_identities::proc_category::SLAYERS_DOMINANCE,
98        )
99        && ctx.roll_accumulating_proc(cfg.slayers_dominance_driver, cfg.prd_c)
100    {
101        ctx.start_proc_category_cooldown(
102            wowlab_engine_combat::generated_identities::proc_category::SLAYERS_DOMINANCE,
103        );
104        ctx.deal_physical_damage_ap_if_positive(cfg.slayers_strike, cfg.slayers_strike_coef);
105        ctx.apply_aura(cfg.executioner);
106
107        if ctx.advance_proc_counter(
108            cfg.imminent_demise_driver,
109            wowlab_types::numeric::i32_to_u32_nonnegative(cfg.imminent_demise_every),
110        ) {
111            grant_sudden_death(ctx, cfg.sudden_death_aura, cfg.execute);
112        }
113    }
114}
115
116/// Consume Imminent Demise and resolve this Bladestorm activation's lifetime.
117///
118/// Each stack adds one data-resolved tick while extending only the real Bladestorm aura.
119/// Mechanic callbacks use the returned lifetime.
120pub(crate) fn consume_bladestorm_extension(
121    ctx: &mut HookCtx<'_>,
122    imminent_demise: LocalAuraIdx,
123    bladestorm: LocalAuraIdx,
124    base_duration_ms: u32,
125    tick_ms: u32,
126) -> Option<std::num::NonZeroU32> {
127    let stacks = wowlab_types::numeric::i32_to_u32_nonnegative(ctx.aura_stacks(imminent_demise));
128    let extension_ms = tick_ms.saturating_mul(stacks);
129
130    if extension_ms > 0 {
131        ctx.extend_aura(bladestorm, extension_ms);
132    }
133
134    ctx.consume_aura(imminent_demise);
135
136    std::num::NonZeroU32::new(base_duration_ms.saturating_add(extension_ms))
137}
138
139pub(crate) fn sync_execute_gate(
140    handler: &mut CombatHandler,
141    threshold: f64,
142    sudden_death: LocalAuraIdx,
143    execute_spell: LocalSpellIdx,
144    ctx: &mut wowlab_engine_ports::SimContext<'_>,
145) {
146    let now = ctx.state.current_time;
147    let request =
148        wowlab_engine_combat::HookCtxRequest::for_optional_target(now, handler.current_target());
149
150    handler.with_hook_ctx(ctx.telemetry, request, |hook| {
151        let sudden_death = hook.is_aura_active(sudden_death);
152        let health_fraction = hook.target_health_fraction();
153
154        hook.set_spell_gate(
155            execute_spell,
156            SpellGate::TargetHealth,
157            sudden_death || health_fraction < threshold,
158        );
159    });
160}