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
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, &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}