1use wowlab_engine_domain::{constants::OFF_HAND_DAMAGE_MULT, dbc::EffectLookup};
2use wowlab_engine_gamedata::ResolvedGameData;
3use wowlab_types::{constants::HUNDRED, sim::EffectRef};
4
5use super::SpellDefinitionDraft;
6use crate::{
7 DamageFlags,
8 builder::{
9 BuilderError,
10 def::{BuilderDamageDef, BuilderSpellEffect},
11 },
12 systems::damage_flags_from_data,
13};
14
15fn damage_geometry_effect(lookup: EffectLookup<'_>) -> EffectRef {
16 let effect_type = lookup.effect_type();
17
18 if wowlab_engine_domain::dbc::spell_effect_is_any(
19 effect_type,
20 &[
21 wowlab_engine_domain::dbc::SpellEffectKind::TriggerMissile,
22 wowlab_engine_domain::dbc::SpellEffectKind::TriggerSpell,
23 wowlab_engine_domain::dbc::SpellEffectKind::TriggerSpellWithValue,
24 wowlab_engine_domain::dbc::SpellEffectKind::TriggerSpell2,
25 ],
26 ) && let Ok(triggered) = wowlab_engine_domain::dbc::resolve_triggered_damage(lookup)
27 {
28 return EffectRef::new(triggered.spell_id, triggered.damage.effect_index);
29 }
30
31 lookup.effect
32}
33
34fn append_interrupt_effect(
35 spell: &mut SpellDefinitionDraft,
36 data: &ResolvedGameData,
37 spell_id: wowlab_types::sim::SpellIdx,
38 effect_type: i32,
39) {
40 if wowlab_engine_domain::dbc::spell_effect_is(
41 effect_type,
42 wowlab_engine_domain::dbc::SpellEffectKind::InterruptCast,
43 ) {
44 spell
45 .followup_effects
46 .push(BuilderSpellEffect::InterruptCast {
47 lockout_ms: data.aura_duration_ms(spell_id).unwrap_or_default(),
48 });
49 }
50}
51
52fn append_dispel_effect(
53 spell: &mut SpellDefinitionDraft,
54 lookup: EffectLookup<'_>,
55 effect_type: i32,
56) {
57 let steal = wowlab_engine_domain::dbc::spell_effect_is(
58 effect_type,
59 wowlab_engine_domain::dbc::SpellEffectKind::StealBeneficialBuff,
60 );
61
62 if steal
63 || wowlab_engine_domain::dbc::spell_effect_is(
64 effect_type,
65 wowlab_engine_domain::dbc::SpellEffectKind::Dispel,
66 )
67 {
68 spell.followup_effects.push(BuilderSpellEffect::Dispel {
69 dispel_type: lookup.effect_misc_value_0(),
70 steal,
71 });
72 }
73}
74
75#[derive(Clone, Copy, Debug, Default)]
76struct DamageEquipmentRequirements {
77 main_hand: bool,
78 off_hand: bool,
79 equipped_item: Option<wowlab_types::data::EquippedItemRequirement>,
80}
81
82#[expect(
83 clippy::struct_excessive_bools,
84 reason = "resolved spell behavior mirrors independent DBC flags"
85)]
86struct ResolvedSpellBehavior {
87 cooldown_hasted: bool,
88 gcd_haste_type: wowlab_types::combat::GcdHasteType,
89 hasted_ticks: bool,
90 duration_hasted: bool,
91 cannot_crit: bool,
92 requires_unshifted: bool,
93 allow_while_unshifted: bool,
94 requires_stealth: bool,
95 requires_behind_target: bool,
96 treat_as_area_effect: bool,
97 shapeshift_required_mask: u64,
98 shapeshift_excluded_mask: u64,
99 requires_main_hand: bool,
100 requires_off_hand: bool,
101 equipped_item_requirement: Option<wowlab_types::data::EquippedItemRequirement>,
102 breaks_stealth: bool,
103 usable_while_casting: bool,
104 usable_while_moving: bool,
105 explicit_target_mask: u32,
106 required_explicit_target_mask: u32,
107}
108
109#[expect(
110 clippy::struct_excessive_bools,
111 reason = "resolved timing behavior mirrors independent DBC flags"
112)]
113struct ResolvedTimingBehavior {
114 cooldown_hasted: bool,
115 gcd_haste_type: wowlab_types::combat::GcdHasteType,
116 hasted_ticks: bool,
117 duration_hasted: bool,
118 cannot_crit: bool,
119 breaks_stealth: bool,
120 usable_while_casting: bool,
121 usable_while_moving: bool,
122}
123
124#[expect(
125 clippy::struct_excessive_bools,
126 reason = "resolved targeting behavior mirrors independent DBC flags"
127)]
128struct ResolvedTargetBehavior {
129 requires_unshifted: bool,
130 allow_while_unshifted: bool,
131 requires_stealth: bool,
132 requires_behind_target: bool,
133 treat_as_area_effect: bool,
134 shapeshift_required_mask: u64,
135 shapeshift_excluded_mask: u64,
136 requires_main_hand: bool,
137 requires_off_hand: bool,
138 equipped_item_requirement: Option<wowlab_types::data::EquippedItemRequirement>,
139 explicit_target_mask: u32,
140 required_explicit_target_mask: u32,
141}
142
143fn resolve_timing_behavior(
144 data: &ResolvedGameData,
145 spell_id: u32,
146) -> Result<ResolvedTimingBehavior, BuilderError> {
147 let idx = wowlab_types::sim::SpellIdx::from_raw(spell_id);
148 let missing = |field| crate::builder::BuilderErrorKind::MissingSpellData { spell_id, field };
149
150 Ok(ResolvedTimingBehavior {
151 cooldown_hasted: data
152 .cooldown_hasted(idx)
153 .ok_or_else(|| missing("cooldown_hasted"))?,
154 gcd_haste_type: data
155 .gcd_haste_type(idx)
156 .ok_or_else(|| missing("gcd_haste_type"))?,
157 hasted_ticks: data
158 .hasted_ticks(idx)
159 .ok_or_else(|| missing("hasted_ticks"))?,
160 duration_hasted: data
161 .duration_hasted(idx)
162 .ok_or_else(|| missing("duration_hasted"))?,
163 cannot_crit: data
164 .cannot_crit(idx)
165 .ok_or_else(|| missing("cannot_crit"))?,
166 breaks_stealth: data
167 .breaks_stealth(idx)
168 .ok_or_else(|| missing("breaks_stealth"))?,
169 usable_while_casting: data
170 .usable_while_casting(idx)
171 .ok_or_else(|| missing("usable_while_casting"))?,
172 usable_while_moving: data
173 .usable_while_moving(idx)
174 .ok_or_else(|| missing("usable_while_moving"))?,
175 })
176}
177
178fn resolve_target_behavior(
179 data: &ResolvedGameData,
180 spell_id: u32,
181) -> Result<ResolvedTargetBehavior, BuilderError> {
182 let idx = wowlab_types::sim::SpellIdx::from_raw(spell_id);
183 let missing = |field| crate::builder::BuilderErrorKind::MissingSpellData { spell_id, field };
184
185 Ok(ResolvedTargetBehavior {
186 requires_unshifted: data
187 .requires_unshifted(idx)
188 .ok_or_else(|| missing("requires_unshifted"))?,
189 allow_while_unshifted: data
190 .allow_while_unshifted(idx)
191 .ok_or_else(|| missing("allow_while_unshifted"))?,
192 requires_stealth: data
193 .requires_stealth(idx)
194 .ok_or_else(|| missing("requires_stealth"))?,
195 requires_behind_target: data
196 .requires_behind_target(idx)
197 .ok_or_else(|| missing("requires_behind_target"))?,
198 treat_as_area_effect: data
199 .treat_as_area_effect(idx)
200 .ok_or_else(|| missing("treat_as_area_effect"))?,
201 shapeshift_required_mask: data
202 .shapeshift_required_mask(idx)
203 .ok_or_else(|| missing("shapeshift_required_mask"))?,
204 shapeshift_excluded_mask: data
205 .shapeshift_excluded_mask(idx)
206 .ok_or_else(|| missing("shapeshift_excluded_mask"))?,
207 requires_main_hand: data
208 .requires_main_hand(idx)
209 .ok_or_else(|| missing("requires_main_hand"))?,
210 requires_off_hand: data
211 .requires_off_hand(idx)
212 .ok_or_else(|| missing("requires_off_hand"))?,
213 equipped_item_requirement: data.equipped_item_requirement(idx),
214 explicit_target_mask: data
215 .explicit_target_mask(idx)
216 .ok_or_else(|| missing("explicit_target_mask"))?,
217 required_explicit_target_mask: data
218 .required_explicit_target_mask(idx)
219 .ok_or_else(|| missing("required_explicit_target_mask"))?,
220 })
221}
222
223fn resolve_spell_behavior(
224 data: &ResolvedGameData,
225 spell_id: u32,
226) -> Result<ResolvedSpellBehavior, BuilderError> {
227 let timing = resolve_timing_behavior(data, spell_id)?;
228 let target = resolve_target_behavior(data, spell_id)?;
229
230 Ok(ResolvedSpellBehavior {
231 cooldown_hasted: timing.cooldown_hasted,
232 gcd_haste_type: timing.gcd_haste_type,
233 hasted_ticks: timing.hasted_ticks,
234 duration_hasted: timing.duration_hasted,
235 cannot_crit: timing.cannot_crit,
236 requires_unshifted: target.requires_unshifted,
237 allow_while_unshifted: target.allow_while_unshifted,
238 requires_stealth: target.requires_stealth,
239 requires_behind_target: target.requires_behind_target,
240 treat_as_area_effect: target.treat_as_area_effect,
241 shapeshift_required_mask: target.shapeshift_required_mask,
242 shapeshift_excluded_mask: target.shapeshift_excluded_mask,
243 requires_main_hand: target.requires_main_hand,
244 requires_off_hand: target.requires_off_hand,
245 equipped_item_requirement: target.equipped_item_requirement,
246 breaks_stealth: timing.breaks_stealth,
247 usable_while_casting: timing.usable_while_casting,
248 usable_while_moving: timing.usable_while_moving,
249 explicit_target_mask: target.explicit_target_mask,
250 required_explicit_target_mask: target.required_explicit_target_mask,
251 })
252}
253
254#[expect(
255 clippy::missing_errors_doc,
256 reason = "DBC-backed fluent methods uniformly return BuilderError when required spell data is absent or invalid"
257)]
258impl SpellDefinitionDraft {
259 pub fn additional_damage_ap_from_data(
260 self,
261 lookup: EffectLookup<'_>,
262 multiplier: f64,
263 ) -> Result<Self, BuilderError> {
264 self.additional_damage_ap_from_data_typed(
265 lookup,
266 multiplier,
267 crate::state::WeaponApType::MainHand,
268 )
269 }
270
271 pub fn additional_damage_ap_from_data_typed(
272 self,
273 lookup: EffectLookup<'_>,
274 multiplier: f64,
275 ap_type: crate::state::WeaponApType,
276 ) -> Result<Self, BuilderError> {
277 let data = lookup.data;
278 let idx = lookup.effect.spell;
279 let spell_id = idx.as_u32();
280 let effect_index = lookup.effect.effect_index;
281 let is_physical = wowlab_engine_domain::dbc::is_physical(data, idx).ok_or(
282 crate::builder::BuilderErrorKind::MissingSpellData {
283 spell_id,
284 field: "is_physical",
285 },
286 )?;
287
288 Ok(self.additional_damage(
289 crate::state::DamageEffectRef::new(spell_id, effect_index),
290 BuilderDamageDef::ApCoefficient {
291 coef: data.ap_coef(idx, effect_index) * multiplier,
292 is_physical,
293 ap_type,
294 },
295 data.base_points(idx, effect_index),
296 !data.cannot_crit(idx).unwrap_or(false),
297 damage_flags_from_data(data, idx, is_physical),
298 DamageEquipmentRequirements::default(),
299 ))
300 }
301
302 pub fn additional_damage_sp_from_data(
303 self,
304 lookup: EffectLookup<'_>,
305 multiplier: f64,
306 ) -> Result<Self, BuilderError> {
307 let data = lookup.data;
308 let idx = lookup.effect.spell;
309 let spell_id = idx.as_u32();
310 let effect_index = lookup.effect.effect_index;
311 let is_physical = wowlab_engine_domain::dbc::is_physical(data, idx).ok_or(
312 crate::builder::BuilderErrorKind::MissingSpellData {
313 spell_id,
314 field: "is_physical",
315 },
316 )?;
317
318 Ok(self.additional_damage(
319 crate::state::DamageEffectRef::new(spell_id, effect_index),
320 BuilderDamageDef::SpCoefficient {
321 coef: data.sp_coef(idx, effect_index) * multiplier,
322 is_physical,
323 },
324 data.base_points(idx, effect_index),
325 !data.cannot_crit(idx).unwrap_or(false),
326 damage_flags_from_data(data, idx, is_physical),
327 DamageEquipmentRequirements::default(),
328 ))
329 }
330
331 pub fn triggered_damage_from_data(
333 self,
334 lookup: EffectLookup<'_>,
335 multiplier: f64,
336 ) -> Result<Self, BuilderError> {
337 let data = lookup.data;
338 let triggered = wowlab_engine_domain::dbc::resolve_triggered_damage(lookup)?;
339 let may_crit = !data.cannot_crit(triggered.spell_id).unwrap_or(false);
340 let attribute_flags =
341 damage_flags_from_data(data, triggered.spell_id, triggered.damage.is_physical);
342
343 self.additional_triggered_damage(triggered, multiplier, may_crit, attribute_flags)
344 }
345
346 pub fn apply_cooldown_pools_from_data(
348 self,
349 data: &ResolvedGameData,
350 spell_id: u32,
351 ) -> Result<Self, BuilderError> {
352 let idx = wowlab_types::sim::SpellIdx::from_raw(spell_id);
353 let missing =
354 |field| crate::builder::BuilderErrorKind::MissingSpellData { spell_id, field };
355 let cooldown_s = data.cooldown_s(idx).ok_or_else(|| missing("cooldown_s"))?;
356 let charges = data.charges(idx).ok_or_else(|| missing("charges"))?;
357 let charge_cd_s = data
358 .charge_cd_s(idx)
359 .ok_or_else(|| missing("charge_cd_s"))?;
360 let category_cooldown_s = data
361 .category_cooldown_s(idx)
362 .ok_or_else(|| missing("category_cooldown_s"))?;
363
364 let mut spell = self
365 .cooldown(cooldown_s)
366 .charges(charges, charge_cd_s)
367 .cooldown_categories(
368 data.cooldown_category(idx),
369 category_cooldown_s,
370 data.charge_category(idx),
371 )
372 .start_recovery_category(data.start_recovery_category(idx));
373
374 if data.is_empower_spell(idx) {
375 spell = spell.apply_empower_from_data(data, spell_id)?;
376 }
377
378 Ok(spell)
379 }
380
381 pub fn apply_base_from_data(
385 self,
386 data: &ResolvedGameData,
387 spell_id: u32,
388 ) -> Result<Self, BuilderError> {
389 let idx = wowlab_types::sim::SpellIdx::from_raw(spell_id);
390 let missing =
391 |field| crate::builder::BuilderErrorKind::MissingSpellData { spell_id, field };
392
393 let cast_time_ms = data
394 .cast_time_ms(idx)
395 .ok_or_else(|| missing("cast_time_ms"))?;
396 let behavior = resolve_spell_behavior(data, spell_id)?;
397 let gcd_ms = data.gcd_ms(idx).ok_or_else(|| missing("gcd_ms"))?;
398 let resources = data
399 .spell_resources(idx)
400 .ok_or_else(|| missing("spell_resources"))?;
401 let damage =
402 wowlab_engine_domain::dbc::damage_def(data, idx).ok_or_else(|| missing("damage"))?;
403 let geometry = damage_geometry_effect(EffectLookup::new(
404 data,
405 EffectRef::new(idx, damage.effect_index),
406 ));
407 let target_plan = wowlab_engine_domain::targeting::compile_target_plan(
408 wowlab_engine_domain::targeting::TargetPlanInput {
409 spell_id: geometry.spell.as_u32(),
410 effect_index: geometry.effect_index,
411 effect_type: data.effect_type(geometry.spell, geometry.effect_index),
412 target_a: data.implicit_target_a(geometry.spell, geometry.effect_index),
413 target_b: data.implicit_target_b(geometry.spell, geometry.effect_index),
414 },
415 )?;
416
417 let mut spell = self
418 .apply_cooldown_pools_from_data(data, spell_id)?
419 .cost(resources.primary_cost)
420 .optional_cost(resources.primary_optional_cost)
421 .gcd(gcd_ms)
422 .gain(resources.primary_gain)
423 .secondary_cost(resources.secondary_cost)
424 .secondary_optional_cost(resources.secondary_optional_cost)
425 .secondary_gain(resources.secondary_gain)
426 .damage_auto(damage)?
427 .damage_effect(geometry.spell.as_u32(), geometry.effect_index);
428
429 if !data.is_empower_spell(idx) {
430 spell = spell.cast_time(cast_time_ms);
431 }
432
433 spell.cooldown_hasted = behavior.cooldown_hasted;
434 spell.gcd_haste_type = behavior.gcd_haste_type;
435 spell.damage_may_crit = !behavior.cannot_crit;
436 spell.channel_tick_may_crit = !behavior.cannot_crit;
437 spell.damage_attribute_flags = damage_flags_from_data(data, idx, damage.is_physical);
438 spell.requires_unshifted = behavior.requires_unshifted;
439 spell.allow_while_unshifted = behavior.allow_while_unshifted;
440 spell.requires_stealth = behavior.requires_stealth;
441 spell.requires_behind_target = behavior.requires_behind_target;
442 spell.is_aoe |= behavior.treat_as_area_effect
443 || wowlab_engine_domain::targeting::target_plan_is_multi_target(&target_plan);
444 spell.shapeshift_required_mask = behavior.shapeshift_required_mask;
445 spell.shapeshift_excluded_mask = behavior.shapeshift_excluded_mask;
446 spell.caster_aura_spell = data.caster_aura_spell(idx).unwrap_or_default();
447 spell.caster_aura_state = data
448 .caster_aura_state(idx)
449 .and_then(wowlab_engine_domain::dbc::AuraState::from_dbc);
450 spell.exclude_caster_aura_spell = data.exclude_caster_aura_spell(idx).unwrap_or_default();
451 spell.exclude_caster_aura_state = data
452 .exclude_caster_aura_state(idx)
453 .and_then(wowlab_engine_domain::dbc::AuraState::from_dbc);
454 spell.target_aura_spell = data.target_aura_spell(idx).unwrap_or_default();
455 spell.target_aura_state = data
456 .target_aura_state(idx)
457 .and_then(wowlab_engine_domain::dbc::AuraState::from_dbc);
458 spell.exclude_target_aura_spell = data.exclude_target_aura_spell(idx).unwrap_or_default();
459 spell.exclude_target_aura_state = data
460 .exclude_target_aura_state(idx)
461 .and_then(wowlab_engine_domain::dbc::AuraState::from_dbc);
462 spell.requires_main_hand = behavior.requires_main_hand;
463 spell.requires_off_hand = behavior.requires_off_hand;
464 spell.equipped_item_requirement = behavior.equipped_item_requirement;
465 spell.breaks_stealth = behavior.breaks_stealth;
466 spell.usable_while_casting = behavior.usable_while_casting;
467 spell.usable_while_moving = behavior.usable_while_moving;
468 spell.explicit_target_mask = behavior.explicit_target_mask;
469 spell.required_explicit_target_mask = behavior.required_explicit_target_mask;
470 spell.resource_cost_pct = resources.primary_cost_pct;
471 spell.maximum_resource_cost_pct = resources.primary_max_cost_pct;
472 spell.optional_resource_cost_pct = resources.primary_optional_cost_pct;
473 spell.health_cost = resources.health_cost;
474 spell.health_cost_pct = resources.health_cost_pct;
475 spell.health_max_cost_pct = resources.health_max_cost_pct;
476 spell.health_optional_cost = resources.health_optional_cost;
477 spell.health_optional_cost_pct = resources.health_optional_cost_pct;
478 spell.channel_hasted_ticks = behavior.hasted_ticks;
479 spell.channel_duration_hasted = behavior.duration_hasted;
480 spell.channel_tick_interval_ms = data.aura_tick_ms(idx).unwrap_or_default();
481 spell.channel_tick_zero = data.tick_on_application(idx).unwrap_or_default();
482 spell.projectile_speed = data
483 .projectile_speed(idx)
484 .ok_or_else(|| missing("projectile_speed"))?;
485 spell.launch_delay_s = data
486 .launch_delay_s(idx)
487 .ok_or_else(|| missing("launch_delay_s"))?;
488 spell.dbc_fixed_travel_time = data
489 .fixed_travel_time(idx)
490 .ok_or_else(|| missing("fixed_travel_time"))?;
491
492 let mut has_heal = false;
493 let mut has_damage = false;
494
495 for effect_index in 1..=data.max_effect_index(idx) {
496 let effect_type = data.effect_type(idx, effect_index);
497
498 has_damage |= wowlab_engine_domain::dbc::spell_effect_is_any(
499 effect_type,
500 &[
501 wowlab_engine_domain::dbc::SpellEffectKind::SchoolDamage,
502 wowlab_engine_domain::dbc::SpellEffectKind::HealthLeech,
503 wowlab_engine_domain::dbc::SpellEffectKind::WeaponDamageNoSchool,
504 wowlab_engine_domain::dbc::SpellEffectKind::WeaponPercentDamage,
505 wowlab_engine_domain::dbc::SpellEffectKind::WeaponDamage,
506 wowlab_engine_domain::dbc::SpellEffectKind::NormalizedWeaponDamage,
507 ],
508 );
509
510 append_interrupt_effect(&mut spell, data, idx, effect_type);
511 append_dispel_effect(
512 &mut spell,
513 EffectLookup::new(data, EffectRef::new(idx, effect_index)),
514 effect_type,
515 );
516
517 if !wowlab_engine_domain::dbc::spell_effect_is_any(
518 effect_type,
519 &[
520 wowlab_engine_domain::dbc::SpellEffectKind::Heal,
521 wowlab_engine_domain::dbc::SpellEffectKind::HealMaxHealthPercent,
522 wowlab_engine_domain::dbc::SpellEffectKind::DirectHealPercent,
523 ],
524 ) {
525 continue;
526 }
527
528 has_heal = true;
529 let is_percent = wowlab_engine_domain::dbc::spell_effect_is_any(
530 effect_type,
531 &[
532 wowlab_engine_domain::dbc::SpellEffectKind::HealMaxHealthPercent,
533 wowlab_engine_domain::dbc::SpellEffectKind::DirectHealPercent,
534 ],
535 );
536
537 spell.followup_effects.push(BuilderSpellEffect::Heal {
538 effect: Some(crate::state::DamageEffectRef::new(spell_id, effect_index)),
539 base: if is_percent {
540 0.0
541 } else {
542 data.base_points(idx, effect_index).max(0.0)
543 },
544 ap_coef: if is_percent {
545 0.0
546 } else {
547 data.ap_coef(idx, effect_index)
548 },
549 sp_coef: if is_percent {
550 0.0
551 } else {
552 data.sp_coef(idx, effect_index)
553 },
554 percent_of_max: if is_percent {
555 data.base_points(idx, effect_index).max(0.0) / HUNDRED
556 } else {
557 0.0
558 },
559 may_crit: !behavior.cannot_crit,
560 });
561 }
562
563 if has_heal && !has_damage {
564 spell = spell.without_data_damage();
565 }
566
567 Ok(spell)
568 }
569
570 pub fn apply_aoe_from_data(mut self, data: &ResolvedGameData) -> Self {
572 let Some(effect) = self.damage_effect else {
573 return self;
574 };
575 let idx = wowlab_types::sim::SpellIdx::from_raw(effect.spell_id);
576 let geometry = damage_geometry_effect(EffectLookup::new(
577 data,
578 EffectRef::new(idx, effect.effect_index),
579 ));
580 let target_plan = wowlab_engine_domain::targeting::compile_target_plan(
581 wowlab_engine_domain::targeting::TargetPlanInput {
582 spell_id: geometry.spell.as_u32(),
583 effect_index: geometry.effect_index,
584 effect_type: data.effect_type(geometry.spell, geometry.effect_index),
585 target_a: data.implicit_target_a(geometry.spell, geometry.effect_index),
586 target_b: data.implicit_target_b(geometry.spell, geometry.effect_index),
587 },
588 );
589
590 self.is_aoe |= data.treat_as_area_effect(idx).unwrap_or(false)
591 || data.treat_as_area_effect(geometry.spell).unwrap_or(false)
592 || target_plan
593 .as_ref()
594 .is_ok_and(wowlab_engine_domain::targeting::target_plan_is_multi_target);
595 let max_targets = wowlab_types::numeric::i32_to_u8_saturating(
596 data.chain_targets(geometry.spell, geometry.effect_index),
597 );
598 let chain_multiplier = data.chain_multiplier(geometry.spell, geometry.effect_index);
599
600 self.chain_targets(max_targets)
601 .chain_multiplier(chain_multiplier)
602 }
603
604 pub fn damage_ap_from_data(self, lookup: EffectLookup<'_>) -> Result<Self, BuilderError> {
606 let data = lookup.data;
607 let idx = lookup.effect.spell;
608 let spell_id = idx.as_u32();
609 let effect_index = lookup.effect.effect_index;
610 let coef = lookup.ap_coef();
611 let is_physical = wowlab_engine_domain::dbc::is_physical(data, idx).ok_or(
612 crate::builder::BuilderErrorKind::MissingSpellData {
613 spell_id,
614 field: "is_physical",
615 },
616 )?;
617 let flags = damage_flags_from_data(data, idx, is_physical);
618
619 Ok(self
620 .damage_ap(coef, flags)
621 .damage_effect(spell_id, effect_index))
622 }
623
624 pub fn damage_ap_from_data_typed(
626 self,
627 lookup: EffectLookup<'_>,
628 ap_type: crate::state::WeaponApType,
629 ) -> Result<Self, BuilderError> {
630 let data = lookup.data;
631 let idx = lookup.effect.spell;
632 let spell_id = idx.as_u32();
633 let effect_index = lookup.effect.effect_index;
634 let coef = lookup.ap_coef();
635 let is_physical = wowlab_engine_domain::dbc::is_physical(data, idx).ok_or(
636 crate::builder::BuilderErrorKind::MissingSpellData {
637 spell_id,
638 field: "is_physical",
639 },
640 )?;
641 let flags = damage_flags_from_data(data, idx, is_physical);
642
643 Ok(self
644 .damage_ap_typed(coef, flags, ap_type)
645 .damage_effect(spell_id, effect_index))
646 }
647
648 pub fn damage_sp_from_data(self, lookup: EffectLookup<'_>) -> Result<Self, BuilderError> {
650 let data = lookup.data;
651 let idx = lookup.effect.spell;
652 let spell_id = idx.as_u32();
653 let effect_index = lookup.effect.effect_index;
654 let coef = lookup.sp_coef();
655 let is_physical = wowlab_engine_domain::dbc::is_physical(data, idx).ok_or(
656 crate::builder::BuilderErrorKind::MissingSpellData {
657 spell_id,
658 field: "is_physical",
659 },
660 )?;
661 let flags = damage_flags_from_data(data, idx, is_physical);
662
663 Ok(self
664 .damage_sp(coef, flags)
665 .damage_effect(spell_id, effect_index))
666 }
667
668 pub fn channel_tick_damage_ap_from_data(
670 self,
671 lookup: EffectLookup<'_>,
672 ) -> Result<Self, BuilderError> {
673 let data = lookup.data;
674 let idx = lookup.effect.spell;
675 let spell_id = idx.as_u32();
676 let coef = lookup.ap_coef();
677 let is_physical = wowlab_engine_domain::dbc::is_physical(data, idx).ok_or(
678 crate::builder::BuilderErrorKind::MissingSpellData {
679 spell_id,
680 field: "is_physical",
681 },
682 )?;
683 let flags = damage_flags_from_data(data, idx, is_physical);
684
685 Ok(self
686 .channel_tick_damage_ap(coef, flags)
687 .channel_tick_source(spell_id))
688 }
689
690 pub fn channel_tick_damage_sp_from_data(
692 self,
693 lookup: EffectLookup<'_>,
694 ) -> Result<Self, BuilderError> {
695 let data = lookup.data;
696 let idx = lookup.effect.spell;
697 let spell_id = idx.as_u32();
698 let coef = lookup.sp_coef();
699 let is_physical = wowlab_engine_domain::dbc::is_physical(data, idx).ok_or(
700 crate::builder::BuilderErrorKind::MissingSpellData {
701 spell_id,
702 field: "is_physical",
703 },
704 )?;
705 let flags = damage_flags_from_data(data, idx, is_physical);
706
707 Ok(self
708 .channel_tick_damage_sp(coef, flags)
709 .channel_tick_source(spell_id))
710 }
711
712 pub fn channel_tick_damage_alt_ap_from_data(
714 self,
715 lookup: EffectLookup<'_>,
716 aura_id: u32,
717 ) -> Result<Self, BuilderError> {
718 let data = lookup.data;
719 let idx = lookup.effect.spell;
720 let spell_id = idx.as_u32();
721 let coef = lookup.ap_coef();
722 let is_physical = wowlab_engine_domain::dbc::is_physical(data, idx).ok_or(
723 crate::builder::BuilderErrorKind::MissingSpellData {
724 spell_id,
725 field: "is_physical",
726 },
727 )?;
728
729 Ok(self.channel_tick_damage_alt_ap(
730 coef,
731 damage_flags_from_data(data, idx, is_physical),
732 spell_id,
733 aura_id,
734 ))
735 }
736
737 pub fn channel_tick_damage_alt_sp_from_data(
739 self,
740 lookup: EffectLookup<'_>,
741 aura_id: u32,
742 ) -> Result<Self, BuilderError> {
743 let data = lookup.data;
744 let idx = lookup.effect.spell;
745 let spell_id = idx.as_u32();
746 let coef = lookup.sp_coef();
747 let is_physical = wowlab_engine_domain::dbc::is_physical(data, idx).ok_or(
748 crate::builder::BuilderErrorKind::MissingSpellData {
749 spell_id,
750 field: "is_physical",
751 },
752 )?;
753
754 Ok(self.channel_tick_damage_alt_sp(
755 coef,
756 damage_flags_from_data(data, idx, is_physical),
757 spell_id,
758 aura_id,
759 ))
760 }
761
762 fn apply_empower_from_data(
763 mut self,
764 data: &ResolvedGameData,
765 spell_id: u32,
766 ) -> Result<Self, BuilderError> {
767 let cast_times_ms =
768 data.require_empower_cast_times_ms(wowlab_types::sim::SpellIdx::from_raw(spell_id))?;
769
770 u8::try_from(cast_times_ms.len()).map_err(|source| {
771 crate::builder::BuilderErrorKind::InvalidEmpowerRankCount {
772 spell_id,
773 rank_count: cast_times_ms.len(),
774 source,
775 }
776 })?;
777
778 self.cast_time_ms = cast_times_ms.first().copied().unwrap_or(0);
779 self.empower_cast_times_ms = cast_times_ms;
780
781 Ok(self)
782 }
783
784 fn additional_damage(
785 mut self,
786 effect: crate::state::DamageEffectRef,
787 damage: BuilderDamageDef,
788 base_points: f64,
789 may_crit: bool,
790 attribute_flags: DamageFlags,
791 equipment: DamageEquipmentRequirements,
792 ) -> Self {
793 self.followup_effects.push(BuilderSpellEffect::Damage {
794 effect,
795 damage,
796 base_points,
797 may_crit,
798 attribute_flags,
799 requires_main_hand: equipment.main_hand,
800 requires_off_hand: equipment.off_hand,
801 equipped_item_requirement: equipment.equipped_item,
802 });
803
804 self
805 }
806
807 fn additional_triggered_damage(
808 self,
809 triggered: wowlab_engine_domain::dbc::TriggeredDamage,
810 multiplier: f64,
811 may_crit: bool,
812 attribute_flags: DamageFlags,
813 ) -> Result<Self, BuilderError> {
814 if self.followup_effects.iter().any(|effect| {
815 matches!(
816 effect,
817 BuilderSpellEffect::Damage { effect, .. }
818 if effect.spell_id == triggered.spell_id.as_u32()
819 && effect.effect_index == triggered.damage.effect_index
820 )
821 }) {
822 return Ok(self);
823 }
824
825 let damage = BuilderDamageDef::from_resolved(triggered.damage, OFF_HAND_DAMAGE_MULT)
826 .ok_or(crate::builder::BuilderErrorKind::UnsupportedDamageKind)?
827 .scaled(multiplier);
828 let base_points = crate::state::resolved_damage_base_points(triggered.damage, multiplier);
829
830 Ok(self.additional_damage(
831 crate::state::DamageEffectRef::new(
832 triggered.spell_id.as_u32(),
833 triggered.damage.effect_index,
834 ),
835 damage,
836 base_points,
837 may_crit,
838 attribute_flags,
839 DamageEquipmentRequirements {
840 main_hand: triggered.requires_main_hand,
841 off_hand: triggered.requires_off_hand,
842 equipped_item: triggered.equipped_item_requirement,
843 },
844 ))
845 }
846}
847
848#[cfg(test)]
849#[path = "from_data/tests.rs"]
850mod tests;