Skip to main content

wowlab_engine_combat/state/program/
ground_effects.rs

1//! Finalized location-locked damage sequences.
2
3use crate::state::{
4    DamageEffectRef, GroundEffectDefinition, RuntimeDamageDef,
5    program::{DamagePlan, DamageRequestBinding},
6};
7
8/// Runtime-ready metadata for one authored ground effect.
9#[derive(Clone, Copy, Debug)]
10pub(crate) struct GroundEffectPlan {
11    pub(crate) binding: DamageRequestBinding,
12    pub(crate) geometry_effect: DamageEffectRef,
13    pub(crate) damage_effect: DamageEffectRef,
14    pub(crate) profile_spell_id: u32,
15    pub(crate) damage: RuntimeDamageDef,
16    pub(crate) base_duration_ms: u32,
17    pub(crate) base_period_ms: u32,
18    pub(crate) duration_hasted: bool,
19    pub(crate) period_hasted: bool,
20    pub(crate) pulse_bound: u8,
21    pub(crate) pulses_from_duration: bool,
22    pub(crate) first_pulse_delay_ms: u32,
23    pub(crate) flags: crate::DamageFlags,
24}
25
26#[derive(Debug, thiserror::Error)]
27pub(crate) enum GroundEffectCompileError {
28    #[error("ground effect {index} has an invalid damage multiplier")]
29    InvalidDamageMultiplier { index: usize },
30    #[error("ground effect {index} has an unresolved {coordinate} identity")]
31    UnresolvedIdentity {
32        index: usize,
33        coordinate: &'static str,
34    },
35    #[error("ground effect {index} has an unresolved damage binding")]
36    UnresolvedDamageBinding { index: usize },
37    #[error("ground effect {index} has an unresolved damage effect")]
38    UnresolvedDamageEffect { index: usize },
39    #[error("ground effect {index} has no lowered damage payload")]
40    MissingDamagePayload { index: usize },
41    #[error(
42        "ground effect {index} has invalid duration/period metadata: duration={duration_ms}ms, period={period}"
43    )]
44    InvalidTiming {
45        index: usize,
46        duration_ms: u32,
47        period: f64,
48    },
49    #[error("ground effect {index} pulse bound exceeds u8: {source}")]
50    PulseBound {
51        index: usize,
52        #[source]
53        source: std::num::TryFromIntError,
54    },
55    #[error("ground effect {index} has no pulses")]
56    MissingPulses { index: usize },
57    #[error("ground effect {index} has multiple pulses with no period")]
58    MissingPulsePeriod { index: usize },
59    #[error("ground effect {index} must declare both timing coordinates")]
60    IncompleteTimingCoordinates { index: usize },
61}
62
63type GroundEffectTiming = (u32, u32, bool, bool, u8, bool);
64
65pub(crate) fn compile_ground_effect_plans(
66    definitions: &[GroundEffectDefinition],
67    damage: &DamagePlan,
68) -> Result<Box<[GroundEffectPlan]>, GroundEffectCompileError> {
69    definitions
70        .iter()
71        .copied()
72        .enumerate()
73        .map(|(index, definition)| compile_ground_effect(index, definition, damage))
74        .collect::<Result<Vec<_>, _>>()
75        .map(Vec::into_boxed_slice)
76}
77
78fn compile_ground_effect(
79    index: usize,
80    definition: GroundEffectDefinition,
81    damage_plan: &DamagePlan,
82) -> Result<GroundEffectPlan, GroundEffectCompileError> {
83    if !definition.multiplier.is_finite() || definition.multiplier <= 0.0 {
84        return Err(GroundEffectCompileError::InvalidDamageMultiplier { index });
85    }
86
87    let identities = (
88        Some(definition.damage_effect),
89        Some(definition.geometry_effect),
90        Some(definition.profile_action),
91    );
92
93    #[cfg(test)]
94    let identities = definition.cold_coordinates.map_or(identities, |cold| {
95        (
96            damage_plan.diagnostic_effect_id(cold.damage_effect),
97            damage_plan.diagnostic_effect_id(cold.geometry_effect),
98            damage_plan.diagnostic_action_id(cold.damage_effect.spell_id),
99        )
100    });
101
102    let (damage_id, geometry_id, profile_id) = identities;
103    let damage_id = damage_id.ok_or(GroundEffectCompileError::UnresolvedIdentity {
104        index,
105        coordinate: "damage",
106    })?;
107    let geometry_id = geometry_id.ok_or(GroundEffectCompileError::UnresolvedIdentity {
108        index,
109        coordinate: "geometry",
110    })?;
111    let profile_id = profile_id.ok_or(GroundEffectCompileError::UnresolvedIdentity {
112        index,
113        coordinate: "profile",
114    })?;
115    let binding = damage_plan
116        .effect_id_request(damage_id, geometry_id, profile_id)
117        .ok_or(GroundEffectCompileError::UnresolvedDamageBinding { index })?;
118    let (_, damage_effect) = damage_plan
119        .effect_id_plan(damage_id)
120        .ok_or(GroundEffectCompileError::UnresolvedDamageEffect { index })?;
121    let damage = damage_effect
122        .resolved_damage
123        .ok_or(GroundEffectCompileError::MissingDamagePayload { index })?
124        .scaled(definition.multiplier);
125    let (
126        base_duration_ms,
127        base_period_ms,
128        duration_hasted,
129        period_hasted,
130        pulse_bound,
131        pulses_from_duration,
132    ) = compile_ground_effect_timing(index, definition, damage_plan)?;
133
134    Ok(GroundEffectPlan {
135        binding,
136        geometry_effect: DamageEffectRef::new(
137            geometry_id.action().spell_id(),
138            geometry_id.effect_index(),
139        ),
140        damage_effect: DamageEffectRef::new(
141            damage_id.action().spell_id(),
142            damage_id.effect_index(),
143        ),
144        profile_spell_id: profile_id.spell_id(),
145        damage,
146        base_duration_ms,
147        base_period_ms,
148        duration_hasted,
149        period_hasted,
150        pulse_bound,
151        pulses_from_duration,
152        first_pulse_delay_ms: definition.first_pulse_delay_ms,
153        flags: definition.flags,
154    })
155}
156
157fn compile_ground_effect_timing(
158    index: usize,
159    definition: GroundEffectDefinition,
160    damage_plan: &DamagePlan,
161) -> Result<GroundEffectTiming, GroundEffectCompileError> {
162    match (definition.driver_action, definition.timing_effect) {
163        (Some(driver), Some(timing)) => {
164            compile_driver_timing(index, definition, damage_plan, driver, timing)
165        }
166        (None, None) => compile_fixed_timing(index, definition),
167        _ => Err(GroundEffectCompileError::IncompleteTimingCoordinates { index }),
168    }
169}
170
171#[cfg_attr(
172    not(test),
173    expect(
174        unused_variables,
175        reason = "cold diagnostic coordinates are compiled only for test builds"
176    )
177)]
178fn compile_driver_timing(
179    index: usize,
180    definition: GroundEffectDefinition,
181    damage_plan: &DamagePlan,
182    driver: crate::DamageActionId,
183    timing: crate::DamageEffectId,
184) -> Result<GroundEffectTiming, GroundEffectCompileError> {
185    let identities = (Some(driver), Some(timing));
186
187    #[cfg(test)]
188    let identities = definition.cold_coordinates.map_or(identities, |cold| {
189        (
190            cold.driver_spell_id
191                .and_then(|driver| damage_plan.diagnostic_action_id(driver)),
192            cold.timing_effect
193                .and_then(|timing| damage_plan.diagnostic_effect_id(timing)),
194        )
195    });
196
197    let (driver, timing) = identities;
198    let driver = damage_plan
199        .action_id_plan(driver.ok_or(GroundEffectCompileError::UnresolvedIdentity {
200            index,
201            coordinate: "timing driver",
202        })?)
203        .ok_or(GroundEffectCompileError::UnresolvedIdentity {
204            index,
205            coordinate: "timing driver",
206        })?;
207    let (_, timing) = damage_plan
208        .effect_id_plan(timing.ok_or(GroundEffectCompileError::UnresolvedIdentity {
209            index,
210            coordinate: "timing effect",
211        })?)
212        .ok_or(GroundEffectCompileError::UnresolvedIdentity {
213            index,
214            coordinate: "timing effect",
215        })?;
216    let period = timing.period;
217
218    if driver.aura_duration_ms == 0
219        || !period.is_finite()
220        || period <= 0.0
221        || period > f64::from(u32::MAX)
222    {
223        return Err(GroundEffectCompileError::InvalidTiming {
224            index,
225            duration_ms: driver.aura_duration_ms,
226            period,
227        });
228    }
229
230    let period = wowlab_types::numeric::f64_to_u32_saturating_round(period);
231    let pulses = driver.aura_duration_ms.div_ceil(period);
232
233    u8::try_from(pulses)
234        .map_err(|source| GroundEffectCompileError::PulseBound { index, source })?;
235
236    Ok((
237        driver.aura_duration_ms,
238        period,
239        driver.duration_hasted,
240        driver.hasted_ticks,
241        u8::MAX,
242        true,
243    ))
244}
245
246fn compile_fixed_timing(
247    index: usize,
248    definition: GroundEffectDefinition,
249) -> Result<GroundEffectTiming, GroundEffectCompileError> {
250    if definition.pulse_count == 0 {
251        return Err(GroundEffectCompileError::MissingPulses { index });
252    }
253
254    if definition.pulse_count > 1 && definition.pulse_period_ms == 0 {
255        return Err(GroundEffectCompileError::MissingPulsePeriod { index });
256    }
257
258    let duration = definition
259        .pulse_period_ms
260        .saturating_mul(u32::from(definition.pulse_count));
261
262    Ok((
263        duration,
264        definition.pulse_period_ms,
265        false,
266        false,
267        definition.pulse_count,
268        false,
269    ))
270}