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