1use wowlab_engine_combat::{
2 DamageEffectRef, ImpactActorFilter, ImpactProc, LandedImpactProc, PeriodicKind, ProcDriver,
3 ProcPolicy, ProcTriggerSide,
4};
5use wowlab_engine_ports::HandlerParams;
6use wowlab_types::{constants::HUNDRED, sim::SpellIdx};
7
8use crate::generated::specs::balance_druid::{
9 AURA, AURA_ECLIPSE_LUNAR, AURA_ECLIPSE_SOLAR, EFFECT, REPORTED_SPELL, SET_BONUS, SPELL,
10 SPELL_CELESTIAL_ALIGNMENT, SPELL_MOONFIRE, SPELL_SOLAR_ECLIPSE, SPELL_STARFIRE,
11 SPELL_STARSURGE, SPELL_WRATH, TALENT,
12};
13
14use super::hooks;
15
16const AP_DISPLAY: f64 = 10.0;
18const GOLDRINN_PROC_CHANCE: f64 = 0.33;
20const CA_BASE_RECHARGE_MS: i64 = 180_000;
22const WHIRLING_STARS_CHARGES: u8 = 2;
24const HAIL_OF_STARS_FREE_MS: u32 = 1_000;
26const INCARNATION_ECLIPSE_MS: u32 = 20_000;
28
29const fn foe_tick_filter(spell_id: u32) -> bool {
31 matches!(
32 spell_id,
33 AURA::FURY_OF_ELUNE_BEAM | AURA::SUNDERED_FIRMAMENT_BEAM
34 )
35}
36
37const fn smolder_filter(spell_id: u32) -> bool {
39 matches!(
40 spell_id,
41 SPELL::WRATH
42 | SPELL::STARFIRE
43 | SPELL::STARSURGE
44 | AURA::MOONFIRE
45 | AURA::SUNFIRE
46 | REPORTED_SPELL::SHOOTING_STARS
47 | AURA::FURY_OF_ELUNE_BEAM
48 | REPORTED_SPELL::STARSURGE_CASCADE
49 | REPORTED_SPELL::EXPLODING_STAR
50 | REPORTED_SPELL::GOLDRINN
51 )
52}
53
54const fn dot_filter(spell_id: u32) -> bool {
55 matches!(spell_id, AURA::MOONFIRE | AURA::SUNFIRE)
56}
57
58const fn sunseeker_mushroom_filter(spell_id: u32) -> bool {
59 spell_id == REPORTED_SPELL::SUNSEEKER_MUSHROOM
60}
61
62const fn wild_mushroom_filter(spell_id: u32) -> bool {
63 spell_id == REPORTED_SPELL::WILD_MUSHROOM
64}
65
66fn deterministic_payload_driver() -> ProcDriver {
67 ProcDriver {
68 policy: ProcPolicy {
69 can_proc_from_procs: true,
70 ..ProcPolicy::default()
71 },
72 trigger_side: ProcTriggerSide::Caster,
73 }
74}
75
76fn points(params: &HandlerParams<'_>, eff: (u32, u8)) -> f64 {
77 params
78 .game_data
79 .base_points(SpellIdx::from_raw(eff.0), eff.1)
80}
81
82fn coef(params: &HandlerParams<'_>, eff: (u32, u8)) -> f64 {
83 params.game_data.sp_coef(SpellIdx::from_raw(eff.0), eff.1)
84}
85
86#[derive(Clone, Copy, Debug, Default)]
87pub(super) struct BalanceRuntime {
88 pub(super) eclipse_entries: u32,
89}
90
91crate::hooks::define_spec_config! {
92pub(super) struct BalanceConfig {
93 lunar_calling: bool { provenance: "Lunar Calling talent selection.", gate: params.talent_picked(TALENT::LUNAR_CALLING), source: true, transform: |value| value },
94 sunseeker: bool { provenance: "Sunseeker Mushroom selects its Fungal Growth action profile.", gate: params.talent_picked(TALENT::SUNSEEKER_MUSHROOM), source: true, transform: |value| value },
95 sunseeker_fungal_coef: f64 { provenance: "Sunseeker Fungal Growth 1280213 e1 SP coefficient.", gate: params.talent_picked(TALENT::SUNSEEKER_MUSHROOM), source: coef(params, EFFECT::FUNGAL_GROWTH_SUNSEEKER), transform: |value| value },
96 incarnation: bool { provenance: "Incarnation: Chosen of Elune talent selection.", gate: params.talent_picked(TALENT::INCARNATION_CHOSEN_OF_ELUNE), source: true, transform: |value| value },
97 has_boat: bool { provenance: "Balance of All Things 394048 selection.", gate: params.talent_picked(TALENT::BALANCE_OF_ALL_THINGS), source: true, transform: |value| value },
98 shooting_stars_chance: f64 { provenance: "Shooting Stars 202342 e1 chance.", gate: shooting_stars, source: points(params, EFFECT::SHOOTING_STARS_CHANCE), transform: |value| value / HUNDRED },
99 shooting_stars_coef: f64 { provenance: "Shooting Stars 202497 e1 SP coefficient.", gate: shooting_stars, source: coef(params, EFFECT::SHOOTING_STARS_DMG), transform: |value| value },
100 shooting_stars_ap: f64 { provenance: "Shooting Stars 202497 e2 energize.", gate: shooting_stars, source: points(params, EFFECT::SHOOTING_STARS_AP), transform: |value| value / AP_DISPLAY },
101 solstice_rate: f64 { provenance: "Solstice 343647 e1 rate bonus.", gate: shooting_stars && has_solstice, source: points(params, EFFECT::SOLSTICE_RATE), transform: |value| value / HUNDRED },
102 has_solstice: bool { provenance: "Solstice talent selection.", gate: has_solstice, source: true, transform: |value| value },
103 mid1_2pc_ap: f64 { provenance: "MID1 Balance 2pc 1264810 e2 extra Astral Power.", gate: shooting_stars && has_2pc, source: points(params, EFFECT::MID1_2PC_AP), transform: |value| value / AP_DISPLAY },
104 smolder_pct: f64 { provenance: "Astral Smolder 1261565 e1 crit-damage share.", gate: smolder, source: points(params, EFFECT::ASTRAL_SMOLDER_PCT), transform: |value| value / HUNDRED },
105 cascade_chance: f64 { provenance: "Star Cascade 1271206 e1 proc chance.", gate: cascade, source: points(params, EFFECT::STAR_CASCADE_CHANCE), transform: |value| value / HUNDRED },
106 cascade_coef: f64 { provenance: "Starsurge Cascade 1271222 e1 scaled by 1271206 e3.", gate: cascade, source: coef(params, EFFECT::STARSURGE_CASCADE_DMG) * points(params, EFFECT::STAR_CASCADE_EFFECTIVENESS), transform: |value| value / HUNDRED },
107 goldrinn_chance: f64 { provenance: "Power of Goldrinn `SpellAuraOptions` proc chance.", gate: goldrinn, source: GOLDRINN_PROC_CHANCE, transform: |value| value },
108 goldrinn_coef: f64 { provenance: "Goldrinn 394047 e1 SP coefficient.", gate: goldrinn, source: coef(params, EFFECT::GOLDRINN_DMG), transform: |value| value },
109 ttc_chance_wrath: f64 { provenance: "Touch the Cosmos 450356 e1 Wrath chance.", gate: touch_cosmos, source: points(params, EFFECT::TTC_CHANCE_WRATH), transform: |value| value / HUNDRED },
110 ttc_chance_starfire: f64 { provenance: "Touch the Cosmos 450356 e2 Starfire chance.", gate: touch_cosmos, source: points(params, EFFECT::TTC_CHANCE_STARFIRE), transform: |value| value / HUNDRED },
111 exploding_chance: f64 { provenance: "MID1 Balance 4pc 1264811 e2 chance.", gate: has_4pc, source: points(params, EFFECT::MID1_4PC_EXPLODING_CHANCE), transform: |value| value / HUNDRED },
112 exploding_coef: f64 { provenance: "Exploding Star 1272339 e1 SP coefficient.", gate: has_4pc, source: coef(params, EFFECT::EXPLODING_STAR_DMG), transform: |value| value },
113 exploding_ap: f64 { provenance: "Exploding Star 1272339 e2 energize.", gate: has_4pc, source: points(params, EFFECT::EXPLODING_STAR_AP), transform: |value| value / AP_DISPLAY },
114 stellar_amp_cap_ms: u32 { provenance: "Stellar Amplification 450212 e1 duration cap.", gate: stellar_amp, source: points(params, EFFECT::STELLAR_AMP_CAP), transform: wowlab_types::numeric::f64_to_u32_saturating_trunc },
115 hail_paid_ms: u32 { provenance: "Hail of Stars 469004 e1 paid-action duration.", gate: hail, source: points(params, EFFECT::HAIL_OF_STARS_MS), transform: wowlab_types::numeric::f64_to_u32_saturating_trunc },
116 hail_free_ms: u32 { provenance: "`SimC` Hail of Stars free-spender duration.", gate: hail, source: HAIL_OF_STARS_FREE_MS, transform: |value| value },
117 has_sundered: bool { provenance: "Sundered Firmament talent selection.", gate: params.talent_picked(TALENT::SUNDERED_FIRMAMENT), source: true, transform: |value| value },
118 sunseeker_mushroom_ap_cap: f64 { provenance: "Sunseeker Mushroom 1280212 e2 impact resource cap.", gate: params.talent_picked(TALENT::SUNSEEKER_MUSHROOM), source: points(params, EFFECT::SUNSEEKER_MUSHROOM_AP_CAP), transform: |value| value },
119 wild_mushroom_ap_cap: f64 { provenance: "Wild Mushroom 88751 e2 impact resource cap.", gate: params.talent_picked(TALENT::WILD_MUSHROOM), source: points(params, EFFECT::WILD_MUSHROOM_AP_CAP), transform: |value| value },
120}
121accessor pub(super) fn balance_config(ctx);
122register pub(super) fn register_balance(built, params) -> BalanceConfig;
123prepare {
124 let has_2pc = params.set_bonus_auras.contains(&SET_BONUS::MID1_BALANCE_2PC);
125 let has_4pc = params.set_bonus_auras.contains(&SET_BONUS::MID1_BALANCE_4PC);
126 let shooting_stars = params.talent_picked(TALENT::SHOOTING_STARS);
127 let has_solstice = params.talent_picked(TALENT::SOLSTICE);
128 let smolder = params.talent_picked(TALENT::ASCENDANT_ECLIPSES_2);
129 let hail = params.talent_picked(TALENT::HAIL_OF_STARS);
130 let cascade = params.talent_picked(TALENT::STAR_CASCADE);
131 let goldrinn = params.talent_picked(TALENT::POWER_OF_GOLDRINN);
132 let touch_cosmos = params.talent_picked(TALENT::TOUCH_THE_COSMOS);
133 let stellar_amp = params.talent_picked(TALENT::STELLAR_AMPLIFICATION);
134}
135auras {}
136wiring {
137 { provenance: "Sunseeker Mushroom 1280212 impact applies the shared Fungal Growth transaction.", gate: params.talent_picked(TALENT::SUNSEEKER_MUSHROOM), apply: {
138 built.register_landed_impact_proc(LandedImpactProc { driver: deterministic_payload_driver(),
139 actor_filter: ImpactActorFilter::Player,
140 chance: 1.0,
141 fire: hooks::sunseeker_fungal_growth_fire,
142 spell_filter: Some(sunseeker_mushroom_filter),
143 periodic_only: false,
144 skip_periodic: true,
145 crit_only: false,
146 });
147 } },
148 { provenance: "Wild Mushroom 88751 impact applies the shared Fungal Growth transaction.", gate: params.talent_picked(TALENT::WILD_MUSHROOM), apply: {
149 built.register_landed_impact_proc(LandedImpactProc { driver: deterministic_payload_driver(),
150 actor_filter: ImpactActorFilter::Player,
151 chance: 1.0,
152 fire: hooks::wild_mushroom_fungal_growth_fire,
153 spell_filter: Some(wild_mushroom_filter),
154 periodic_only: false,
155 skip_periodic: true,
156 crit_only: false,
157 });
158 } },
159 { provenance: "Shooting Stars periodic proc registration.", gate: shooting_stars, apply: {
160 built.register_impact_proc(
161 ImpactProc::new(hooks::shooting_stars_fire)
162 .with_spell_filter(dot_filter)
163 .periodic_only(),
164 );
165 } },
166 { provenance: "Astral Smolder critical proc registration.", gate: smolder, apply: {
167 built.register_impact_proc(
168 ImpactProc::new(hooks::astral_smolder_fire)
169 .with_spell_filter(smolder_filter)
170 .crit_only(),
171 );
172 } },
173 { provenance: "Nature's Balance starting Astral Power.", gate: params.talent_picked(TALENT::NATURE_S_BALANCE), apply: {
174 built.patch_base_stats(|stats| {
175 stats.resource_start = points(params, EFFECT::NATURES_BALANCE_START);
176 });
177 } },
178 { provenance: "Wild Surges flat Starfire and Wrath energize modifiers.", gate: params.talent_picked(TALENT::WILD_SURGES), apply: {
179 let gain = points(params, EFFECT::WILD_SURGES_AP) / AP_DISPLAY;
180 for spell in [SPELL_STARFIRE, SPELL_WRATH] {
181 built.patch_spell(spell, |def| {
182 def.cost.resource_gain += gain;
183 });
184 }
185 } },
186 { provenance: "Moon Guardian flat Starfire and Moonfire energize modifiers.", gate: params.talent_picked(TALENT::MOON_GUARDIAN), apply: {
187 let gain = points(params, EFFECT::MOON_GUARDIAN_AP) / AP_DISPLAY;
188 for spell in [SPELL_STARFIRE, SPELL_MOONFIRE] {
189 built.patch_spell(spell, |def| {
190 def.cost.resource_gain += gain;
191 });
192 }
193 } },
194 { provenance: "Rattle the Stars resource-cost modifier.", gate: params.talent_picked(TALENT::RATTLE_THE_STARS), apply: {
195 let cost_mult = 1.0 + points(params, EFFECT::RATTLE_COST_PCT) / HUNDRED;
196 for idx in [SPELL_STARSURGE, crate::generated::specs::balance_druid::SPELL_STARFALL] {
197 built.patch_spell(idx, |spell| {
198 spell.cost.resource_cost = (spell.cost.resource_cost * cost_mult).round();
199 });
200 }
201 } },
202 { provenance: "Whirling Stars Celestial Alignment charge and recharge modifier.", gate: params.talent_picked(TALENT::WHIRLING_STARS), apply: {
203 built.patch_spell(SPELL_CELESTIAL_ALIGNMENT, |spell| {
204 spell.cooldown.max_charges = WHIRLING_STARS_CHARGES;
205 let recharge_ms = CA_BASE_RECHARGE_MS
206 + wowlab_types::numeric::f64_to_i64_saturating_trunc(points(params, EFFECT::WHIRLING_STARS_CDR));
207 spell.cooldown.recharge_ms = u32::try_from(recharge_ms)
208 .expect("Whirling Stars recharge remains a non-negative u32 duration");
209 });
210 } },
211 { provenance: "Improved Eclipse extra Solar Eclipse charge.", gate: params.talent_picked(TALENT::IMPROVED_ECLIPSE), apply: {
212 built.patch_spell(SPELL_SOLAR_ECLIPSE, |spell| {
213 spell.cooldown.max_charges = spell.cooldown.max_charges.saturating_add(1);
214 });
215 } },
216 { provenance: "Atmospheric Exposure impact proc registration.", gate: params.talent_picked(TALENT::ATMOSPHERIC_EXPOSURE), apply: {
217 built.register_impact_proc(
218 ImpactProc::new(hooks::atmospheric_exposure_fire).with_spell_filter(foe_tick_filter),
219 );
220 } },
221}
222finish |cfg| {
223 built.patch_spell(SPELL_WRATH, |s| {
225 s.cost.resource_gain += points(params, EFFECT::ASTRAL_POWER_WRATH_AP) / AP_DISPLAY;
226 });
227
228 if cfg.incarnation && cfg.lunar_calling {
230 for idx in [AURA_ECLIPSE_SOLAR, AURA_ECLIPSE_LUNAR] {
231 built.patch_aura(idx, |a| {
232 a.base_duration_ms = INCARNATION_ECLIPSE_MS;
233 });
234 }
235 }
236 if cfg.lunar_calling {
237 built.disable_spell(SPELL::SOLAR_ECLIPSE);
238 }
239 if cfg.sunseeker {
240 built.patch_aura(crate::generated::specs::balance_druid::AURA_FUNGAL_GROWTH, |aura| {
241 let periodic = aura
242 .periodic
243 .as_mut()
244 .expect("Fungal Growth remains a periodic damage aura");
245
246 periodic.damage_spell_id = EFFECT::FUNGAL_GROWTH_SUNSEEKER.0;
247 periodic.effect_ref = Some(DamageEffectRef::new(
248 EFFECT::FUNGAL_GROWTH_SUNSEEKER.0,
249 EFFECT::FUNGAL_GROWTH_SUNSEEKER.1,
250 ));
251 let PeriodicKind::RollingDamageSp { coef, .. } = &mut periodic.effect else {
252 panic!("Fungal Growth remains rolling spell-power damage");
253 };
254
255 *coef = cfg.sunseeker_fungal_coef;
256 });
257 }
258
259 built.state.set_spec_config(cfg);
260 built.state.set_spec_runtime(BalanceRuntime::default());
261 cfg
262}
263}
264
265#[cfg(test)]
266mod tests {
267 use googletest::prelude::*;
268
269 use super::*;
270
271 #[gtest]
272 fn mushroom_payload_riders_accept_only_their_exact_child_identities() -> Result<()> {
273 verify_true!(sunseeker_mushroom_filter(
274 REPORTED_SPELL::SUNSEEKER_MUSHROOM
275 ))?;
276 verify_false!(sunseeker_mushroom_filter(REPORTED_SPELL::WILD_MUSHROOM))?;
277 verify_true!(wild_mushroom_filter(REPORTED_SPELL::WILD_MUSHROOM))?;
278
279 verify_false!(wild_mushroom_filter(
280 REPORTED_SPELL::SUNSEEKER_MUSHROOM
281 ))
282 }
283
284 #[gtest]
285 fn deterministic_payload_rider_accepts_proc_origin_damage() -> Result<()> {
286 let driver = deterministic_payload_driver();
287
288 verify_true!(driver.policy.can_proc_from_procs)?;
289
290 verify_that!(driver.trigger_side, eq(ProcTriggerSide::Caster))
291 }
292}