1use super::{
2 BuilderDamageDef, BuilderSpellEffect, BuilderTriggerProgramPredicate, CastHookFn, CdrEffect,
3 CooldownCategoryId, DEFAULT_GCD_MS, DamageEffectRef, DamageFlags, EmpowerReleaseHookFn,
4 LocalAuraIdx, TriggerProgramPredicateFn,
5};
6
7#[derive(Debug)]
9#[must_use]
10#[expect(
11 clippy::struct_excessive_bools,
12 reason = "spell behavior flags are independent DBC coordinates, not mutually exclusive states"
13)]
14pub struct SpellDefinitionDraft {
15 pub(crate) cast_time_ms: u32,
16 pub(crate) empower_cast_times_ms: Vec<u32>,
17 pub(crate) max_empower_aura_id: u32,
18 pub(crate) cooldown_hasted: bool,
19 pub(crate) gcd_haste_type: wowlab_types::combat::GcdHasteType,
20 pub(crate) cooldown_category: Option<CooldownCategoryId>,
21 pub(crate) category_cooldown_secs: f64,
22 pub(crate) charge_category: Option<CooldownCategoryId>,
23 pub(crate) start_recovery_category: Option<CooldownCategoryId>,
24 pub(crate) usable_while_casting: bool,
25 pub(crate) usable_while_moving: bool,
26 pub(crate) explicit_target_mask: u32,
27 pub(crate) required_explicit_target_mask: u32,
28 pub(crate) shapeshift_required_mask: u64,
29 pub(crate) shapeshift_excluded_mask: u64,
30 pub(crate) caster_aura_spell: i32,
31 pub(crate) caster_aura_state: Option<crate::state::AuraState>,
32 pub(crate) exclude_caster_aura_spell: i32,
33 pub(crate) exclude_caster_aura_state: Option<crate::state::AuraState>,
34 pub(crate) target_aura_spell: i32,
35 pub(crate) target_aura_state: Option<crate::state::AuraState>,
36 pub(crate) exclude_target_aura_spell: i32,
37 pub(crate) exclude_target_aura_state: Option<crate::state::AuraState>,
38 pub(crate) fixed_travel_time_override_ms: Option<u32>,
39 pub(crate) projectile_speed: f64,
40 pub(crate) launch_delay_s: f64,
41 pub(crate) dbc_fixed_travel_time: bool,
42 pub(crate) gcd_ms: u32,
43 pub(crate) cooldown_secs: Option<f64>,
44 pub(crate) max_charges: u8,
45 pub(crate) recharge_secs: Option<f64>,
46 pub(crate) resource_cost: f64,
47 pub(crate) optional_resource_cost: f64,
48 pub(crate) resource_cost_pct: wowlab_types::data::ResourceCostPercent,
49 pub(crate) maximum_resource_cost_pct: wowlab_types::data::ResourceCostPercent,
50 pub(crate) optional_resource_cost_pct: wowlab_types::data::ResourceCostPercent,
51 pub(crate) resource_gain: f64,
52 pub(crate) secondary_resource_cost: f64,
53 pub(crate) secondary_optional_resource_cost: f64,
54 pub(crate) secondary_resource_gain: f64,
55 pub(crate) health_cost: f64,
56 pub(crate) health_cost_pct: f64,
57 pub(crate) health_max_cost_pct: f64,
58 pub(crate) health_optional_cost: f64,
59 pub(crate) health_optional_cost_pct: wowlab_types::data::ResourceCostPercent,
60 pub(crate) damage: BuilderDamageDef,
61 pub(crate) damage_may_crit: bool,
62 pub(crate) damage_attribute_flags: DamageFlags,
63 pub(crate) damage_effect: Option<DamageEffectRef>,
64 pub(crate) base_points: f64,
65 pub(crate) applies_aura: Option<LocalAuraIdx>,
66 pub(crate) followup_effects: Vec<BuilderSpellEffect>,
67 pub(crate) trigger_program_predicates: Vec<BuilderTriggerProgramPredicate>,
68 pub(crate) infer_data_damage: bool,
69 pub(crate) applies_aura_ids: Vec<u32>,
70 pub(crate) cdr_effects: Vec<CdrEffect>,
71 pub(crate) hook: Option<CastHookFn>,
72 pub(crate) empower_release_hook: Option<EmpowerReleaseHookFn>,
73 pub(crate) tick_hook: Option<CastHookFn>,
74 pub(crate) is_pet: bool,
75 pub(crate) requires_unshifted: bool,
76 pub(crate) allow_while_unshifted: bool,
77 pub(crate) requires_stealth: bool,
78 pub(crate) requires_behind_target: bool,
79 pub(crate) requires_main_hand: bool,
80 pub(crate) requires_off_hand: bool,
81 pub(crate) equipped_item_requirement: Option<wowlab_types::data::EquippedItemRequirement>,
82 pub(crate) breaks_stealth: bool,
83 pub(crate) aoe_max_targets: u8,
84 pub(crate) aoe_reduced_targets: u8,
85 pub(crate) aoe_full_targets: u8,
86 pub(crate) aoe_base_mult: f64,
87 pub(crate) chain_targeting: bool,
88 pub(crate) chain_multiplier: f64,
89 pub(crate) split: bool,
90 pub(crate) is_aoe: bool,
91 pub(crate) is_channel: bool,
92 pub(crate) channel_tick_count: u8,
93 pub(crate) channel_tick_interval_ms: u32,
94 pub(crate) channel_tick_zero: bool,
95 pub(crate) channel_tick_is_periodic: bool,
96 pub(crate) channel_hasted_ticks: bool,
97 pub(crate) channel_duration_hasted: bool,
98 pub(crate) channel_tick_damage: BuilderDamageDef,
99 pub(crate) channel_tick_may_crit: bool,
100 pub(crate) channel_tick_spell_id: Option<u32>,
101 pub(crate) channel_tick_damage_alt: BuilderDamageDef,
102 pub(crate) channel_tick_damage_alt_may_crit: bool,
103 pub(crate) channel_tick_damage_alt_spell_id: u32,
104 pub(crate) channel_tick_damage_aura: u32,
105 pub(crate) channel_tick_cost: f64,
106 pub(crate) channel_tick_cost_alt: f64,
107 pub(crate) channel_tick_cost_aura: u32,
108 pub(crate) channel_tick_effects: Vec<BuilderSpellEffect>,
109 pub(crate) channel_complete_effects: Vec<BuilderSpellEffect>,
110 pub(crate) channel_gcd_on_start: bool,
111 pub(crate) channel_apply_lag: bool,
112 pub(crate) gating_aura_id: u32,
113 pub(crate) gating_aura_min_stacks: u8,
114 pub(crate) cost_bypass_aura_id: u32,
115 pub(crate) consume_aura_on_cast_id: u32,
116 pub(crate) cooldown_bypass_aura_id: u32,
117 pub(crate) execute_below_pct: f64,
118 pub(crate) instant_cast_aura_id: u32,
119 pub(crate) override_aura_id: u32,
120 pub(crate) override_spell_id: u32,
121 pub(crate) override_aura_min_stacks: u8,
122 pub(crate) override_shares_base_cooldown: bool,
123 pub(crate) override_preserves_aura: bool,
124 pub(crate) guaranteed_crit: bool,
125}
126
127impl SpellDefinitionDraft {
128 pub fn new(_name: &str, _id: u32) -> Self {
129 Self {
130 cast_time_ms: 0,
131 empower_cast_times_ms: Vec::new(),
132 max_empower_aura_id: 0,
133 cooldown_hasted: false,
134 gcd_haste_type: wowlab_types::combat::GcdHasteType::SpellCastSpeed,
135 cooldown_category: None,
136 category_cooldown_secs: 0.0,
137 charge_category: None,
138 start_recovery_category: Some(CooldownCategoryId::GLOBAL),
139 usable_while_casting: false,
140 usable_while_moving: false,
141 explicit_target_mask: wowlab_engine_domain::dbc::SpellCastTargetFlags::UNIT_ENEMY
142 .bits(),
143 required_explicit_target_mask:
144 wowlab_engine_domain::dbc::SpellCastTargetFlags::UNIT_ENEMY.bits(),
145 shapeshift_required_mask: 0,
146 shapeshift_excluded_mask: 0,
147 caster_aura_spell: 0,
148 caster_aura_state: None,
149 exclude_caster_aura_spell: 0,
150 exclude_caster_aura_state: None,
151 target_aura_spell: 0,
152 target_aura_state: None,
153 exclude_target_aura_spell: 0,
154 exclude_target_aura_state: None,
155 fixed_travel_time_override_ms: None,
156 projectile_speed: 0.0,
157 launch_delay_s: 0.0,
158 dbc_fixed_travel_time: false,
159 gcd_ms: DEFAULT_GCD_MS,
160 cooldown_secs: None,
161 max_charges: 0,
162 recharge_secs: None,
163 resource_cost: 0.0,
164 optional_resource_cost: 0.0,
165 resource_cost_pct: wowlab_types::data::ResourceCostPercent::default(),
166 maximum_resource_cost_pct: wowlab_types::data::ResourceCostPercent::default(),
167 optional_resource_cost_pct: wowlab_types::data::ResourceCostPercent::default(),
168 resource_gain: 0.0,
169 secondary_resource_cost: 0.0,
170 secondary_optional_resource_cost: 0.0,
171 secondary_resource_gain: 0.0,
172 health_cost: 0.0,
173 health_cost_pct: 0.0,
174 health_max_cost_pct: 0.0,
175 health_optional_cost: 0.0,
176 health_optional_cost_pct: wowlab_types::data::ResourceCostPercent::maximum(0.0),
177 damage: BuilderDamageDef::None,
178 damage_may_crit: true,
179 damage_attribute_flags: DamageFlags::empty(),
180 damage_effect: None,
181 base_points: 0.0,
182 applies_aura: None,
183 followup_effects: Vec::new(),
184 trigger_program_predicates: Vec::new(),
185 infer_data_damage: true,
186 applies_aura_ids: Vec::new(),
187 cdr_effects: Vec::new(),
188 hook: None,
189 empower_release_hook: None,
190 tick_hook: None,
191 breaks_stealth: true,
192 is_pet: false,
193 requires_unshifted: false,
194 allow_while_unshifted: false,
195 requires_stealth: false,
196 requires_behind_target: false,
197 requires_main_hand: false,
198 requires_off_hand: false,
199 equipped_item_requirement: None,
200 aoe_max_targets: 0,
201 aoe_reduced_targets: 0,
202 aoe_full_targets: 0,
203 aoe_base_mult: 1.0,
204 chain_targeting: false,
205 chain_multiplier: 1.0,
206 split: false,
207 is_aoe: false,
208 is_channel: false,
209 channel_tick_count: 0,
210 channel_tick_interval_ms: 0,
211 channel_tick_zero: false,
212 channel_tick_is_periodic: true,
213 channel_hasted_ticks: false,
214 channel_duration_hasted: false,
215 channel_tick_damage: BuilderDamageDef::None,
216 channel_tick_may_crit: true,
217 channel_tick_spell_id: None,
218 channel_tick_damage_alt: BuilderDamageDef::None,
219 channel_tick_damage_alt_may_crit: true,
220 channel_tick_damage_alt_spell_id: 0,
221 channel_tick_damage_aura: 0,
222 channel_tick_cost: 0.0,
223 channel_tick_cost_alt: 0.0,
224 channel_tick_cost_aura: 0,
225 channel_tick_effects: Vec::new(),
226 channel_complete_effects: Vec::new(),
227 channel_gcd_on_start: true,
228 channel_apply_lag: true,
229 gating_aura_id: 0,
230 gating_aura_min_stacks: 0,
231 cost_bypass_aura_id: 0,
232 consume_aura_on_cast_id: 0,
233 cooldown_bypass_aura_id: 0,
234 execute_below_pct: 0.0,
235 instant_cast_aura_id: 0,
236 override_aura_id: 0,
237 override_spell_id: 0,
238 override_aura_min_stacks: 0,
239 override_shares_base_cooldown: false,
240 override_preserves_aura: false,
241 guaranteed_crit: false,
242 }
243 }
244
245 pub fn usable_while_moving(mut self) -> Self {
246 self.usable_while_moving = true;
247
248 self
249 }
250
251 pub fn guaranteed_crit(mut self) -> Self {
253 self.guaranteed_crit = true;
254
255 self
256 }
257
258 pub fn usable_while_casting(mut self) -> Self {
259 self.usable_while_casting = true;
260
261 self
262 }
263
264 pub fn targets_player(mut self) -> Self {
266 self.explicit_target_mask = 0;
267 self.required_explicit_target_mask = 0;
268
269 self
270 }
271
272 pub fn requires_stealth(mut self) -> Self {
273 self.requires_stealth = true;
274
275 self
276 }
277
278 pub const fn requires_behind_target(mut self) -> Self {
280 self.requires_behind_target = true;
281
282 self
283 }
284
285 pub fn cast_time(mut self, ms: u32) -> Self {
286 self.cast_time_ms = ms;
287
288 self
289 }
290
291 pub fn max_empower_when_aura(mut self, aura_id: u32) -> Self {
294 self.max_empower_aura_id = aura_id;
295 self.instant_cast_aura_id = aura_id;
296 self.consume_aura_on_cast_id = aura_id;
297
298 self
299 }
300
301 pub fn travel_time(mut self, ms: u32) -> Self {
302 self.fixed_travel_time_override_ms = Some(ms);
303
304 self
305 }
306
307 pub fn gcd(mut self, ms: u32) -> Self {
308 self.gcd_ms = ms;
309
310 self
311 }
312
313 pub fn extends_aura(mut self, aura: LocalAuraIdx, amount_ms: u32) -> Self {
314 self.followup_effects.push(BuilderSpellEffect::ExtendAura {
315 aura_local: aura,
316 amount_ms,
317 });
318
319 self
320 }
321
322 pub fn applies_aura(mut self, aura: LocalAuraIdx) -> Self {
323 if self.applies_aura.is_none() {
324 self.applies_aura = Some(aura);
325 } else if self.applies_aura != Some(aura)
326 && !self.followup_effects.iter().any(
327 |effect| matches!(effect, BuilderSpellEffect::ApplyAura { aura_local } if *aura_local == aura),
328 )
329 {
330 self.followup_effects
331 .push(BuilderSpellEffect::ApplyAura { aura_local: aura });
332 }
333
334 self
335 }
336
337 pub fn applies_inferred_aura(self, aura: LocalAuraIdx, aura_spell_id: u32) -> Self {
344 if i64::from(self.caster_aura_spell) == i64::from(aura_spell_id) {
345 return self;
346 }
347
348 self.applies_aura(aura)
349 }
350
351 pub fn applies_aura_id(mut self, aura_id: u32) -> Self {
353 if !self.applies_aura_ids.contains(&aura_id) {
354 self.applies_aura_ids.push(aura_id);
355 }
356
357 self
358 }
359
360 pub fn on_cast(mut self, hook: CastHookFn) -> Self {
361 self.hook = Some(hook);
362
363 self
364 }
365
366 pub fn trigger_program_predicate(
368 mut self,
369 effect_index: u8,
370 predicate: TriggerProgramPredicateFn,
371 ) -> Self {
372 self.trigger_program_predicates
373 .push(BuilderTriggerProgramPredicate {
374 effect_index,
375 predicate,
376 });
377
378 self
379 }
380
381 pub fn on_empower_release(mut self, hook: EmpowerReleaseHookFn) -> Self {
383 self.empower_release_hook = Some(hook);
384
385 self
386 }
387
388 pub fn pet(mut self) -> Self {
389 self.is_pet = true;
390
391 self
392 }
393
394 pub fn no_stealth_break(mut self) -> Self {
395 self.breaks_stealth = false;
396
397 self
398 }
399
400 #[cfg(test)]
401 pub(crate) fn empower_fixture(mut self, cast_times_ms: Vec<u32>) -> Self {
402 self.cast_time_ms = cast_times_ms.first().copied().unwrap_or(0);
403 self.empower_cast_times_ms = cast_times_ms;
404
405 self
406 }
407}