Skip to main content

codegen/
gen_aura_modifiers.rs

1//! Emission of per-stack, rating, and spell-scoped aura modifiers.
2
3use anyhow::bail;
4use proc_macro2::TokenStream;
5use quote::{format_ident, quote};
6use wowlab_manifest_schema::{ManifestAuraDef, ScalarRef};
7
8use crate::{
9    gen_aura::AuraEmitOptions,
10    gen_emit::scalar_ref_call,
11    rust_source::{lit_int, rust_ident},
12};
13
14fn budget_scaled_expr(
15    def: &ManifestAuraDef,
16    scalar: &ScalarRef,
17    budget_field: &str,
18) -> anyhow::Result<TokenStream> {
19    let scalar = scalar_ref_call(scalar)?;
20    let budget_field = rust_ident(budget_field)?;
21    let base = quote!(#scalar * budget.#budget_field);
22
23    if !def.split_across_stacks {
24        return Ok(base);
25    }
26
27    let stacks = def.max_stacks.as_ref().ok_or_else(|| {
28        anyhow::anyhow!(
29            "split_across_stacks on aura {} requires an explicit max_stacks",
30            def.id
31        )
32    })?;
33    let stacks = scalar_ref_call(stacks)?;
34
35    Ok(quote!((#base) / (#stacks)))
36}
37
38pub(super) fn apply_per_stack_scalars(
39    mut expression: TokenStream,
40    def: &ManifestAuraDef,
41) -> anyhow::Result<TokenStream> {
42    let fields = [
43        ("max_resource_per_stack", &def.max_resource_per_stack),
44        ("regen_percent_per_stack", &def.regen_percent_per_stack),
45        ("haste_per_stack", &def.haste_per_stack),
46        ("attack_speed_per_stack", &def.attack_speed_per_stack),
47        (
48            "auto_attack_damage_per_stack",
49            &def.auto_attack_damage_per_stack,
50        ),
51        (
52            "resource_gain_mult_per_stack",
53            &def.resource_gain_mult_per_stack,
54        ),
55        ("crit_per_stack", &def.crit_per_stack),
56        ("mastery_per_stack", &def.mastery_per_stack),
57    ];
58
59    for (method, value) in fields {
60        if let Some(value) = value {
61            let method = format_ident!("{method}");
62            let value = scalar_ref_call(value)?;
63
64            expression = quote!(#expression.#method(#value)?);
65        }
66    }
67
68    if let Some(value) = &def.damage_taken_from_caster_spells {
69        let value = scalar_ref_call(value)?;
70
71        expression = quote!(#expression.damage_taken_from_caster_spells(data, #value)?);
72    }
73
74    Ok(expression)
75}
76
77fn spell_ids(
78    method: &str,
79    names: &[String],
80    aura_id: u32,
81    options: &AuraEmitOptions<'_>,
82) -> anyhow::Result<Vec<syn::LitInt>> {
83    if names.is_empty() {
84        bail!("{method} on aura {aura_id} requires a non-empty spells list");
85    }
86
87    names
88        .iter()
89        .map(|name| {
90            options
91                .spell_id_map
92                .get(name.as_str())
93                .copied()
94                .map(lit_int)
95                .ok_or_else(|| {
96                    anyhow::anyhow!(
97                        "{method} spell {name:?} on aura {aura_id} has no spell binding"
98                    )
99                })
100        })
101        .collect()
102}
103
104fn spell_scoped_call(
105    expression: &TokenStream,
106    method: &str,
107    value: &ScalarRef,
108    spells: &[String],
109    aura_id: u32,
110    options: &AuraEmitOptions<'_>,
111) -> anyhow::Result<TokenStream> {
112    let method_name = method;
113    let method = rust_ident(method)?;
114    let value = scalar_ref_call(value)?;
115    let spell_ids = spell_ids(method_name, spells, aura_id, options)?;
116
117    Ok(quote!(#expression.#method(#value, &[#(#spell_ids,)*])?))
118}
119
120pub(super) fn apply_spell_scoped_effects(
121    mut expression: TokenStream,
122    def: &ManifestAuraDef,
123    options: &AuraEmitOptions<'_>,
124) -> anyhow::Result<TokenStream> {
125    let scoped = [
126        ("crit_chance_spells", &def.crit_chance_spells),
127        ("crit_damage_spells", &def.crit_damage_spells),
128        ("damage_percent_spells", &def.damage_percent_spells),
129        (
130            "damage_percent_spells_linear",
131            &def.damage_percent_spells_linear,
132        ),
133        ("cast_time_percent", &def.cast_time_percent),
134        (
135            "cast_time_percent_per_stack",
136            &def.cast_time_percent_per_stack,
137        ),
138        ("gcd_percent", &def.gcd_percent),
139    ];
140
141    for (method, field) in scoped {
142        if let Some(value) = field {
143            expression = spell_scoped_call(
144                &expression,
145                method,
146                &value.pct,
147                &value.spells,
148                def.id,
149                options,
150            )?;
151        }
152    }
153
154    let singular = [
155        ("spell_cost_flat", &def.spell_cost_flat),
156        ("spell_cost_percent", &def.spell_cost_percent),
157        (
158            "secondary_spell_cost_percent",
159            &def.secondary_spell_cost_percent,
160        ),
161        ("secondary_spell_cost_flat", &def.secondary_spell_cost_flat),
162    ];
163
164    for (method, field) in singular {
165        if let Some(value) = field {
166            expression = spell_scoped_call(
167                &expression,
168                method,
169                &value.value,
170                &value.spells,
171                def.id,
172                options,
173            )?;
174        }
175    }
176
177    for value in &def.recharge_rate_percent {
178        expression = spell_scoped_call(
179            &expression,
180            "recharge_rate_percent",
181            &value.value,
182            &value.spells,
183            def.id,
184            options,
185        )?;
186    }
187
188    Ok(expression)
189}
190
191pub(super) fn apply_rating_effects(
192    mut expression: TokenStream,
193    def: &ManifestAuraDef,
194    options: &AuraEmitOptions<'_>,
195) -> anyhow::Result<TokenStream> {
196    let rating_fields = [
197        (&def.crit_rating_per_stack, format_ident!("Crit")),
198        (&def.haste_rating_per_stack, format_ident!("Haste")),
199        (&def.mastery_rating_per_stack, format_ident!("Mastery")),
200        (
201            &def.versatility_rating_per_stack,
202            format_ident!("Versatility"),
203        ),
204    ];
205    let uses_budget = rating_fields.iter().any(|(field, _)| field.is_some())
206        || def.primary_stat_per_stack.is_some();
207
208    if uses_budget && !options.has_item_budget {
209        bail!(
210            "budget-scaled aura fields are items-manifest-only (aura {})",
211            def.id
212        );
213    }
214
215    for (field, rating) in rating_fields {
216        if let Some(scalar) = field {
217            let amount = budget_scaled_expr(def, scalar, "rating")?;
218
219            expression = quote! {
220                #expression.rating_per_stack(
221                    data,
222                    wowlab_types::game::RatingType::#rating,
223                    #amount,
224                )?
225            };
226        }
227    }
228
229    if let Some(scalar) = &def.primary_stat_per_stack {
230        let amount = budget_scaled_expr(def, scalar, "stat")?;
231
232        expression = quote!(#expression.primary_per_stack(#amount)?);
233    }
234
235    Ok(expression)
236}