Skip to main content

wowlab_engine_combat/builder/spell_builder/
draft.rs

1use super::{
2    BuilderDamageDef, BuilderSpellEffect, BuilderTriggerProgramPredicate, CastHookFn, CdrEffect,
3    CooldownCategoryId, DEFAULT_GCD_MS, DamageEffectRef, DamageFlags, EmpowerReleaseHookFn,
4    LocalAuraIdx, TriggerProgramPredicateFn,
5};
6
7/// Mutable draft of a single castable spell definition.
8#[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    /// Makes every hit of this spell critical.
252    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    /// Cast without an explicit target payload.
265    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    /// Require the player to be behind the current target.
279    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    /// Make an empower instant and release at its DBC maximum rank while `aura_id` is active.
292    /// The one-shot aura is consumed by the completed empower cast.
293    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    /// Applies a name-matched aura unless the data says this spell requires it instead.
338    ///
339    /// A spell and its usability requirement often share a display name.
340    ///   Whirling Dragon Punch 152175 names 196742 as its `SpellAuraRestrictions.CasterAuraSpell`.
341    ///   Inferring application from the shared name would apply the very aura the cast consumes.
342    ///   The requirement stated in the data therefore wins over the inference.
343    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    /// Apply the aura with this game id on cast; resolved to its registered local index.
352    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    /// Gate one source effect's complete DBC trigger program with content-owned runtime logic.
367    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    /// Register a rank-aware driver that adjusts an empower spell's release payload.
382    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}