Skip to main content

wowlab_engine_combat/pipeline/
channel.rs

1use super::{
2    CombatCtx, Event, LocalSpellIdx, SimTime, SpellData, SpellIdx, break_stealth_if_active,
3    current_haste_mult, fire_cast_hook, fire_player_cast_hooks, process_channel_start_payload,
4    ready_after_gcd, schedule_ready_after_cast, start_cooldown_in_slot, update_history,
5};
6
7pub(super) fn process_channel_cast(
8    ctx: &mut CombatCtx<'_>,
9    spell_local: LocalSpellIdx,
10    cooldown_local: LocalSpellIdx,
11    spell: &SpellData,
12    empower_rank: u8,
13    now: SimTime,
14) {
15    let spell_id = spell.spell_id;
16
17    if spell.cooldown.has_cooldown {
18        let cooldown_ms =
19            start_cooldown_in_slot(ctx.state, ctx.buf, spell_local, cooldown_local, now);
20
21        ctx.sink
22            .emit_cooldown_start(spell_id, cooldown_ms, now.as_millis());
23    }
24
25    let haste_mult = current_haste_mult(&ctx.view());
26    let channel_ms = if spell.channel.behavior.duration_hasted {
27        wowlab_types::numeric::f64_to_u32_saturating_trunc(
28            f64::from(spell.cast_time_ms) / haste_mult,
29        )
30    } else {
31        spell.cast_time_ms
32    };
33
34    if channel_ms == 0 {
35        tracing::warn!(
36            spell_id,
37            "channel spell has cast_time_ms=0; treating as instant no-op"
38        );
39        schedule_ready_after_cast(ctx, spell, now);
40
41        return;
42    }
43
44    let channel_end = now.saturating_add(SimTime::from_millis(channel_ms));
45    let channel_end_secs = channel_end.as_secs_f64();
46
47    ctx.buf.player_mut().channel_end = channel_end_secs;
48    ctx.state.runtime.casting.channel_generation =
49        ctx.state.runtime.casting.channel_generation.wrapping_add(1);
50    let channel_generation = ctx.state.runtime.casting.channel_generation;
51
52    ctx.state.runtime.casting.active_channel_target = Some(ctx.target);
53    ctx.state.runtime.casting.active_channel_damage_mult = 1.0;
54    ctx.state.runtime.casting.active_channel_spell_id = Some(spell_id);
55    ctx.state.runtime.casting.active_channel_pushback_count = 0;
56    let spell_idx = SpellIdx(spell_id);
57
58    if let Some(s) = ctx.buf.spell_mut(spell_idx) {
59        s.is_channeling = 1;
60        s.channel_end = channel_end_secs;
61    }
62
63    let tick_count = spell.channel.tick_count;
64
65    if tick_count == 0 {
66        tracing::warn!(
67            spell_id,
68            "channel has tick_count=0; no tick damage will be delivered"
69        );
70    } else {
71        let base_interval_ms = if spell.channel.tick_interval_ms > 0 {
72            spell.channel.tick_interval_ms
73        } else {
74            let elapsed_intervals =
75                tick_count.saturating_sub(u8::from(spell.channel.timing.tick_zero));
76
77            spell.cast_time_ms / u32::from(elapsed_intervals.max(1))
78        };
79        let interval_ms = if spell.channel.timing.hasted_ticks {
80            wowlab_types::numeric::f64_to_u32_saturating_trunc(
81                f64::from(base_interval_ms) / haste_mult,
82            )
83        } else {
84            base_interval_ms
85        };
86
87        if interval_ms == 0 {
88            tracing::warn!(
89                spell_id,
90                tick_count,
91                channel_ms,
92                "channel tick interval rounds to zero; skipping ticks"
93            );
94        } else {
95            let tick_offsets = channel_tick_offsets(spell, channel_ms, interval_ms);
96
97            ctx.state.runtime.casting.active_channel_last_tick = now;
98            ctx.state.runtime.casting.active_channel_tick_interval_ms = interval_ms;
99            ctx.state.runtime.casting.active_channel_ticks_remaining =
100                u8::try_from(tick_offsets.len()).unwrap_or(u8::MAX);
101            ctx.state.runtime.casting.active_channel_on_last_tick = false;
102
103            for offset_ms in tick_offsets {
104                let tick_t = now.saturating_add(SimTime::from_millis(offset_ms));
105
106                ctx.state.schedule(Event::ChannelTick {
107                    t: tick_t,
108                    spell_id: SpellIdx::from_raw(spell_id),
109                    source: ctx.source,
110                    target: ctx.target,
111                    generation: channel_generation,
112                });
113            }
114        }
115    }
116
117    ctx.state.schedule(Event::PlayerReady {
118        t: ready_after_gcd(ctx.buf.player().gcd_end, channel_end).saturating_add(
119            SimTime::from_millis(
120                u32::from(spell.channel.completion.apply_lag)
121                    * ctx.state.config.cast_latency.channel_ms,
122            ),
123        ),
124    });
125
126    process_channel_start_payload(ctx, spell);
127
128    fire_cast_hook(ctx, spell_local, empower_rank);
129    fire_player_cast_hooks(ctx, spell.spell_id, empower_rank);
130
131    if spell.behavior.breaks_stealth {
132        break_stealth_if_active(&mut ctx.hook_ctx());
133    }
134
135    if let Some(s) = ctx.buf.spell_mut(spell_idx) {
136        s.last_used = now.as_secs_f64();
137    }
138
139    update_history(ctx.buf, spell_idx, spell.behavior.off_gcd);
140}
141
142pub(super) fn channel_tick_offsets(
143    spell: &SpellData,
144    channel_ms: u32,
145    interval_ms: u32,
146) -> Vec<u32> {
147    if !spell.channel.timing.hasted_ticks || spell.channel.behavior.duration_hasted {
148        let first_tick = u32::from(!spell.channel.timing.tick_zero);
149
150        return (first_tick..first_tick + u32::from(spell.channel.tick_count))
151            .map(|tick| interval_ms * tick)
152            .collect();
153    }
154
155    let initial_ticks = u32::from(spell.channel.timing.tick_zero);
156    let interval_ticks = channel_ms.saturating_sub(1) / interval_ms;
157    let interval_ticks = interval_ticks.min(u32::from(u8::MAX) - initial_ticks - 1);
158    let capacity = usize::try_from(initial_ticks + interval_ticks + 1)
159        .expect("channel tick count is clamped below usize capacity");
160    let mut offsets = Vec::with_capacity(capacity);
161
162    if spell.channel.timing.tick_zero {
163        offsets.push(0);
164    }
165
166    offsets.extend((1..=interval_ticks).map(|tick| interval_ms * tick));
167    offsets.push(channel_ms);
168
169    offsets
170}