wowlab_engine_content/hooks/balance_druid/
hooks.rs1use wowlab_engine_combat::DamageOps as _;
2use wowlab_engine_combat::ResourceOps as _;
3use wowlab_engine_combat::AuraOps as _;
4use wowlab_engine_combat::{
5 DamageFlags, HookCtx, ImpactEvent, LandedImpactEvent, MasteryCtx, ResourceGainSource,
6};
7use wowlab_engine_rng::proc_chance;
8
9use crate::generated::specs::balance_druid::{
10 AURA_ATMOSPHERIC_EXPOSURE, AURA_BOAT_ARCANE, AURA_BOAT_NATURE, AURA_CELESTIAL_ALIGNMENT,
11 AURA_ECLIPSE_LUNAR, AURA_ECLIPSE_SOLAR, AURA_FUNGAL_GROWTH,
12 AURA_INCARNATION_CHOSEN_OF_ELUNE, AURA_SOLSTICE, AURA_STARFALL, AURA_STARLORD,
13 AURA_SUNDERED_FIRMAMENT_BEAM, AURA_SUNDERED_FIRMAMENT_ENERGIZE,
14 AURA_TOUCH_THE_COSMOS, EFFECT, REPORTED_SPELL,
15};
16
17use super::config::{balance_config, BalanceConfig, BalanceRuntime};
18
19const MASTERY_BOTH_SCHOOLS_LABEL: i32 = 2391;
21const SUNDERED_ECLIPSE_PERIOD: u32 = 2;
23const WILD_MUSHROOM_AP_PER_TARGET_STEP: f64 = 5.0;
25
26#[must_use]
28pub(crate) fn mastery(ctx: &MasteryCtx<'_>) -> f64 {
29 let (arcane_eff, nature_eff) = if ctx.is_periodic {
30 (
31 EFFECT::ASTRAL_INVOCATION_ARCANE_PERIODIC.1,
32 EFFECT::ASTRAL_INVOCATION_NATURE_PERIODIC.1,
33 )
34 } else {
35 (
36 EFFECT::ASTRAL_INVOCATION_ARCANE_DIRECT.1,
37 EFFECT::ASTRAL_INVOCATION_NATURE_DIRECT.1,
38 )
39 };
40 let both = ctx.spell_has_label(MASTERY_BOTH_SCHOOLS_LABEL);
41 let mut mult = 1.0;
42
43 if both || ctx.affects_spell(arcane_eff) {
44 mult *= ctx.bonus(arcane_eff);
45 }
46
47 if both || ctx.affects_spell(nature_eff) {
48 mult *= ctx.bonus(nature_eff);
49 }
50
51 mult
52}
53
54fn eclipse_active(ctx: &HookCtx<'_>) -> bool {
55 ctx.is_aura_active(AURA_ECLIPSE_SOLAR.raw()) || ctx.is_aura_active(AURA_ECLIPSE_LUNAR.raw())
56}
57
58fn grant_solstice(ctx: &mut HookCtx<'_>, dur_ms: u32) {
59 if ctx.is_aura_active(AURA_SOLSTICE.raw()) {
60 ctx.extend_aura(AURA_SOLSTICE.raw(), dur_ms);
61
62 return;
63 }
64
65 ctx.apply_aura_with_duration(AURA_SOLSTICE.raw(), dur_ms);
66}
67
68fn star_cascade(ctx: &mut HookCtx<'_>, cfg: &BalanceConfig) {
69 if cfg.cascade_chance > 0.0 && proc_chance(ctx.rng(), cfg.cascade_chance) {
70 ctx.deal_damage_sp(
71 EFFECT::STARSURGE_CASCADE_DMG.0,
72 cfg.cascade_coef,
73 DamageFlags::empty(),
74 );
75
76 if cfg.hail_free_ms > 0 {
77 grant_solstice(ctx, cfg.hail_free_ms);
78 }
79 }
80}
81
82pub(crate) fn starfall_tick_hook(ctx: &mut HookCtx<'_>) {
83 let stacks = f64::from(ctx.aura_stacks(AURA_STARFALL.raw()).max(1));
84
85 ctx.deal_effect_damage_sp_with_geometry(
86 EFFECT::STARFALL_DAMAGE,
87 EFFECT::STARFALL_GEOMETRY,
88 stacks,
89 DamageFlags::PERIODIC,
90 );
91}
92
93fn touch_the_cosmos(ctx: &mut HookCtx<'_>, chance: f64) {
94 if chance > 0.0 && proc_chance(ctx.rng(), chance) {
95 ctx.apply_aura(AURA_TOUCH_THE_COSMOS.raw());
96 }
97}
98
99fn shooting_star_hit(
100 ctx: &mut HookCtx<'_>,
101 cfg: &BalanceConfig,
102 spell_id: u32,
103 coef: f64,
104 base_ap: f64,
105) {
106 ctx.deal_damage_sp(spell_id, coef, DamageFlags::empty());
107 let mut gain = base_ap;
108
109 if cfg.mid1_2pc_ap > 0.0 && !eclipse_active(ctx) {
110 gain += cfg.mid1_2pc_ap;
111 }
112
113 ctx.gain_resource(gain);
114}
115
116fn enter_eclipse(ctx: &mut HookCtx<'_>) {
117 ctx.expire_aura(AURA_STARLORD.raw());
118 let cfg = balance_config(ctx);
119
120 if cfg.has_solstice {
121 ctx.expire_aura(AURA_SOLSTICE.raw());
122 ctx.apply_aura(AURA_SOLSTICE.raw());
123 }
124
125 if cfg.has_sundered {
126 let fire = ctx.spec_runtime_mut::<BalanceRuntime>().is_some_and(|runtime| {
127 runtime.eclipse_entries += 1;
128
129 runtime.eclipse_entries % SUNDERED_ECLIPSE_PERIOD == 0
130 });
131
132 if fire {
133 ctx.apply_aura(AURA_SUNDERED_FIRMAMENT_ENERGIZE.raw());
134 ctx.apply_aura(AURA_SUNDERED_FIRMAMENT_BEAM.raw());
135 }
136 }
137}
138
139pub(crate) fn wrath_hook(ctx: &mut HookCtx<'_>) {
140 let cfg = balance_config(ctx);
141
142 star_cascade(ctx, &cfg);
143 touch_the_cosmos(ctx, cfg.ttc_chance_wrath);
144}
145
146pub(crate) fn starfire_hook(ctx: &mut HookCtx<'_>) {
147 let cfg = balance_config(ctx);
148
149 star_cascade(ctx, &cfg);
150 touch_the_cosmos(ctx, cfg.ttc_chance_starfire);
151}
152
153pub(crate) fn starsurge_hook(ctx: &mut HookCtx<'_>) {
154 let cfg = balance_config(ctx);
155
156 if ctx.consume_aura(AURA_TOUCH_THE_COSMOS.raw()) && cfg.hail_paid_ms > 0 {
157 grant_solstice(ctx, cfg.hail_paid_ms);
158 }
159
160 if cfg.goldrinn_chance > 0.0 && proc_chance(ctx.rng(), cfg.goldrinn_chance) {
161 ctx.deal_damage_sp(
162 EFFECT::GOLDRINN_DMG.0,
163 cfg.goldrinn_coef,
164 DamageFlags::empty(),
165 );
166 }
167
168 if cfg.exploding_chance > 0.0 && proc_chance(ctx.rng(), cfg.exploding_chance) {
169 shooting_star_hit(
170 ctx,
171 &cfg,
172 EFFECT::EXPLODING_STAR_DMG.0,
173 cfg.exploding_coef,
174 cfg.exploding_ap,
175 );
176 }
177}
178
179pub(crate) fn eclipse_hook(ctx: &mut HookCtx<'_>) {
180 enter_eclipse(ctx);
181
182 if balance_config(ctx).has_boat {
183 ctx.apply_aura(AURA_BOAT_NATURE.raw());
184 }
185}
186
187pub(crate) fn celestial_alignment_hook(ctx: &mut HookCtx<'_>) {
188 let cfg = balance_config(ctx);
189
190 if cfg.incarnation {
191 ctx.apply_aura(AURA_INCARNATION_CHOSEN_OF_ELUNE.raw());
192 } else {
193 ctx.apply_aura(AURA_CELESTIAL_ALIGNMENT.raw());
194 }
195
196 enter_eclipse(ctx);
197
198 if cfg.has_boat {
199 ctx.apply_aura(AURA_BOAT_ARCANE.raw());
200 ctx.apply_aura(AURA_BOAT_NATURE.raw());
201 }
202}
203
204pub(super) fn shooting_stars_fire(
205 ctx: &mut HookCtx<'_>,
206 _impact: ImpactEvent,
207) {
208 let cfg = balance_config(ctx);
209
210 if cfg.shooting_stars_chance <= 0.0 {
211 return;
212 }
213
214 let mut chance = cfg.shooting_stars_chance;
215
216 if ctx.is_aura_active(AURA_SOLSTICE.raw()) {
217 chance *= 1.0 + cfg.solstice_rate;
218 }
219
220 if proc_chance(ctx.rng(), chance) {
221 shooting_star_hit(
222 ctx,
223 &cfg,
224 EFFECT::SHOOTING_STARS_DMG.0,
225 cfg.shooting_stars_coef,
226 cfg.shooting_stars_ap,
227 );
228 }
229}
230
231pub(super) fn atmospheric_exposure_fire(
232 ctx: &mut HookCtx<'_>,
233 _impact: ImpactEvent,
234) {
235 ctx.apply_aura(AURA_ATMOSPHERIC_EXPOSURE.raw());
236}
237
238pub(super) fn sunseeker_fungal_growth_fire(
239 ctx: &mut HookCtx<'_>,
240 impact: LandedImpactEvent,
241) {
242 let cfg = balance_config(ctx);
243
244 mushroom_impact_transaction(
245 ctx,
246 impact,
247 EFFECT::SUNSEEKER_MUSHROOM_AP_CAP,
248 cfg.sunseeker_mushroom_ap_cap,
249 );
250}
251
252pub(super) fn wild_mushroom_fungal_growth_fire(
253 ctx: &mut HookCtx<'_>,
254 impact: LandedImpactEvent,
255) {
256 let cfg = balance_config(ctx);
257
258 mushroom_impact_transaction(
259 ctx,
260 impact,
261 EFFECT::WILD_MUSHROOM_AP_CAP,
262 cfg.wild_mushroom_ap_cap,
263 );
264}
265
266fn mushroom_impact_transaction(
267 ctx: &mut HookCtx<'_>,
268 impact: LandedImpactEvent,
269 resource_effect: (u32, u8),
270 resource_cap: f64,
271) {
272 ctx.apply_aura(AURA_FUNGAL_GROWTH.raw());
273
274 if impact.target_ordinal != impact.targets_hit.saturating_sub(1) {
278 return;
279 }
280
281 let target_count = f64::from(impact.targets_hit);
282 let amount = mushroom_resource_gain(target_count, resource_cap);
283
284 ctx.gain_resource_from(
285 amount,
286 ResourceGainSource::SpellEffect {
287 spell_id: resource_effect.0,
288 effect_index: resource_effect.1,
289 },
290 );
291}
292
293fn mushroom_resource_gain(target_count: f64, cap: f64) -> f64 {
294 ((target_count + 1.0) * WILD_MUSHROOM_AP_PER_TARGET_STEP).min(cap)
295}
296
297pub(super) fn astral_smolder_fire(
299 ctx: &mut HookCtx<'_>,
300 impact: ImpactEvent,
301) {
302 let cfg = balance_config(ctx);
303
304 if cfg.smolder_pct <= 0.0 || !eclipse_active(ctx) {
305 return;
306 }
307
308 ctx.deal_damage_flat(
309 REPORTED_SPELL::ASTRAL_SMOLDER,
310 impact.amount * cfg.smolder_pct,
311 );
312}
313
314#[cfg(test)]
315mod mushroom_tests {
316 use googletest::prelude::*;
317
318 use super::mushroom_resource_gain;
319
320 #[gtest]
321 fn resource_gain_counts_targets_plus_one_and_respects_data_cap() -> Result<()> {
322 verify_that!(mushroom_resource_gain(1.0, 20.0), eq(10.0))?;
323 verify_that!(mushroom_resource_gain(3.0, 20.0), eq(20.0))?;
324
325 verify_that!(mushroom_resource_gain(8.0, 17.0), eq(17.0))
326 }
327}