Skip to main content

wowlab_manifest_schema/
aura.rs

1//! Aura authoring schema and scalar-reference traversal.
2
3use serde::Deserialize;
4use wowlab_types::data::RefreshBehavior;
5
6use crate::{
7    EventEffectDef, PeriodicApplyAura, PeriodicDamage, PeriodicHook, PeriodicRemoveStack,
8    PeriodicResidualDamage, PeriodicResource, PeriodicResourceDrain, ScalarRef,
9};
10
11/// `[auras.<name>]` block describing a single registered aura.
12#[derive(Debug, Deserialize)]
13#[serde(deny_unknown_fields)]
14#[expect(
15    clippy::struct_excessive_bools,
16    reason = "fields are independent switches in the stable flat aura manifest schema"
17)]
18pub struct ManifestAuraDef {
19    pub id: u32,
20    /// Server-side `spell_proc.AttributesMask` identities absent from client DBC data.
21    #[serde(default)]
22    pub proc_attributes: Vec<crate::ManifestProcAttribute>,
23    #[serde(default = "default_aura_on")]
24    pub on: String,
25    #[serde(default)]
26    pub duration_ms: Option<ScalarRef>,
27    #[serde(default)]
28    pub no_pandemic: bool,
29    #[serde(default)]
30    pub refresh_behavior: Option<RefreshBehavior>,
31    /// Forwards a value into compatible zero-valued DBC effects while preserving their scope.
32    #[serde(default)]
33    pub forwarded_effect_value: Option<ScalarRef>,
34    #[serde(default)]
35    pub damage_mult: Option<ScalarRef>,
36    #[serde(default)]
37    pub damage_mult_stacking: Option<StackingDamageMult>,
38    #[serde(default)]
39    pub pet_damage_mult: Option<ScalarRef>,
40    /// Percent points the debuffed target takes from the caster's affected spells (aura 271); overrides a value-override talent's zero DBC base.
41    #[serde(default)]
42    pub damage_taken_from_caster_spells: Option<ScalarRef>,
43    #[serde(default)]
44    pub max_stacks: Option<ScalarRef>,
45    #[serde(default)]
46    pub haste_per_stack: Option<ScalarRef>,
47    /// Percent points of auto-attack speed per stack (aura 319/342); swing frequency only.
48    #[serde(default)]
49    pub attack_speed_per_stack: Option<ScalarRef>,
50    #[serde(default)]
51    pub auto_attack_damage_per_stack: Option<ScalarRef>,
52    /// Percent points of extra primary-resource gain per stack (aura 213); discrete gains only.
53    #[serde(default)]
54    pub resource_gain_mult_per_stack: Option<ScalarRef>,
55    #[serde(default)]
56    pub regen_percent_per_stack: Option<ScalarRef>,
57    #[serde(default)]
58    pub max_resource_per_stack: Option<ScalarRef>,
59    #[serde(default)]
60    pub crit_per_stack: Option<ScalarRef>,
61    #[serde(default)]
62    pub mastery_per_stack: Option<ScalarRef>,
63    #[serde(default)]
64    pub primary_stat_percent_per_stack: Option<ScalarRef>,
65    #[serde(default)]
66    pub attack_power_percent_per_stack: Option<ScalarRef>,
67    #[serde(default)]
68    pub crit_rating_per_stack: Option<ScalarRef>,
69    #[serde(default)]
70    pub haste_rating_per_stack: Option<ScalarRef>,
71    #[serde(default)]
72    pub mastery_rating_per_stack: Option<ScalarRef>,
73    #[serde(default)]
74    pub versatility_rating_per_stack: Option<ScalarRef>,
75    #[serde(default)]
76    pub primary_stat_per_stack: Option<ScalarRef>,
77    #[serde(default)]
78    pub split_across_stacks: bool,
79    #[serde(default)]
80    pub apply_at_max_stacks: bool,
81    /// Starts at maximum stacks and decrements instead of incrementing.
82    #[serde(default)]
83    pub reverse: bool,
84    /// Prevents the periodic tick from changing stacks.
85    #[serde(default)]
86    pub freeze_stacks: bool,
87    /// Whether reapplication restarts or preserves the current tick cadence.
88    #[serde(default)]
89    pub tick_behavior: Option<ManifestAuraTickBehavior>,
90    /// Stack count added or removed by each stack-changing tick.
91    #[serde(default)]
92    pub tick_stack_change: Option<u8>,
93    /// Period used when stack mutation itself is the aura's periodic effect.
94    #[serde(default)]
95    pub stack_tick_ms: Option<ScalarRef>,
96    /// Gives each stack an independent expiry; overflow displaces the oldest.
97    #[serde(default)]
98    pub async_stacks: bool,
99    #[serde(default)]
100    pub periodic_damage_scales_with_stacks: bool,
101    #[serde(default)]
102    pub reapply_on_expire: bool,
103    /// Resolves under the spec hook module; item hooks use a full function path.
104    #[serde(default)]
105    pub expire_hook: Option<String>,
106    #[serde(default)]
107    pub on_consume: Vec<EventEffectDef>,
108    #[serde(default)]
109    pub periodic_remove_stack: Option<PeriodicRemoveStack>,
110    #[serde(default)]
111    pub periodic_damage: Option<PeriodicDamage>,
112    #[serde(default)]
113    pub periodic_residual_damage: Option<PeriodicResidualDamage>,
114    #[serde(default)]
115    pub periodic_hasted: Option<bool>,
116    /// Fires `tick_hook` at natural expiry when time elapsed since the last tick.
117    #[serde(default)]
118    pub periodic_partial_tick: bool,
119    #[serde(default)]
120    pub periodic_resource: Option<PeriodicResource>,
121    #[serde(default)]
122    pub periodic_resource_drain: Option<PeriodicResourceDrain>,
123    #[serde(default)]
124    pub periodic_apply_aura: Option<PeriodicApplyAura>,
125    /// Provides a hook-only periodic when game data or authored behavior did not register one.
126    #[serde(default)]
127    pub periodic_hook: Option<PeriodicHook>,
128    /// Resolves like `expire_hook` and requires a registered periodic.
129    #[serde(default)]
130    pub tick_hook: Option<String>,
131    #[serde(default)]
132    pub precombat: bool,
133    #[serde(default)]
134    pub snapshot: bool,
135    /// Uses percent points; `100.0` guarantees critical strikes.
136    #[serde(default)]
137    pub crit_chance_spells: Option<SpellScopedPct>,
138    /// Uses percent points; `25.0` raises a 2x critical strike to 2.25x.
139    #[serde(default)]
140    pub crit_damage_spells: Option<SpellScopedPct>,
141    #[serde(default)]
142    pub damage_percent_spells: Option<SpellScopedPct>,
143    /// Scales linearly as `1 + pct / 100 * stacks`.
144    #[serde(default)]
145    pub damage_percent_spells_linear: Option<SpellScopedPct>,
146    #[serde(default)]
147    pub cast_time_percent: Option<SpellScopedPct>,
148    #[serde(default)]
149    pub cast_time_percent_per_stack: Option<SpellScopedPct>,
150    #[serde(default)]
151    pub gcd_percent: Option<SpellScopedPct>,
152    #[serde(default)]
153    pub spell_cost_flat: Option<SpellScopedValue>,
154    #[serde(default)]
155    pub spell_cost_percent: Option<SpellScopedValue>,
156    #[serde(default)]
157    pub secondary_spell_cost_percent: Option<SpellScopedValue>,
158    #[serde(default)]
159    pub secondary_spell_cost_flat: Option<SpellScopedValue>,
160    #[serde(default)]
161    pub recharge_rate_percent: Vec<SpellScopedValue>,
162}
163
164#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
165#[serde(rename_all = "snake_case")]
166#[non_exhaustive]
167pub enum ManifestAuraTickBehavior {
168    Clip,
169    Refresh,
170}
171
172impl ManifestAuraDef {
173    /// Returns every game-data scalar referenced by this aura: direct, spell-scoped, and event payloads.
174    pub fn scalar_refs(&self) -> impl Iterator<Item = &ScalarRef> {
175        [
176            self.duration_ms.as_ref(),
177            self.max_stacks.as_ref(),
178            self.forwarded_effect_value.as_ref(),
179            self.damage_mult.as_ref(),
180            self.damage_mult_stacking
181                .as_ref()
182                .map(|effect| &effect.initial),
183            self.damage_mult_stacking
184                .as_ref()
185                .map(|effect| &effect.per_stack),
186            self.pet_damage_mult.as_ref(),
187            self.damage_taken_from_caster_spells.as_ref(),
188            self.haste_per_stack.as_ref(),
189            self.attack_speed_per_stack.as_ref(),
190            self.auto_attack_damage_per_stack.as_ref(),
191            self.resource_gain_mult_per_stack.as_ref(),
192            self.regen_percent_per_stack.as_ref(),
193            self.max_resource_per_stack.as_ref(),
194            self.crit_per_stack.as_ref(),
195            self.mastery_per_stack.as_ref(),
196            self.primary_stat_percent_per_stack.as_ref(),
197            self.attack_power_percent_per_stack.as_ref(),
198            self.crit_rating_per_stack.as_ref(),
199            self.haste_rating_per_stack.as_ref(),
200            self.mastery_rating_per_stack.as_ref(),
201            self.versatility_rating_per_stack.as_ref(),
202            self.primary_stat_per_stack.as_ref(),
203            self.crit_chance_spells.as_ref().map(|effect| &effect.pct),
204            self.crit_damage_spells.as_ref().map(|effect| &effect.pct),
205            self.damage_percent_spells
206                .as_ref()
207                .map(|effect| &effect.pct),
208            self.damage_percent_spells_linear
209                .as_ref()
210                .map(|effect| &effect.pct),
211            self.cast_time_percent.as_ref().map(|effect| &effect.pct),
212            self.cast_time_percent_per_stack
213                .as_ref()
214                .map(|effect| &effect.pct),
215            self.gcd_percent.as_ref().map(|effect| &effect.pct),
216            self.spell_cost_flat.as_ref().map(|effect| &effect.value),
217            self.spell_cost_percent.as_ref().map(|effect| &effect.value),
218            self.secondary_spell_cost_percent
219                .as_ref()
220                .map(|effect| &effect.value),
221            self.secondary_spell_cost_flat
222                .as_ref()
223                .map(|effect| &effect.value),
224        ]
225        .into_iter()
226        .flatten()
227        .chain(
228            self.recharge_rate_percent
229                .iter()
230                .map(|effect| &effect.value),
231        )
232        .chain(self.on_consume.iter().map(|effect| match effect {
233            EventEffectDef::Energize { amount, .. } => amount,
234            EventEffectDef::ExtendAura { amount_ms, .. } => amount_ms,
235        }))
236        .chain(self.periodic_damage.iter().flat_map(|periodic| {
237            [
238                Some(&periodic.tick_ms),
239                periodic.ap_coef.as_ref(),
240                periodic.sp_coef.as_ref(),
241                periodic.item_damage_total.as_ref(),
242                periodic.resource_gain.as_ref(),
243                periodic.crit_resource_gain.as_ref(),
244                periodic.crit_resource_chance.as_ref(),
245            ]
246            .into_iter()
247            .flatten()
248        }))
249        .chain(
250            self.periodic_residual_damage
251                .iter()
252                .map(|periodic| &periodic.tick_ms),
253        )
254        .chain(
255            self.periodic_resource
256                .iter()
257                .flat_map(|periodic| [&periodic.tick_ms, &periodic.amount]),
258        )
259        .chain(
260            self.periodic_resource_drain
261                .iter()
262                .flat_map(|periodic| [&periodic.tick_ms, &periodic.amount]),
263        )
264        .chain(
265            self.periodic_apply_aura
266                .iter()
267                .map(|periodic| &periodic.tick_ms),
268        )
269        .chain(
270            self.periodic_remove_stack
271                .iter()
272                .map(|periodic| &periodic.tick_ms),
273        )
274        .chain(self.stack_tick_ms.iter())
275        .chain(self.periodic_hook.iter().map(|periodic| &periodic.tick_ms))
276    }
277
278    pub(crate) fn scoped_spell_names(&self) -> impl Iterator<Item = &String> {
279        self.crit_chance_spells
280            .iter()
281            .flat_map(|effect| &effect.spells)
282            .chain(
283                self.crit_damage_spells
284                    .iter()
285                    .flat_map(|effect| &effect.spells),
286            )
287            .chain(
288                self.damage_percent_spells
289                    .iter()
290                    .flat_map(|effect| &effect.spells),
291            )
292            .chain(
293                self.damage_percent_spells_linear
294                    .iter()
295                    .flat_map(|effect| &effect.spells),
296            )
297            .chain(
298                self.cast_time_percent
299                    .iter()
300                    .flat_map(|effect| &effect.spells),
301            )
302            .chain(
303                self.cast_time_percent_per_stack
304                    .iter()
305                    .flat_map(|effect| &effect.spells),
306            )
307            .chain(self.gcd_percent.iter().flat_map(|effect| &effect.spells))
308            .chain(
309                self.spell_cost_flat
310                    .iter()
311                    .flat_map(|effect| &effect.spells),
312            )
313            .chain(
314                self.spell_cost_percent
315                    .iter()
316                    .flat_map(|effect| &effect.spells),
317            )
318            .chain(
319                self.secondary_spell_cost_percent
320                    .iter()
321                    .flat_map(|effect| &effect.spells),
322            )
323            .chain(
324                self.secondary_spell_cost_flat
325                    .iter()
326                    .flat_map(|effect| &effect.spells),
327            )
328            .chain(
329                self.recharge_rate_percent
330                    .iter()
331                    .flat_map(|effect| &effect.spells),
332            )
333    }
334}
335
336/// A per-stack percent magnitude scoped to a list of manifest spell names.
337#[derive(Debug, Deserialize)]
338pub struct SpellScopedPct {
339    pub pct: ScalarRef,
340    pub spells: Vec<String>,
341}
342
343/// A scalar magnitude scoped to a list of manifest spell names.
344#[derive(Debug, Deserialize)]
345pub struct SpellScopedValue {
346    pub value: ScalarRef,
347    pub spells: Vec<String>,
348}
349
350/// Additive damage fractions for an aura whose first stack differs from later stacks.
351#[derive(Debug, Deserialize)]
352pub struct StackingDamageMult {
353    pub initial: ScalarRef,
354    pub per_stack: ScalarRef,
355}
356
357fn default_aura_on() -> String {
358    "player".to_string()
359}