wowlab_engine_content/hooks/shared/
rogue.rs1use 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 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}