Skip to main content

wowlab_engine_combat/pipeline/
channel.rs

1use super::{
2    CombatCtx, Event, SimTime, SpellData, SpellIdx, break_stealth_if_active, current_haste_mult,
3    fire_cast_hook, fire_player_cast_hooks, process_channel_start_payload, ready_after_gcd,
4    schedule_ready_after_cast, start_cooldown_in_slot, update_history,
5};
6
7pub(super) fn process_channel_cast(
8    ctx: &mut CombatCtx<'_>,
9    candidate: crate::handler::can_cast::CastCandidate,
10    tick_cost_aura: Option<crate::state::CastAuraRequirement>,
11    tick_damage_aura: Option<crate::state::CastAuraRequirement>,
12    spell: &SpellData,
13    empower_rank: u8,
14) {
15    let spell_local = candidate.effective_local;
16    let cooldown_local = candidate.cooldown_local;
17    let spell_handle = candidate.effective_slot(ctx.program);
18    let history_handle = candidate.effective_history(ctx.program);
19    let now = ctx.now;
20    let spell_id = spell.spell_id;
21
22    if spell.cooldown.has_cooldown {
23        let cooldown_ms =
24            start_cooldown_in_slot(ctx.program, ctx.instance, spell_local, cooldown_local, now);
25
26        ctx.sink
27            .emit_cooldown_start(spell_id, cooldown_ms, now.as_millis());
28    }
29
30    let haste_mult = current_haste_mult(&ctx.view());
31    let channel_ms = if spell.channel.behavior.duration_hasted {
32        wowlab_types::numeric::f64_to_u32_saturating_trunc(
33            f64::from(spell.cast_time_ms) / haste_mult,
34        )
35    } else {
36        spell.cast_time_ms
37    };
38
39    if channel_ms == 0 {
40        tracing::warn!(
41            spell_id,
42            "channel spell has cast_time_ms=0; treating as instant no-op"
43        );
44        schedule_ready_after_cast(ctx, spell, now);
45
46        return;
47    }
48
49    let channel_end = now.saturating_add(SimTime::from_millis(channel_ms));
50    let channel_end_secs = channel_end.as_secs_f64();
51
52    ctx.instance.buffer.player_mut().channel_end = channel_end_secs;
53    ctx.instance.runtime.casting.channel_generation = ctx
54        .instance
55        .runtime
56        .casting
57        .channel_generation
58        .wrapping_add(1);
59    let channel_generation = ctx.instance.runtime.casting.channel_generation;
60
61    ctx.instance.runtime.casting.active_channel_target = Some(ctx.target);
62    ctx.instance.runtime.casting.active_channel_damage_mult = 1.0;
63    let channel_identity = crate::state::ActiveChannelIdentity {
64        local: spell_local,
65        spell: SpellIdx::from_raw(spell_id),
66        handle: spell_handle,
67        tick_cost_aura,
68        tick_damage_aura,
69    };
70
71    ctx.instance.runtime.casting.active_channel = Some(channel_identity);
72    ctx.instance.runtime.casting.active_channel_pushback_count = 0;
73    let s = ctx.instance.buffer.spell_mut_at(channel_identity.handle);
74
75    s.is_channeling = 1;
76    s.channel_end = channel_end_secs;
77
78    #[cfg(feature = "rotation-optimizations")]
79    ctx.instance.runtime.rotation_mutations.record_semantic(
80        crate::state::runtime::RotationSemanticMutation::SpellState,
81        crate::state::runtime::RotationMutationDirection::Rise,
82        now,
83    );
84
85    let tick_count = spell.channel.tick_count;
86
87    if tick_count == 0 {
88        tracing::warn!(
89            spell_id,
90            "channel has tick_count=0; no tick damage will be delivered"
91        );
92    } else {
93        let base_interval_ms = if spell.channel.tick_interval_ms > 0 {
94            spell.channel.tick_interval_ms
95        } else {
96            let elapsed_intervals =
97                tick_count.saturating_sub(u8::from(spell.channel.timing.tick_zero));
98
99            spell.cast_time_ms / u32::from(elapsed_intervals.max(1))
100        };
101        let interval_ms = if spell.channel.timing.hasted_ticks {
102            wowlab_types::numeric::f64_to_u32_saturating_trunc(
103                f64::from(base_interval_ms) / haste_mult,
104            )
105        } else {
106            base_interval_ms
107        };
108
109        if interval_ms == 0 {
110            tracing::warn!(
111                spell_id,
112                tick_count,
113                channel_ms,
114                "channel tick interval rounds to zero; skipping ticks"
115            );
116        } else {
117            ctx.instance.runtime.casting.active_channel_last_tick = now;
118            ctx.instance.runtime.casting.active_channel_tick_interval_ms = interval_ms;
119            ctx.instance.runtime.casting.active_channel_on_last_tick = false;
120            let mut scheduled_ticks = 0_u8;
121
122            visit_channel_tick_offsets(spell, channel_ms, interval_ms, |offset_ms| {
123                let tick_t = now.saturating_add(SimTime::from_millis(offset_ms));
124
125                ctx.instance.schedule(Event::ChannelTick {
126                    t: tick_t,
127                    spell: ctx.program.scheduled_spell(spell_local),
128                    source: ctx.source,
129                    target: ctx.target,
130                    generation: channel_generation,
131                });
132                scheduled_ticks = scheduled_ticks.saturating_add(1);
133            });
134            ctx.instance.runtime.casting.active_channel_ticks_remaining = scheduled_ticks;
135        }
136    }
137
138    ctx.instance.schedule(Event::PlayerReady {
139        t: ready_after_gcd(ctx.instance.buffer.player().gcd_end, channel_end).saturating_add(
140            SimTime::from_millis(
141                u32::from(spell.channel.completion.apply_lag)
142                    * ctx.program.config.cast_latency.channel_ms,
143            ),
144        ),
145    });
146
147    process_channel_start_payload(ctx, spell_local, spell);
148
149    fire_cast_hook(ctx, spell_local, empower_rank);
150    fire_player_cast_hooks(ctx, spell.spell_id, empower_rank);
151
152    if spell.behavior.breaks_stealth {
153        break_stealth_if_active(&mut ctx.hook_ctx());
154    }
155
156    ctx.instance.buffer.spell_mut_at(spell_handle).last_used = now.as_secs_f64();
157
158    update_history(ctx.instance, history_handle, spell.behavior.off_gcd, now);
159}
160
161pub(super) fn visit_channel_tick_offsets(
162    spell: &SpellData,
163    channel_ms: u32,
164    interval_ms: u32,
165    mut visit: impl FnMut(u32),
166) {
167    if !spell.channel.timing.hasted_ticks || spell.channel.behavior.duration_hasted {
168        let first_tick = u32::from(!spell.channel.timing.tick_zero);
169
170        for tick in first_tick..first_tick + u32::from(spell.channel.tick_count) {
171            visit(interval_ms * tick);
172        }
173
174        return;
175    }
176
177    let initial_ticks = u32::from(spell.channel.timing.tick_zero);
178    let interval_ticks = channel_ms.saturating_sub(1) / interval_ms;
179    let interval_ticks = interval_ticks.min(u32::from(u8::MAX) - initial_ticks - 1);
180
181    if spell.channel.timing.tick_zero {
182        visit(0);
183    }
184
185    for tick in 1..=interval_ticks {
186        visit(interval_ms * tick);
187    }
188
189    visit(channel_ms);
190}