Skip to main content

wowlab_engine_content/hooks/subtlety_rogue/
config.rs

1use wowlab_engine_combat::{
2    BuffEffect, BuiltCombatSystem, DamageEffectRef, DamageReplication, DamageSource,
3    DriverSpellModifier, DriverSpellModifierResult, MasteryCtx,
4};
5use wowlab_engine_domain::{
6    damage::armor_mitigation,
7    dbc::ResolvedGameDataEffectExt as _,
8};
9use wowlab_engine_ports::{HandlerParams};
10use wowlab_engine_gamedata::{ResolvedGameData};
11use wowlab_types::{constants::HUNDRED, sim::SpellIdx};
12
13use crate::generated::specs::subtlety_rogue::{
14    AURA_FIND_WEAKNESS, AURA_SHADOW_BLADES, AURA_SHADOW_DANCE, AURA_UNSEEN_BLADE_CD, EFFECT, HERO,
15    REPORTED_SPELL, SPELL, TALENT,
16};
17use crate::hooks::try_gated;
18
19/// Rogue-attack impacts that trigger Shadow Blades' residual hit.
20#[rustfmt::skip]
21static SHADOW_BLADES_SOURCE_KIT: [u32; 26] = [
22    REPORTED_SPELL::MELEE,
23    SPELL::BACKSTAB,
24    SPELL::SHADOWSTRIKE,
25    SPELL::SHURIKEN_STORM,
26    SPELL::SHURIKEN_TOSS,
27    SPELL::EVISCERATE,
28    SPELL::BLACK_POWDER,
29    SPELL::INSTANT_POISON_DAMAGE,
30    HERO::TRICKSTER::SPELL::UNSEEN_BLADE,
31    HERO::TRICKSTER::SPELL::COUP_DE_GRACE_DAMAGE_1,
32    HERO::TRICKSTER::SPELL::COUP_DE_GRACE_DAMAGE_2,
33    HERO::TRICKSTER::SPELL::COUP_DE_GRACE_DAMAGE_3,
34    REPORTED_SPELL::CDG_BONUS_1,
35    REPORTED_SPELL::CDG_BONUS_2,
36    REPORTED_SPELL::CDG_BONUS_3,
37    SPELL::EVISCERATE_BONUS,
38    SPELL::BLACK_POWDER_BONUS,
39    SPELL::SECRET_TECHNIQUE_PLAYER,
40    SPELL::SECRET_TECHNIQUE_CLONES,
41    SPELL::LASHE_MACABRE,
42    SPELL::BACKSTAB_SHADOW_CLONE,
43    SPELL::SHADOWSTRIKE_SHADOW_CLONE,
44    SPELL::EVISCERATE_SHADOW_CLONE,
45    SPELL::SHURIKEN_STORM_SHADOW_CLONE,
46    SPELL::SECRET_TECHNIQUE_SHADOW_CLONE,
47    SPELL::BLACK_POWDER_SHADOW_CLONE,
48];
49
50/// Improved Secret Technique's +15% (1279444 e1) applies to the whole ST family.
51#[rustfmt::skip]
52static SECRET_TECHNIQUE_KIT: [u32; 3] = [
53    SPELL::SECRET_TECHNIQUE_PLAYER,
54    SPELL::SECRET_TECHNIQUE_CLONES,
55    SPELL::SECRET_TECHNIQUE_SHADOW_CLONE,
56];
57
58/// Umbral Edge's +15% (1281468 e1) applies to Shadowstrike and its clone.
59static SHADOWSTRIKE_KIT: [u32; 2] = [SPELL::SHADOWSTRIKE, SPELL::SHADOWSTRIKE_SHADOW_CLONE];
60
61/// Physical-school subset for the Find Weakness armor-penetration multiplier.
62#[rustfmt::skip]
63static PHYSICAL_KIT: [u32; 9] = [
64    SPELL::BACKSTAB,
65    SPELL::SHADOWSTRIKE,
66    SPELL::SHURIKEN_STORM,
67    SPELL::SHURIKEN_TOSS,
68    SPELL::EVISCERATE,
69    SPELL::BLACK_POWDER,
70    HERO::TRICKSTER::SPELL::COUP_DE_GRACE_DAMAGE_1,
71    HERO::TRICKSTER::SPELL::COUP_DE_GRACE_DAMAGE_2,
72    HERO::TRICKSTER::SPELL::COUP_DE_GRACE_DAMAGE_3,
73];
74
75crate::hooks::define_spec_config! {
76    /// Talent-gated Subtlety hook parameters.
77    pub(super) struct SubtletyConfig {
78        sht_energy: f64 { provenance: "Baseline Shadow Techniques energize effect.", gate: true, source: data.require_effect_base_points(EFFECT::SHT_ENERGIZE)?, transform: |value| value },
79        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 },
80        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 },
81        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 },
82        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 },
83        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 },
84        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 },
85        find_weakness: bool { provenance: "Find Weakness enables physical armor penetration.", gate: params.talent_ranked_or_specialization(TALENT::FIND_WEAKNESS), source: true, transform: |value| value },
86        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 },
87        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 },
88        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) },
89        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 },
90        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 },
91        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 },
92        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 },
93        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 },
94        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 },
95        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 },
96        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 },
97        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 },
98        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 },
99        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 },
100        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 },
101        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 },
102        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 },
103        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 },
104        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 },
105        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 },
106        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 },
107    }
108    accessor pub(super) fn subtlety_config(ctx);
109    register pub(super) fn register_subtlety(built, params) -> Result<(), wowlab_engine_ports::EngineError>;
110    prepare { let data = &params.game_data; }
111    auras {
112        { 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 }) },
113        { 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 }) },
114    }
115    wiring {
116        { provenance: "Deepening Shadows haste-scaled Shadow Dance duration.", gate: params.talent_ranked_or_specialization(TALENT::DEEPENING_SHADOWS), apply: {
117            let scale = effect_pct(data, EFFECT::DEEPENING_SHADOWS_DURATION)?;
118            let haste = built.state.config.base_stats.stats.haste / HUNDRED;
119            built.patch_aura(AURA_SHADOW_DANCE, |aura| {
120                aura.base_duration_ms = wowlab_types::numeric::f64_to_u32_saturating_trunc(f64::from(aura.base_duration_ms) * (1.0 + scale * haste));
121            });
122        } },
123    }
124    finish |cfg| {
125        wire_subtlety_effects(built, params, &cfg)?;
126        if cfg.improved_secret_technique_mult > 1.0 {
127            built.register_driver_spell_modifier(DriverSpellModifier::new(
128                TALENT::IMPROVED_SECRET_TECHNIQUE,
129                &SECRET_TECHNIQUE_KIT,
130                improved_secret_technique_modifier,
131            ));
132        }
133        if cfg.umbral_edge_mult > 1.0 {
134            built.register_driver_spell_modifier(DriverSpellModifier::new(
135                TALENT::UMBRAL_EDGE,
136                &SHADOWSTRIKE_KIT,
137                umbral_edge_modifier,
138            ));
139        }
140        built.state.set_spec_config(cfg);
141        Ok(())
142    }
143}
144
145fn improved_secret_technique_modifier(
146    view: wowlab_engine_combat::HookView<'_>,
147) -> DriverSpellModifierResult {
148    DriverSpellModifierResult::damage_mult(
149        view.state
150            .spec_config::<SubtletyConfig>()
151            .map_or(1.0, |cfg| cfg.improved_secret_technique_mult),
152    )
153}
154
155fn umbral_edge_modifier(view: wowlab_engine_combat::HookView<'_>) -> DriverSpellModifierResult {
156    DriverSpellModifierResult::damage_mult(
157        view.state
158            .spec_config::<SubtletyConfig>()
159            .map_or(1.0, |cfg| cfg.umbral_edge_mult),
160    )
161}
162
163fn effect_pct(
164    data: &ResolvedGameData,
165    eff: (u32, u8),
166) -> Result<f64, wowlab_engine_ports::EngineError> {
167    Ok(data.require_base_points(SpellIdx::from_raw(eff.0), eff.1)? / HUNDRED)
168}
169
170fn ancient_arts_clone_chance(
171    params: &HandlerParams<'_>,
172    data: &ResolvedGameData,
173) -> Result<f64, wowlab_engine_ports::EngineError> {
174    let ranks = f64::from(params.talent_ranks(TALENT::ANCIENT_ARTS_1));
175    let chance = effect_pct(data, EFFECT::ANCIENT_ARTS_1_CLONE)? * ranks
176        + try_gated(
177            params.talent_ranked_or_specialization(TALENT::REPLICATING_SHADOWS),
178            || effect_pct(data, EFFECT::REPLICATING_SHADOWS_CLONE),
179        )?;
180
181    Ok(chance)
182}
183
184fn supercharger_bonus_cp(
185    params: &HandlerParams<'_>,
186    data: &ResolvedGameData,
187) -> Result<f64, wowlab_engine_ports::EngineError> {
188    let bonus = data.require_effect_base_points(EFFECT::SUPERCHARGER_BONUS_CP)?
189        + try_gated(
190            params.talent_ranked_or_specialization(TALENT::FORCED_INDUCTION),
191            || data.require_effect_base_points(EFFECT::FORCED_INDUCTION_BONUS),
192        )?;
193
194    Ok(bonus)
195}
196
197fn wire_passive_damage_effects(
198    built: &mut BuiltCombatSystem,
199    cfg: &SubtletyConfig,
200    data: &ResolvedGameData,
201) -> Result<(), wowlab_engine_ports::EngineError> {
202    if cfg.find_weakness {
203        let pen = effect_pct(data, EFFECT::FIND_WEAKNESS_ARMOR_PEN)?;
204        let armor = data.creature_armor();
205        let armor_k = data.armor_k();
206
207        if armor > 0.0 && armor_k > 0.0 {
208            let mult =
209                armor_mitigation(armor * (1.0 - pen), armor_k) / armor_mitigation(armor, armor_k);
210
211            built.set_aura_effect(
212                AURA_FIND_WEAKNESS,
213                BuffEffect::DamageMultSpells(mult, &PHYSICAL_KIT),
214            );
215        }
216    }
217
218    Ok(())
219}
220
221fn wire_timing_effects(
222    built: &mut BuiltCombatSystem,
223    cfg: &SubtletyConfig,
224    data: &ResolvedGameData,
225) {
226    if cfg.disorienting_free_blades > 0 {
227        // Negative value (-10); require_base_points rejects non-positive data.
228        let cdr = data.effect_base_points(EFFECT::DISORIENTING_ST_CDR) / HUNDRED;
229
230        if let Some((local, _)) = built.state.spell_data(SPELL::SECRET_TECHNIQUE) {
231            built.patch_spell(local, |spell| {
232                let scaled = f64::from(spell.cooldown.cooldown_duration_ms) * (1.0 + cdr);
233
234                spell.cooldown.cooldown_duration_ms =
235                    wowlab_types::numeric::f64_to_u32_saturating_trunc(scaled.max(0.0));
236            });
237        }
238    }
239
240    if cfg.flashing_steel_chance > 0.0 {
241        // Negative value (-5000 ms); require_base_points rejects non-positive data.
242        let delta_ms = data.effect_base_points(EFFECT::FLASHING_STEEL_WINDOW);
243
244        built.patch_aura(AURA_UNSEEN_BLADE_CD, |aura| {
245            let new_ms = f64::from(aura.base_duration_ms) + delta_ms;
246
247            aura.base_duration_ms =
248                wowlab_types::numeric::f64_to_u32_saturating_trunc(new_ms.max(0.0));
249        });
250    }
251}
252
253fn wire_subtlety_effects(
254    built: &mut BuiltCombatSystem,
255    params: &HandlerParams<'_>,
256    cfg: &SubtletyConfig,
257) -> Result<(), wowlab_engine_ports::EngineError> {
258    let data = &params.game_data;
259
260    wire_passive_damage_effects(built, cfg, data)?;
261    wire_timing_effects(built, cfg, data);
262
263    Ok(())
264}
265
266/// Applies Executioner mastery (spell 76808).
267#[must_use]
268pub(crate) fn mastery(ctx: &MasteryCtx<'_>) -> f64 {
269    let effect = if ctx.is_periodic {
270        EFFECT::EXECUTIONER_PERIODIC.1
271    } else {
272        EFFECT::EXECUTIONER_DIRECT.1
273    };
274
275    if ctx.affects_spell(effect) {
276        ctx.bonus(effect)
277    } else {
278        1.0
279    }
280}
281
282/// Surprising Strikes crit-damage bonus applies only while the target is Fazed.
283#[must_use]
284pub(crate) fn crit_damage(ctx: &MasteryCtx<'_>) -> f64 {
285    let Some(cfg) = ctx.state.spec_config::<SubtletyConfig>() else {
286        return 0.0;
287    };
288    let mut pts = 0.0;
289
290    if cfg.surprising_strikes_crit_pts > 0.0
291        && ctx.is_aura_active_id(crate::generated::specs::subtlety_rogue::AURA::FAZED)
292    {
293        pts += cfg.surprising_strikes_crit_pts;
294    }
295
296    pts
297}