Skip to main content

wowlab_manifest_schema/
spell.rs

1//! Spell and auto-attack authoring schema.
2
3use serde::Deserialize;
4
5use crate::{
6    AoeDef, ChannelDef, EmpowerDef, EventEffectDef, ManifestDamageDef, ScalarRef,
7    manifest::default_true,
8};
9
10/// `[spells.<name>]` block describing a single registered spell.
11///
12/// ```compile_fail
13/// use wowlab_manifest_schema::{ManifestAuraDef, ManifestSpellDef};
14///
15/// fn lower_aura(_: &ManifestAuraDef) {}
16/// fn invalid(spell: &ManifestSpellDef) {
17///     lower_aura(spell);
18/// }
19/// ```
20// docref:start codegen-spell-def
21#[derive(Debug, Deserialize)]
22#[serde(deny_unknown_fields)]
23#[expect(
24    clippy::struct_excessive_bools,
25    reason = "fields are independent switches in the stable flat spell manifest schema"
26)]
27pub struct ManifestSpellDef {
28    pub id: u32,
29    /// Server-side `spell_proc.AttributesMask` identities absent from client DBC data.
30    #[serde(default)]
31    pub proc_attributes: Vec<ManifestProcAttribute>,
32    #[serde(default)]
33    pub cooldown: Option<ScalarRef>,
34    #[serde(default)]
35    pub cost: Option<ScalarRef>,
36    #[serde(default)]
37    pub gain: Option<ScalarRef>,
38    #[serde(default)]
39    pub damage: Option<ManifestDamageDef>,
40    #[serde(default = "default_true")]
41    pub infer_damage: bool,
42    #[serde(default = "default_true")]
43    pub infer_primary_damage: bool,
44    #[serde(default)]
45    pub damage_effects: Vec<DamageEffectDef>,
46    #[serde(default)]
47    pub trigger_program_predicates: Vec<TriggerProgramPredicateDef>,
48    #[serde(default)]
49    pub usable_while_casting: bool,
50    #[serde(default)]
51    pub travel_time_ms: Option<ScalarRef>,
52    #[serde(default)]
53    pub cast_time_ms: Option<ScalarRef>,
54    #[serde(default)]
55    pub gcd_ms: Option<ScalarRef>,
56    #[serde(default)]
57    pub charges: Option<ScalarRef>,
58    #[serde(default)]
59    pub charge_cd: Option<ScalarRef>,
60    #[serde(default)]
61    pub cooldown_hasted: Option<bool>,
62    #[serde(default)]
63    pub applies_aura: Option<String>,
64    #[serde(default = "default_true")]
65    pub infer_applies_aura: bool,
66    #[serde(default)]
67    pub applies_auras: Vec<String>,
68    #[serde(default)]
69    pub extends_auras: Vec<ExtendAuraDef>,
70    #[serde(default)]
71    pub reduces_cd: Option<Vec<ReducesCd>>,
72    #[serde(default)]
73    pub reduces_cd_chance: Option<ReducesCdChance>,
74    #[serde(default)]
75    pub resets_cd_while: Option<ResetsCdWhile>,
76    #[serde(default)]
77    pub hook: Option<String>,
78    #[serde(default = "default_breaks_stealth")]
79    pub breaks_stealth: bool,
80    #[serde(default)]
81    pub aoe: Option<AoeDef>,
82    #[serde(default)]
83    pub channel: Option<ChannelDef>,
84    #[serde(default)]
85    pub empower: Option<EmpowerDef>,
86    #[serde(default)]
87    pub requires_aura_id: Option<u32>,
88    #[serde(default)]
89    pub requires_aura_min_stacks: Option<u8>,
90    #[serde(default)]
91    pub cost_free_when_aura: Option<u32>,
92    #[serde(default)]
93    pub consume_aura_on_cast: Option<u32>,
94    #[serde(default)]
95    pub cooldown_bypass_when_aura: Option<String>,
96    #[serde(default)]
97    pub execute_below_pct: Option<f64>,
98    #[serde(default)]
99    pub instant_when_aura: Option<String>,
100    #[serde(default)]
101    pub override_when_aura: Option<String>,
102    #[serde(default)]
103    pub override_with_spell: Option<String>,
104    #[serde(default)]
105    pub override_aura_min_stacks: Option<u8>,
106    #[serde(default)]
107    pub override_shares_base_cooldown: bool,
108    #[serde(default)]
109    pub override_preserves_aura: bool,
110    #[serde(default)]
111    pub guaranteed_crit: bool,
112}
113// docref:end codegen-spell-def
114
115/// Authored server-side `SpellProcEntry` attribute.
116#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
117#[serde(rename_all = "snake_case")]
118#[non_exhaustive]
119pub enum ManifestProcAttribute {
120    RequireExperienceOrHonor,
121    TriggeredCanProc,
122    RequirePowerCost,
123    RequireSpellModifier,
124    UseStacksForCharges,
125    ReduceProcAboveLevel60,
126    CannotProcFromItemCast,
127}
128
129impl ManifestSpellDef {
130    /// Returns every scalar game-data reference used by spell-level overrides.
131    pub fn scalar_refs(&self) -> impl Iterator<Item = &ScalarRef> {
132        [
133            self.cooldown.as_ref(),
134            self.cost.as_ref(),
135            self.gain.as_ref(),
136            self.travel_time_ms.as_ref(),
137            self.cast_time_ms.as_ref(),
138            self.gcd_ms.as_ref(),
139            self.charges.as_ref(),
140            self.charge_cd.as_ref(),
141            self.channel
142                .as_ref()
143                .and_then(|channel| channel.tick_count.as_ref()),
144            self.channel
145                .as_ref()
146                .and_then(|channel| channel.tick_interval_ms.as_ref()),
147        ]
148        .into_iter()
149        .flatten()
150        .chain(
151            self.channel
152                .iter()
153                .flat_map(|channel| {
154                    channel
155                        .tick_effects
156                        .iter()
157                        .chain(channel.complete_effects.iter())
158                })
159                .map(|effect| match effect {
160                    EventEffectDef::Energize { amount, .. } => amount,
161                    EventEffectDef::ExtendAura { amount_ms, .. } => amount_ms,
162                }),
163        )
164        .chain(
165            self.reduces_cd
166                .iter()
167                .flatten()
168                .map(|cdr| &cdr.amount_ms)
169                .chain(self.reduces_cd_chance.iter().map(|cdr| &cdr.amount_ms)),
170        )
171    }
172}
173
174/// One exact DBC trigger edge appended to a spell's ordered damage program.
175#[derive(Debug, Deserialize)]
176pub struct DamageEffectDef {
177    pub spell_id: u32,
178    pub effect: u8,
179    pub kind: String,
180    #[serde(default)]
181    pub ap_type: Option<String>,
182    #[serde(default = "default_one")]
183    pub multiplier: f64,
184}
185
186/// Content-owned runtime condition applied to one or more DBC trigger edges.
187#[derive(Debug, Deserialize)]
188pub struct TriggerProgramPredicateDef {
189    pub effects: Vec<u8>,
190    pub predicate: String,
191}
192
193const fn default_one() -> f64 {
194    1.0
195}
196
197#[derive(Debug, Deserialize)]
198pub struct ExtendAuraDef {
199    pub aura: String,
200    pub amount_ms: u32,
201}
202
203const fn default_breaks_stealth() -> bool {
204    true
205}
206
207/// Cooldown reduction that fires unconditionally on a [`ManifestSpellDef`]'s cast.
208#[derive(Debug, Deserialize)]
209pub struct ReducesCd {
210    pub target: String,
211    pub amount_ms: ScalarRef,
212}
213
214/// Probabilistic cooldown reduction triggered on a [`ManifestSpellDef`]'s cast.
215#[derive(Debug, Deserialize)]
216pub struct ReducesCdChance {
217    pub target: String,
218    pub amount_ms: ScalarRef,
219    pub chance: f64,
220}
221
222/// Conditional cooldown reset on a [`ManifestSpellDef`] while the named aura is active.
223#[derive(Debug, Deserialize)]
224pub struct ResetsCdWhile {
225    pub target: String,
226    pub aura: String,
227}
228
229#[cfg(test)]
230mod tests {
231    use googletest::prelude::*;
232
233    use super::*;
234
235    #[gtest]
236    fn server_proc_attributes_parse_as_typed_manifest_identities() -> Result<()> {
237        let spell: ManifestSpellDef = toml::from_str(
238            r#"
239id = 44544
240proc_attributes = ["triggered_can_proc", "use_stacks_for_charges"]
241"#,
242        )
243        .or_fail()?;
244
245        verify_that!(
246            spell.proc_attributes.as_slice(),
247            elements_are![
248                eq(&ManifestProcAttribute::TriggeredCanProc),
249                eq(&ManifestProcAttribute::UseStacksForCharges),
250            ]
251        )
252    }
253
254    #[gtest]
255    fn a_channel_tick_count_accepts_a_game_data_reference_and_a_literal() -> Result<()> {
256        let referenced: ManifestSpellDef = toml::from_str(
257            r#"
258id = 257044
259
260[channel]
261tick_count = { effect = 1, field = "base_points", spell_id = 257044 }
262"#,
263        )
264        .or_fail()?;
265        let literal: ManifestSpellDef = toml::from_str(
266            r"
267id = 113656
268
269[channel]
270tick_count = 5
271",
272        )
273        .or_fail()?;
274        let referenced_count = referenced
275            .channel
276            .as_ref()
277            .and_then(|channel| channel.tick_count.as_ref());
278        let literal_count = literal
279            .channel
280            .as_ref()
281            .and_then(|channel| channel.tick_count.as_ref());
282
283        verify_that!(
284            referenced_count,
285            some(pat!(&ScalarRef::EffectRef {
286                spell_id: eq(257_044),
287                effect: eq(1),
288                ..
289            }))
290        )?;
291        verify_that!(literal_count, some(pat!(&ScalarRef::Literal(eq(5.0)))))?;
292
293        // Referenced tick counts must reach the reference audit like every other scalar override.
294
295        verify_that!(
296            referenced
297                .scalar_refs()
298                .filter_map(ScalarRef::effect_spell_id)
299                .collect::<Vec<_>>(),
300            eq(&vec![257_044])
301        )
302    }
303}
304
305/// `[auto_attacks.<name>]` block describing a swing-driven physical hit.
306#[derive(Debug, Deserialize)]
307#[serde(deny_unknown_fields)]
308pub struct AutoAttackDef {
309    pub spell_id: u32,
310    pub swing_ms: u32,
311    /// Creature/NPC identity used by NPC-filtered companion modifiers.
312    #[serde(default)]
313    pub npc_id: Option<u32>,
314    #[serde(default)]
315    pub ap_coef: Option<f64>,
316    /// Pet AP/SP inherited from owner power; converted with the shared `/6` weapon formula.
317    #[serde(default)]
318    pub pet_power_from_owner: Option<f64>,
319    /// Unhasted weapon speed used for pet weapon damage; defaults to `swing_ms`.
320    #[serde(default)]
321    pub pet_weapon_speed_ms: Option<u32>,
322    #[serde(default)]
323    pub pet_weapon_multiplier: Option<f64>,
324    #[serde(default)]
325    pub is_pet: bool,
326    /// Scale the authored coefficient from owner spell power (caster pets) instead of owner attack power.
327    #[serde(default)]
328    pub uses_spell_power: bool,
329    #[serde(default)]
330    pub on_crit_reduce_charge: Option<OnCritReduceCharge>,
331    /// Rust path of a swing hook fired after each landed swing (procs, poisons).
332    #[serde(default)]
333    pub hook: Option<String>,
334}
335
336/// Charge-reduction proc fired on an [`AutoAttackDef`]'s crits.
337#[derive(Debug, Deserialize)]
338pub struct OnCritReduceCharge {
339    pub target: String,
340    pub chance: f64,
341}