Skip to main content

wowlab_engine_content/hooks/subtlety_rogue/
config.rs

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/// Rogue-attack impacts that trigger Shadow Blades' residual hit.
17#[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/// Improved Secret Technique's +15% (1279444 e1) applies to the whole ST family.
48#[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
55/// Umbral Edge's +15% (1281468 e1) applies to Shadowstrike and its clone.
56static SHADOWSTRIKE_KIT: [u32; 2] = [SPELL::SHADOWSTRIKE, SPELL::SHADOWSTRIKE_SHADOW_CLONE];
57
58/// Physical-school subset for the Find Weakness armor-penetration multiplier.
59#[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    /// Talent-gated Subtlety hook parameters.
83    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 = &params.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                    // Killing Spree is Outlaw-only; 12.1 retains this unreachable exclusion.
144                    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        // Negative value (-10); require_base_points rejects non-positive data.
287        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        // Negative value (-5000 ms); require_base_points rejects non-positive data.
301        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 = &params.game_data;
318
319    wire_passive_damage_effects(built, cfg, data)?;
320    wire_timing_effects(built, cfg, data);
321
322    Ok(())
323}
324
325/// Applies Executioner mastery (spell 76808).
326#[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/// Surprising Strikes crit-damage bonus applies only while the target is Fazed.
342#[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}