1use wowlab_engine_combat::{
2 BuffEffect, CombatSystemDraft, DamageEffectRef, DamageReplication, DamageSource,
3 DriverSpellModifier, DriverSpellModifierResult, MasteryCtx,
4};
5use wowlab_engine_domain::{damage::armor_mitigation, dbc::ResolvedGameDataEffectExt as _};
6use wowlab_engine_gamedata::ResolvedGameData;
7use wowlab_engine_ports::HandlerParams;
8use wowlab_types::{constants::HUNDRED, sim::SpellIdx};
9
10use crate::generated::specs::subtlety_rogue::{
11 AURA_FAZED, AURA_FIND_WEAKNESS, AURA_SHADOW_BLADES, AURA_SHADOW_DANCE, AURA_UNSEEN_BLADE_CD,
12 EFFECT, HERO, REPORTED_SPELL, SET_BONUS, SPELL, TALENT,
13};
14use crate::hooks::try_gated;
15
16#[rustfmt::skip]
18static SHADOW_BLADES_SOURCE_KIT: [u32; 26] = [
19 REPORTED_SPELL::MELEE,
20 SPELL::BACKSTAB,
21 SPELL::SHADOWSTRIKE,
22 SPELL::SHURIKEN_STORM,
23 SPELL::SHURIKEN_TOSS,
24 SPELL::EVISCERATE,
25 SPELL::BLACK_POWDER,
26 SPELL::INSTANT_POISON_DAMAGE,
27 HERO::TRICKSTER::SPELL::UNSEEN_BLADE,
28 HERO::TRICKSTER::SPELL::COUP_DE_GRACE_DAMAGE_1,
29 HERO::TRICKSTER::SPELL::COUP_DE_GRACE_DAMAGE_2,
30 HERO::TRICKSTER::SPELL::COUP_DE_GRACE_DAMAGE_3,
31 REPORTED_SPELL::CDG_BONUS_1,
32 REPORTED_SPELL::CDG_BONUS_2,
33 REPORTED_SPELL::CDG_BONUS_3,
34 SPELL::EVISCERATE_BONUS,
35 SPELL::BLACK_POWDER_BONUS,
36 SPELL::SECRET_TECHNIQUE_PLAYER,
37 SPELL::SECRET_TECHNIQUE_CLONES,
38 SPELL::LASHE_MACABRE,
39 SPELL::BACKSTAB_SHADOW_CLONE,
40 SPELL::SHADOWSTRIKE_SHADOW_CLONE,
41 SPELL::EVISCERATE_SHADOW_CLONE,
42 SPELL::SHURIKEN_STORM_SHADOW_CLONE,
43 SPELL::SECRET_TECHNIQUE_SHADOW_CLONE,
44 SPELL::BLACK_POWDER_SHADOW_CLONE,
45];
46
47#[rustfmt::skip]
49static SECRET_TECHNIQUE_KIT: [u32; 3] = [
50 SPELL::SECRET_TECHNIQUE_PLAYER,
51 SPELL::SECRET_TECHNIQUE_CLONES,
52 SPELL::SECRET_TECHNIQUE_SHADOW_CLONE,
53];
54
55static SHADOWSTRIKE_KIT: [u32; 2] = [SPELL::SHADOWSTRIKE, SPELL::SHADOWSTRIKE_SHADOW_CLONE];
57
58#[rustfmt::skip]
60static PHYSICAL_KIT: [u32; 9] = [
61 SPELL::BACKSTAB,
62 SPELL::SHADOWSTRIKE,
63 SPELL::SHURIKEN_STORM,
64 SPELL::SHURIKEN_TOSS,
65 SPELL::EVISCERATE,
66 SPELL::BLACK_POWDER,
67 HERO::TRICKSTER::SPELL::COUP_DE_GRACE_DAMAGE_1,
68 HERO::TRICKSTER::SPELL::COUP_DE_GRACE_DAMAGE_2,
69 HERO::TRICKSTER::SPELL::COUP_DE_GRACE_DAMAGE_3,
70];
71static MID2_BACKSTAB_KIT: [u32; 1] = [SPELL::BACKSTAB];
72static MID2_SHURIKEN_KIT: [u32; 1] = [SPELL::SHURIKEN_STORM];
73static LINGERING_SHADOW_BASE_KIT: [u32; 2] = [SPELL::BACKSTAB, SPELL::SHADOWSTRIKE];
74static LINGERING_SHADOW_MID2_KIT: [u32; 4] = [
75 SPELL::BACKSTAB,
76 SPELL::SHADOWSTRIKE,
77 SPELL::EVISCERATE,
78 SPELL::BLACK_POWDER,
79];
80
81crate::hooks::define_spec_config! {
82 pub(super) struct SubtletyConfig {
84 sht_energy: f64 { provenance: "Baseline Shadow Techniques energize effect.", gate: true, source: data.require_effect_base_points(EFFECT::SHT_ENERGIZE)?, transform: |value| value },
85 shadowcraft: bool { provenance: "Shadowcraft selection or specialization grant enables its finisher rider.", gate: params.talent_ranked_or_specialization(TALENT::SHADOWCRAFT), source: true, transform: |value| value },
86 relentless_energy_per_cp: f64 { provenance: "Relentless Strikes energy returned per combo point.", gate: params.talent_ranked_or_specialization(TALENT::RELENTLESS_STRIKES), source: data.require_effect_base_points(EFFECT::RELENTLESS_STRIKES_ENERGY)?, transform: |value| value },
87 master_of_shadows: bool { provenance: "Master of Shadows selection or specialization grant enables periodic energy.", gate: params.talent_ranked_or_specialization(TALENT::MASTER_OF_SHADOWS), source: true, transform: |value| value },
88 lashe_coef: f64 { provenance: "Danse Macabre Lashe Macabre AP coefficient.", gate: params.talent_ranked_or_specialization(TALENT::DANSE_MACABRE), source: data.effect_ap_coefficient(EFFECT::LASHE_MACABRE_COEF), transform: |value| value },
89 shadow_blades_cp_pct: f64 { provenance: "Shadow Blades combo-point energize fraction.", gate: params.talent_ranked_or_specialization(TALENT::SHADOW_BLADES), source: effect_pct(data, EFFECT::SHADOW_BLADES_CP)?, transform: |value| value },
90 shadowed_finishers_pct: f64 { provenance: "Shadowed Finishers damage fraction.", gate: params.talent_ranked_or_specialization(TALENT::SHADOWED_FINISHERS), source: effect_pct(data, EFFECT::SHADOWED_FINISHERS_PCT)?, transform: |value| value },
91 find_weakness: bool { provenance: "Find Weakness enables physical armor penetration.", gate: params.talent_ranked_or_specialization(TALENT::FIND_WEAKNESS), source: true, transform: |value| value },
92 improved_backstab: bool { provenance: "Improved Backstab enables its positional replacement rider.", gate: params.talent_ranked_or_specialization(TALENT::IMPROVED_BACKSTAB), source: true, transform: |value| value },
93 clone_chance_per_stack: f64 { provenance: "Ancient Arts rank one clone chance plus Replicating Shadows.", gate: params.talent_ranks(TALENT::ANCIENT_ARTS_1) > 0, source: ancient_arts_clone_chance(params, data)?, transform: |value| value },
94 clone_grants_sht_chance: f64 { provenance: "Ancient Arts rank two stored-stack refund chance capped at one.", gate: params.talent_ranks(TALENT::ANCIENT_ARTS_2) > 0, source: effect_pct(data, EFFECT::ANCIENT_ARTS_2_SHT)? * f64::from(params.talent_ranks(TALENT::ANCIENT_ARTS_2)), transform: |value: f64| value.min(1.0) },
95 finisher_sht_consume: bool { provenance: "Ancient Arts rank three lets finishers release stored stacks.", gate: params.talent_ranks(TALENT::ANCIENT_ARTS_3) > 0, source: true, transform: |value| value },
96 premeditation_cp: f64 { provenance: "Premeditation effect combo-point grant.", gate: params.talent_ranked_or_specialization(TALENT::PREMEDITATION), source: data.require_effect_base_points(EFFECT::PREMEDITATION_CP)?, transform: |value| value },
97 st_clone_pet_mult: f64 { provenance: "Subtlety specialization aura pet-damage fraction for Secret Technique clones.", gate: true, source: effect_pct(data, EFFECT::SPEC_AURA_PET_DMG)?, transform: |value| 1.0 + value },
98 supercharger_charges: u32 { provenance: "Supercharger effect charges multiplied by selected ranks.", gate: params.talent_ranks(TALENT::SUPERCHARGER) > 0, source: data.require_effect_base_points(EFFECT::SUPERCHARGER_CHARGES)? * f64::from(params.talent_ranks(TALENT::SUPERCHARGER)), transform: wowlab_types::numeric::f64_to_u32_saturating_trunc },
99 supercharger_bonus_cp: f64 { provenance: "Supercharger bonus combo points plus Forced Induction.", gate: params.talent_ranks(TALENT::SUPERCHARGER) > 0, source: supercharger_bonus_cp(params, data)?, transform: |value| value },
100 alacrity_chance_per_cp: f64 { provenance: "Alacrity chance per combo point multiplied by selected ranks.", gate: params.talent_ranks(TALENT::ALACRITY) > 0, source: effect_pct(data, EFFECT::ALACRITY_CHANCE_PER_CP)? * f64::from(params.talent_ranks(TALENT::ALACRITY)), transform: |value| value },
101 unseen_blade: bool { provenance: "Trickster Unseen Blade enables its hero-tree strike.", gate: params.talent_ranked_or_specialization(HERO::TRICKSTER::SPELL::UNSEEN_BLADE_TALENT), source: true, transform: |value| value },
102 unseen_blade_coef: f64 { provenance: "Unseen Blade effect AP coefficient.", gate: params.talent_ranked_or_specialization(HERO::TRICKSTER::SPELL::UNSEEN_BLADE_TALENT), source: data.effect_ap_coefficient(EFFECT::UNSEEN_BLADE_COEF), transform: |value| value },
103 flashing_steel_chance: f64 { provenance: "Flashing Steel proc chance while Unseen Blade is selected.", gate: params.talent_ranked_or_specialization(HERO::TRICKSTER::SPELL::UNSEEN_BLADE_TALENT) && params.talent_ranked_or_specialization(HERO::TRICKSTER::SPELL::FLASHING_STEEL), source: effect_pct(data, EFFECT::FLASHING_STEEL_CHANCE)?, transform: |value| value },
104 flawless_form: bool { provenance: "Flawless Form enables finisher damage stacks.", gate: params.talent_ranked_or_specialization(HERO::TRICKSTER::SPELL::FLAWLESS_FORM), source: true, transform: |value| value },
105 coup_de_grace: bool { provenance: "Coup de Grace enables its finisher sequence.", gate: params.talent_ranked_or_specialization(HERO::TRICKSTER::SPELL::COUP_DE_GRACE), source: true, transform: |value| value },
106 cdg_bonus_cp: f64 { provenance: "Coup de Grace effect bonus combo points.", gate: params.talent_ranked_or_specialization(HERO::TRICKSTER::SPELL::COUP_DE_GRACE), source: data.require_effect_base_points(EFFECT::COUP_DE_GRACE_BONUS_CP)?, transform: |value| value },
107 cdg_flawless_stacks: i32 { provenance: "Coup de Grace effect Flawless Form stacks.", gate: params.talent_ranked_or_specialization(HERO::TRICKSTER::SPELL::COUP_DE_GRACE), source: data.require_effect_base_points(EFFECT::COUP_DE_GRACE_FLAWLESS)?, transform: wowlab_types::numeric::f64_to_i32_saturating_trunc },
108 disorienting_free_blades: i32 { provenance: "Disorienting Strikes effect free Unseen Blade count.", gate: params.talent_ranked_or_specialization(HERO::TRICKSTER::SPELL::DISORIENTING_STRIKES), source: data.require_effect_base_points(EFFECT::DISORIENTING_FREE_BLADES)?, transform: wowlab_types::numeric::f64_to_i32_saturating_trunc },
109 clever_combatant: bool { provenance: "Clever Combatant enables its Trickster rider.", gate: params.talent_ranked_or_specialization(HERO::TRICKSTER::SPELL::CLEVER_COMBATANT), source: true, transform: |value| value },
110 surprising_strikes_crit_pts: f64 { provenance: "Surprising Strikes critical-damage points while Fazed is active.", gate: params.talent_ranked_or_specialization(HERO::TRICKSTER::SPELL::SURPRISING_STRIKES), source: data.require_effect_base_points(EFFECT::SURPRISING_STRIKES_CRIT_DMG)?, transform: |value| value },
111 improved_secret_technique_mult: f64 { provenance: "Improved Secret Technique's passive damage multiplier.", gate: params.talent_ranked_or_specialization(TALENT::IMPROVED_SECRET_TECHNIQUE), source: effect_pct(data, EFFECT::IMPROVED_SECRET_TECHNIQUE_DMG)?, transform: |value| 1.0 + value },
112 umbral_edge_mult: f64 { provenance: "Umbral Edge's passive Shadowstrike damage multiplier.", gate: params.talent_ranked_or_specialization(TALENT::UMBRAL_EDGE), source: effect_pct(data, EFFECT::UMBRAL_EDGE_DMG)?, transform: |value| 1.0 + value },
113 mid2_backstab_mult: f64 { provenance: "MID2 Subtlety two-piece Backstab damage from 1296592 e2.", gate: has_mid2_2pc, source: data.require_effect_base_points(EFFECT::MID2_SUBTLETY_2PC_BACKSTAB_DAMAGE)?, transform: |value| 1.0 + value / HUNDRED, fallback: 1.0 },
114 mid2_shuriken_mult: f64 { provenance: "MID2 Subtlety two-piece Shuriken Storm damage from 1296592 e4.", gate: has_mid2_2pc, source: data.require_effect_base_points(EFFECT::MID2_SUBTLETY_2PC_SHURIKEN_DAMAGE)?, transform: |value| 1.0 + value / HUNDRED, fallback: 1.0 },
115 has_lingering_shadow: bool { provenance: "Lingering Shadow selection enables its decaying post-Dance damage window.", gate: params.talent_ranked_or_specialization(TALENT::LINGERING_SHADOW), source: true, transform: |value| value },
116 mid2_lingering_finishers: bool { provenance: "MID2 Subtlety four-piece extends Lingering Shadow to Eviscerate and Black Powder.", gate: has_mid2_4pc, source: true, transform: |value| value },
117 }
118 accessor pub(super) fn subtlety_config(ctx);
119 register pub(super) fn register_subtlety(built, params) -> Result<(), wowlab_engine_ports::EngineError>;
120 prepare {
121 let data = ¶ms.game_data;
122 let has_mid2_2pc = params.set_bonus_auras.contains(&SET_BONUS::MID2_SUBTLETY_2PC);
123 let has_mid2_4pc = params.set_bonus_auras.contains(&SET_BONUS::MID2_SUBTLETY_4PC);
124 }
125 auras {
126 { provenance: "Shadow Blades resolved-damage echo on the original target.", gate: params.talent_ranked_or_specialization(TALENT::SHADOW_BLADES), aura: AURA_SHADOW_BLADES, effect: BuffEffect::DamageReplication(DamageReplication { fraction: effect_pct(data, EFFECT::SHADOW_BLADES_ECHO)?, effect: DamageEffectRef::new(EFFECT::SHADOW_BLADES_ATTACK.0, EFFECT::SHADOW_BLADES_ATTACK.1), source: DamageSource::Player, source_spells: &SHADOW_BLADES_SOURCE_KIT, include_magic: true, include_indirect: true, include_primary: true, max_targets: 1, reduced_targets: 0 }) },
127 { provenance: "Shadow Blades also echoes qualifying companion-authored rogue clone impacts.", gate: params.talent_ranked_or_specialization(TALENT::SHADOW_BLADES), aura: AURA_SHADOW_BLADES, effect: BuffEffect::DamageReplication(DamageReplication { fraction: effect_pct(data, EFFECT::SHADOW_BLADES_ECHO)?, effect: DamageEffectRef::new(EFFECT::SHADOW_BLADES_ATTACK.0, EFFECT::SHADOW_BLADES_ATTACK.1), source: DamageSource::Pet, source_spells: &SHADOW_BLADES_SOURCE_KIT, include_magic: true, include_indirect: true, include_primary: true, max_targets: 1, reduced_targets: 0 }) },
128 }
129 wiring {
130 { provenance: "Deepening Shadows haste-scaled Shadow Dance duration.", gate: params.talent_ranked_or_specialization(TALENT::DEEPENING_SHADOWS), apply: {
131 let scale = effect_pct(data, EFFECT::DEEPENING_SHADOWS_DURATION)?;
132 let haste = built.base_stats().stats.haste / HUNDRED;
133 built.patch_aura(AURA_SHADOW_DANCE, |aura| {
134 aura.base_duration_ms = wowlab_types::numeric::f64_to_u32_saturating_trunc(f64::from(aura.base_duration_ms) * (1.0 + scale * haste));
135 });
136 } },
137 }
138 finish |cfg| {
139 wire_subtlety_effects(built, params, &cfg)?;
140 for spell_id in [SPELL::SHURIKEN_STORM, SPELL::SHURIKEN_STORM_SHADOW_CLONE] {
141 if let Some(local) = built.spell_local(spell_id) {
142 built.patch_spell(local, |spell| {
143 spell.requirements.exclude_caster_aura_spell = 0;
145 });
146 }
147 }
148 if has_mid2_2pc {
149 if let Some(local) = built.spell_local(SPELL::BACKSTAB) {
150 let adjustment = data.effect_base_points(EFFECT::MID2_SUBTLETY_2PC_BACKSTAB_COST);
151 built.patch_spell(local, |spell| spell.cost.resource_cost = (spell.cost.resource_cost + adjustment).max(0.0));
152 }
153 if let Some(local) = built.spell_local(SPELL::SHURIKEN_STORM) {
154 let adjustment = data.effect_base_points(EFFECT::MID2_SUBTLETY_2PC_SHURIKEN_COST);
155 built.patch_spell(local, |spell| {
156 spell.cost.resource_cost = (spell.cost.resource_cost + adjustment).max(0.0);
157 });
158 }
159 built.register_driver_spell_modifier(DriverSpellModifier::new(SET_BONUS::MID2_SUBTLETY_2PC, &MID2_BACKSTAB_KIT, mid2_backstab_modifier));
160 built.register_driver_spell_modifier(DriverSpellModifier::new(SET_BONUS::MID2_SUBTLETY_2PC, &MID2_SHURIKEN_KIT, mid2_shuriken_modifier));
161 }
162 if cfg.has_lingering_shadow {
163 let kit = if cfg.mid2_lingering_finishers { &LINGERING_SHADOW_MID2_KIT[..] } else { &LINGERING_SHADOW_BASE_KIT[..] };
164 built.set_aura_effect(
165 crate::generated::specs::subtlety_rogue::AURA_LINGERING_SHADOW,
166 BuffEffect::DamageMultSpellsLinear(0.01, kit),
167 );
168 }
169 if cfg.improved_secret_technique_mult > 1.0 {
170 built.register_driver_spell_modifier(DriverSpellModifier::new(
171 TALENT::IMPROVED_SECRET_TECHNIQUE,
172 &SECRET_TECHNIQUE_KIT,
173 improved_secret_technique_modifier,
174 ));
175 }
176 if cfg.umbral_edge_mult > 1.0 {
177 built.register_driver_spell_modifier(DriverSpellModifier::new(
178 TALENT::UMBRAL_EDGE,
179 &SHADOWSTRIKE_KIT,
180 umbral_edge_modifier,
181 ));
182 }
183 built.set_spec_config(cfg);
184 Ok(())
185 }
186}
187
188fn mid2_backstab_modifier(view: wowlab_engine_combat::HookView<'_>) -> DriverSpellModifierResult {
189 DriverSpellModifierResult::damage_mult(
190 view.program()
191 .spec_config::<SubtletyConfig>()
192 .map_or(1.0, |cfg| cfg.mid2_backstab_mult),
193 )
194}
195
196fn mid2_shuriken_modifier(view: wowlab_engine_combat::HookView<'_>) -> DriverSpellModifierResult {
197 DriverSpellModifierResult::damage_mult(
198 view.program()
199 .spec_config::<SubtletyConfig>()
200 .map_or(1.0, |cfg| cfg.mid2_shuriken_mult),
201 )
202}
203
204fn improved_secret_technique_modifier(
205 view: wowlab_engine_combat::HookView<'_>,
206) -> DriverSpellModifierResult {
207 DriverSpellModifierResult::damage_mult(
208 view.program()
209 .spec_config::<SubtletyConfig>()
210 .map_or(1.0, |cfg| cfg.improved_secret_technique_mult),
211 )
212}
213
214fn umbral_edge_modifier(view: wowlab_engine_combat::HookView<'_>) -> DriverSpellModifierResult {
215 DriverSpellModifierResult::damage_mult(
216 view.program()
217 .spec_config::<SubtletyConfig>()
218 .map_or(1.0, |cfg| cfg.umbral_edge_mult),
219 )
220}
221
222fn effect_pct(
223 data: &ResolvedGameData,
224 eff: (u32, u8),
225) -> Result<f64, wowlab_engine_ports::EngineError> {
226 Ok(data.require_base_points(SpellIdx::from_raw(eff.0), eff.1)? / HUNDRED)
227}
228
229fn ancient_arts_clone_chance(
230 params: &HandlerParams<'_>,
231 data: &ResolvedGameData,
232) -> Result<f64, wowlab_engine_ports::EngineError> {
233 let ranks = f64::from(params.talent_ranks(TALENT::ANCIENT_ARTS_1));
234 let chance = effect_pct(data, EFFECT::ANCIENT_ARTS_1_CLONE)? * ranks
235 + try_gated(
236 params.talent_ranked_or_specialization(TALENT::REPLICATING_SHADOWS),
237 || effect_pct(data, EFFECT::REPLICATING_SHADOWS_CLONE),
238 )?;
239
240 Ok(chance)
241}
242
243fn supercharger_bonus_cp(
244 params: &HandlerParams<'_>,
245 data: &ResolvedGameData,
246) -> Result<f64, wowlab_engine_ports::EngineError> {
247 let bonus = data.require_effect_base_points(EFFECT::SUPERCHARGER_BONUS_CP)?
248 + try_gated(
249 params.talent_ranked_or_specialization(TALENT::FORCED_INDUCTION),
250 || data.require_effect_base_points(EFFECT::FORCED_INDUCTION_BONUS),
251 )?;
252
253 Ok(bonus)
254}
255
256fn wire_passive_damage_effects(
257 built: &mut CombatSystemDraft,
258 cfg: &SubtletyConfig,
259 data: &ResolvedGameData,
260) -> Result<(), wowlab_engine_ports::EngineError> {
261 if cfg.find_weakness {
262 let pen = effect_pct(data, EFFECT::FIND_WEAKNESS_ARMOR_PEN)?;
263 let armor = data.creature_armor();
264 let armor_k = data.armor_k();
265
266 if armor > 0.0 && armor_k > 0.0 {
267 let mult =
268 armor_mitigation(armor * (1.0 - pen), armor_k) / armor_mitigation(armor, armor_k);
269
270 built.set_aura_effect(
271 AURA_FIND_WEAKNESS,
272 BuffEffect::DamageMultSpells(mult, &PHYSICAL_KIT),
273 );
274 }
275 }
276
277 Ok(())
278}
279
280fn wire_timing_effects(
281 built: &mut CombatSystemDraft,
282 cfg: &SubtletyConfig,
283 data: &ResolvedGameData,
284) {
285 if cfg.disorienting_free_blades > 0 {
286 let cdr = data.effect_base_points(EFFECT::DISORIENTING_ST_CDR) / HUNDRED;
288
289 if let Some(local) = built.spell_local(SPELL::SECRET_TECHNIQUE) {
290 built.patch_spell(local, |spell| {
291 let scaled = f64::from(spell.cooldown.cooldown_duration_ms) * (1.0 + cdr);
292
293 spell.cooldown.cooldown_duration_ms =
294 wowlab_types::numeric::f64_to_u32_saturating_trunc(scaled.max(0.0));
295 });
296 }
297 }
298
299 if cfg.flashing_steel_chance > 0.0 {
300 let delta_ms = data.effect_base_points(EFFECT::FLASHING_STEEL_WINDOW);
302
303 built.patch_aura(AURA_UNSEEN_BLADE_CD, |aura| {
304 let new_ms = f64::from(aura.base_duration_ms) + delta_ms;
305
306 aura.base_duration_ms =
307 wowlab_types::numeric::f64_to_u32_saturating_trunc(new_ms.max(0.0));
308 });
309 }
310}
311
312fn wire_subtlety_effects(
313 built: &mut CombatSystemDraft,
314 params: &HandlerParams<'_>,
315 cfg: &SubtletyConfig,
316) -> Result<(), wowlab_engine_ports::EngineError> {
317 let data = ¶ms.game_data;
318
319 wire_passive_damage_effects(built, cfg, data)?;
320 wire_timing_effects(built, cfg, data);
321
322 Ok(())
323}
324
325#[must_use]
327pub(crate) fn mastery(ctx: &MasteryCtx<'_>) -> f64 {
328 let effect = if ctx.is_periodic {
329 EFFECT::EXECUTIONER_PERIODIC.1
330 } else {
331 EFFECT::EXECUTIONER_DIRECT.1
332 };
333
334 if ctx.affects_spell(effect) {
335 ctx.bonus(effect)
336 } else {
337 1.0
338 }
339}
340
341#[must_use]
343pub(crate) fn crit_damage(ctx: &MasteryCtx<'_>) -> f64 {
344 let Some(cfg) = ctx.program.spec_config::<SubtletyConfig>() else {
345 return 0.0;
346 };
347 let mut pts = 0.0;
348
349 if cfg.surprising_strikes_crit_pts > 0.0 && ctx.is_aura_active(AURA_FAZED) {
350 pts += cfg.surprising_strikes_crit_pts;
351 }
352
353 pts
354}