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::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
17pub(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
65pub(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)?;
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) -> anyhow::Result<TokenStream> {
110 let spells = item
111 .spells
112 .iter()
113 .map(|(name, spell)| documented_u32_constant(name, spell.id, "spell"))
114 .collect::<anyhow::Result<Vec<_>>>()?;
115 let auras = item
116 .auras
117 .iter()
118 .map(|(name, aura)| documented_u32_constant(name, aura.id, "spell"))
119 .collect::<anyhow::Result<Vec<_>>>()?;
120 let effects = item
121 .effects
122 .iter()
123 .map(|(name, effect)| {
124 let documentation = lit_str(&inspect_link("spell", effect.spell_id));
125 let constant = effect_constant(name, effect.spell_id, effect.effect)?;
126
127 Ok::<_, anyhow::Error>(quote! {
128 #[doc = #documentation]
129 #constant
130 })
131 })
132 .collect::<anyhow::Result<Vec<_>>>()?;
133 let effects = (!effects.is_empty()).then(|| {
134 quote! {
135 pub(crate) mod EFFECT {
136 #(#effects)*
137 }
138 }
139 });
140
141 Ok(quote! {
142 pub(crate) mod SPELL {
143 #(#spells)*
144 }
145 pub(crate) mod AURA {
146 #(#auras)*
147 }
148 #effects
149 })
150}
151
152fn item_aura_functions(item: &ItemManifestEntry, item_snake: &str) -> anyhow::Result<TokenStream> {
153 let empty_idx_map = FastMap::default();
154 let spell_id_map = item
155 .spells
156 .iter()
157 .map(|(name, spell)| (name.as_str(), spell.id))
158 .collect::<FastMap<_, _>>();
159 let hook_module = format!("items::{item_snake}");
160 let functions = item
161 .auras
162 .iter()
163 .map(|(name, def)| {
164 let options = AuraEmitOptions {
165 spell_group: None,
166 aura_idx_map: &empty_idx_map,
167 spell_id_map: &spell_id_map,
168 emit_default_max_stacks: def.max_stacks.is_none(),
169 has_item_budget: true,
170 hook_module: &hook_module,
171 hook_suffix: "",
172 };
173
174 emit_aura_fn(name, def, &options)
175 })
176 .collect::<anyhow::Result<Vec<_>>>()?;
177
178 Ok(quote!(#(#functions)*))
179}
180
181fn item_spell_functions(item: &ItemManifestEntry, item_snake: &str) -> anyhow::Result<TokenStream> {
182 let hook_module = format!("items::{item_snake}");
183 let functions = item
184 .spells
185 .iter()
186 .map(|(name, def)| {
187 let function_name = format_ident!("spell_{}", to_snake(name));
188 let options = SpellChainOpts {
189 kind: SpellChainKind::Item,
190 inferred_aura: None,
191 hook_module: &hook_module,
192 hook_suffix: "",
193 };
194 let expression = lower_manifest_spell(def, &options)?.into_result_expression()?;
195
196 Ok::<_, anyhow::Error>(quote! {
197 fn #function_name(
198 s: SpellDefinitionDraft,
199 data: &ResolvedGameData,
200 ) -> Result<SpellDefinitionDraft, wowlab_engine_combat::BuilderError> {
201 #expression
202 }
203 })
204 })
205 .collect::<anyhow::Result<Vec<_>>>()?;
206
207 Ok(quote!(#(#functions)*))
208}
209
210fn item_register_function(key: &str, item: &ItemManifestEntry) -> anyhow::Result<TokenStream> {
211 let display = lit_str(&to_display_name(key));
212 let rppm = if let Some(driver_spell_id) = item.driver_spell_id {
213 let driver_spell_id = lit_int(driver_spell_id);
214
215 quote!(, rppm_driver: (ITEM_ID, #driver_spell_id))
216 } else if let Some(rppm) = item.rppm {
217 let rppm = lit_f64(rppm);
218 let haste_scales = item.rppm_haste_scales;
219
220 quote!(, rppm: (ITEM_ID, #rppm, #haste_scales))
221 } else {
222 TokenStream::new()
223 };
224 let aura_rows = item
225 .auras
226 .keys()
227 .map(|name| {
228 let aura = rust_ident(name)?;
229 let function = format_ident!("aura_{}", to_snake(name));
230
231 Ok::<_, syn::Error>(quote!((AURA::#aura, #function)))
232 })
233 .collect::<Result<Vec<_>, _>>()?;
234 let auras = (!aura_rows.is_empty()).then(|| quote!(, auras: (budget) [#(#aura_rows,)*]));
235 let spell_rows = item
236 .spells
237 .keys()
238 .map(|name| {
239 let spell = rust_ident(name)?;
240 let function = format_ident!("spell_{}", to_snake(name));
241
242 Ok::<_, syn::Error>(quote!((SPELL::#spell, #function)))
243 })
244 .collect::<Result<Vec<_>, _>>()?;
245 let spells = (!spell_rows.is_empty()).then(|| quote!(, spells: [#(#spell_rows,)*]));
246 let precombat = item
247 .auras
248 .iter()
249 .filter(|(_, aura)| aura.precombat)
250 .map(|(name, _)| rust_ident(name))
251 .collect::<Result<Vec<_>, _>>()?;
252 let precombat = (!precombat.is_empty()).then(|| quote!(, precombat: [#(AURA::#precombat,)*]));
253 let budget = if item.auras.is_empty() {
254 quote!(_budget)
255 } else {
256 quote!(budget)
257 };
258
259 Ok(quote! {
260 pub(crate) fn register(
261 builder: CombatSystemBuilder,
262 data: &ResolvedGameData,
263 #budget: ItemBudget,
264 ) -> CombatSystemBuilder {
265 crate::items::register_item!(
266 builder,
267 data,
268 #display
269 #rppm
270 #auras
271 #spells
272 #precombat
273 )
274 }
275 })
276}