Skip to main content

wowlab_engine_content/hooks/assassination_rogue/
config.rs

1use wowlab_engine_combat::{
2    BuffEffect, DriverSpellModifier, DriverSpellModifierResult, MasteryCtx, PeriodicDriver,
3    RuntimeDamageDef,
4};
5use wowlab_engine_domain::dbc::ResolvedGameDataEffectExt as _;
6use wowlab_types::constants::HUNDRED;
7
8use crate::generated::specs::assassination_rogue::{
9    AURA_AMPLIFYING_POISON_DEBUFF, AURA_ATROPHIC_POISON, AURA_CRIPPLING_POISON,
10    AURA_DEADLY_POISON_DOT, AURA_DEATHMARK, AURA_FATEBOUND_COIN_HEADS, AURA_GARROTE,
11    AURA_INTERNAL_BLEEDING, AURA_KINGSBANE, AURA_RUPTURE, EFFECT, HERO, REPORTED_SPELL, SET_BONUS,
12    SPELL, SPELL_AMBUSH, SPELL_DEATHMARK, SPELL_ENVENOM, SPELL_GARROTE, SPELL_RUPTURE, TALENT,
13};
14#[rustfmt::skip]
15static LETHAL_DOSE_SPELLS: [u32; 10] = [
16    SPELL::GARROTE,
17    SPELL::RUPTURE,
18    REPORTED_SPELL::DEADLY_POISON_DOT,
19    SPELL::DEADLY_POISON_INSTANT,
20    SPELL::DEATHMARK,
21    SPELL::INTERNAL_BLEEDING_DRIVER,
22    REPORTED_SPELL::AMPLIFYING_POISON,
23    SPELL::KINGSBANE,
24    SPELL::MUTILATED_FLESH,
25    SPELL::IMPLACABLE_STRIKES_NATURE,
26];
27
28#[rustfmt::skip]
29static ZOLDYCK_RECIPE_SPELLS: [u32; 14] = [
30    SPELL::GARROTE,
31    SPELL::RUPTURE,
32    REPORTED_SPELL::DEADLY_POISON_DOT,
33    SPELL::AMBUSH,
34    SPELL::MUTILATE_MH,
35    SPELL::MUTILATE_OH,
36    SPELL::ENVENOM,
37    SPELL::DEADLY_POISON_INSTANT,
38    SPELL::DEATHMARK,
39    SPELL::INTERNAL_BLEEDING_DRIVER,
40    REPORTED_SPELL::AMPLIFYING_POISON,
41    SPELL::KINGSBANE,
42    SPELL::MUTILATED_FLESH,
43    SPELL::IMPLACABLE_STRIKES_NATURE,
44];
45
46static MID2_ENVENOM_SPELLS: [u32; 1] = [SPELL::ENVENOM];
47static MID2_BLEED_SPELLS: [u32; 3] = [
48    SPELL::GARROTE,
49    SPELL::RUPTURE,
50    SPELL::INTERNAL_BLEEDING_DRIVER,
51];
52
53crate::hooks::define_spec_config! {
54/// Talent-gated Assassination hook parameters.
55pub(super) struct AssassinationConfig {
56    seal_fate_chance: f64 { provenance: "Seal Fate chance from 14190 e1.", gate: params.talent_selected(TALENT::SEAL_FATE), source: params.require_effect_base_points(EFFECT::SEAL_FATE_CHANCE)?, transform: |value| value / HUNDRED, },
57    seal_fate_cp: f64 { provenance: "Seal Fate combo points from 14190 e2.", gate: params.talent_selected(TALENT::SEAL_FATE), source: params.require_effect_base_points(EFFECT::SEAL_FATE_CP)?, transform: |value| value, },
58    deal_fate_cp: f64 { provenance: "Deal Fate combo points from its manifest effect binding.", gate: params.talent_selected(HERO::FATEBOUND::SPELL::DEAL_FATE), source: params.require_effect_base_points(EFFECT::DEAL_FATE_CP)?, transform: |value| value, },
59    blindside_chance: f64 { provenance: "Blindside base chance 328085 e1 above the execute threshold.", gate: params.talent_selected(TALENT::BLINDSIDE), source: params.require_effect_base_points(EFFECT::BLINDSIDE_CHANCE)?, transform: |value| value / HUNDRED, },
60    blindside_execute_chance: f64 { provenance: "Blindside chance 328085 e2 below its execute threshold.", gate: params.talent_selected(TALENT::BLINDSIDE), source: params.require_effect_base_points(EFFECT::BLINDSIDE_EXECUTE_CHANCE)?, transform: |value| value / HUNDRED, },
61    blindside_execute_threshold: f64 { provenance: "Blindside execute threshold from 328085 e3.", gate: params.talent_selected(TALENT::BLINDSIDE), source: params.require_effect_base_points(EFFECT::BLINDSIDE_EXECUTE_THRESHOLD)?, transform: |value| value / HUNDRED, },
62    doomblade_pct: f64 { provenance: "Doomblade 1217954 damage fraction.", gate: params.talent_selected(TALENT::DOOMBLADE), source: params.require_effect_base_points(EFFECT::DOOMBLADE_PCT)?, transform: |value| value / HUNDRED, },
63    systemic_failure_mult: f64 { provenance: "Systemic Failure multiplicative damage modifier.", gate: params.talent_selected(TALENT::SYSTEMIC_FAILURE), source: params.require_effect_base_points(EFFECT::SYSTEMIC_FAILURE_PCT)?, transform: |value| 1.0 + value / HUNDRED, fallback: 1.0, },
64    lethal_dose_per_effect: f64 { provenance: "Lethal Dose damage per active bleed or poison; resolved talent data already includes selected ranks.", gate: params.talent_ranks(TALENT::LETHAL_DOSE) > 0, source: params.require_effect_base_points(EFFECT::LETHAL_DOSE_PER_EFFECT)?, transform: |value| value / HUNDRED, },
65    zoldyck_mult: f64 { provenance: "Zoldyck Recipe execute-range damage multiplier.", gate: params.talent_selected(TALENT::ZOLDYCK_RECIPE), source: params.require_effect_base_points(EFFECT::ZOLDYCK_DAMAGE)?, transform: |value| 1.0 + value / HUNDRED, fallback: 1.0, },
66    zoldyck_threshold: f64 { provenance: "Zoldyck Recipe target-health threshold.", gate: params.talent_selected(TALENT::ZOLDYCK_RECIPE), source: params.require_effect_base_points(EFFECT::ZOLDYCK_THRESHOLD)?, transform: |value| value / HUNDRED, },
67    delivered_doom_mult: f64 { provenance: "Delivered Doom Envenom damage multiplier.", gate: params.talent_selected(HERO::FATEBOUND::SPELL::DELIVERED_DOOM_TALENT), source: params.require_effect_base_points(EFFECT::DELIVERED_DOOM_DMG)?, transform: |value| 1.0 + value / HUNDRED, fallback: 1.0, },
68    delivered_doom_cp: f64 { provenance: "Delivered Doom combo-point threshold.", gate: params.talent_selected(HERO::FATEBOUND::SPELL::DELIVERED_DOOM_TALENT), source: params.require_effect_base_points(EFFECT::DELIVERED_DOOM_CP)?, transform: |value| value, },
69    inspiring_strike_mult: f64 { provenance: "Inspiring Strike damage multiplier while refreshed Envenom is active.", gate: params.talent_selected(TALENT::INSPIRING_STRIKE), source: params.require_effect_base_points(EFFECT::INSPIRING_STRIKE_DAMAGE)?, transform: |value| 1.0 + value / HUNDRED, fallback: 1.0, },
70    regicides_reward_divisor: i32 { provenance: "Kingsbane ramp stacks required per Regicide's Reward haste stack.", gate: params.talent_selected(TALENT::REGICIDES_REWARD), source: params.require_effect_base_points(EFFECT::REGICIDES_REWARD_DIVISOR)?, transform: wowlab_types::numeric::f64_to_i32_saturating_trunc, },
71    mid2_envenom_mult: f64 { provenance: "MID2 Assassination two-piece Envenom multiplier from 1296590 e1.", gate: has_2pc, source: params.require_effect_base_points(EFFECT::MID2_ASSASSINATION_2PC_ENVENOM)?, transform: |value| 1.0 + value / HUNDRED, fallback: 1.0, },
72    mid2_bleed_mult: f64 { provenance: "MID2 Assassination four-piece bleed multiplier from 1296591 e1.", gate: has_4pc, source: params.require_effect_base_points(EFFECT::MID2_ASSASSINATION_4PC_BLEED)?, transform: |value| 1.0 + value / HUNDRED, fallback: 1.0, },
73    mid2_4pc: bool { provenance: "MID2 Assassination four-piece enables poison-specific riders.", gate: has_4pc, source: true, transform: |value| value },
74    has_internal_bleeding: bool { provenance: "Internal Bleeding selection gate.", gate: params.talent_selected(TALENT::INTERNAL_BLEEDING), source: true, transform: |value| value, },
75    has_deadly_poison: bool { provenance: "Deadly Poison selection gate.", gate: params.talent_selected(TALENT::DEADLY_POISON), source: true, transform: |value| value, },
76    has_amplifying_poison: bool { provenance: "Amplifying Poison selection gate.", gate: params.talent_selected(TALENT::AMPLIFYING_POISON), source: true, transform: |value| value, },
77    envenom_poison_bonus: f64 { provenance: "Envenom 32645 e2 poison chance bonus.", gate: true, source: params.require_effect_base_points(EFFECT::ENVENOM_POISON_BONUS)?, transform: |value| value / HUNDRED, },
78    rapid_injection_mult: f64 { provenance: "Rapid Injection per-rank damage multiplier.", gate: params.talent_ranks(TALENT::RAPID_INJECTION) > 0, source: params.require_effect_base_points(EFFECT::RAPID_INJECTION_PCT)?, transform: |value| 1.0 + value / HUNDRED * f64::from(params.talent_ranks(TALENT::RAPID_INJECTION)), fallback: 1.0, },
79    amplifying_consume_stacks: i32 { provenance: "Amplifying Poison stacks consumed by Envenom.", gate: params.talent_selected(TALENT::AMPLIFYING_POISON), source: params.require_effect_base_points(EFFECT::AMPLIFYING_CONSUME_STACKS)?, transform: wowlab_types::numeric::f64_to_i32_saturating_trunc, },
80    amplifying_bonus_mult: f64 { provenance: "Amplifying Poison Envenom bonus multiplier.", gate: params.talent_selected(TALENT::AMPLIFYING_POISON), source: params.require_effect_base_points(EFFECT::AMPLIFYING_ENVENOM_BONUS)?, transform: |value| 1.0 + value / HUNDRED, },
81    alacrity_chance_per_cp: f64 { provenance: "Alacrity chance per combo point.", gate: params.talent_selected(TALENT::ALACRITY), source: params.require_effect_base_points(EFFECT::ALACRITY_CHANCE_PER_CP)?, transform: |value| value / HUNDRED, },
82    dashing_scoundrel_energy: f64 { provenance: "Dashing Scoundrel energy gain.", gate: params.talent_selected(TALENT::DASHING_SCOUNDREL), source: params.require_effect_base_points(EFFECT::DASHING_SCOUNDREL_ENERGY)?, transform: |value| value, },
83    has_implacable_regen: bool { provenance: "Implacable rank-one regeneration gate.", gate: params.talent_selected(TALENT::IMPLACABLE_1), source: true, transform: |value| value, },
84    has_implacable_strikes: bool { provenance: "Implacable rank-three strike gate.", gate: params.talent_selected(TALENT::IMPLACABLE_3), source: true, transform: |value| value, },
85    implacable_strike_cp: f64 { provenance: "Implacable strike combo-point gain.", gate: params.talent_selected(TALENT::IMPLACABLE_3), source: params.require_effect_base_points(EFFECT::IMPLACABLE_STRIKE_CP)?, transform: |value| value, },
86    has_finish_the_job: bool { provenance: "Finish the Job selection gate.", gate: params.talent_selected(TALENT::FINISH_THE_JOB), source: true, transform: |value| value, },
87    hand_of_fate_cp: f64 { provenance: "Hand of Fate combo-point gain.", gate: hand_of_fate, source: params.require_effect_base_points(EFFECT::HAND_OF_FATE_CP)?, transform: |value| value, },
88    mean_streak_odds: f64 { provenance: "Mean Streak matching-face odds modifier.", gate: hand_of_fate && params.talent_selected(HERO::FATEBOUND::SPELL::MEAN_STREAK), source: params.require_effect_base_points(EFFECT::MEAN_STREAK_ODDS)?, transform: |value| value / HUNDRED, },
89    lucky_coin_flips: i32 { provenance: "Lucky Coin extra flips.", gate: hand_of_fate && params.talent_selected(HERO::FATEBOUND::SPELL::LUCKY_COIN), source: params.require_effect_base_points(EFFECT::LUCKY_COIN_FLIPS)?, transform: wowlab_types::numeric::f64_to_i32_saturating_trunc, },
90    lucky_coin_odds: f64 { provenance: "Lucky Coin proc odds.", gate: hand_of_fate && params.talent_selected(HERO::FATEBOUND::SPELL::LUCKY_COIN), source: params.require_effect_base_points(EFFECT::LUCKY_COIN_ODDS)?, transform: |value| value / HUNDRED, },
91    has_edge_case: bool { provenance: "Edge Case selection gate.", gate: hand_of_fate && params.talent_selected(HERO::FATEBOUND::SPELL::EDGE_CASE), source: true, transform: |value| value, },
92    rush_energy: f64 { provenance: "Rush to the Inevitable energy gain.", gate: hand_of_fate && params.talent_selected(HERO::FATEBOUND::SPELL::RUSH_TO_THE_INEVITABLE), source: params.require_effect_base_points(EFFECT::RUSH_ENERGY)?, transform: |value| value, },
93    rush_energy_edge: f64 { provenance: "Rush to the Inevitable Edge Case energy gain.", gate: hand_of_fate && params.talent_selected(HERO::FATEBOUND::SPELL::RUSH_TO_THE_INEVITABLE), source: params.require_effect_base_points(EFFECT::RUSH_ENERGY_EDGE)?, transform: |value| value, },
94    overflowing_extra_coins: u32 { provenance: "Overflowing Purse coin count beyond the baseline flip.", gate: hand_of_fate && params.talent_selected(HERO::FATEBOUND::SPELL::OVERFLOWING_PURSE), source: params.require_effect_base_points(EFFECT::OVERFLOWING_PURSE_COINS)?, transform: |value| wowlab_types::numeric::f64_to_u32_saturating_trunc(value).saturating_sub(1), },
95    overflowing_chance: f64 { provenance: "Overflowing Purse proc chance.", gate: hand_of_fate && params.talent_selected(HERO::FATEBOUND::SPELL::OVERFLOWING_PURSE), source: params.require_effect_base_points(EFFECT::OVERFLOWING_PURSE_CHANCE)?, transform: |value| value / HUNDRED, },
96    controlled_chaos_streak: i32 { provenance: "Controlled Chaos forced-alternation streak.", gate: hand_of_fate && params.talent_selected(HERO::FATEBOUND::SPELL::CONTROLLED_CHAOS), source: params.require_effect_base_points(EFFECT::CONTROLLED_CHAOS_STREAK)?, transform: wowlab_types::numeric::f64_to_i32_saturating_trunc, },
97    deathmark_energy_per_tick: f64 { provenance: "Deathmark periodic Energy gain.", gate: true, source: params.require_effect_base_points(EFFECT::DEATHMARK_ENERGY)?, transform: |value| value, },
98    deathmark_period_ms: u32 { provenance: "Deathmark periodic cadence.", gate: true, source: params.game_data.effect_lookup(EFFECT::DEATHMARK_PERIOD).period(), transform: wowlab_types::numeric::f64_to_u32_saturating_round, },
99    deathmark_duration_ms: u32 { provenance: "Deathmark duration from resolved aura data.", gate: true, source: params.game_data.require_aura_duration_ms(wowlab_types::sim::SpellIdx::from_raw(SPELL::DEATHMARK))?, transform: |value| value, },
100}
101accessor pub(super) fn sin_config(ctx);
102register pub(super) fn register_assassination(built, params) -> Result<(), wowlab_engine_ports::EngineError>;
103prepare {
104    let has_2pc = params
105        .set_bonus_auras
106        .contains(&SET_BONUS::MID2_ASSASSINATION_2PC);
107    let has_4pc = params
108        .set_bonus_auras
109        .contains(&SET_BONUS::MID2_ASSASSINATION_4PC);
110    let mid2_damage = if has_2pc {
111        params.require_effect_base_points(EFFECT::MID2_ASSASSINATION_2PC_DAMAGE)? / HUNDRED
112    } else {
113        0.0
114    };
115    let mid2_auto_attack = if has_4pc {
116        params.require_effect_base_points(EFFECT::MID2_ASSASSINATION_4PC_AUTO_ATTACK)?
117    } else {
118        0.0
119    };
120    let hand_of_fate = params.talent_selected(HERO::FATEBOUND::SPELL::HAND_OF_FATE);
121    let coin_heads_initial = (params.require_effect_base_points(EFFECT::COIN_HEADS_INITIAL)?
122        + if params.talent_selected(HERO::FATEBOUND::SPELL::RAVENHOLDT_MINT) {
123            params.require_effect_base_points(EFFECT::RAVENHOLDT_MINT_HEADS)?
124        } else {
125            0.0
126        }) / HUNDRED;
127    let coin_heads_per_stack = params.require_effect_base_points(EFFECT::COIN_HEADS_PER_STACK)? / HUNDRED;
128}
129auras {
130    { provenance: "MID2 two-piece grants three percent damage while Envenom is active.", gate: has_2pc, aura: crate::generated::specs::assassination_rogue::AURA_ENVENOM, effect: BuffEffect::DamageMult(mid2_damage) },
131    { provenance: "MID2 four-piece increases auto-attack damage per Amplifying Poison stack.", gate: has_4pc, aura: AURA_AMPLIFYING_POISON_DEBUFF, effect: BuffEffect::AutoAttackDamageTaken(mid2_auto_attack) },
132    {
133        provenance: "Fatebound Heads combines its base damage with Ravenholdt Mint before per-stack growth.",
134        gate: hand_of_fate,
135        aura: AURA_FATEBOUND_COIN_HEADS,
136        effect: BuffEffect::DamageMultStacking { initial: coin_heads_initial, per_stack: coin_heads_per_stack },
137    },
138}
139wiring {
140    { provenance: "MID2 two-piece increases Envenom damage.", gate: has_2pc, apply: {
141        built.register_driver_spell_modifier(DriverSpellModifier::new(
142            SET_BONUS::MID2_ASSASSINATION_2PC,
143            &MID2_ENVENOM_SPELLS,
144            mid2_envenom_modifier,
145        ));
146    } },
147    { provenance: "MID2 four-piece increases bleeds only while Deadly Poison is active on the victim.", gate: has_4pc, apply: {
148        built.register_driver_spell_modifier(DriverSpellModifier::new(
149            SET_BONUS::MID2_ASSASSINATION_4PC,
150            &MID2_BLEED_SPELLS,
151            mid2_bleed_modifier,
152        ));
153    } },
154    { provenance: "Fatebound Coin Heads carries its scripted initial-stack bonus and its per-stack bonus as one additive modifier, so the data-derived rows would double the per-stack half.", gate: hand_of_fate, apply: {
155        for effect in [EFFECT::COIN_HEADS_PER_STACK, EFFECT::COIN_HEADS_PER_STACK_PERIODIC] {
156            built.mask_aura_dbc_effect(AURA_FATEBOUND_COIN_HEADS, effect.0, effect.1);
157        }
158    } },
159    { provenance: "Lethal Dose evaluates active target bleeds and poisons for affected damage events.", gate: params.talent_selected(TALENT::LETHAL_DOSE), apply: {
160        built.register_driver_spell_modifier(DriverSpellModifier::new(
161            TALENT::LETHAL_DOSE,
162            &LETHAL_DOSE_SPELLS,
163            lethal_dose_modifier,
164        ));
165    } },
166    { provenance: "Zoldyck Recipe evaluates target health for mastery-affected damage events.", gate: params.talent_selected(TALENT::ZOLDYCK_RECIPE), apply: {
167        built.register_driver_spell_modifier(DriverSpellModifier::new(
168            TALENT::ZOLDYCK_RECIPE,
169            &ZOLDYCK_RECIPE_SPELLS,
170            zoldyck_recipe_modifier,
171        ));
172    } },
173    { provenance: "Inspiring Strike is a triggered Envenom-overlap buff, not a passive precombat aura.", gate: params.talent_selected(TALENT::INSPIRING_STRIKE), apply: {
174        built.remove_precombat_aura(crate::generated::specs::assassination_rogue::AURA_INSPIRING_STRIKE);
175    } },
176}
177
178finish |cfg| {
179    // Data-derived parent damage would double-count child hits or invent direct DoT hits.
180    for local in [
181        SPELL_AMBUSH,
182        SPELL_ENVENOM,
183        SPELL_GARROTE,
184        SPELL_RUPTURE,
185        SPELL_DEATHMARK,
186    ] {
187        built.patch_spell(local, |spell| {
188            spell.damage = RuntimeDamageDef::None;
189        });
190    }
191
192    match (
193        std::num::NonZeroU32::new(cfg.deathmark_period_ms),
194        std::num::NonZeroU32::new(cfg.deathmark_duration_ms),
195    ) {
196        (Some(period), Some(duration)) => built.register_periodic_driver(
197            PeriodicDriver::new(
198                SPELL::DEATHMARK,
199                period.get(),
200                super::hooks::deathmark_energize_tick,
201            )
202            .hasted()
203            .triggered_for(duration.get()),
204        ),
205        _ if !params.game_data.is_empty() => {
206            return Err(wowlab_engine_ports::EngineError::spec_construction(
207                "Deathmark periodic driver resolved a zero duration or interval",
208            ));
209        }
210        _ => {}
211    }
212
213    built.set_spec_config(cfg);
214    Ok(())
215}
216}
217
218fn mid2_envenom_modifier(view: wowlab_engine_combat::HookView<'_>) -> DriverSpellModifierResult {
219    DriverSpellModifierResult::damage_mult(
220        view.program()
221            .spec_config::<AssassinationConfig>()
222            .map_or(1.0, |cfg| cfg.mid2_envenom_mult),
223    )
224}
225
226fn mid2_bleed_modifier(view: wowlab_engine_combat::HookView<'_>) -> DriverSpellModifierResult {
227    let Some(cfg) = view.program().spec_config::<AssassinationConfig>() else {
228        return DriverSpellModifierResult::default();
229    };
230
231    if wowlab_engine_combat::is_aura_active(
232        &view.combat(),
233        AURA_DEADLY_POISON_DOT,
234        wowlab_types::sim::ActorId::Player,
235        view.target,
236    ) {
237        DriverSpellModifierResult::damage_mult(cfg.mid2_bleed_mult)
238    } else {
239        DriverSpellModifierResult::default()
240    }
241}
242
243fn lethal_dose_modifier(view: wowlab_engine_combat::HookView<'_>) -> DriverSpellModifierResult {
244    let per_effect = view
245        .program()
246        .spec_config::<AssassinationConfig>()
247        .map_or(0.0, |config| config.lethal_dose_per_effect);
248    let active = [
249        AURA_ATROPHIC_POISON,
250        AURA_CRIPPLING_POISON,
251        AURA_DEADLY_POISON_DOT,
252        AURA_AMPLIFYING_POISON_DEBUFF,
253        AURA_GARROTE,
254        AURA_RUPTURE,
255        AURA_INTERNAL_BLEEDING,
256        AURA_DEATHMARK,
257        AURA_KINGSBANE,
258    ]
259    .into_iter()
260    .filter(|local| {
261        wowlab_engine_combat::is_aura_active(
262            &view.combat(),
263            *local,
264            wowlab_types::sim::ActorId::Player,
265            view.target,
266        )
267    })
268    .count();
269
270    DriverSpellModifierResult::damage_mult(
271        1.0 + per_effect * wowlab_types::numeric::usize_to_f64(active),
272    )
273}
274
275fn zoldyck_recipe_modifier(view: wowlab_engine_combat::HookView<'_>) -> DriverSpellModifierResult {
276    let Some(config) = view.program().spec_config::<AssassinationConfig>() else {
277        return DriverSpellModifierResult::default();
278    };
279    let health = view.target_health_fraction();
280
281    if health < config.zoldyck_threshold {
282        DriverSpellModifierResult::damage_mult(config.zoldyck_mult)
283    } else {
284        DriverSpellModifierResult::default()
285    }
286}
287
288/// Returns Potent Assassin mastery damage for spell 76803.
289#[must_use]
290pub(crate) fn mastery(ctx: &MasteryCtx<'_>) -> f64 {
291    if ctx.is_periodic {
292        return if ctx.affects_spell(EFFECT::POTENT_ASSASSIN_PERIODIC.1) {
293            ctx.bonus(EFFECT::POTENT_ASSASSIN_PERIODIC.1)
294        } else {
295            1.0
296        };
297    }
298
299    [
300        EFFECT::POTENT_ASSASSIN_DIRECT.1,
301        EFFECT::POTENT_ASSASSIN_DIRECT_LABEL.1,
302        EFFECT::POTENT_ASSASSIN_SECONDARY_DIRECT_LABEL.1,
303    ]
304    .into_iter()
305    .find(|&effect| ctx.affects_spell(effect))
306    .map_or(1.0, |effect| ctx.bonus(effect))
307}