1use anyhow::{Context, bail};
4use proc_macro2::TokenStream;
5use quote::{format_ident, quote};
6use wowlab_manifest_schema::{Manifest, ManifestAuraDef, SpellGroupRule};
7use wowlab_types::sim::FastMap;
8
9use crate::{
10 gen_aura_behavior::{apply_aura_consume_effects, apply_refresh_behavior, apply_tick_behavior},
11 gen_aura_modifiers::{
12 apply_per_stack_scalars, apply_rating_effects, apply_spell_scoped_effects,
13 },
14 gen_aura_periodic::{
15 apply_aura_duration, apply_periodic_damage, apply_periodic_hook_source,
16 apply_periodic_hooks, periodic_behavior_spell_id,
17 },
18 gen_emit::{
19 require_get, scalar_ref_call, spell_idx, validated_max_stacks_call,
20 validated_milliseconds_call,
21 },
22 helpers::to_snake,
23 rust_source::{ExpressionRequirements, expression_requirements, lit_int, result_expression},
24};
25
26pub(crate) fn aura_functions(
27 manifest: &Manifest,
28 module_name: &str,
29) -> anyhow::Result<TokenStream> {
30 let aura_count = manifest
31 .spell_groups
32 .iter()
33 .map(|group| group.auras.len())
34 .sum();
35 let mut spell_group_map = FastMap::default();
36
37 spell_group_map.reserve(aura_count);
38
39 for (group_index, group) in manifest.spell_groups.iter().enumerate() {
40 let group_index =
41 u8::try_from(group_index).context("manifest has more than 256 spell groups")?;
42
43 for aura_name in &group.auras {
44 spell_group_map.insert(aura_name.as_str(), (group_index, group.rule));
45 }
46 }
47
48 let mut aura_idx_map = FastMap::default();
49
50 aura_idx_map.reserve(manifest.auras.len());
51
52 for (index, name) in manifest.auras.keys().enumerate() {
53 let index = u8::try_from(index).context("manifest has more than 256 auras")?;
54
55 aura_idx_map.insert(name.as_str(), index);
56 }
57
58 let spell_id_map = manifest_spell_id_map(manifest)?;
59 let functions = manifest
60 .auras
61 .iter()
62 .map(|(name, def)| {
63 let options = AuraEmitOptions {
64 spell_group: spell_group_map.get(name.as_str()).copied(),
65 aura_idx_map: &aura_idx_map,
66 spell_id_map: &spell_id_map,
67 emit_default_max_stacks: def.max_stacks.is_none(),
68 has_item_budget: false,
69 hook_module: module_name,
70 hook_suffix: "_hook",
71 };
72
73 emit_aura_fn(name, def, &options)
74 })
75 .collect::<anyhow::Result<Vec<_>>>()?;
76
77 Ok(quote!(#(#functions)*))
78}
79
80fn manifest_spell_id_map(manifest: &Manifest) -> anyhow::Result<FastMap<&str, u32>> {
81 let mut ids: FastMap<&str, u32> = manifest
82 .spells
83 .iter()
84 .map(|(name, spell)| (name.as_str(), spell.id))
85 .collect();
86
87 for (name, id) in &manifest.reported_spells {
88 ids.entry(name).or_insert(*id);
89 }
90
91 let hero_spell_count = manifest
92 .hero_talents
93 .values()
94 .map(|tree| tree.spells.len())
95 .sum();
96 let mut hero_ids = FastMap::default();
97
98 hero_ids.reserve(hero_spell_count);
99
100 for tree in manifest.hero_talents.values() {
101 for (name, id) in &tree.spells {
102 if let Some(previous) = hero_ids.insert(name.as_str(), *id)
103 && previous != *id
104 && !ids.contains_key(name.as_str())
105 {
106 bail!("hero spell binding {name:?} is ambiguous between ids {previous} and {id}");
107 }
108
109 ids.entry(name).or_insert(*id);
110 }
111 }
112
113 Ok(ids)
114}
115
116pub(crate) struct AuraEmitOptions<'a> {
118 pub spell_group: Option<(u8, SpellGroupRule)>,
119 pub aura_idx_map: &'a FastMap<&'a str, u8>,
120 pub spell_id_map: &'a FastMap<&'a str, u32>,
121 pub emit_default_max_stacks: bool,
122 pub has_item_budget: bool,
123 pub hook_module: &'a str,
124 pub hook_suffix: &'a str,
125}
126
127struct GeneratedAuraBuilderCode {
128 expression: TokenStream,
129 requirements: ExpressionRequirements,
130 _stage: AuraBuilderStage,
131}
132
133struct AuraBuilderStage;
134
135impl GeneratedAuraBuilderCode {
136 fn uses_data(&self) -> bool {
137 self.requirements.data
138 }
139
140 fn uses_budget(&self) -> bool {
141 self.requirements.budget
142 }
143
144 fn into_result_expression(self) -> anyhow::Result<TokenStream> {
145 Ok(result_expression(self.expression)?)
146 }
147
148 #[cfg(test)]
149 fn body_string(&self) -> String {
150 self.expression.to_string()
151 }
152}
153
154fn lower_manifest_aura(
155 def: &ManifestAuraDef,
156 options: &AuraEmitOptions<'_>,
157) -> anyhow::Result<GeneratedAuraBuilderCode> {
158 let id = def.id;
159 let spell_id = spell_idx(id);
160 let mut expression = match def.on.as_str() {
161 "target" => quote!(a.on_target()),
162 "player" => quote!(a.on_player()),
163 "pet" => quote!(a.on_pet()),
164 other => bail!("unsupported aura target {other:?} on aura {id}"),
165 };
166
167 expression = apply_core_aura_options(expression, def, options, &spell_id)?;
168 expression = apply_tick_behavior(expression, def)?;
169
170 if let Some(remove_stack) = &def.periodic_remove_stack {
171 let tick_ms = validated_milliseconds_call(
172 &remove_stack.tick_ms,
173 id,
174 "periodic_remove_stack.tick_ms",
175 )?;
176
177 expression = quote!(#expression.periodic_remove_stack(#tick_ms));
178 }
179
180 if let Some(value) = &def.primary_stat_percent_per_stack {
181 let value = scalar_ref_call(value)?;
182
183 expression = quote!(#expression.primary_pct_per_stack(#value)?);
184 }
185
186 if let Some(value) = &def.attack_power_percent_per_stack {
187 let value = scalar_ref_call(value)?;
188
189 expression = quote!(#expression.attack_power_pct_per_stack(#value)?);
190 }
191
192 expression = apply_spell_scoped_effects(expression, def, options)?;
193 expression = apply_periodic_damage(expression, def, id, options.has_item_budget)?;
194
195 if let Some(resource) = &def.periodic_resource {
196 let tick_ms =
197 validated_milliseconds_call(&resource.tick_ms, id, "periodic_resource.tick_ms")?;
198 let amount = scalar_ref_call(&resource.amount)?;
199
200 expression = quote!(#expression.periodic_resource(#tick_ms, #amount));
201 }
202
203 if let Some(drain) = &def.periodic_resource_drain {
204 let tick_ms =
205 validated_milliseconds_call(&drain.tick_ms, id, "periodic_resource_drain.tick_ms")?;
206 let amount = scalar_ref_call(&drain.amount)?;
207
208 expression = quote!(#expression.periodic_resource_drain(#tick_ms, #amount));
209 }
210
211 if let Some(apply) = &def.periodic_apply_aura {
212 let index = options
213 .aura_idx_map
214 .get(apply.aura.as_str())
215 .copied()
216 .ok_or_else(|| {
217 anyhow::anyhow!(
218 "periodic_apply_aura.aura {:?} not found in [auras.*]",
219 apply.aura
220 )
221 })?;
222 let tick_ms =
223 validated_milliseconds_call(&apply.tick_ms, id, "periodic_apply_aura.tick_ms")?;
224 let index = lit_int(index);
225
226 expression = quote!(#expression.periodic_apply_aura(#tick_ms, #index));
227 }
228
229 expression = apply_periodic_hook_source(expression, def)?;
230
231 if let Some(source_spell_id) = periodic_behavior_spell_id(def) {
232 let source_spell_id = lit_int(source_spell_id);
233
234 expression = quote!(#expression.apply_periodic_behavior_from_data(data, #source_spell_id)?);
235 }
236
237 expression = apply_periodic_hooks(expression, def, options)?;
238 expression = apply_aura_consume_effects(expression, def, options)?;
239
240 if let Some((group_index, rule)) = options.spell_group {
241 let group_index = lit_int(group_index);
242 let rule = match rule {
243 SpellGroupRule::Exclusive => {
244 quote!(wowlab_types::combat::SpellGroupRule::Exclusive)
245 }
246 };
247
248 expression = quote!(#expression.spell_group(#group_index, #rule));
249 }
250
251 let requirements = expression_requirements(expression.clone())?;
252
253 Ok(GeneratedAuraBuilderCode {
254 expression,
255 requirements,
256 _stage: AuraBuilderStage,
257 })
258}
259
260fn apply_core_aura_options(
261 mut expression: TokenStream,
262 def: &ManifestAuraDef,
263 options: &AuraEmitOptions<'_>,
264 spell_id: &TokenStream,
265) -> anyhow::Result<TokenStream> {
266 let id = def.id;
267
268 expression = apply_aura_duration(expression, def)?;
269
270 if options.emit_default_max_stacks {
271 let max_stacks = require_get("aura_max_stacks", spell_id);
272
273 expression = quote!(#expression.max_stacks(#max_stacks));
274 }
275
276 expression = apply_refresh_behavior(expression, def)?;
277
278 if def.snapshot {
279 expression = quote!(#expression.snapshot());
280 }
281
282 if let Some(value) = &def.forwarded_effect_value {
283 let value = scalar_ref_call(value)?;
284
285 expression = quote!(#expression.forwarded_effect_value(data, #value)?);
286 }
287
288 if let Some(multiplier) = &def.damage_mult {
289 let multiplier = scalar_ref_call(multiplier)?;
290
291 expression = quote!(#expression.damage_multiplier(#multiplier)?);
292 }
293
294 if let Some(multiplier) = &def.damage_mult_stacking {
295 let initial = scalar_ref_call(&multiplier.initial)?;
296 let per_stack = scalar_ref_call(&multiplier.per_stack)?;
297
298 expression = quote!(#expression.damage_multiplier_stacking(#initial, #per_stack)?);
299 }
300
301 if let Some(multiplier) = &def.pet_damage_mult {
302 let multiplier = scalar_ref_call(multiplier)?;
303
304 expression = quote!(#expression.pet_damage_multiplier(#multiplier)?);
305 }
306
307 if let Some(stacks) = &def.max_stacks {
308 let stacks = validated_max_stacks_call(stacks, id, "aura.max_stacks")?;
309
310 expression = quote!(#expression.max_stacks(#stacks));
311 }
312
313 expression = apply_per_stack_scalars(expression, def)?;
314 expression = apply_rating_effects(expression, def, options)?;
315
316 if def.apply_at_max_stacks {
317 expression = quote!(#expression.apply_at_max_stacks());
318 }
319
320 if def.reverse {
321 expression = quote!(#expression.reverse());
322 }
323
324 if def.freeze_stacks {
325 expression = quote!(#expression.freeze_stacks());
326 }
327
328 if def.async_stacks {
329 expression = quote!(#expression.async_stacks());
330 }
331
332 if def.periodic_damage_scales_with_stacks {
333 expression = quote!(#expression.periodic_damage_scales_with_stacks());
334 }
335
336 if def.reapply_on_expire {
337 expression = quote!(#expression.reapply_on_expire());
338 }
339
340 Ok(expression)
341}
342
343pub(crate) fn emit_aura_fn(
345 name: &str,
346 def: &ManifestAuraDef,
347 options: &AuraEmitOptions<'_>,
348) -> anyhow::Result<TokenStream> {
349 let function_name = format_ident!("aura_{}", to_snake(name));
350 let generated = lower_manifest_aura(def, options)?;
351 let data_parameter = if generated.uses_data() {
352 quote!(data)
353 } else {
354 quote!(_data)
355 };
356 let uses_budget = generated.uses_budget();
357 let expression = generated.into_result_expression()?;
358
359 Ok(if options.has_item_budget {
360 let budget_parameter = if uses_budget {
361 quote!(budget)
362 } else {
363 quote!(_budget)
364 };
365
366 quote! {
367 fn #function_name(
368 a: AuraDefinitionDraft,
369 #data_parameter: &ResolvedGameData,
370 #budget_parameter: ItemBudget,
371 ) -> Result<AuraDefinitionDraft, wowlab_engine_combat::BuilderError> {
372 #expression
373 }
374 }
375 } else {
376 quote! {
377 fn #function_name(
378 a: AuraDefinitionDraft,
379 #data_parameter: &ResolvedGameData,
380 ) -> Result<AuraDefinitionDraft, wowlab_engine_combat::BuilderError> {
381 #expression
382 }
383 }
384 })
385}
386
387#[cfg(test)]
388#[path = "gen_aura/periodic_tests.rs"]
389mod periodic_tests;
390
391#[cfg(test)]
392#[path = "gen_aura/tests.rs"]
393mod tests;