Skip to main content

codegen/
gen_aura_periodic.rs

1//! Emission of aura duration, periodic damage, resource, and hook behavior.
2
3use anyhow::bail;
4use proc_macro2::TokenStream;
5use quote::{format_ident, quote};
6use wowlab_manifest_schema::{
7    DamageSchoolKind, ManifestAuraDef, PeriodicDamage, PeriodicDamageMode, ScalarField, ScalarRef,
8    SpellScalarField,
9};
10
11use crate::{
12    gen_aura::AuraEmitOptions,
13    gen_emit::{damage_flags, effect_lookup, scalar_ref_call, validated_milliseconds_call},
14    rust_source::{lit_f64, lit_int, rust_path},
15};
16
17type DerivedPeriodicData = (u32, u8, Option<f64>, Option<f64>, Option<u8>);
18
19#[derive(Clone, Copy)]
20enum PeriodicCoefficient {
21    AttackPower,
22    SpellPower,
23}
24
25fn derived_periodic_data(
26    scalar: &ScalarRef,
27    coefficient: PeriodicCoefficient,
28    has_explicit_school: bool,
29    attribution_spell_id: Option<u32>,
30) -> Option<DerivedPeriodicData> {
31    let ScalarRef::EffectRef {
32        spell_id,
33        effect,
34        field,
35        multiplier,
36        offset,
37        round_decimals,
38    } = scalar
39    else {
40        return None;
41    };
42    let field_matches = matches!(
43        (field, coefficient),
44        (ScalarField::ApCoef, PeriodicCoefficient::AttackPower)
45            | (ScalarField::SpCoef, PeriodicCoefficient::SpellPower)
46    );
47
48    (field_matches
49        && !has_explicit_school
50        && attribution_spell_id.is_none_or(|attribution| attribution == *spell_id))
51    .then_some((*spell_id, *effect, *multiplier, *offset, *round_decimals))
52}
53
54fn option_u8(value: Option<u8>) -> TokenStream {
55    value.map_or_else(
56        || quote!(None),
57        |value| {
58            let value = lit_int(value);
59
60            quote!(Some(#value))
61        },
62    )
63}
64
65fn item_scaled_effect_call(scalar: &ScalarRef) -> anyhow::Result<TokenStream> {
66    let ScalarRef::EffectRef {
67        spell_id,
68        effect,
69        field,
70        multiplier,
71        offset,
72        round_decimals,
73    } = scalar
74    else {
75        bail!("item_damage_total must reference an item's scaling effect");
76    };
77
78    if !matches!(field, ScalarField::Coefficient) {
79        bail!("item_damage_total must reference the effect coefficient");
80    }
81
82    let lookup = effect_lookup(&quote!(data), *spell_id, *effect);
83    let raw_spell_id = lit_int(spell_id);
84    let effect_index = lit_int(effect);
85    let mut value = quote! {
86        wowlab_engine_domain::dbc::item_scaled_effect_value(#lookup, budget.item_level)
87            .or_else(|| data.is_empty().then_some(0.0))
88            .ok_or_else(|| {
89                wowlab_engine_combat::BuilderError::missing_item_scaling_data(
90                    #raw_spell_id,
91                    #effect_index,
92                    budget.item_level,
93                )
94            })?
95    };
96
97    if let Some(multiplier) = multiplier {
98        let multiplier = lit_f64(*multiplier);
99
100        value = quote!((#value) * #multiplier);
101    }
102
103    if let Some(offset) = offset {
104        let offset = lit_f64(*offset);
105
106        value = quote!((#value) + #offset);
107    }
108
109    if let Some(decimals) = round_decimals {
110        let scale = lit_f64(10_f64.powi(i32::from(*decimals)));
111
112        value = quote!(((#value) * #scale).round() / #scale);
113    }
114
115    Ok(value)
116}
117
118pub(super) fn apply_periodic_hook_source(
119    mut expression: TokenStream,
120    def: &ManifestAuraDef,
121) -> anyhow::Result<TokenStream> {
122    let id = def.id;
123
124    if let Some(periodic_hook) = &def.periodic_hook {
125        if def.tick_hook.is_none() {
126            bail!("periodic_hook on aura {id} requires a tick_hook");
127        }
128
129        let tick_ms =
130            validated_milliseconds_call(&periodic_hook.tick_ms, id, "periodic_hook.tick_ms")?;
131
132        expression = quote!(#expression.periodic_hook(#tick_ms));
133    }
134
135    Ok(expression)
136}
137
138pub(super) fn apply_periodic_hooks(
139    mut expression: TokenStream,
140    def: &ManifestAuraDef,
141    options: &AuraEmitOptions<'_>,
142) -> anyhow::Result<TokenStream> {
143    let id = def.id;
144
145    match def.periodic_hasted {
146        Some(true) => expression = quote!(#expression.hasted_periodic()),
147        Some(false) => expression = quote!(#expression.unhasted_periodic()),
148        None => {}
149    }
150
151    if def.periodic_partial_tick {
152        if def.tick_hook.is_none() {
153            bail!("periodic_partial_tick on aura {id} requires a tick_hook");
154        }
155
156        expression = quote!(#expression.partial_tick());
157    }
158
159    if let Some(hook) = &def.expire_hook {
160        let module = rust_path(options.hook_module)?;
161        let hook = format_ident!("{hook}{}", options.hook_suffix);
162
163        expression = quote!(#expression.on_expire(crate::hooks::#module::#hook));
164    }
165
166    if let Some(hook) = &def.tick_hook {
167        let module = rust_path(options.hook_module)?;
168        let hook = format_ident!("{hook}{}", options.hook_suffix);
169
170        expression = quote!(#expression.tick_hook(crate::hooks::#module::#hook));
171    }
172
173    Ok(expression)
174}
175
176pub(super) fn apply_periodic_damage(
177    mut expression: TokenStream,
178    def: &ManifestAuraDef,
179    id: u32,
180    has_item_budget: bool,
181) -> anyhow::Result<TokenStream> {
182    if let Some(periodic) = &def.periodic_damage {
183        expression = apply_periodic_payload(expression, periodic, id, has_item_budget)?;
184    }
185
186    if let Some(periodic) = &def.periodic_residual_damage {
187        let tick =
188            validated_milliseconds_call(&periodic.tick_ms, id, "periodic_residual_damage.tick_ms")?;
189
190        expression = quote!(#expression.periodic_residual_damage(#tick));
191    }
192
193    Ok(expression)
194}
195
196fn apply_periodic_payload(
197    mut expression: TokenStream,
198    periodic: &PeriodicDamage,
199    id: u32,
200    has_item_budget: bool,
201) -> anyhow::Result<TokenStream> {
202    let tick = validated_milliseconds_call(&periodic.tick_ms, id, "periodic_damage.tick_ms")?;
203    let payload_count = [
204        periodic.ap_coef.is_some(),
205        periodic.sp_coef.is_some(),
206        periodic.item_damage_total.is_some(),
207    ]
208    .into_iter()
209    .filter(|present| *present)
210    .count();
211
212    if payload_count != 1 {
213        bail!(
214            "periodic_damage for spell {id} must have exactly one of ap_coef, sp_coef, or item_damage_total"
215        );
216    }
217
218    if let Some(total) = &periodic.item_damage_total {
219        if !has_item_budget {
220            bail!("periodic_damage.item_damage_total is items-manifest-only (aura {id})");
221        }
222
223        let spell_id = lit_int(periodic.spell_id.unwrap_or(id));
224        let total = item_scaled_effect_call(total)?;
225
226        expression = quote! {
227            #expression.periodic_total_flat_damage_from_data(
228                data,
229                #tick,
230                #spell_id,
231                #total,
232            )?
233        };
234    } else if let Some(sp) = &periodic.sp_coef {
235        let derived = derived_periodic_data(
236            sp,
237            PeriodicCoefficient::SpellPower,
238            periodic.school.is_some(),
239            periodic.spell_id,
240        );
241
242        expression = if let Some((spell_id, effect, multiplier, offset, round_decimals)) = derived {
243            let spell_id = lit_int(spell_id);
244            let effect = lit_int(effect);
245            let multiplier = lit_f64(multiplier.unwrap_or(1.0));
246            let offset = lit_f64(offset.unwrap_or(0.0));
247            let round_decimals = option_u8(round_decimals);
248
249            quote! {
250                #expression.periodic_damage_sp_from_data(
251                    data,
252                    wowlab_engine_combat::PeriodicDamageDataInput {
253                        tick_ms: #tick,
254                        spell_id: #spell_id,
255                        effect_index: #effect,
256                        multiplier: #multiplier,
257                        offset: #offset,
258                        round_decimals: #round_decimals,
259                    },
260                )?
261            }
262        } else if let Some(spell_id) = periodic.spell_id {
263            let school = periodic.school.ok_or_else(|| {
264                        anyhow::anyhow!(
265                            "periodic_damage for aura {id} needs school when its coefficient is not derived from its attribution spell"
266                        )
267                    })?;
268            let flags = damage_flags(school);
269            let spell_id = lit_int(spell_id);
270            let coefficient = scalar_ref_call(sp)?;
271
272            quote! {
273                #expression.periodic_damage_sp_from_spell(
274                    #tick,
275                    wowlab_engine_combat::DamagePayload::new(
276                        #spell_id,
277                        #coefficient,
278                        #flags,
279                    ),
280                )
281            }
282        } else {
283            let flags = damage_flags(periodic.school.unwrap_or(DamageSchoolKind::Magic));
284            let coefficient = scalar_ref_call(sp)?;
285
286            quote!(#expression.periodic_damage_sp(#tick, #coefficient, #flags))
287        };
288    } else if let Some(ap) = &periodic.ap_coef {
289        expression = apply_periodic_ap(&expression, periodic, ap, &tick, id)?;
290    }
291
292    if let Some(resource_gain) = &periodic.resource_gain {
293        let resource_gain = scalar_ref_call(resource_gain)?;
294
295        expression = quote!(#expression.periodic_damage_resource_gain(#resource_gain));
296    }
297
298    if let Some(crit_resource_gain) = &periodic.crit_resource_gain {
299        let chance = periodic.crit_resource_chance.as_ref().ok_or_else(|| {
300                anyhow::anyhow!(
301                    "periodic_damage for aura {id} with crit_resource_gain requires crit_resource_chance"
302                )
303            })?;
304        let chance = scalar_ref_call(chance)?;
305        let gain = scalar_ref_call(crit_resource_gain)?;
306
307        expression = quote!(#expression.periodic_damage_crit_resource_gain(#chance, #gain));
308    }
309
310    if periodic.direct {
311        expression = quote!(#expression.periodic_tick_direct_damage());
312    }
313
314    Ok(expression)
315}
316
317fn apply_periodic_ap(
318    expression: &TokenStream,
319    periodic: &PeriodicDamage,
320    coefficient: &ScalarRef,
321    tick: &TokenStream,
322    id: u32,
323) -> anyhow::Result<TokenStream> {
324    let derived = derived_periodic_data(
325        coefficient,
326        PeriodicCoefficient::AttackPower,
327        periodic.school.is_some(),
328        periodic.spell_id,
329    );
330
331    if let Some((spell_id, effect, multiplier, offset, round_decimals)) = derived {
332        let spell_id = lit_int(spell_id);
333        let effect = lit_int(effect);
334        let multiplier = lit_f64(multiplier.unwrap_or(1.0));
335        let offset = lit_f64(offset.unwrap_or(0.0));
336        let round_decimals = option_u8(round_decimals);
337        let mode = match periodic.mode {
338            PeriodicDamageMode::Standard => {
339                quote!(wowlab_engine_combat::PeriodicDamageMode::Standard)
340            }
341            PeriodicDamageMode::Rolling => {
342                quote!(wowlab_engine_combat::PeriodicDamageMode::Rolling)
343            }
344        };
345
346        return Ok(quote! {
347            #expression.periodic_damage_ap_from_data(
348                data,
349                wowlab_engine_combat::PeriodicDamageDataInput {
350                    tick_ms: #tick,
351                    spell_id: #spell_id,
352                    effect_index: #effect,
353                    multiplier: #multiplier,
354                    offset: #offset,
355                    round_decimals: #round_decimals,
356                },
357                #mode,
358            )?
359        });
360    }
361
362    if let Some(spell_id) = periodic.spell_id {
363        let school = periodic.school.ok_or_else(|| {
364            anyhow::anyhow!(
365                "periodic_damage for aura {id} needs school when its coefficient is not derived from its attribution spell"
366            )
367        })?;
368        let flags = damage_flags(school);
369        let spell_id = lit_int(spell_id);
370        let coefficient = scalar_ref_call(coefficient)?;
371
372        return Ok(quote! {
373            #expression.periodic_damage_from_spell(
374                #tick,
375                wowlab_engine_combat::DamagePayload::new(
376                    #spell_id,
377                    #coefficient,
378                    #flags,
379                ),
380            )
381        });
382    }
383
384    let flags = damage_flags(periodic.school.unwrap_or(DamageSchoolKind::Magic));
385    let coefficient = scalar_ref_call(coefficient)?;
386
387    Ok(match periodic.mode {
388        PeriodicDamageMode::Standard => {
389            quote!(#expression.periodic_damage(#tick, #coefficient, #flags))
390        }
391        PeriodicDamageMode::Rolling => {
392            quote!(#expression.periodic_rolling_damage(#tick, #coefficient, #flags))
393        }
394    })
395}
396
397pub(super) fn apply_aura_duration(
398    mut expression: TokenStream,
399    def: &ManifestAuraDef,
400) -> anyhow::Result<TokenStream> {
401    let id = lit_int(def.id);
402
403    match &def.duration_ms {
404        Some(duration) => {
405            let source_spell_id = duration_spell_id(duration);
406            let duration_expr = validated_milliseconds_call(duration, def.id, "aura.duration_ms")?;
407
408            expression = quote!(#expression.duration_ms(#duration_expr));
409            expression = quote!(#expression.apply_periodic_effect_from_data(data, #id)?);
410
411            if let Some(source_spell_id) = source_spell_id {
412                let source_spell_id = lit_int(source_spell_id);
413
414                expression = quote! {
415                    #expression.apply_duration_behavior_from_data(data, #source_spell_id)?
416                };
417                expression = quote!(#expression.apply_timing_modifiers_from_data(data, #id)?);
418            }
419        }
420        None => expression = quote!(#expression.apply_base_from_data(data, #id)?),
421    }
422
423    Ok(expression)
424}
425
426const fn duration_spell_id(value: &ScalarRef) -> Option<u32> {
427    match value {
428        ScalarRef::SpellRef {
429            spell_id,
430            field: SpellScalarField::DurationMs,
431            ..
432        } => Some(*spell_id),
433        _ => None,
434    }
435}
436
437pub(super) fn periodic_behavior_spell_id(def: &ManifestAuraDef) -> Option<u32> {
438    let direct_damage = def
439        .periodic_damage
440        .as_ref()
441        .and_then(|periodic| periodic_behavior_source(&periodic.tick_ms));
442    let direct_resource = def
443        .periodic_resource
444        .as_ref()
445        .and_then(|periodic| periodic_behavior_source(&periodic.tick_ms));
446    let residual_damage = def
447        .periodic_residual_damage
448        .as_ref()
449        .and_then(|periodic| periodic_behavior_source(&periodic.tick_ms));
450    let direct_damage_or_resource = direct_damage.or(direct_resource).or(residual_damage);
451    let drained_or_applied = def
452        .periodic_resource_drain
453        .as_ref()
454        .and_then(|periodic| periodic_behavior_source(&periodic.tick_ms))
455        .or_else(|| {
456            def.periodic_apply_aura
457                .as_ref()
458                .and_then(|periodic| periodic_behavior_source(&periodic.tick_ms))
459        });
460    let hook = def
461        .periodic_hook
462        .as_ref()
463        .and_then(|periodic| periodic_behavior_source(&periodic.tick_ms));
464    let periodic_source = direct_damage_or_resource.or(drained_or_applied).or(hook);
465
466    periodic_source
467        .or_else(|| def.duration_ms.as_ref().and_then(duration_spell_id))
468        .or_else(|| {
469            (def.periodic_damage.is_some()
470                || def.periodic_resource.is_some()
471                || def.periodic_residual_damage.is_some()
472                || def.periodic_resource_drain.is_some()
473                || def.periodic_apply_aura.is_some()
474                || def.periodic_hook.is_some())
475            .then_some(def.id)
476        })
477}
478
479const fn periodic_behavior_source(value: &ScalarRef) -> Option<u32> {
480    match value {
481        ScalarRef::EffectRef { spell_id, .. }
482        | ScalarRef::SpellRef {
483            spell_id,
484            field: SpellScalarField::DurationMs,
485            ..
486        } => Some(*spell_id),
487        ScalarRef::SpellRef { .. } | ScalarRef::Literal(_) => None,
488    }
489}