Skip to main content

wowlab_engine_combat/handler/can_cast/
wait.rs

1//! Wait decisions derived from cast rejection and resource regeneration.
2
3use wowlab_engine_domain::rotation::DenseBuffer;
4use wowlab_engine_ports::SpecAction;
5use wowlab_types::{
6    constants::MS_PER_SECOND,
7    sim::{SimTime, SpellIdx},
8};
9
10use super::{CastReject, predicates::cast_rejection};
11use crate::{
12    context::CombatView,
13    state::CombatState,
14    systems::{effective_spell_ids, rune_time_to_regen},
15};
16
17pub(super) const MIN_WAIT_MS: u32 = 10;
18
19fn resource_wait(
20    state: &CombatState,
21    buf: &DenseBuffer,
22    now: SimTime,
23    target_cost: Option<f64>,
24) -> SimTime {
25    let (current, regen) = if let Some(resource_type) = state.config.base_stats.resource_type {
26        buf.resource(resource_type).map_or((0.0, 0.0), |resource| {
27            (resource.current, resource.regen_per_sec)
28        })
29    } else {
30        (0.0, 0.0)
31    };
32
33    let cost = match target_cost {
34        Some(cost) if cost > 0.0 => cost,
35        _ => {
36            if state.config.min_primary_cost <= 0.0 || state.config.min_primary_cost >= f64::MAX {
37                return now.saturating_add(SimTime::from_millis(MIN_WAIT_MS));
38            }
39
40            state.config.min_primary_cost
41        }
42    };
43
44    if regen <= 0.0 || current >= cost {
45        return now.saturating_add(SimTime::from_millis(MIN_WAIT_MS));
46    }
47
48    let deficit = cost - current;
49
50    if state.config.base_stats.resource_type == Some(wowlab_types::combat::ResourceType::Runes) {
51        let needed = wowlab_types::numeric::f64_to_u32_saturating_ceil(deficit.max(0.0)) as usize;
52
53        if let Some(seconds) = rune_time_to_regen(state, buf, needed) {
54            return now.saturating_add(SimTime::from_millis(
55                wowlab_types::numeric::f64_to_u32_saturating_ceil(seconds * MS_PER_SECOND)
56                    .max(MIN_WAIT_MS),
57            ));
58        }
59    }
60
61    let wait_ms =
62        wowlab_types::numeric::f64_to_u32_saturating_ceil((deficit / regen) * MS_PER_SECOND)
63            .max(MIN_WAIT_MS);
64
65    now.saturating_add(SimTime::from_millis(wait_ms))
66}
67
68pub(in crate::handler) fn reject_to_wait(
69    view: &CombatView<'_>,
70    spell_id: u32,
71    now: SimTime,
72) -> SpecAction {
73    let CombatView { state, buf, .. } = view;
74
75    if let Err(CastReject::StartRecoveryCategory { ready_at, .. }) =
76        cast_rejection(view, spell_id, now)
77    {
78        return SpecAction::Wait { until_ms: ready_at };
79    }
80
81    let spell_id = state.current_target().map_or(spell_id, |target| {
82        effective_spell_ids(view, spell_id, target).0
83    });
84    let cost = buf
85        .spell(SpellIdx(spell_id))
86        .map(|spell| spell.cost)
87        .filter(|cost| *cost > 0.0);
88
89    SpecAction::Wait {
90        until_ms: resource_wait(state, buf, now, cost),
91    }
92}
93
94pub(in crate::handler) fn pool_wait(
95    state: &CombatState,
96    buf: &DenseBuffer,
97    now: SimTime,
98) -> SpecAction {
99    SpecAction::Wait {
100        until_ms: resource_wait(state, buf, now, None),
101    }
102}