1use 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}