Skip to main content

codegen/
gen_items.rs

1use proc_macro2::TokenStream;
2use quote::{format_ident, quote};
3use wowlab_manifest_schema::{ItemManifestEntry, ManifestAuraDef};
4use wowlab_types::sim::FastMap;
5
6use crate::{
7    gen_aura::{AuraEmitOptions, emit_aura_fn},
8    gen_constants::{damage_action_expr, effect_constant},
9    gen_spells::{SpellChainKind, SpellChainOpts, lower_manifest_spell},
10    helpers::{to_display_name, to_snake},
11    rust_source::{
12        FileComment, documented_u32_constant, inspect_link, lit_f64, lit_int, lit_str, render_rust,
13        rust_ident,
14    },
15};
16
17/// True when the item registers only plain on-player equip auras.
18pub(crate) fn is_simple_equip_aura(item: &ItemManifestEntry) -> bool {
19    if !item.spells.is_empty()
20        || item.player_cast_hook.is_some()
21        || item.player_impact_hook.is_some()
22        || item.rppm.is_some()
23        || item.driver_spell_id.is_some()
24        || item.auras.is_empty()
25    {
26        return false;
27    }
28
29    item.auras.values().all(is_plain_equip_aura)
30}
31
32fn is_plain_equip_aura(aura: &ManifestAuraDef) -> bool {
33    aura.on == "player"
34        && aura.duration_ms.is_none()
35        && aura.max_stacks.is_none()
36        && !aura.no_pandemic
37        && aura.refresh_behavior.is_none()
38        && !aura.precombat
39        && !aura.snapshot
40        && aura.damage_mult.is_none()
41        && aura.pet_damage_mult.is_none()
42        && aura.haste_per_stack.is_none()
43        && aura.crit_per_stack.is_none()
44        && aura.crit_rating_per_stack.is_none()
45        && aura.haste_rating_per_stack.is_none()
46        && aura.mastery_rating_per_stack.is_none()
47        && aura.versatility_rating_per_stack.is_none()
48        && aura.primary_stat_per_stack.is_none()
49        && !aura.apply_at_max_stacks
50        && !aura.reverse
51        && !aura.freeze_stacks
52        && aura.tick_behavior.is_none()
53        && aura.tick_stack_change.is_none()
54        && aura.stack_tick_ms.is_none()
55        && !aura.reapply_on_expire
56        && aura.periodic_remove_stack.is_none()
57        && aura.periodic_damage.is_none()
58        && aura.periodic_residual_damage.is_none()
59        && aura.periodic_hasted.is_none()
60        && aura.periodic_resource.is_none()
61        && aura.periodic_resource_drain.is_none()
62        && aura.periodic_apply_aura.is_none()
63}
64
65/// Generate one Rust file for a complex item.
66pub(crate) fn generate_item_file(key: &str, item: &ItemManifestEntry) -> anyhow::Result<String> {
67    let item_snake = to_snake(key);
68    let aura_import = (!item.auras.is_empty()).then(|| quote!(AuraDefinitionDraft,));
69    let spell_import = (!item.spells.is_empty()).then(|| quote!(SpellDefinitionDraft,));
70    let item_id = documented_u32_constant("ITEM_ID", item.id, "item")?;
71    let driver_spell_id = item
72        .driver_spell_id
73        .map(|id| documented_u32_constant("DRIVER_SPELL_ID", id, "spell"))
74        .transpose()?;
75    let constants = item_constants(item, &item_snake)?;
76    let auras = item_aura_functions(item, &item_snake)?;
77    let spells = item_spell_functions(item, &item_snake)?;
78    let register = item_register_function(key, item)?;
79    let preamble = [
80        FileComment::inner_doc(format!(
81            "@generated by codegen-cli from manifests/items.toml ({key}) -- do not edit by hand."
82        ))?,
83        FileComment::line("Generated code: naming follows game data conventions.")?,
84    ];
85    let lint_level = format_ident!("allow");
86
87    Ok(render_rust(
88        quote! {
89            #![#lint_level(non_snake_case, dead_code)]
90
91            use wowlab_engine_combat::{
92                CombatSystemBuilder,
93                #aura_import
94                #spell_import
95            };
96            use wowlab_engine_gamedata::{ItemBudget, ResolvedGameData};
97
98            #item_id
99            #driver_spell_id
100            #constants
101            #auras
102            #spells
103            #register
104        },
105        &preamble,
106    )?)
107}
108
109fn item_constants(item: &ItemManifestEntry, item_snake: &str) -> anyhow::Result<TokenStream> {
110    let binding_module = rust_ident(item_snake)?;
111    let damage_namespace = quote!(
112        wowlab_engine_combat::generated_identities::items::#binding_module::damage
113    );
114    let spells = item
115        .spells
116        .iter()
117        .map(|(name, spell)| documented_u32_constant(name, spell.id, "spell"))
118        .collect::<anyhow::Result<Vec<_>>>()?;
119    let auras = item
120        .auras
121        .iter()
122        .map(|(name, aura)| documented_u32_constant(name, aura.id, "spell"))
123        .collect::<anyhow::Result<Vec<_>>>()?;
124    let effects = item
125        .effects
126        .iter()
127        .map(|(name, effect)| {
128            let documentation = lit_str(&inspect_link("spell", effect.spell_id));
129            let constant = effect_constant(name, effect.spell_id, effect.effect)?;
130
131            Ok::<_, anyhow::Error>(quote! {
132                #[doc = #documentation]
133                #constant
134            })
135        })
136        .collect::<anyhow::Result<Vec<_>>>()?;
137    let effects = (!effects.is_empty()).then(|| {
138        quote! {
139            pub(crate) mod EFFECT {
140                #(#effects)*
141            }
142        }
143    });
144    let damage_spells = item
145        .spells
146        .iter()
147        .map(|(name, spell)| {
148            let name = rust_ident(name)?;
149            let action = damage_action_expr(&damage_namespace, spell.id);
150
151            Ok::<_, anyhow::Error>(quote! {
152                pub(crate) const #name: wowlab_engine_combat::DamageActionId =
153                    #action;
154            })
155        })
156        .collect::<anyhow::Result<Vec<_>>>()?;
157    let damage_auras = item
158        .auras
159        .iter()
160        .map(|(name, aura)| {
161            let name = rust_ident(name)?;
162            let action = damage_action_expr(&damage_namespace, aura.id);
163
164            Ok::<_, anyhow::Error>(quote! {
165                pub(crate) const #name: wowlab_engine_combat::DamageActionId =
166                    #action;
167            })
168        })
169        .collect::<anyhow::Result<Vec<_>>>()?;
170    let damage_effects = item
171        .effects
172        .iter()
173        .map(|(name, effect)| {
174            let name = rust_ident(name)?;
175            let effect = crate::gen_constants::damage_effect_expr(
176                &damage_namespace,
177                effect.spell_id,
178                effect.effect,
179            );
180
181            Ok::<_, anyhow::Error>(quote! {
182                pub(crate) const #name: wowlab_engine_combat::DamageEffectId =
183                    #effect;
184            })
185        })
186        .collect::<anyhow::Result<Vec<_>>>()?;
187    let aura_bindings = item
188        .auras
189        .keys()
190        .map(|name| {
191            let name = rust_ident(name)?;
192
193            Ok::<_, anyhow::Error>(quote! {
194                pub(crate) const #name: wowlab_engine_combat::AuraBinding =
195                    wowlab_engine_combat::generated_identities::items::#binding_module::#name;
196            })
197        })
198        .collect::<anyhow::Result<Vec<_>>>()?;
199    let spell_bindings = item
200        .spells
201        .keys()
202        .map(|name| {
203            let name = rust_ident(name)?;
204            let identity_name = format_ident!("SPELL_{name}");
205
206            Ok::<_, anyhow::Error>(quote! {
207                pub(crate) const #name: wowlab_engine_combat::SpellBinding =
208                    wowlab_engine_combat::generated_identities::items::#binding_module::#identity_name;
209            })
210        })
211        .collect::<anyhow::Result<Vec<_>>>()?;
212    let rppm_binding = (item.rppm.is_some() || item.driver_spell_id.is_some()).then(|| {
213        quote! {
214            pub(crate) const RPPM_BINDING: wowlab_engine_combat::RppmBinding =
215                wowlab_engine_combat::generated_identities::items::#binding_module::RPPM;
216        }
217    });
218
219    Ok(quote! {
220        pub(crate) mod SPELL {
221            #(#spells)*
222        }
223        pub(crate) mod AURA {
224            #(#auras)*
225        }
226        pub(crate) mod AURA_BINDING {
227            #(#aura_bindings)*
228        }
229        pub(crate) mod SPELL_BINDING {
230            #(#spell_bindings)*
231        }
232        #rppm_binding
233        pub(crate) mod DAMAGE_SPELL {
234            #(#damage_spells)*
235        }
236        pub(crate) mod DAMAGE_AURA {
237            #(#damage_auras)*
238        }
239        pub(crate) mod DAMAGE_EFFECT {
240            #(#damage_effects)*
241        }
242        #effects
243    })
244}
245
246fn item_aura_functions(item: &ItemManifestEntry, item_snake: &str) -> anyhow::Result<TokenStream> {
247    let empty_idx_map = FastMap::default();
248    let spell_id_map = item
249        .spells
250        .iter()
251        .map(|(name, spell)| (name.as_str(), spell.id))
252        .collect::<FastMap<_, _>>();
253    let hook_module = format!("items::{item_snake}");
254    let functions = item
255        .auras
256        .iter()
257        .map(|(name, def)| {
258            let options = AuraEmitOptions {
259                spell_group: None,
260                aura_idx_map: &empty_idx_map,
261                spell_id_map: &spell_id_map,
262                emit_default_max_stacks: def.max_stacks.is_none(),
263                has_item_budget: true,
264                hook_module: &hook_module,
265                hook_suffix: "",
266            };
267
268            emit_aura_fn(name, def, &options)
269        })
270        .collect::<anyhow::Result<Vec<_>>>()?;
271
272    Ok(quote!(#(#functions)*))
273}
274
275fn item_spell_functions(item: &ItemManifestEntry, item_snake: &str) -> anyhow::Result<TokenStream> {
276    let hook_module = format!("items::{item_snake}");
277    let functions = item
278        .spells
279        .iter()
280        .map(|(name, def)| {
281            let function_name = format_ident!("spell_{}", to_snake(name));
282            let options = SpellChainOpts {
283                kind: SpellChainKind::Item,
284                inferred_aura: None,
285                hook_module: &hook_module,
286                hook_suffix: "",
287            };
288            let expression = lower_manifest_spell(def, &options)?.into_result_expression()?;
289
290            Ok::<_, anyhow::Error>(quote! {
291                fn #function_name(
292                    s: SpellDefinitionDraft,
293                    data: &ResolvedGameData,
294                ) -> Result<SpellDefinitionDraft, wowlab_engine_combat::BuilderError> {
295                    #expression
296                }
297            })
298        })
299        .collect::<anyhow::Result<Vec<_>>>()?;
300
301    Ok(quote!(#(#functions)*))
302}
303
304fn item_register_function(key: &str, item: &ItemManifestEntry) -> anyhow::Result<TokenStream> {
305    let display = lit_str(&to_display_name(key));
306    let identity_module = rust_ident(&to_snake(key))?;
307    let rppm = if let Some(driver_spell_id) = item.driver_spell_id {
308        let driver_spell_id = lit_int(driver_spell_id);
309
310        quote!(, rppm_driver: (RPPM_BINDING, ITEM_ID, #driver_spell_id))
311    } else if let Some(rppm) = item.rppm {
312        let rppm = lit_f64(rppm);
313        let haste_scales = item.rppm_haste_scales;
314
315        quote!(, rppm: (RPPM_BINDING, ITEM_ID, #rppm, #haste_scales))
316    } else {
317        TokenStream::new()
318    };
319    let aura_rows = item
320        .auras
321        .keys()
322        .map(|name| {
323            let aura = rust_ident(name)?;
324            let function = format_ident!("aura_{}", to_snake(name));
325
326            Ok::<_, syn::Error>(quote!((AURA::#aura, AURA_BINDING::#aura, #function)))
327        })
328        .collect::<Result<Vec<_>, _>>()?;
329    let auras = (!aura_rows.is_empty()).then(|| quote!(, auras: (budget) [#(#aura_rows,)*]));
330    let spell_rows = item
331        .spells
332        .keys()
333        .map(|name| {
334            let spell = rust_ident(name)?;
335            let function = format_ident!("spell_{}", to_snake(name));
336
337            Ok::<_, syn::Error>(quote!((SPELL::#spell, SPELL_BINDING::#spell, #function)))
338        })
339        .collect::<Result<Vec<_>, _>>()?;
340    let spells = (!spell_rows.is_empty()).then(|| quote!(, spells: [#(#spell_rows,)*]));
341    let precombat = item
342        .auras
343        .iter()
344        .filter(|(_, aura)| aura.precombat)
345        .map(|(name, _)| rust_ident(name))
346        .collect::<Result<Vec<_>, _>>()?;
347    let precombat = (!precombat.is_empty()).then(|| quote!(, precombat: [#(AURA::#precombat,)*]));
348    let budget = if item.auras.is_empty() {
349        quote!(_budget)
350    } else {
351        quote!(budget)
352    };
353
354    Ok(quote! {
355        pub(crate) fn register(
356            builder: CombatSystemBuilder,
357            data: &ResolvedGameData,
358            #budget: ItemBudget,
359        ) -> CombatSystemBuilder {
360            let builder = builder.effect_identities(
361                wowlab_engine_combat::generated_identities::items::#identity_module::damage::ALL,
362            );
363
364            crate::items::register_item!(
365                builder,
366                data,
367                #display
368                #rppm
369                #auras
370                #spells
371                #precombat
372            )
373        }
374    })
375}