1use wowlab_engine_domain::dbc::EffectLookup;
4use wowlab_engine_gamedata::ResolvedGameData;
5use wowlab_types::{combat::PeriodicDamageMode, numeric::round_to_decimals};
6
7use super::{AuraDefinitionDraft, PeriodicDamageDataInput};
8use crate::{
9 BuilderError, DamageFlags, DamagePayload,
10 builder::def::{PeriodicDef, PeriodicEffectDef},
11 state::{CastHookFn, LocalAuraIdx},
12};
13
14fn damage_payload_is_periodic(lookup: EffectLookup<'_>) -> bool {
15 !wowlab_engine_domain::dbc::spell_effect_is_any(
16 lookup.effect_type(),
17 &[
18 wowlab_engine_domain::dbc::SpellEffectKind::SchoolDamage,
19 wowlab_engine_domain::dbc::SpellEffectKind::HealthLeech,
20 ],
21 )
22}
23
24#[expect(
25 clippy::missing_errors_doc,
26 reason = "fluent aura methods uniformly return BuilderError for invalid data"
27)]
28impl AuraDefinitionDraft {
29 pub fn apply_periodic_behavior_from_data(
31 mut self,
32 data: &ResolvedGameData,
33 spell_id: u32,
34 ) -> Result<Self, BuilderError> {
35 let idx = wowlab_types::sim::SpellIdx::from_raw(spell_id);
36 let missing =
37 |field| crate::builder::BuilderErrorKind::MissingSpellData { spell_id, field };
38
39 self.refresh_behavior = data
40 .refresh_behavior(idx)
41 .ok_or_else(|| missing("refresh_behavior"))?;
42 self.pandemic = self.refresh_behavior == wowlab_types::data::RefreshBehavior::Pandemic;
43 let hasted = data
44 .hasted_ticks(idx)
45 .ok_or_else(|| missing("hasted_ticks"))?;
46 let rolling = data
47 .rolling_periodic(idx)
48 .ok_or_else(|| missing("rolling_periodic"))?;
49 let may_crit = data
50 .tick_may_crit(idx)
51 .ok_or_else(|| missing("tick_may_crit"))?;
52 let tick_on_application = data
53 .tick_on_application(idx)
54 .ok_or_else(|| missing("tick_on_application"))?;
55
56 if let Some(periodic) = self.periodic.as_mut() {
57 periodic.hasted = hasted;
58 periodic.may_crit = may_crit;
59 periodic.tick_on_application = tick_on_application;
60
61 if rolling {
62 periodic.effect = match periodic.effect {
63 PeriodicEffectDef::FlatDamage {
64 amount,
65 is_physical,
66 } => PeriodicEffectDef::RollingFlatDamage {
67 amount,
68 is_physical,
69 },
70 PeriodicEffectDef::Damage {
71 ap_coef,
72 is_physical,
73 } => PeriodicEffectDef::RollingDamage {
74 ap_coef,
75 is_physical,
76 },
77 PeriodicEffectDef::SpDamage {
78 sp_coef,
79 is_physical,
80 } => PeriodicEffectDef::RollingSpDamage {
81 sp_coef,
82 is_physical,
83 },
84 ref other => other.clone(),
85 };
86 }
87 }
88
89 Ok(self)
90 }
91
92 pub fn periodic_damage(mut self, tick_ms: u32, ap_coef: f64, flags: DamageFlags) -> Self {
93 self.periodic = Some(PeriodicDef::new(
94 tick_ms,
95 true,
96 PeriodicEffectDef::Damage {
97 ap_coef,
98 is_physical: flags.contains(DamageFlags::PHYSICAL),
99 },
100 ));
101
102 self
103 }
104
105 pub fn periodic_flat_damage_from_spell(mut self, tick_ms: u32, payload: DamagePayload) -> Self {
106 self.periodic = Some(PeriodicDef::new(
107 tick_ms,
108 true,
109 PeriodicEffectDef::FlatDamage {
110 amount: payload.coef,
111 is_physical: payload.flags.contains(DamageFlags::PHYSICAL),
112 },
113 ));
114
115 if let Some(periodic) = &mut self.periodic {
116 periodic.damage_spell_id = Some(payload.spell_id);
117 }
118
119 self
120 }
121
122 pub fn periodic_total_flat_damage_from_data(
124 self,
125 data: &ResolvedGameData,
126 tick_ms: u32,
127 spell_id: u32,
128 total: f64,
129 ) -> Result<Self, BuilderError> {
130 if self.duration_ms == 0 || tick_ms == 0 {
131 return Err(crate::builder::BuilderErrorKind::MissingSpellData {
132 spell_id,
133 field: "periodic_total_flat_damage_timing",
134 }
135 .into());
136 }
137
138 let total =
139 crate::builder::validated_nonnegative(total, spell_id, "periodic_total_flat_damage")?;
140 let tick_count = f64::from(self.duration_ms) / f64::from(tick_ms);
141 let idx = wowlab_types::sim::SpellIdx::from_raw(spell_id);
142 let is_physical = wowlab_engine_domain::dbc::is_physical(data, idx).ok_or(
143 crate::builder::BuilderErrorKind::MissingSpellData {
144 spell_id,
145 field: "is_physical",
146 },
147 )?;
148 let flags = if is_physical {
149 DamageFlags::PHYSICAL
150 } else {
151 DamageFlags::empty()
152 };
153
154 Ok(self.periodic_flat_damage_from_spell(
155 tick_ms,
156 DamagePayload::new(spell_id, total / tick_count, flags),
157 ))
158 }
159
160 pub fn periodic_rolling_damage(
161 mut self,
162 tick_ms: u32,
163 ap_coef: f64,
164 flags: DamageFlags,
165 ) -> Self {
166 self.periodic = Some(PeriodicDef::new(
167 tick_ms,
168 true,
169 PeriodicEffectDef::RollingDamage {
170 ap_coef,
171 is_physical: flags.contains(DamageFlags::PHYSICAL),
172 },
173 ));
174
175 self
176 }
177
178 pub fn periodic_damage_from_spell(mut self, tick_ms: u32, payload: DamagePayload) -> Self {
179 self = self.periodic_damage(tick_ms, payload.coef, payload.flags);
180
181 if let Some(periodic) = &mut self.periodic {
182 periodic.damage_spell_id = Some(payload.spell_id);
183 }
184
185 self
186 }
187
188 pub fn periodic_damage_sp(mut self, tick_ms: u32, sp_coef: f64, flags: DamageFlags) -> Self {
189 self.periodic = Some(PeriodicDef::new(
190 tick_ms,
191 true,
192 PeriodicEffectDef::SpDamage {
193 sp_coef,
194 is_physical: flags.contains(DamageFlags::PHYSICAL),
195 },
196 ));
197
198 self
199 }
200
201 pub fn periodic_tick_direct_damage(mut self) -> Self {
207 if let Some(periodic) = &mut self.periodic {
208 periodic.damage_is_periodic = false;
209 }
210
211 self
212 }
213
214 pub fn periodic_damage_sp_from_spell(mut self, tick_ms: u32, payload: DamagePayload) -> Self {
215 self = self.periodic_damage_sp(tick_ms, payload.coef, payload.flags);
216
217 if let Some(periodic) = &mut self.periodic {
218 periodic.damage_spell_id = Some(payload.spell_id);
219 }
220
221 self
222 }
223
224 pub fn periodic_max_health_damage(
225 mut self,
226 tick_ms: u32,
227 percent: f64,
228 is_physical: bool,
229 ) -> Self {
230 self.periodic = Some(PeriodicDef::new(
231 tick_ms,
232 true,
233 PeriodicEffectDef::MaxHealthDamage {
234 percent,
235 is_physical,
236 },
237 ));
238
239 self
240 }
241
242 pub fn periodic_damage_ap_from_data(
244 self,
245 data: &ResolvedGameData,
246 input: PeriodicDamageDataInput,
247 mode: PeriodicDamageMode,
248 ) -> Result<Self, BuilderError> {
249 let idx = wowlab_types::sim::SpellIdx::from_raw(input.spell_id);
250 let is_physical = wowlab_engine_domain::dbc::is_physical(data, idx).ok_or(
251 crate::builder::BuilderErrorKind::MissingSpellData {
252 spell_id: input.spell_id,
253 field: "is_physical",
254 },
255 )?;
256 let coefficient = data.ap_coef(idx, input.effect_index) * input.multiplier + input.offset;
257 let coefficient = input.round_decimals.map_or(coefficient, |decimals| {
258 round_to_decimals(coefficient, decimals)
259 });
260 let flags = if is_physical {
261 DamageFlags::PHYSICAL
262 } else {
263 DamageFlags::empty()
264 };
265 let mut builder = match mode {
266 PeriodicDamageMode::Standard => self.periodic_damage(input.tick_ms, coefficient, flags),
267 PeriodicDamageMode::Rolling => {
268 self.periodic_rolling_damage(input.tick_ms, coefficient, flags)
269 }
270 };
271
272 if let Some(periodic) = &mut builder.periodic {
273 periodic.damage_spell_id = Some(input.spell_id);
274 periodic.damage_is_periodic = damage_payload_is_periodic(EffectLookup::new(
275 data,
276 wowlab_types::sim::EffectRef::new(idx, input.effect_index),
277 ));
278 }
279
280 Ok(builder)
281 }
282
283 pub fn periodic_damage_sp_from_data(
285 self,
286 data: &ResolvedGameData,
287 input: PeriodicDamageDataInput,
288 ) -> Result<Self, BuilderError> {
289 let idx = wowlab_types::sim::SpellIdx::from_raw(input.spell_id);
290 let is_physical = wowlab_engine_domain::dbc::is_physical(data, idx).ok_or(
291 crate::builder::BuilderErrorKind::MissingSpellData {
292 spell_id: input.spell_id,
293 field: "is_physical",
294 },
295 )?;
296 let coefficient = data.sp_coef(idx, input.effect_index) * input.multiplier + input.offset;
297 let coefficient = input.round_decimals.map_or(coefficient, |decimals| {
298 round_to_decimals(coefficient, decimals)
299 });
300 let flags = if is_physical {
301 DamageFlags::PHYSICAL
302 } else {
303 DamageFlags::empty()
304 };
305
306 let mut builder = self.periodic_damage_sp_from_spell(
307 input.tick_ms,
308 DamagePayload::new(input.spell_id, coefficient, flags),
309 );
310
311 if let Some(periodic) = &mut builder.periodic {
312 periodic.damage_is_periodic = damage_payload_is_periodic(EffectLookup::new(
313 data,
314 wowlab_types::sim::EffectRef::new(idx, input.effect_index),
315 ));
316 }
317
318 Ok(builder)
319 }
320
321 pub fn periodic_residual_damage(mut self, tick_ms: u32) -> Self {
322 self.periodic = Some(PeriodicDef::new(
323 tick_ms,
324 false,
325 PeriodicEffectDef::ResidualDamage,
326 ));
327
328 self
329 }
330
331 pub fn periodic_resource(mut self, tick_ms: u32, amount: f64) -> Self {
332 self.periodic = Some(PeriodicDef::new(
333 tick_ms,
334 true,
335 PeriodicEffectDef::ResourceGain {
336 amount,
337 resource_type: None,
338 },
339 ));
340
341 self
342 }
343
344 pub fn periodic_resource_drain(mut self, tick_ms: u32, amount: f64) -> Self {
346 self.periodic = Some(PeriodicDef::new(
347 tick_ms,
348 true,
349 PeriodicEffectDef::ResourceDrain { amount },
350 ));
351
352 self
353 }
354
355 pub fn periodic_apply_aura(mut self, tick_ms: u32, target_aura_local: u8) -> Self {
357 self.periodic = Some(PeriodicDef::new(
358 tick_ms,
359 true,
360 PeriodicEffectDef::ApplyAura {
361 target_aura_local: LocalAuraIdx::new(target_aura_local),
362 },
363 ));
364
365 self
366 }
367
368 pub fn periodic_remove_stack(mut self, tick_ms: u32) -> Self {
370 self.periodic = Some(PeriodicDef::new(
371 tick_ms,
372 false,
373 PeriodicEffectDef::RemoveStack,
374 ));
375
376 self
377 }
378
379 pub fn periodic_stack_tick(mut self, tick_ms: u32) -> Self {
381 self.periodic = Some(PeriodicDef::new(tick_ms, false, PeriodicEffectDef::Hook));
382
383 self
384 }
385
386 pub fn periodic_damage_resource_gain(mut self, amount: f64) -> Self {
388 if let Some(periodic) = self.periodic.as_mut() {
389 periodic.resource_gain = amount;
390 }
391
392 self
393 }
394
395 pub fn periodic_damage_crit_resource_gain(mut self, chance: f64, amount: f64) -> Self {
397 if let Some(periodic) = self.periodic.as_mut() {
398 periodic.crit_resource_chance = chance;
399 periodic.crit_resource_gain = amount;
400 }
401
402 self
403 }
404
405 pub fn unhasted_periodic(mut self) -> Self {
407 if let Some(periodic) = self.periodic.as_mut() {
408 periodic.hasted = false;
409 }
410
411 self
412 }
413
414 pub fn hasted_periodic(mut self) -> Self {
416 if let Some(periodic) = self.periodic.as_mut() {
417 periodic.hasted = true;
418 }
419
420 self
421 }
422
423 pub fn partial_tick(mut self) -> Self {
425 if let Some(periodic) = self.periodic.as_mut() {
426 periodic.partial_tick = true;
427 }
428
429 self
430 }
431
432 pub fn apply_at_max_stacks(mut self) -> Self {
434 self.apply_at_max_stacks = true;
435
436 self
437 }
438
439 pub fn async_stacks(mut self) -> Self {
441 self.async_stacks = true;
442
443 self
444 }
445
446 pub fn periodic_damage_scales_with_stacks(mut self) -> Self {
448 self.periodic_damage_scales_with_stacks = true;
449
450 self
451 }
452
453 pub fn periodic_hook(mut self, tick_ms: u32) -> Self {
455 if self.periodic.is_none() {
456 self.periodic = Some(PeriodicDef::new(tick_ms, false, PeriodicEffectDef::Hook));
457 }
458
459 self
460 }
461
462 pub fn tick_hook(mut self, hook: CastHookFn) -> Self {
464 self.on_tick = Some(hook);
465
466 self
467 }
468
469 pub(super) fn periodic_resource_type(
470 mut self,
471 tick_ms: u32,
472 amount: f64,
473 resource_type: wowlab_types::combat::ResourceType,
474 ) -> Self {
475 self.periodic = Some(PeriodicDef::new(
476 tick_ms,
477 true,
478 PeriodicEffectDef::ResourceGain {
479 amount,
480 resource_type: Some(resource_type),
481 },
482 ));
483
484 self
485 }
486}