Skip to main content

wowlab_engine_content/hooks/outlaw_rogue/
config.rs

1use wowlab_engine_domain::dbc::ResolvedGameDataEffectExt as _;
2use wowlab_types::{constants::HUNDRED, constants::MS_PER_SECOND};
3
4use wowlab_engine_combat::{
5    BuffEffect, DamageEffectRef, DamageReplication, DamageSource, DriverSpellModifier,
6    DriverSpellModifierResult, RuntimeDamageDef,
7};
8
9use crate::{
10    generated::specs::outlaw_rogue::{
11        AURA_BLADE_FLURRY, AURA_FATEBOUND_COIN_HEADS, EFFECT, HERO, SET_BONUS, SPELL,
12        SPELL_DISPATCH, TALENT,
13    },
14    hooks::{shared::rogue::COIN_FAIR_ODDS, try_gated},
15};
16
17// Restless Blades effect 1 stores tenths of a second.
18const RESTLESS_BLADES_CDR_DIVISOR: f64 = 10.0;
19
20const DEFT_MANEUVERS_CP: f64 = 1.0;
21
22// Mean Streak applies half its listed percentage to matching-face odds.
23const MEAN_STREAK_ODDS_FACTOR: f64 = 0.5;
24
25static DELIVERED_DOOM_SPELLS: [u32; 2] = [SPELL::DISPATCH, SPELL::BETWEEN_THE_EYES];
26static SUMMARILY_DISPATCHED_SPELLS: [u32; 1] = [SPELL::DISPATCH];
27static MID2_DISPATCH_SPELLS: [u32; 1] = [SPELL::DISPATCH];
28
29crate::hooks::define_spec_config! {
30pub(super) struct OutlawConfig {
31    restless_blades_cdr_ms_per_cp: u32 { provenance: "Restless Blades CDR per combo point.", gate: params.talent_selected(TALENT::RESTLESS_BLADES), source: data.require_effect_base_points(EFFECT::RESTLESS_BLADES_CDR)?, transform: |value| wowlab_types::numeric::f64_to_u32_saturating_trunc(value / RESTLESS_BLADES_CDR_DIVISOR * MS_PER_SECOND) },
32    restless_blades_rtb_bonus: f64 { provenance: "Jackpot Restless Blades bonus.", gate: true, source: data.require_effect_base_points(EFFECT::JACKPOT_RB)?, transform: |value| value / HUNDRED },
33    ruthlessness_cp_per_cp: f64 { provenance: "Ruthlessness points per resource.", gate: params.talent_selected(TALENT::RUTHLESSNESS), source: data.require_effect_points_per_resource(EFFECT::RUTHLESSNESS_CP)?, transform: |value| value / HUNDRED },
34    quick_draw_cp: f64 { provenance: "Quick Draw energize.", gate: params.talent_selected(TALENT::QUICK_DRAW), source: data.require_effect_base_points(EFFECT::QUICK_DRAW_ENERGIZE)?, transform: |value| value },
35    hidden_opportunity_mod: f64 { provenance: "Hidden Opportunity damage fraction.", gate: params.talent_selected(TALENT::HIDDEN_OPPORTUNITY), source: data.require_effect_base_points(EFFECT::HIDDEN_OPPORTUNITY_DAMAGE)?, transform: |value| value / HUNDRED },
36    has_audacity: bool { provenance: "Audacity selection.", gate: params.talent_selected(TALENT::AUDACITY), source: true, transform: |value| value },
37    has_loaded_dice: bool { provenance: "Loaded Dice selection.", gate: params.talent_selected(TALENT::LOADED_DICE), source: true, transform: |value| value },
38    has_improved_adrenaline_rush: bool { provenance: "Improved Adrenaline Rush selection.", gate: params.talent_selected(TALENT::IMPROVED_ADRENALINE_RUSH), source: true, transform: |value| value },
39    has_deadly_pursuit: bool { provenance: "Deadly Pursuit selection.", gate: params.talent_selected(TALENT::DEADLY_PURSUIT), source: true, transform: |value| value },
40    sleight_of_hand_chance: f64 { provenance: "Sleight of Hand proc chance.", gate: params.talent_selected(TALENT::SLEIGHT_OF_HAND), source: data.require_effect_base_points(EFFECT::SLEIGHT_OF_HAND_CHANCE)?, transform: |value| value / HUNDRED },
41    deft_maneuvers_cp: f64 { provenance: "Deft Maneuvers fixed combo-point gain.", gate: params.talent_selected(TALENT::DEFT_MANEUVERS), source: DEFT_MANEUVERS_CP, transform: |value| value },
42    blade_flurry_targets: u8 { provenance: "Blade Flurry target cap.", gate: true, source: bf_targets_raw, transform: wowlab_types::numeric::f64_to_u8_saturating_trunc },
43    fan_the_hammer_extra: u32 { provenance: "Fan the Hammer extra casts.", gate: fan_the_hammer, source: data.require_effect_base_points(EFFECT::FAN_THE_HAMMER_EXTRA)?, transform: wowlab_types::numeric::f64_to_u32_saturating_trunc },
44    fan_the_hammer_mult: f64 { provenance: "Fan the Hammer damage reduction.", gate: fan_the_hammer, source: data.require_effect_base_points(EFFECT::FAN_THE_HAMMER_REDUCTION)?, transform: |value| 1.0 - value / HUNDRED },
45    hand_of_fate_cp: f64 { provenance: "Hand of Fate combo-point gain.", gate: fatebound, source: data.require_effect_base_points(EFFECT::HAND_OF_FATE_CP)?, transform: |value| value },
46    coin_matching_odds: f64 { provenance: "Fatebound matching odds composed from Mean Streak and Destiny Defined.", gate: fatebound, source: matching_odds, transform: |value| value },
47    deal_fate_cp: f64 { provenance: "Deal Fate combo-point gain.", gate: params.talent_selected(HERO::FATEBOUND::SPELL::DEAL_FATE), source: data.require_effect_base_points(EFFECT::DEAL_FATE_CP)?, transform: |value| value },
48    delivered_doom_mult: f64 { provenance: "Delivered Doom finisher damage multiplier.", gate: params.talent_selected(HERO::FATEBOUND::SPELL::DELIVERED_DOOM), source: data.require_effect_base_points(EFFECT::DELIVERED_DOOM_DMG)?, transform: |value| 1.0 + value / HUNDRED, fallback: 1.0 },
49    delivered_doom_cp: f64 { provenance: "Delivered Doom combo-point threshold.", gate: params.talent_selected(HERO::FATEBOUND::SPELL::DELIVERED_DOOM), source: data.require_effect_base_points(EFFECT::DELIVERED_DOOM_CP)?, transform: |value| value },
50    ace_up_chance_per_cp: f64 { provenance: "Rank-scaled Ace Up Your Sleeve chance per combo point.", gate: ace_ranks > 0.0, source: data.require_effect_base_points(EFFECT::ACE_UP_CHANCE)? * ace_ranks, transform: |value| value / HUNDRED },
51    ace_up_cp_gain: f64 { provenance: "Ace Up Your Sleeve energize.", gate: ace_ranks > 0.0, source: data.require_effect_base_points(EFFECT::ACE_UP_ENERGIZE)?, transform: |value| value },
52    summarily_dispatched_mult: f64 { provenance: "Summarily Dispatched Dispatch damage.", gate: params.talent_ranks(TALENT::SUMMARILY_DISPATCHED) > 0, source: data.require_effect_base_points(EFFECT::SUMMARILY_DISPATCHED_DMG)?, transform: |value| value / HUNDRED },
53    summarily_dispatched_btte_bonus: f64 { provenance: "Between the Eyes magnifies Summarily Dispatched while it is up.", gate: params.talent_ranks(TALENT::SUMMARILY_DISPATCHED) > 0, source: data.require_effect_base_points(EFFECT::BETWEEN_THE_EYES_SUMMARILY_DISPATCHED)?, transform: |value| value / HUNDRED },
54    scoundrel_cp_threshold: f64 { provenance: "Gravedigger tier 2 Scoundrel threshold.", gate: params.talent_selected(TALENT::GRAVEDIGGER_2), source: data.require_effect_base_points(EFFECT::SCOUNDREL_CP_THRESHOLD)?, transform: |value| value },
55    palmed_bullets_chance_per_cp: f64 { provenance: "Gravedigger tier 3 Palmed Bullets chance.", gate: gravedigger3, source: data.require_effect_base_points(EFFECT::PALMED_BULLETS_CHANCE)?, transform: |value| value / HUNDRED },
56    gravedigger_cp: f64 { provenance: "Gravedigger energize.", gate: gravedigger3, source: data.require_effect_base_points(EFFECT::GRAVEDIGGER_ENERGIZE)?, transform: |value| value },
57    lucky_coin_flips: i32 { provenance: "Lucky Coin flip count.", gate: lucky_coin, source: data.require_effect_base_points(EFFECT::LUCKY_COIN_FLIPS)?, transform: wowlab_types::numeric::f64_to_i32_saturating_trunc },
58    lucky_coin_odds: f64 { provenance: "Lucky Coin matching-odds bonus.", gate: lucky_coin, source: data.require_effect_base_points(EFFECT::LUCKY_COIN_ODDS)?, transform: |value| value / HUNDRED },
59    has_edge_case: bool { provenance: "Edge Case selection.", gate: fatebound && params.talent_selected(HERO::FATEBOUND::SPELL::EDGE_CASE), source: true, transform: |value| value },
60    rush_energy: f64 { provenance: "Rush to the Inevitable energy.", gate: rush, source: data.require_effect_base_points(EFFECT::RUSH_ENERGY)?, transform: |value| value },
61    rush_energy_edge: f64 { provenance: "Rush to the Inevitable Edge Case energy.", gate: rush, source: data.require_effect_base_points(EFFECT::RUSH_ENERGY_EDGE)?, transform: |value| value },
62    controlled_chaos_streak: i32 { provenance: "Controlled Chaos streak length.", gate: controlled_chaos, source: data.require_effect_base_points(EFFECT::CONTROLLED_CHAOS_STREAK)?, transform: wowlab_types::numeric::f64_to_i32_saturating_trunc },
63    mid2_dispatch_mult: f64 { provenance: "MID2 Outlaw two-piece Dispatch damage from 1296588 e1.", gate: has_mid2_2pc, source: data.require_effect_base_points(EFFECT::MID2_OUTLAW_2PC_DISPATCH)?, transform: |value| 1.0 + value / HUNDRED, fallback: 1.0 },
64    mid2_fang_chance: f64 { provenance: "MID2 Outlaw four-piece Fang Strike chance from 1296589 e1.", gate: has_mid2_4pc, source: data.require_effect_base_points(EFFECT::MID2_OUTLAW_4PC_CHANCE)?, transform: |value| value / HUNDRED },
65    killing_spree_excess_mult: f64 { provenance: "Killing Spree 12.1 damage per combo point spent above the normal maximum from 51690 e4.", gate: params.talent_selected(TALENT::KILLING_SPREE), source: data.require_effect_base_points(EFFECT::KILLING_SPREE_EXCESS_DAMAGE)?, transform: |value| value / HUNDRED },
66}
67accessor pub(super) fn outlaw_config(ctx);
68register pub(super) fn register_outlaw(built, params) -> Result<(), wowlab_engine_ports::EngineError>;
69prepare {
70    let data = &params.game_data;
71    let ace_ranks = f64::from(params.talent_ranks(TALENT::ACE_UP_YOUR_SLEEVE));
72    let fan_the_hammer = params.talent_selected(TALENT::FAN_THE_HAMMER);
73    let fatebound = params.talent_selected(HERO::FATEBOUND::SPELL::HAND_OF_FATE);
74    let lucky_coin = fatebound && params.talent_selected(HERO::FATEBOUND::SPELL::LUCKY_COIN);
75    let rush = fatebound && params.talent_selected(HERO::FATEBOUND::SPELL::RUSH_TO_THE_INEVITABLE);
76    let controlled_chaos = fatebound && params.talent_selected(HERO::FATEBOUND::SPELL::CONTROLLED_CHAOS);
77    let has_mid2_2pc = params.set_bonus_auras.contains(&SET_BONUS::MID2_OUTLAW_2PC);
78    let has_mid2_4pc = params.set_bonus_auras.contains(&SET_BONUS::MID2_OUTLAW_4PC);
79    let gravedigger3 = params.talent_selected(TALENT::GRAVEDIGGER_3);
80    let bf_targets_raw = data.require_effect_base_points(EFFECT::BLADE_FLURRY_TARGETS)?;
81    let bf_targets = wowlab_types::numeric::f64_to_u8_saturating_trunc(bf_targets_raw);
82    let bf_cleave = data.require_effect_base_points(EFFECT::BLADE_FLURRY_CLEAVE)? / HUNDRED;
83    let coin_heads_initial = try_gated(fatebound, || -> Result<f64, wowlab_engine_ports::EngineError> {
84            Ok(data.require_effect_base_points(EFFECT::COIN_HEADS_INITIAL)? / HUNDRED
85                + try_gated(params.talent_selected(HERO::FATEBOUND::SPELL::RAVENHOLDT_MINT), || data.require_effect_base_points(EFFECT::RAVENHOLDT_MINT_HEADS))? / HUNDRED)
86        })?;
87    let coin_heads_per_stack = try_gated(fatebound, || data.require_effect_base_points(EFFECT::COIN_HEADS_PER_STACK))? / HUNDRED;
88    let matching_odds = COIN_FAIR_ODDS
89        + try_gated(fatebound && params.talent_selected(HERO::FATEBOUND::SPELL::MEAN_STREAK), || data.require_effect_base_points(EFFECT::MEAN_STREAK_ODDS))? * MEAN_STREAK_ODDS_FACTOR / HUNDRED
90        + try_gated(fatebound && params.talent_selected(HERO::FATEBOUND::SPELL::DESTINY_DEFINED), || data.require_effect_base_points(EFFECT::DESTINY_DEFINED_ODDS))? / HUNDRED;
91}
92auras {
93    { provenance: "Blade Flurry copies resolved physical damage to nearby enemies.", gate: true, aura: AURA_BLADE_FLURRY, effect: BuffEffect::DamageReplication(DamageReplication { fraction: bf_cleave, effect: DamageEffectRef::new(EFFECT::BLADE_FLURRY_DAMAGE.0, EFFECT::BLADE_FLURRY_DAMAGE.1), source: DamageSource::Player, source_spells: &[], include_magic: false, include_indirect: false, include_primary: false, max_targets: bf_targets, reduced_targets: 0 }) },
94    { provenance: "Fatebound Coin Heads stacking damage.", gate: fatebound, aura: AURA_FATEBOUND_COIN_HEADS, effect: BuffEffect::DamageMultStacking { initial: coin_heads_initial, per_stack: coin_heads_per_stack } },
95}
96wiring {
97    { provenance: "MID2 two-piece increases Dispatch damage.", gate: has_mid2_2pc, apply: {
98        built.register_driver_spell_modifier(DriverSpellModifier::new(
99            SET_BONUS::MID2_OUTLAW_2PC,
100            &MID2_DISPATCH_SPELLS,
101            mid2_dispatch_modifier,
102        ));
103    } },
104    { provenance: "Summarily Dispatched is applied under its own driver so Between the Eyes can magnify it, which a static fold cannot express.", gate: params.talent_ranks(TALENT::SUMMARILY_DISPATCHED) > 0, apply: {
105        built.register_driver_spell_modifier(DriverSpellModifier::new(
106            TALENT::SUMMARILY_DISPATCHED,
107            &SUMMARILY_DISPATCHED_SPELLS,
108            summarily_dispatched_modifier,
109        ));
110    } },
111    { 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: fatebound, apply: {
112        for effect in [EFFECT::COIN_HEADS_PER_STACK, EFFECT::COIN_HEADS_PER_STACK_PERIODIC] {
113            built.mask_aura_dbc_effect(AURA_FATEBOUND_COIN_HEADS, effect.0, effect.1);
114        }
115    } },
116    { provenance: "Delivered Doom evaluates the finisher's spent combo points under its real talent driver.", gate: params.talent_selected(HERO::FATEBOUND::SPELL::DELIVERED_DOOM), apply: {
117        built.register_driver_spell_modifier(DriverSpellModifier::new(
118            HERO::FATEBOUND::SPELL::DELIVERED_DOOM,
119            &DELIVERED_DOOM_SPELLS,
120            delivered_doom_modifier,
121        ));
122    } },
123}
124
125finish |cfg| {
126    built.patch_spell(SPELL_DISPATCH, |spell| spell.damage = RuntimeDamageDef::None);
127    built.set_spec_config(cfg);
128    Ok(())
129}
130}
131
132fn mid2_dispatch_modifier(view: wowlab_engine_combat::HookView<'_>) -> DriverSpellModifierResult {
133    DriverSpellModifierResult::damage_mult(
134        view.program()
135            .spec_config::<OutlawConfig>()
136            .map_or(1.0, |cfg| cfg.mid2_dispatch_mult),
137    )
138}
139
140fn summarily_dispatched_modifier(
141    view: wowlab_engine_combat::HookView<'_>,
142) -> DriverSpellModifierResult {
143    let Some(config) = view.program().spec_config::<OutlawConfig>() else {
144        return DriverSpellModifierResult::default();
145    };
146    let between_the_eyes_up = wowlab_engine_combat::is_aura_active(
147        &view.combat(),
148        crate::generated::specs::outlaw_rogue::AURA_BETWEEN_THE_EYES,
149        wowlab_types::sim::ActorId::Player,
150        view.target,
151    );
152    let bonus = if between_the_eyes_up {
153        config.summarily_dispatched_btte_bonus
154    } else {
155        0.0
156    };
157
158    DriverSpellModifierResult::damage_mult(1.0 + config.summarily_dispatched_mult * (1.0 + bonus))
159}
160
161fn delivered_doom_modifier(view: wowlab_engine_combat::HookView<'_>) -> DriverSpellModifierResult {
162    let Some(config) = view.program().spec_config::<OutlawConfig>() else {
163        return DriverSpellModifierResult::default();
164    };
165
166    if view.last_secondary_spent() >= config.delivered_doom_cp {
167        DriverSpellModifierResult::damage_mult(config.delivered_doom_mult)
168    } else {
169        DriverSpellModifierResult::default()
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use googletest::prelude::*;
176    use wowlab_engine_combat::AuraOps as _;
177    use wowlab_engine_combat::HookCtx;
178    use wowlab_engine_domain::dbc::{AuraSubtypeKind, ModifierOperation, ModifierPropertyKind};
179    use wowlab_engine_gamedata::{AuraProps, ResolvedGameData, SpellProps};
180    use wowlab_engine_ports::{CombatStats, ConsumableFlags, TalentSelection};
181    use wowlab_engine_telemetry::TelemetrySink;
182    use wowlab_types::{
183        game::RaceId,
184        sim::{SimTime, SpellIdx},
185    };
186
187    use super::*;
188    use crate::generated::specs::outlaw_rogue::{AURA, AURA_GRAVEDIGGER};
189
190    const UNUSUAL_COST_PERCENT: f64 = -37.0;
191
192    #[gtest]
193    fn gravedigger_cost_uses_one_dbc_effect_and_its_affected_spell_filter() -> Result<()> {
194        let encounter = wowlab_engine_ports::test_support::introspection_fixture(60.0).or_fail()?;
195        let environment = wowlab_engine_ports::test_support::introspection_game_data(&encounter);
196        let gravedigger = SpellIdx::from_raw(AURA::GRAVEDIGGER);
197        let between_the_eyes = SpellIdx::from_raw(SPELL::BETWEEN_THE_EYES);
198        let dispatch = SpellIdx::from_raw(SPELL::DISPATCH);
199        let mut data = ResolvedGameData::builder();
200
201        data.set_armor_constant(environment.armor_constant());
202        data.set_creature_armor(environment.creature_armor());
203        data.set_armor_constant_mod(environment.armor_constant_mod());
204        data.insert_aura_props(
205            gravedigger,
206            AuraProps {
207                duration_ms: 30_000,
208                ..AuraProps::default()
209            },
210        );
211        data.insert_spell_props(gravedigger, SpellProps::default());
212        data.insert_effect_aura(
213            wowlab_types::sim::EffectRef::new(gravedigger, 1),
214            AuraSubtypeKind::AddPercentModifier as i32,
215        );
216        data.insert_effect_misc_value_0(
217            wowlab_types::sim::EffectRef::new(gravedigger, 1),
218            ModifierPropertyKind::ResourceCost1 as i32,
219        );
220        data.insert_base_points(gravedigger, 1, UNUSUAL_COST_PERCENT);
221        data.insert_effect_class_mask(gravedigger, 1, [1, 0, 0, 0]);
222        data.insert_spell_class_flags(gravedigger, 1, [0, 0, 0, 0]);
223        data.insert_spell_class_flags(between_the_eyes, 1, [1, 0, 0, 0]);
224        data.insert_spell_class_flags(dispatch, 1, [2, 0, 0, 0]);
225
226        for spell in [between_the_eyes, dispatch] {
227            data.insert_spell_props(
228                spell,
229                SpellProps {
230                    primary_cost_entry: 1,
231                    ..SpellProps::default()
232                },
233            );
234        }
235
236        for (effect, value) in [
237            (EFFECT::JACKPOT_RB, 0.0),
238            (EFFECT::BLADE_FLURRY_TARGETS, 2.0),
239            (EFFECT::BLADE_FLURRY_CLEAVE, 50.0),
240            (EFFECT::PALMED_BULLETS_CHANCE, 10.0),
241            (EFFECT::GRAVEDIGGER_ENERGIZE, 6.0),
242        ] {
243            data.insert_base_points(SpellIdx::from_raw(effect.0), effect.1, value);
244        }
245
246        let data = data.build();
247        let stats = CombatStats::default();
248        let talents = [TalentSelection {
249            spell_id: TALENT::GRAVEDIGGER_3,
250            override_spell_id: 0,
251            replaces_spell_id: 0,
252            ranks: 1,
253            precombat_aura: false,
254            hero_tree: None,
255            effect_overrides: Vec::new(),
256        }];
257        let rotation = crate::test_support::wait_rotation();
258        let params =
259            crate::test_support::handler_params(crate::test_support::HandlerParamsFixture {
260                game_data: data.clone(),
261                rotation: &rotation,
262                stats: &stats,
263                talent_selections: &talents,
264                encounter: &encounter,
265                consumables: ConsumableFlags::default(),
266                race: RaceId::Human,
267            })
268            .or_fail()?;
269
270        let mut builder = crate::test_combat_builder_for(
271            wowlab_engine_combat::generated_identities::specs::outlaw_rogue::PROOF,
272            CombatStats::default(),
273        )
274        .game_data(data);
275
276        for local in 0..=AURA_GRAVEDIGGER.as_usize() {
277            let aura_id = if local == AURA_GRAVEDIGGER.as_usize() {
278                AURA::GRAVEDIGGER
279            } else if local == AURA_BLADE_FLURRY.as_usize() {
280                AURA::BLADE_FLURRY
281            } else {
282                900_000 + u32::try_from(local).expect("fixture local fits u32")
283            };
284
285            builder = builder.aura(&format!("outlaw_aura_{local}"), aura_id, |aura| {
286                if local == AURA_GRAVEDIGGER.as_usize() {
287                    aura.apply_base_from_data(&params.game_data, AURA::GRAVEDIGGER)
288                } else {
289                    Ok(aura.duration_ms(12_000))
290                }
291            });
292        }
293
294        let mut draft = builder
295            .spell("between_the_eyes", SPELL::BETWEEN_THE_EYES, |spell| {
296                Ok(spell.cost(100.0))
297            })
298            .spell("dispatch", SPELL::DISPATCH, |spell| Ok(spell.cost(100.0)))
299            .into_draft()
300            .or_fail()?;
301
302        register_outlaw(&mut draft, &params)?;
303
304        let local = draft.aura_local(AURA::GRAVEDIGGER).or_fail()?;
305        let effects: Vec<_> = draft
306            .aura(local)
307            .or_fail()?
308            .effects
309            .iter()
310            .flatten()
311            .collect();
312
313        verify_that!(effects.len(), eq(1))?;
314        verify_true!(matches!(
315            effects[0],
316            BuffEffect::SpellCostFromEffect {
317                value: UNUSUAL_COST_PERCENT,
318                source_spell_id: AURA::GRAVEDIGGER,
319                effect_index: 1,
320                cost_entry: 1,
321                operation: ModifierOperation::Percent,
322            }
323        ))?;
324        let mut built = draft
325            .finalize(&crate::test_support::wait_rotation())
326            .or_fail()?;
327        let mut sink = TelemetrySink::default();
328        let mut rng = || 0.5;
329
330        HookCtx::new(
331            wowlab_engine_combat::HookCtxServices {
332                program: &built.program,
333                instance: &mut built.instance,
334                sink: &mut sink,
335                rng: &mut rng,
336            },
337            wowlab_engine_combat::HookCtxRequest::targetless(SimTime::ZERO),
338        )
339        .apply_aura(AURA_GRAVEDIGGER);
340
341        let between_the_eyes_cost = built
342            .instance
343            .buffer
344            .spell(between_the_eyes)
345            .or_fail()?
346            .cost;
347        let dispatch_cost = built.instance.buffer.spell(dispatch).or_fail()?.cost;
348
349        verify_that!(between_the_eyes_cost, near(63.0, f64::EPSILON))?;
350
351        verify_that!(dispatch_cost, near(100.0, f64::EPSILON))
352    }
353}