Skip to main content

wowlab_engine_content/hooks/shared/
rogue.rs

1use wowlab_engine_combat::{
2    AuraOps as _, DamageFlags, DamageOps as _, HookCtx, LocalAuraIdx, ResourceOps as _,
3};
4use wowlab_engine_rng::proc_chance;
5
6const MS_PER_COMBO_POINT: u32 = 3_000;
7pub(crate) const COIN_FAIR_ODDS: f64 = 0.5;
8
9pub(crate) fn cut_to_the_chase(ctx: &mut HookCtx<'_>, aura: LocalAuraIdx, combo_points: f64) {
10    if !ctx.is_aura_active(aura) {
11        ctx.apply_aura(aura);
12    }
13
14    ctx.extend_aura(
15        aura,
16        wowlab_types::numeric::f64_to_u32_saturating_trunc(combo_points) * MS_PER_COMBO_POINT,
17    );
18}
19
20#[derive(Clone, Copy, Debug)]
21pub(crate) struct FateboundConfig {
22    pub hand_of_fate_cp: f64,
23    pub matching_odds: f64,
24    pub lucky_coin_odds: f64,
25    pub controlled_chaos_streak: i32,
26    pub rush_energy: f64,
27    pub rush_energy_edge: f64,
28    pub lucky_coin_flips: i32,
29    pub has_edge_case: bool,
30}
31
32#[derive(Clone, Copy, Debug)]
33pub(crate) struct FateboundIds {
34    pub heads: LocalAuraIdx,
35    pub tails: LocalAuraIdx,
36    pub lucky_coin: LocalAuraIdx,
37    pub lucky_coin_counter: LocalAuraIdx,
38    pub tails_damage_effect: wowlab_engine_combat::DamageEffectId,
39    pub tails_damage_spell: wowlab_engine_combat::DamageActionId,
40}
41
42#[derive(Clone, Copy, Debug)]
43pub(crate) struct FateboundBinding {
44    pub config: fn(&HookCtx<'_>) -> FateboundConfig,
45    pub ids: FateboundIds,
46    pub on_tails: fn(&mut HookCtx<'_>),
47}
48
49#[derive(Clone, Copy, Debug)]
50pub(crate) struct ResolvedFatebound {
51    config: FateboundConfig,
52    ids: FateboundIds,
53    on_tails: fn(&mut HookCtx<'_>),
54}
55
56impl FateboundBinding {
57    pub(crate) fn resolve(self, ctx: &HookCtx<'_>) -> ResolvedFatebound {
58        ResolvedFatebound {
59            config: (self.config)(ctx),
60            ids: self.ids,
61            on_tails: self.on_tails,
62        }
63    }
64}
65
66pub(crate) fn bound_hand_of_fate(ctx: &mut HookCtx<'_>, biased: bool, binding: FateboundBinding) {
67    let fatebound = binding.resolve(ctx);
68
69    hand_of_fate(ctx, fatebound, biased);
70}
71
72pub(crate) fn bound_edge_case(ctx: &mut HookCtx<'_>, binding: FateboundBinding) {
73    let fatebound = binding.resolve(ctx);
74
75    edge_case(ctx, fatebound);
76}
77
78#[derive(Clone, Copy, PartialEq)]
79enum Flip {
80    Heads,
81    Tails,
82    Edge,
83}
84
85pub(crate) fn hand_of_fate(ctx: &mut HookCtx<'_>, fatebound: ResolvedFatebound, biased: bool) {
86    let config = fatebound.config;
87
88    if config.hand_of_fate_cp <= 0.0 || ctx.last_secondary_spent() < config.hand_of_fate_cp {
89        return;
90    }
91
92    flip_coin(ctx, fatebound, biased);
93}
94
95pub(crate) fn edge_case(ctx: &mut HookCtx<'_>, fatebound: ResolvedFatebound) {
96    let config = fatebound.config;
97
98    if config.has_edge_case && config.hand_of_fate_cp > 0.0 {
99        resolve_flip(ctx, fatebound, Flip::Edge);
100    }
101}
102
103pub(crate) fn flip_coin(ctx: &mut HookCtx<'_>, fatebound: ResolvedFatebound, biased: bool) {
104    let config = fatebound.config;
105    let ids = fatebound.ids;
106    let heads = ctx.aura_stacks(ids.heads);
107    let tails = ctx.aura_stacks(ids.tails);
108    let result = if heads == tails {
109        if proc_chance(ctx.rng(), COIN_FAIR_ODDS) {
110            Flip::Heads
111        } else {
112            Flip::Tails
113        }
114    } else {
115        let matching = if biased {
116            config.matching_odds
117                + if ctx.is_aura_active(ids.lucky_coin) {
118                    config.lucky_coin_odds
119                } else {
120                    0.0
121                }
122        } else {
123            COIN_FAIR_ODDS
124        };
125        let current_is_heads = heads > tails;
126
127        if proc_chance(ctx.rng(), matching) == current_is_heads {
128            Flip::Heads
129        } else {
130            Flip::Tails
131        }
132    };
133
134    resolve_flip(ctx, fatebound, result);
135
136    if config.controlled_chaos_streak > 0 {
137        let opposite_streak = match result {
138            Flip::Heads => tails >= config.controlled_chaos_streak,
139            Flip::Tails => heads >= config.controlled_chaos_streak,
140            Flip::Edge => false,
141        };
142
143        if opposite_streak {
144            resolve_flip(ctx, fatebound, result);
145        }
146    }
147}
148
149fn resolve_flip(ctx: &mut HookCtx<'_>, fatebound: ResolvedFatebound, result: Flip) {
150    let config = fatebound.config;
151    let ids = fatebound.ids;
152
153    if matches!(result, Flip::Heads | Flip::Edge) {
154        if result != Flip::Edge {
155            ctx.consume_aura(ids.tails);
156        }
157
158        ctx.apply_aura(ids.heads);
159    }
160
161    // Tails per-stack (452917 e1) and Lucky Coin (1248971 e3) are live-aura DBC modifiers on the
162    // coin damage; the generic aura-modifier path applies both, so the coefficient stays raw.
163
164    if matches!(result, Flip::Tails | Flip::Edge) {
165        if result != Flip::Edge {
166            ctx.consume_aura(ids.heads);
167        }
168
169        let coefficient = ctx.effect_ap_coefficient(ids.tails_damage_effect);
170
171        ctx.deal_damage_ap(ids.tails_damage_spell, coefficient, DamageFlags::empty());
172        ctx.apply_aura(ids.tails);
173        (fatebound.on_tails)(ctx);
174    }
175
176    let energy = if result == Flip::Edge {
177        config.rush_energy_edge
178    } else {
179        config.rush_energy
180    };
181
182    if energy > 0.0 {
183        ctx.gain_resource(energy);
184    }
185
186    if config.lucky_coin_flips > 0 && !ctx.is_aura_active(ids.lucky_coin) {
187        ctx.apply_aura(ids.lucky_coin_counter);
188
189        if ctx.aura_stacks(ids.lucky_coin_counter) >= config.lucky_coin_flips {
190            ctx.apply_aura(ids.lucky_coin);
191            ctx.consume_aura(ids.lucky_coin_counter);
192        }
193    }
194}