wowlab_engine_combat/state/program/
ground_effects.rs1use crate::state::{
4 DamageEffectRef, GroundEffectDefinition, RuntimeDamageDef,
5 program::{DamagePlan, DamageRequestBinding},
6};
7
8#[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}