1use wowlab_engine_domain::dbc::{ResolvedGameDataSemanticExt as _, SpellAuraInterruptFlags};
2use wowlab_engine_gamedata::ResolvedGameData;
3use wowlab_types::constants::HUNDRED;
4
5use super::{
6 BuilderError,
7 def::{AuraOn, BuilderSpellEffect, PeriodicDef},
8};
9use crate::state::{
10 AuraTickBehavior, BuffEffect, CastHookFn, DynamicTickAction, LocalAuraIdx,
11 MAX_AURA_APPLICATION_EXCLUSIONS, MAX_AURA_BUFF_EFFECTS, SpellGroup, SpellGroupRule,
12};
13
14mod effects;
15mod finalize;
16mod from_data;
17mod health_threshold;
18mod input;
19mod numbers;
20mod periodic;
21use effects::{
22 apply_active_stat_effect, apply_actor_scope_from_data, apply_automatic_periodic_effect,
23 apply_critical_modifier_effect, apply_effect_points_modifier, apply_forwarded_value,
24 apply_non_passive_effect, apply_spell_modifier_effect, apply_timing_modifier_effect,
25};
26pub use input::PeriodicDamageDataInput;
27
28pub(crate) fn apply_forwarded_effect_value(
29 aura_id: u32,
30 effects: &mut [Option<BuffEffect>; MAX_AURA_BUFF_EFFECTS],
31 data: &ResolvedGameData,
32 value: f64,
33) -> usize {
34 let spell_id = wowlab_types::sim::SpellIdx::from_raw(aura_id);
35 let mut patched = 0;
36
37 for effect_index in 1..=data.max_effect_index(spell_id) {
38 if data.base_points(spell_id, effect_index).abs() > f64::EPSILON {
39 continue;
40 }
41
42 let subtype = data.effect_aura(spell_id, effect_index);
43
44 if effects
45 .iter_mut()
46 .flatten()
47 .any(|effect| apply_forwarded_value(effect, aura_id, effect_index, subtype, value))
48 {
49 patched += 1;
50 }
51 }
52
53 patched
54}
55
56#[derive(Debug)]
57#[must_use]
58#[expect(
59 clippy::struct_excessive_bools,
60 reason = "aura behavior flags are independent DBC coordinates, not mutually exclusive states"
61)]
62pub struct AuraDefinitionDraft {
63 pub(crate) name: String,
64 pub(crate) aura_id: u32,
65 pub(crate) on: AuraOn,
66 pub(crate) propagates_to_pet: bool,
67 pub(crate) duration_ms: u32,
68 pub(crate) doses: u8,
69 pub(crate) max_stacks: u8,
70 pub(crate) pandemic: bool,
71 pub(crate) refresh_behavior: wowlab_types::data::RefreshBehavior,
72 pub(crate) duration_hasted: bool,
73 pub(crate) shapeshift_form: i32,
74 pub(crate) shapeshift_form_flags: i32,
75 pub(crate) shapeshift_combat_round_time_ms: u32,
76 pub(crate) periodic: Option<PeriodicDef>,
77 pub(crate) effects: [Option<BuffEffect>; MAX_AURA_BUFF_EFFECTS],
78 pub(crate) spell_group: Option<SpellGroup>,
79 pub(crate) granted_aura_state: Option<crate::state::AuraState>,
80 pub(crate) crowd_control: Option<wowlab_engine_domain::dbc::CrowdControlSemantic>,
81 pub(crate) is_snapshot: bool,
82 pub(crate) dynamic_tick_action: DynamicTickAction,
83 pub(crate) apply_at_max_stacks: bool,
84 pub(crate) reverse: bool,
85 pub(crate) freeze_stacks: bool,
86 pub(crate) tick_behavior: AuraTickBehavior,
87 pub(crate) tick_stack_change: u8,
88 pub(crate) async_stacks: bool,
89 pub(crate) periodic_damage_scales_with_stacks: bool,
90 pub(crate) interrupt_flags: SpellAuraInterruptFlags,
91 pub(crate) reapply_on_expire: bool,
92 pub(crate) application_excluded_auras: [u32; MAX_AURA_APPLICATION_EXCLUSIONS],
93 pub(crate) application_followup_aura_id: u32,
94 pub(crate) apply_effects: Vec<BuilderSpellEffect>,
95 pub(crate) apply_profile_spell_id: u32,
96 pub(crate) on_consume: Vec<BuilderSpellEffect>,
97 pub(crate) tick_effects: Vec<BuilderSpellEffect>,
98 pub(crate) tick_profile_spell_id: u32,
99 pub(crate) expire_trigger_spell_id: u32,
100 pub(crate) expire_trigger_from_caster: bool,
101 pub(crate) on_expire: Option<CastHookFn>,
102 pub(crate) on_tick: Option<CastHookFn>,
103}
104
105#[expect(
106 clippy::missing_errors_doc,
107 reason = "fluent aura methods uniformly return BuilderError for invalid data or fixed-capacity effect overflow"
108)]
109impl AuraDefinitionDraft {
110 pub fn new(name: &str, id: u32) -> Self {
111 Self {
112 name: name.to_string(),
113 aura_id: id,
114 on: AuraOn::Player,
115 propagates_to_pet: false,
116 duration_ms: 0,
117 doses: 1,
118 max_stacks: 1,
119 pandemic: true,
120 refresh_behavior: wowlab_types::data::RefreshBehavior::Pandemic,
121 duration_hasted: false,
122 shapeshift_form: 0,
123 shapeshift_form_flags: 0,
124 shapeshift_combat_round_time_ms: 0,
125 periodic: None,
126 effects: [None; MAX_AURA_BUFF_EFFECTS],
127 spell_group: None,
128 granted_aura_state: None,
129 crowd_control: None,
130 is_snapshot: false,
131 dynamic_tick_action: DynamicTickAction::None,
132 apply_at_max_stacks: false,
133 reverse: false,
134 freeze_stacks: false,
135 tick_behavior: AuraTickBehavior::None,
136 tick_stack_change: 1,
137 async_stacks: false,
138 periodic_damage_scales_with_stacks: false,
139 interrupt_flags: SpellAuraInterruptFlags::empty(),
140 reapply_on_expire: false,
141 application_excluded_auras: [0; MAX_AURA_APPLICATION_EXCLUSIONS],
142 application_followup_aura_id: 0,
143 apply_effects: Vec::new(),
144 apply_profile_spell_id: 0,
145 on_consume: Vec::new(),
146 tick_effects: Vec::new(),
147 tick_profile_spell_id: 0,
148 expire_trigger_spell_id: 0,
149 expire_trigger_from_caster: false,
150 on_expire: None,
151 on_tick: None,
152 }
153 }
154
155 pub fn apply_base_from_data(
156 self,
157 data: &ResolvedGameData,
158 spell_id: u32,
159 ) -> Result<Self, BuilderError> {
160 let idx = wowlab_types::sim::SpellIdx::from_raw(spell_id);
161 let duration_ms = data.aura_duration_ms(idx).ok_or(
162 crate::builder::BuilderErrorKind::MissingSpellData {
163 spell_id,
164 field: "aura_duration_ms",
165 },
166 )?;
167 let folded_passive = data.is_folded_passive(idx);
168
169 if folded_passive {
170 tracing::debug!(
171 spell_id,
172 "statically folded passive registered as aura; skipping modifier semantics"
173 );
174 }
175
176 let mut aura = self
177 .duration_ms(duration_ms)
178 .doses(data.aura_doses(idx).unwrap_or(1))
179 .max_stacks(data.aura_max_stacks(idx).unwrap_or(1).max(1))
180 .apply_duration_behavior_from_data(data, spell_id)?;
181
182 if aura.granted_aura_state.is_none() {
183 aura.granted_aura_state = data.granted_aura_state(idx);
184 }
185
186 aura.interrupt_flags =
187 SpellAuraInterruptFlags::from_dbc(data.aura_interrupt_flags(idx).unwrap_or_default());
188
189 for effect_index in 1..=data.max_effect_index(idx) {
190 aura = from_data::apply_base_effect(
191 aura,
192 wowlab_engine_domain::dbc::EffectLookup::new(
193 data,
194 wowlab_types::sim::EffectRef::new(idx, effect_index),
195 ),
196 folded_passive,
197 )?;
198 }
199
200 if aura.async_stacks && aura.periodic.is_some() {
205 aura.periodic_damage_scales_with_stacks = true;
206 }
207
208 Ok(aura)
209 }
210
211 pub fn apply_periodic_effect_from_data(
213 mut self,
214 data: &ResolvedGameData,
215 spell_id: u32,
216 ) -> Result<Self, BuilderError> {
217 let idx = wowlab_types::sim::SpellIdx::from_raw(spell_id);
218
219 for effect_index in 1..=data.max_effect_index(idx) {
220 self = apply_automatic_periodic_effect(
221 self,
222 wowlab_engine_domain::dbc::EffectLookup::new(
223 data,
224 wowlab_types::sim::EffectRef::new(idx, effect_index),
225 ),
226 )?;
227 }
228
229 if self.async_stacks && self.periodic.is_some() {
230 self.periodic_damage_scales_with_stacks = true;
231 }
232
233 Ok(self)
234 }
235
236 pub fn apply_timing_modifiers_from_data(
238 mut self,
239 data: &ResolvedGameData,
240 spell_id: u32,
241 ) -> Result<Self, BuilderError> {
242 let idx = wowlab_types::sim::SpellIdx::from_raw(spell_id);
243
244 for effect_index in 1..=data.max_effect_index(idx) {
245 self = apply_timing_modifier_effect(
246 self,
247 wowlab_engine_domain::dbc::EffectLookup::new(
248 data,
249 wowlab_types::sim::EffectRef::new(idx, effect_index),
250 ),
251 )?;
252 }
253
254 Ok(self)
255 }
256
257 pub fn on_player(mut self) -> Self {
258 self.on = AuraOn::Player;
259
260 self
261 }
262
263 pub fn on_target(mut self) -> Self {
264 self.on = AuraOn::Target;
265
266 self
267 }
268
269 pub fn on_pet(mut self) -> Self {
270 self.on = AuraOn::Pet;
271
272 self
273 }
274
275 pub fn overkill_absorb_script(mut self, script: crate::state::OverkillAbsorbScriptFn) -> Self {
277 for effect in self.effects.iter_mut().flatten() {
278 if let BuffEffect::Absorb(absorb) = effect
279 && absorb.kind == crate::state::AbsorbKind::Overkill
280 {
281 absorb.overkill_script = Some(script);
282 }
283 }
284
285 self
286 }
287
288 pub fn duration_ms(mut self, ms: u32) -> Self {
289 self.duration_ms = ms;
290
291 self
292 }
293
294 pub fn trigger_spell_on_expire(mut self, spell_id: u32, from_caster: bool) -> Self {
295 self.expire_trigger_spell_id = spell_id;
296 self.expire_trigger_from_caster = from_caster;
297
298 self
299 }
300
301 pub fn max_stacks(mut self, stacks: u8) -> Self {
302 self.max_stacks = stacks;
303
304 self
305 }
306
307 pub fn reverse(mut self) -> Self {
309 self.reverse = true;
310 self.apply_at_max_stacks = true;
311
312 self
313 }
314
315 pub fn freeze_stacks(mut self) -> Self {
317 self.freeze_stacks = true;
318
319 self
320 }
321
322 pub fn clip_tick_behavior(mut self, amount: u8) -> Self {
324 self.tick_behavior = AuraTickBehavior::Clip;
325 self.tick_stack_change = amount.max(1);
326
327 self
328 }
329
330 pub fn refresh_tick_behavior(mut self, amount: u8) -> Self {
332 self.tick_behavior = AuraTickBehavior::Refresh;
333 self.tick_stack_change = amount.max(1);
334
335 self
336 }
337
338 pub fn dynamic_tick_update(mut self) -> Self {
340 self.dynamic_tick_action = DynamicTickAction::Update;
341
342 self
343 }
344
345 pub fn dynamic_tick_snapshot(mut self) -> Self {
347 self.dynamic_tick_action = DynamicTickAction::Snapshot;
348
349 self
350 }
351
352 pub fn doses(mut self, doses: u8) -> Self {
354 self.doses = doses.max(1);
355
356 self
357 }
358
359 pub fn no_pandemic(mut self) -> Self {
360 self.pandemic = false;
361 self.refresh_behavior = wowlab_types::data::RefreshBehavior::Duration;
362
363 self
364 }
365
366 pub fn pandemic_on_refresh(mut self) -> Self {
368 self.pandemic = true;
369 self.refresh_behavior = wowlab_types::data::RefreshBehavior::Pandemic;
370
371 self
372 }
373
374 pub fn extend_on_refresh(mut self) -> Self {
376 self.pandemic = false;
377 self.refresh_behavior = wowlab_types::data::RefreshBehavior::Extend;
378
379 self
380 }
381
382 pub fn carry_tick_on_refresh(mut self) -> Self {
384 self.pandemic = false;
385 self.refresh_behavior = wowlab_types::data::RefreshBehavior::Tick;
386
387 self
388 }
389
390 pub fn clip_on_refresh(mut self) -> Self {
392 self.pandemic = false;
393 self.refresh_behavior = wowlab_types::data::RefreshBehavior::Clip;
394
395 self
396 }
397
398 pub fn never_refresh(mut self) -> Self {
400 self.pandemic = false;
401 self.refresh_behavior = wowlab_types::data::RefreshBehavior::None;
402
403 self
404 }
405
406 pub fn duration_hasted(mut self) -> Self {
408 self.duration_hasted = true;
409
410 self
411 }
412
413 pub fn apply_duration_behavior_from_data(
415 mut self,
416 data: &ResolvedGameData,
417 spell_id: u32,
418 ) -> Result<Self, BuilderError> {
419 let idx = wowlab_types::sim::SpellIdx::from_raw(spell_id);
420 let duration_hasted = data.duration_hasted(idx).ok_or(
421 crate::builder::BuilderErrorKind::MissingSpellData {
422 spell_id,
423 field: "duration_hasted",
424 },
425 )?;
426
427 self.refresh_behavior = data.refresh_behavior(idx).ok_or(
428 crate::builder::BuilderErrorKind::MissingSpellData {
429 spell_id,
430 field: "refresh_behavior",
431 },
432 )?;
433 self.pandemic = self.refresh_behavior == wowlab_types::data::RefreshBehavior::Pandemic;
434 self.async_stacks = data.asynchronous_stacks(idx).ok_or(
435 crate::builder::BuilderErrorKind::MissingSpellData {
436 spell_id,
437 field: "asynchronous_stacks",
438 },
439 )?;
440
441 Ok(if duration_hasted {
442 self.duration_hasted()
443 } else {
444 self
445 })
446 }
447
448 pub fn reapply_on_expire(mut self) -> Self {
450 self.reapply_on_expire = true;
451
452 self
453 }
454
455 pub fn on_consume_extend(mut self, aura: LocalAuraIdx, amount_ms: u32) -> Self {
456 self.on_consume.push(BuilderSpellEffect::ExtendAura {
457 aura_local: aura,
458 amount_ms,
459 });
460
461 self
462 }
463
464 pub fn spell_cost_percent(
465 self,
466 percent: f64,
467 spells: &'static [u32],
468 ) -> Result<Self, BuilderError> {
469 self.push_effect(BuffEffect::SpellCostPercent(
470 percent,
471 spells,
472 crate::state::ResourcePool::Primary,
473 ))
474 }
475
476 pub fn spell_cost_flat(
477 self,
478 amount: f64,
479 spells: &'static [u32],
480 ) -> Result<Self, BuilderError> {
481 self.push_effect(BuffEffect::SpellCostFlat(
482 amount,
483 spells,
484 crate::state::ResourcePool::Primary,
485 ))
486 }
487
488 pub fn secondary_spell_cost_percent(
489 self,
490 percent: f64,
491 spells: &'static [u32],
492 ) -> Result<Self, BuilderError> {
493 self.push_effect(BuffEffect::SpellCostPercent(
494 percent,
495 spells,
496 crate::state::ResourcePool::Secondary,
497 ))
498 }
499
500 pub fn secondary_spell_cost_flat(
501 self,
502 amount: f64,
503 spells: &'static [u32],
504 ) -> Result<Self, BuilderError> {
505 self.push_effect(BuffEffect::SpellCostFlat(
506 amount,
507 spells,
508 crate::state::ResourcePool::Secondary,
509 ))
510 }
511
512 pub fn recharge_rate_percent(
513 self,
514 percent: f64,
515 spells: &'static [u32],
516 ) -> Result<Self, BuilderError> {
517 self.push_effect(BuffEffect::RechargeRatePercent(percent, spells))
518 }
519
520 pub fn cast_time_percent(
521 self,
522 percent: f64,
523 spells: &'static [u32],
524 ) -> Result<Self, BuilderError> {
525 self.push_effect(BuffEffect::CastTimePercent(percent, spells))
526 }
527
528 pub fn cast_time_percent_per_stack(
530 self,
531 percent: f64,
532 spells: &'static [u32],
533 ) -> Result<Self, BuilderError> {
534 self.push_effect(BuffEffect::CastTimePercentPerStack(percent, spells))
535 }
536
537 pub fn gcd_percent(self, percent: f64, spells: &'static [u32]) -> Result<Self, BuilderError> {
539 self.push_effect(BuffEffect::GcdPercent(percent, spells))
540 }
541
542 pub fn on_expire(mut self, hook: CastHookFn) -> Self {
544 self.on_expire = Some(hook);
545
546 self
547 }
548
549 pub fn rating_per_stack(
551 self,
552 data: &ResolvedGameData,
553 rating: wowlab_types::game::RatingType,
554 amount: f64,
555 ) -> Result<Self, BuilderError> {
556 let pct = wowlab_engine_domain::stats::rating_percent_points(data, rating, amount)
557 .map_err(|error| {
558 crate::builder::BuilderErrorKind::GameData(
559 wowlab_engine_ports::EngineError::spec_construction(format!(
560 "rating conversion failed: {error}"
561 )),
562 )
563 })?;
564 let effect = match rating {
565 wowlab_types::game::RatingType::Crit => BuffEffect::Crit(pct),
566 wowlab_types::game::RatingType::Haste => BuffEffect::Haste(pct),
567 wowlab_types::game::RatingType::Mastery => BuffEffect::Mastery(pct),
568 wowlab_types::game::RatingType::Versatility => BuffEffect::Versatility(pct),
569 other => {
570 return Err(crate::builder::BuilderErrorKind::GameData(
571 wowlab_engine_ports::EngineError::spec_construction(format!(
572 "unmodelled rating {other:?} for aura {} ({})",
573 self.name, self.aura_id
574 )),
575 )
576 .into());
577 }
578 };
579
580 self.push_effect(effect)
581 }
582
583 pub fn haste_per_stack(self, pct: f64) -> Result<Self, BuilderError> {
584 self.push_effect(BuffEffect::HasteMult(pct))
585 }
586
587 pub fn attack_speed_per_stack(self, pct: f64) -> Result<Self, BuilderError> {
589 self.push_effect(BuffEffect::AttackSpeed(pct))
590 }
591
592 pub fn auto_attack_damage_per_stack(self, pct: f64) -> Result<Self, BuilderError> {
594 self.push_effect(BuffEffect::AutoAttackDamage(pct))
595 }
596
597 pub fn forwarded_effect_value(
601 mut self,
602 data: &ResolvedGameData,
603 value: f64,
604 ) -> Result<Self, BuilderError> {
605 let patched = apply_forwarded_effect_value(self.aura_id, &mut self.effects, data, value);
606
607 if patched == 0
608 && data.max_effect_index(wowlab_types::sim::SpellIdx::from_raw(self.aura_id)) > 0
609 {
610 return Err(crate::builder::BuilderErrorKind::GameData(
611 wowlab_engine_ports::EngineError::spec_construction(format!(
612 "aura {} ({}) declares forwarded_effect_value but has no compatible zero-valued DBC effect",
613 self.name, self.aura_id
614 )),
615 )
616 .into());
617 }
618
619 Ok(self)
620 }
621
622 pub fn resource_gain_mult_per_stack(mut self, pct: f64) -> Result<Self, BuilderError> {
624 let mut replaced_narrow_gain = false;
625
626 for effect in self.effects.iter_mut().flatten() {
627 if matches!(effect, BuffEffect::RageGainMult(_)) {
628 *effect = BuffEffect::ResourceGainMult(pct);
629 replaced_narrow_gain = true;
630 }
631 }
632
633 if replaced_narrow_gain {
634 Ok(self)
635 } else {
636 self.push_effect(BuffEffect::ResourceGainMult(pct))
637 }
638 }
639
640 pub fn regen_percent_per_stack(self, pct: f64) -> Result<Self, BuilderError> {
642 self.push_effect(BuffEffect::RegenPercent(pct))
643 }
644
645 pub fn max_resource_per_stack(self, amount: f64) -> Result<Self, BuilderError> {
647 self.push_effect(BuffEffect::MaxResourceFlat(amount))
648 }
649
650 pub fn crit_per_stack(self, pct: f64) -> Result<Self, BuilderError> {
651 self.push_effect(BuffEffect::Crit(pct))
652 }
653
654 pub fn damage_taken_from_caster_spells(
660 mut self,
661 data: &ResolvedGameData,
662 pct: f64,
663 ) -> Result<Self, BuilderError> {
664 let mut found = false;
665
666 for effect in self.effects.iter_mut().flatten() {
667 if let BuffEffect::DamageTakenFromSourceSpells { percent, .. } = effect {
668 *percent = pct;
669 found = true;
670 }
671 }
672
673 if !found && data.max_effect_index(wowlab_types::sim::SpellIdx::from_raw(self.aura_id)) > 0
674 {
675 return Err(crate::builder::BuilderErrorKind::GameData(
676 wowlab_engine_ports::EngineError::spec_construction(format!(
677 "aura {} ({}) declares damage_taken_from_caster_spells but carries no aura-271 effect",
678 self.name, self.aura_id
679 )),
680 )
681 .into());
682 }
683
684 Ok(self)
685 }
686
687 pub fn mastery_per_stack(self, pct: f64) -> Result<Self, BuilderError> {
688 self.push_effect(BuffEffect::Mastery(pct))
689 }
690
691 pub fn vers_per_stack(self, pct: f64) -> Result<Self, BuilderError> {
692 self.push_effect(BuffEffect::Versatility(pct))
693 }
694
695 pub fn primary_per_stack(self, value: f64) -> Result<Self, BuilderError> {
697 self.push_effect(BuffEffect::PrimaryStat(value))
698 }
699
700 pub fn primary_pct_per_stack(self, pct: f64) -> Result<Self, BuilderError> {
702 self.push_effect(BuffEffect::PrimaryStatPercent(pct))
703 }
704
705 pub fn attack_power_pct_per_stack(self, pct: f64) -> Result<Self, BuilderError> {
707 self.push_effect(BuffEffect::AttackPowerPercent(pct))
708 }
709
710 pub fn crit_chance_spells(
712 self,
713 pct: f64,
714 spells: &'static [u32],
715 ) -> Result<Self, BuilderError> {
716 self.push_effect(BuffEffect::CritChanceSpells(pct, spells))
717 }
718
719 pub fn crit_damage_spells(
721 self,
722 pct: f64,
723 spells: &'static [u32],
724 ) -> Result<Self, BuilderError> {
725 self.push_effect(BuffEffect::CritDamageSpells(pct, spells))
726 }
727
728 pub fn damage_percent_spells(
730 self,
731 pct: f64,
732 spells: &'static [u32],
733 ) -> Result<Self, BuilderError> {
734 self.push_effect(BuffEffect::DamageMultSpells(1.0 + pct / HUNDRED, spells))
735 }
736
737 pub fn damage_percent_spells_linear(
739 self,
740 pct: f64,
741 spells: &'static [u32],
742 ) -> Result<Self, BuilderError> {
743 self.push_effect(BuffEffect::DamageMultSpellsLinear(pct, spells))
744 }
745
746 pub fn damage_multiplier(self, mult: f64) -> Result<Self, BuilderError> {
748 self.push_effect(BuffEffect::DamageMult(mult))
749 }
750
751 pub fn damage_multiplier_stacking(
753 self,
754 initial: f64,
755 per_stack: f64,
756 ) -> Result<Self, BuilderError> {
757 self.push_effect(BuffEffect::DamageMultStacking { initial, per_stack })
758 }
759
760 pub fn pet_damage_multiplier(self, mult: f64) -> Result<Self, BuilderError> {
762 self.push_effect(BuffEffect::PetDamageMult(mult))
763 }
764
765 pub fn spell_group(mut self, group_idx: u8, rule: SpellGroupRule) -> Self {
766 self.spell_group = Some(SpellGroup { group_idx, rule });
767
768 self
769 }
770
771 pub fn snapshot(mut self) -> Self {
773 self.is_snapshot = true;
774
775 self
776 }
777
778 fn push_effect(mut self, effect: BuffEffect) -> Result<Self, BuilderError> {
779 if crate::state::try_push_effect(&mut self.effects, effect).is_err() {
780 return Err(crate::builder::BuilderErrorKind::AuraEffectsFull {
781 aura_name: self.name.clone(),
782 aura_id: self.aura_id,
783 slot_count: MAX_AURA_BUFF_EFFECTS,
784 }
785 .into());
786 }
787
788 Ok(self)
789 }
790}
791
792#[cfg(test)]
793#[path = "aura_builder/tests.rs"]
794mod tests;