Skip to main content

codegen/
gen_spells.rs

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/// Which builder-code shape the spell lowerer emits.
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20#[non_exhaustive]
21pub(crate) enum SpellChainKind {
22    Spec,
23    Item,
24}
25
26/// Options that let spell lowering reproduce either the spec or item spell function.
27pub(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
97/// Generated Rust code at the spell-builder stage.
98pub(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
118/// Lower one manifest spell into generated spell-builder code.
119pub(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(&quote!(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;