1use proc_macro2::TokenStream;
2use quote::{format_ident, quote};
3use wowlab_manifest_schema::{Manifest, ManifestSpellDef};
4
5use crate::{
6 gen_aoe::apply_aoe,
7 gen_damage::apply_damage,
8 gen_emit::{effect_lookup, validated_milliseconds_call, validated_nonnegative_call},
9 gen_spell_channel::apply_channel,
10 gen_spell_properties::{apply_core_spell_properties, apply_override},
11 helpers::to_snake,
12 rust_source::{
13 ExpressionRequirements, expression_requirements, lit_f64, lit_int, result_expression,
14 rust_ident, rust_path,
15 },
16};
17
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20#[non_exhaustive]
21pub(crate) enum SpellChainKind {
22 Spec,
23 Item,
24}
25
26pub(crate) struct SpellChainOpts<'a> {
28 pub kind: SpellChainKind,
29 pub inferred_aura: Option<(&'a str, u32)>,
30 pub hook_module: &'a str,
31 pub hook_suffix: &'a str,
32}
33
34pub(crate) fn spell_functions(
35 manifest: &Manifest,
36 module_name: &str,
37) -> anyhow::Result<TokenStream> {
38 let functions = manifest
39 .spells
40 .iter()
41 .map(|(name, def)| {
42 let function_name = format_ident!("spell_{}", to_snake(name));
43 let options = SpellChainOpts {
44 kind: SpellChainKind::Spec,
45 inferred_aura: infer_self_applied_aura(name, def, manifest),
46 hook_module: module_name,
47 hook_suffix: "_hook",
48 };
49 let generated = lower_manifest_spell(def, &options)?;
50 let data_parameter = if generated.uses_data() {
51 quote!(data)
52 } else {
53 quote!(_data)
54 };
55 let expression = generated.into_result_expression()?;
56
57 Ok::<_, anyhow::Error>(quote! {
58 fn #function_name(
59 s: SpellDefinitionDraft,
60 #data_parameter: &ResolvedGameData,
61 ) -> Result<SpellDefinitionDraft, wowlab_engine_combat::BuilderError> {
62 #expression
63 }
64 })
65 })
66 .collect::<anyhow::Result<Vec<_>>>()?;
67
68 Ok(quote!(#(#functions)*))
69}
70
71fn infer_self_applied_aura<'a>(
72 spell_name: &'a str,
73 def: &ManifestSpellDef,
74 manifest: &'a Manifest,
75) -> Option<(&'a str, u32)> {
76 if !def.infer_applies_aura || def.applies_aura.is_some() || !def.applies_auras.is_empty() {
77 return None;
78 }
79
80 if let Some(aura) = manifest.auras.get(spell_name) {
81 return Some((spell_name, aura.id));
82 }
83
84 let matches = manifest
85 .auras
86 .iter()
87 .filter(|(_, aura)| aura.id == def.id)
88 .map(|(name, _)| name.as_str())
89 .collect::<Vec<_>>();
90
91 match matches.as_slice() {
92 [only] => Some((only, def.id)),
93 _ => None,
94 }
95}
96
97pub(crate) struct GeneratedSpellBuilderCode {
99 expression: TokenStream,
100 requirements: ExpressionRequirements,
101}
102
103impl GeneratedSpellBuilderCode {
104 pub(crate) fn into_result_expression(self) -> anyhow::Result<TokenStream> {
105 Ok(result_expression(self.expression)?)
106 }
107
108 fn uses_data(&self) -> bool {
109 self.requirements.data
110 }
111
112 #[cfg(test)]
113 fn body_string(&self) -> String {
114 self.expression.to_string()
115 }
116}
117
118pub(crate) fn lower_manifest_spell(
120 def: &ManifestSpellDef,
121 options: &SpellChainOpts<'_>,
122) -> anyhow::Result<GeneratedSpellBuilderCode> {
123 let id = def.id;
124 let raw_id = lit_int(id);
125 let mut expression = match options.kind {
126 SpellChainKind::Spec => {
127 let mut expression = quote!(s.apply_base_from_data(data, #raw_id)?);
128
129 if !def.infer_damage {
130 expression = quote!(#expression.without_data_damage());
131 }
132
133 if def.infer_damage && !def.infer_primary_damage {
134 expression = quote!(#expression.without_primary_data_damage());
135 }
136
137 if def.usable_while_casting {
138 expression = quote!(#expression.usable_while_casting());
139 }
140
141 expression
142 }
143 SpellChainKind::Item => {
144 let mut expression = quote!(s.apply_cooldown_pools_from_data(data, #raw_id)?);
145
146 if let Some(cooldown) = &def.cooldown {
147 let cooldown = validated_nonnegative_call(cooldown, id, "spell.cooldown")?;
148
149 expression = quote!(#expression.cooldown(#cooldown));
150 }
151
152 if def.usable_while_casting {
153 expression = quote!(#expression.usable_while_casting());
154 }
155
156 expression
157 }
158 };
159
160 match options.kind {
161 SpellChainKind::Spec => {
162 expression = spec_expression(expression, def, options)?;
163 }
164 SpellChainKind::Item => {
165 if let Some(aura) = def.applies_aura.as_deref() {
166 let aura = rust_ident(aura)?;
167
168 expression = quote!(#expression.applies_aura_id(AURA::#aura));
169 }
170
171 for aura in &def.applies_auras {
172 let aura = rust_ident(aura)?;
173
174 expression = quote!(#expression.applies_aura_id(AURA::#aura));
175 }
176
177 if let Some(hook) = &def.hook {
178 let module = rust_path(options.hook_module)?;
179 let hook = format_ident!("{hook}{}", options.hook_suffix);
180
181 expression = quote!(#expression.on_cast(crate::hooks::#module::#hook));
182 }
183 }
184 }
185
186 let requirements = expression_requirements(expression.clone())?;
187
188 Ok(GeneratedSpellBuilderCode {
189 expression,
190 requirements,
191 })
192}
193
194fn spec_expression(
195 mut expression: TokenStream,
196 def: &ManifestSpellDef,
197 options: &SpellChainOpts<'_>,
198) -> anyhow::Result<TokenStream> {
199 let id = def.id;
200
201 expression = apply_core_spell_properties(expression, id, def, options.hook_module)?;
202 expression = apply_damage_payloads(expression, def, options.hook_module)?;
203
204 if let Some(aura) = def.applies_aura.as_deref() {
205 let aura = format_ident!("AURA_{aura}");
206
207 expression = quote!(#expression.applies_aura(#aura));
208 } else if let Some((aura, aura_id)) = options.inferred_aura {
209 let aura = format_ident!("AURA_{aura}");
210 let aura_id = lit_int(aura_id);
211
212 expression = quote!(#expression.applies_inferred_aura(#aura, #aura_id));
213 }
214
215 for aura in &def.applies_auras {
216 let aura = format_ident!("AURA_{aura}");
217
218 expression = quote!(#expression.applies_aura(#aura));
219 }
220
221 for extension in &def.extends_auras {
222 let aura = format_ident!("AURA_{}", extension.aura);
223 let amount_ms = lit_int(extension.amount_ms);
224
225 expression = quote!(#expression.extends_aura(#aura, #amount_ms));
226 }
227
228 if let Some(cooldown_reductions) = &def.reduces_cd {
229 for reduction in cooldown_reductions {
230 let target = format_ident!("SPELL_{}", reduction.target);
231 let amount = validated_milliseconds_call(
232 &reduction.amount_ms,
233 id,
234 "spell.reduces_cd.amount_ms",
235 )?;
236
237 expression = quote!(#expression.reduces_cd(#target, #amount));
238 }
239 }
240
241 if let Some(reduction) = &def.reduces_cd_chance {
242 let target = format_ident!("SPELL_{}", reduction.target);
243 let amount = validated_milliseconds_call(
244 &reduction.amount_ms,
245 id,
246 "spell.reduces_cd_chance.amount_ms",
247 )?;
248 let chance = lit_f64(reduction.chance);
249
250 expression = quote!(#expression.reduces_cd_chance(#target, #amount, #chance));
251 }
252
253 if let Some(reset) = &def.resets_cd_while {
254 let target = format_ident!("SPELL_{}", reset.target);
255 let aura = format_ident!("AURA_{}", reset.aura);
256
257 expression = quote!(#expression.resets_cd_while(#target, #aura));
258 }
259
260 if !def.breaks_stealth {
261 expression = quote!(#expression.no_stealth_break());
262 }
263
264 if def.guaranteed_crit {
265 expression = quote!(#expression.guaranteed_crit());
266 }
267
268 expression = apply_aoe(expression, def);
269
270 if let Some(channel) = &def.channel {
271 expression = apply_channel(expression, id, channel, options.hook_module)?;
272 }
273
274 if let Some(hook) = &def.hook {
275 let module = rust_path(options.hook_module)?;
276 let hook = format_ident!("{hook}{}", options.hook_suffix);
277
278 expression = quote!(#expression.on_cast(crate::hooks::#module::#hook));
279 }
280
281 if let Some(aura_id) = def.requires_aura_id {
282 let aura_id = lit_int(aura_id);
283 let min_stacks = lit_int(def.requires_aura_min_stacks.unwrap_or(0));
284
285 expression = quote!(#expression.requires_aura_at_min_stacks(#aura_id, #min_stacks));
286 }
287
288 if let Some(aura_id) = def.cost_free_when_aura {
289 let aura_id = lit_int(aura_id);
290
291 expression = quote!(#expression.cost_free_when_aura(#aura_id));
292 }
293
294 if let Some(aura_id) = def.consume_aura_on_cast {
295 let aura_id = lit_int(aura_id);
296
297 expression = quote!(#expression.consume_aura_on_cast(#aura_id));
298 }
299
300 if let Some(aura) = &def.cooldown_bypass_when_aura {
301 let aura = rust_ident(aura)?;
302
303 expression = quote!(#expression.cooldown_bypass_when_aura(AURA::#aura));
304 }
305
306 if let Some(threshold) = def.execute_below_pct {
307 let threshold = lit_f64(threshold);
308
309 expression = quote!(#expression.execute_below_pct(#threshold));
310 }
311
312 if let Some(aura) = &def.instant_when_aura {
313 let aura = rust_ident(aura)?;
314
315 expression = quote!(#expression.instant_when_aura(AURA::#aura));
316 }
317
318 apply_override(expression, id, def)
319}
320
321fn apply_damage_payloads(
322 mut expression: TokenStream,
323 def: &ManifestSpellDef,
324 hook_module: &str,
325) -> anyhow::Result<TokenStream> {
326 if let Some(damage) = def.damage.as_ref() {
327 expression = apply_damage(&expression, damage)?;
328 }
329
330 for damage in &def.damage_effects {
331 let lookup = effect_lookup("e!(data), damage.spell_id, damage.effect);
332 let multiplier = lit_f64(damage.multiplier);
333
334 expression = match (damage.kind.as_str(), damage.ap_type.as_deref()) {
335 ("ap", None) => {
336 quote!(#expression.additional_damage_ap_from_data(#lookup, #multiplier)?)
337 }
338 ("ap", Some(ap_type)) => {
339 let ap_type = match ap_type {
340 "mainhand" => format_ident!("MainHand"),
341 "offhand" => format_ident!("OffHand"),
342 "both" => format_ident!("Both"),
343 "none" => format_ident!("None"),
344 other => anyhow::bail!("unknown damage effect ap_type: {other}"),
345 };
346
347 quote! {
348 #expression.additional_damage_ap_from_data_typed(
349 #lookup,
350 #multiplier,
351 wowlab_engine_combat::WeaponApType::#ap_type,
352 )?
353 }
354 }
355 ("sp", _) => {
356 quote!(#expression.additional_damage_sp_from_data(#lookup, #multiplier)?)
357 }
358 (other, _) => anyhow::bail!("unknown damage effect kind: {other}"),
359 };
360 }
361
362 for trigger_predicate in &def.trigger_program_predicates {
363 if trigger_predicate.effects.is_empty() {
364 anyhow::bail!("trigger program predicate effects must not be empty");
365 }
366
367 for effect_index in &trigger_predicate.effects {
368 if *effect_index == 0 {
369 anyhow::bail!("trigger program predicate effect indices are one-based");
370 }
371
372 let effect_index = lit_int(effect_index);
373 let module = rust_path(hook_module)?;
374 let predicate = rust_ident(&trigger_predicate.predicate)?;
375
376 expression = quote! {
377 #expression.trigger_program_predicate(
378 #effect_index,
379 crate::hooks::#module::#predicate,
380 )
381 };
382 }
383 }
384
385 Ok(expression)
386}
387
388#[cfg(test)]
389#[path = "gen_spells/tests.rs"]
390mod tests;