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#[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#[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
58static SHADOWSTRIKE_KIT: [u32; 2] = [SPELL::SHADOWSTRIKE, SPELL::SHADOWSTRIKE_SHADOW_CLONE];
60
61#[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 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 = ¶ms.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 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 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 = ¶ms.game_data;
259
260 wire_passive_damage_effects(built, cfg, data)?;
261 wire_timing_effects(built, cfg, data);
262
263 Ok(())
264}
265
266#[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#[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}