1use proc_macro2::TokenStream;
2use quote::{format_ident, quote};
3use wowlab_manifest_schema::Manifest;
4
5use crate::{
6 gen_proc::impact_proc_builder,
7 helpers::{to_rotation_key, to_snake},
8 rust_source::{lit_f64, lit_int, lit_str, rust_ident, rust_path},
9};
10
11fn validate_auto_attack_npc(
12 auto_attack: &wowlab_manifest_schema::AutoAttackDef,
13) -> anyhow::Result<()> {
14 anyhow::ensure!(
15 auto_attack.npc_id.is_none() || auto_attack.is_pet,
16 "auto-attack npc_id requires is_pet = true"
17 );
18
19 Ok(())
20}
21
22fn auto_attack_expression(
23 auto_attack: &wowlab_manifest_schema::AutoAttackDef,
24) -> anyhow::Result<TokenStream> {
25 validate_auto_attack_npc(auto_attack)?;
26
27 let coefficient = match (auto_attack.ap_coef, auto_attack.pet_power_from_owner) {
28 (Some(coefficient), None) => {
29 let coefficient = lit_f64(coefficient);
30
31 Some(quote!(#coefficient))
32 }
33 (None, Some(power_from_owner)) if auto_attack.is_pet => {
34 let power_from_owner = lit_f64(power_from_owner);
35 let weapon_speed = lit_int(
36 auto_attack
37 .pet_weapon_speed_ms
38 .unwrap_or(auto_attack.swing_ms),
39 );
40 let weapon_multiplier = lit_f64(auto_attack.pet_weapon_multiplier.unwrap_or(1.0));
41
42 Some(quote! {
43 wowlab_engine_domain::damage::pet_swing_power_coefficient(
44 #power_from_owner,
45 #weapon_speed,
46 #weapon_multiplier,
47 )
48 })
49 }
50 (None, None) if !auto_attack.is_pet => None,
51 (None, Some(_)) => anyhow::bail!("pet_power_from_owner requires is_pet = true"),
52 _ => {
53 anyhow::bail!("pet auto attack requires exactly one of ap_coef or pet_power_from_owner")
54 }
55 };
56 let spell_id = lit_int(auto_attack.spell_id);
57 let swing_ms = lit_int(auto_attack.swing_ms);
58 let mut expression = quote! {
59 a.spell_id(#spell_id)
60 .swing_ms(#swing_ms)
61 .apply_base_from_data(¶ms.game_data)
62 };
63
64 if let Some(coefficient) = coefficient {
65 expression = quote!(#expression.ap_coef(#coefficient));
66 }
67
68 if auto_attack.is_pet {
69 expression = quote!(#expression.is_pet());
70 }
71
72 if let Some(npc_id) = auto_attack.npc_id {
73 let npc_id = lit_int(npc_id);
74
75 expression = quote!(#expression.npc_id(#npc_id));
76 }
77
78 if auto_attack.uses_spell_power {
79 expression = quote!(#expression.uses_spell_power());
80 }
81
82 if let Some(crit) = &auto_attack.on_crit_reduce_charge {
83 let target = format_ident!("SPELL_{}", crit.target);
84 let chance = lit_f64(crit.chance);
85
86 expression = quote!(#expression.on_crit_reduce_charge(#target, #chance));
87 }
88
89 if let Some(hook) = &auto_attack.hook {
90 let hook = rust_path(hook)?;
91
92 expression = quote!(#expression.on_swing(#hook));
93 }
94
95 Ok(expression)
96}
97
98pub(crate) fn build_function(
99 manifest: &Manifest,
100 spec_variant: &str,
101 module_name: &str,
102) -> anyhow::Result<TokenStream> {
103 let pre = pre_items_expression(manifest, spec_variant, module_name)?;
104
105 Ok(quote! {
106 pub(crate) fn build_combat_system_draft(
107 params: &wowlab_engine_ports::HandlerParams<'_>,
108 ) -> Result<CombatSystemDraft, wowlab_engine_ports::EngineError> {
109 let builder = #pre;
110
111 let builder = crate::generated::items::register_equipped(
112 builder,
113 ¶ms.game_data,
114 params.equipped_items,
115 )?;
116
117 builder.into_draft()
118 }
119 })
120}
121
122fn power_or_data(value: Option<f64>, method: &str, type_id: u8) -> TokenStream {
123 value.map_or_else(
124 || {
125 let method = rust_ident(method).expect("fixed power accessor is a Rust identifier");
126 let type_id = lit_int(type_id);
127 let message = lit_str(&format!(
128 "missing power-type {method} for type_id {type_id}"
129 ));
130
131 quote! {
132 params.game_data.#method(#type_id).ok_or_else(|| {
133 wowlab_engine_ports::EngineError::spec_construction(#message)
134 })?
135 }
136 },
137 |value| {
138 let value = lit_f64(value);
139
140 quote!(#value)
141 },
142 )
143}
144
145fn pre_items_expression(
146 manifest: &Manifest,
147 spec_variant: &str,
148 module_name: &str,
149) -> anyhow::Result<TokenStream> {
150 let type_id = lit_int(manifest.resource.type_id);
151 let max = power_or_data(
152 manifest.resource.max,
153 "power_max",
154 manifest.resource.type_id,
155 );
156 let regen = manifest.resource.regen.map_or_else(
157 || {
158 let message = lit_str(&format!(
159 "missing power-type power_regen_for_max for type_id {}",
160 manifest.resource.type_id
161 ));
162
163 quote! {
164 params.game_data
165 .power_regen_for_max(#type_id, #max)
166 .ok_or_else(|| {
167 wowlab_engine_ports::EngineError::spec_construction(#message)
168 })?
169 }
170 },
171 |regen| {
172 let regen = lit_f64(regen);
173
174 quote!(#regen)
175 },
176 );
177 let starts_at = power_or_data(
178 manifest.resource.starts_at,
179 "power_default",
180 manifest.resource.type_id,
181 );
182 let spec_variant = rust_ident(spec_variant)?;
183 let spec_identity_module = rust_ident(module_name)?;
184 let mastery_spell = lit_int(manifest.mastery.spell_id);
185 let mastery_hook = rust_path(&manifest.mastery.hook)?;
186 let resource_name = lit_str(&manifest.resource.name);
187 let mut expression = quote! {
188 CombatSystemBuilder::new(params.stats.clone())
189 .spec_id(SpecId::#spec_variant)
190 .spec_identity(wowlab_engine_combat::generated_identities::specs::#spec_identity_module::PROOF)
191 .hero_talent_trees(DECLARED_METADATA.hero_talent_trees)
192 .effect_identities(
193 wowlab_engine_combat::generated_identities::specs::#spec_identity_module::damage::ALL,
194 )
195 .effect_identities(wowlab_engine_combat::generated_identities::shared::ALL)
196 .mastery_spell(#mastery_spell)
197 .mastery_hook(#mastery_hook)
198 .resource(#resource_name, #max, #regen)
199 .resource_type_id(#type_id)
200 .resource_start(#starts_at)
201 };
202
203 if let Some(hook) = &manifest.mastery.crit_damage_hook {
204 let hook = rust_path(hook)?;
205
206 expression = quote!(#expression.mastery_crit_damage_hook(#hook));
207 }
208
209 if manifest.spec.has_pet {
210 expression = quote!(#expression.has_pet(true));
211 }
212
213 if let Some(resource) = &manifest.secondary_resource {
214 let max = power_or_data(resource.max, "power_max", resource.type_id);
215 let name = lit_str(&resource.name);
216 let type_id = lit_int(resource.type_id);
217
218 expression = quote! {
219 #expression
220 .secondary_resource(#name, #max)
221 .secondary_resource_type_id(#type_id)
222 };
223 }
224
225 if let Some(auras) = &manifest.spec.precombat_auras {
226 for name in auras {
227 let aura = format_ident!("AURA_{name}");
228
229 expression = quote!(#expression.precombat_aura(#aura));
230 }
231 }
232
233 if let Some(aura) = &manifest.spec.stealth_aura {
234 let aura = rust_ident(aura)?;
235
236 expression = quote!(#expression.stealth_aura(AURA::#aura));
237 }
238
239 expression = register_definitions(expression, manifest)?;
240
241 expression = quote!(#expression.game_data(params.game_data.clone()));
242
243 if !manifest.talents.is_empty() {
244 expression = quote!(#expression.talent_spell_ids(TALENTS));
245 }
246
247 expression = quote!(#expression.talent_selections(params.talent_selections));
248
249 expression = register_talent_content(expression, manifest)?;
250
251 Ok(quote! {
252 #expression
253 .set_bonus_auras(params.set_bonus_auras)
254 .encounter(params.encounter.clone())
255 })
256}
257
258fn register_definitions(
259 mut expression: TokenStream,
260 manifest: &Manifest,
261) -> anyhow::Result<TokenStream> {
262 for name in manifest.auras.keys() {
263 let display = lit_str(&to_rotation_key(name));
264 let aura = rust_ident(name)?;
265 let function = format_ident!("aura_{}", to_snake(name));
266
267 expression = quote! {
268 #expression.aura(
269 #display,
270 AURA::#aura,
271 |a| #function(a, ¶ms.game_data),
272 )
273 };
274 }
275
276 for name in manifest.spells.keys() {
277 let display = lit_str(&to_rotation_key(name));
278 let spell = rust_ident(name)?;
279 let function = format_ident!("spell_{}", to_snake(name));
280
281 expression = quote! {
282 #expression.spell(
283 #display,
284 SPELL::#spell,
285 |s| #function(s, ¶ms.game_data),
286 )
287 };
288 }
289
290 for auto_attack in manifest.auto_attacks.values() {
291 let auto_attack = auto_attack_expression(auto_attack)?;
292
293 expression = quote!(#expression.auto_attack(|a| #auto_attack));
294 }
295
296 Ok(expression)
297}
298
299fn register_talent_content(
300 mut expression: TokenStream,
301 manifest: &Manifest,
302) -> anyhow::Result<TokenStream> {
303 for gate in &manifest.talent_gated_aura_effects {
304 let source_aura = manifest
305 .auras
306 .iter()
307 .find_map(|(name, aura)| (aura.id == gate.source.spell_id).then_some(name))
308 .ok_or_else(|| {
309 anyhow::anyhow!(
310 "talent-gated effect source spell {} is not a declared aura",
311 gate.source.spell_id
312 )
313 })?;
314 let target_aura = gate.target_aura.as_ref().unwrap_or(source_aura);
315 let talent = rust_ident(&gate.talent)?;
316 let target_aura = format_ident!("AURA_{target_aura}");
317 let source_spell = lit_int(gate.source.spell_id);
318 let source_effect = lit_int(gate.source.effect);
319
320 expression = quote! {
321 #expression.talent_gated_aura_effect(
322 TALENT::#talent,
323 #target_aura,
324 #source_spell,
325 #source_effect,
326 )
327 };
328 }
329
330 for (talent, auras) in &manifest.talent_companion_auras {
331 let talent = rust_ident(talent)?;
332
333 for aura in auras {
334 let aura = format_ident!("AURA_{aura}");
335
336 expression = quote!(#expression.talent_companion_aura(TALENT::#talent, #aura));
337 }
338 }
339
340 for proc in &manifest.impact_procs {
341 let builder = impact_proc_builder(proc)?;
342
343 expression = if let Some(talent) = &proc.talent {
344 let talent = rust_ident(talent)?;
345
346 quote! {
347 #expression.register_impact_effect_proc_if_talent(TALENT::#talent, #builder)
348 }
349 } else {
350 quote!(#expression.register_impact_effect_proc(#builder))
351 };
352 }
353
354 Ok(expression)
355}
356
357pub(crate) fn handler_factory(manifest: &Manifest) -> anyhow::Result<TokenStream> {
358 let finish = if let Some(custom) = &manifest.spec.custom_handler {
359 let custom = rust_path(custom)?;
360
361 quote!(#custom)
362 } else {
363 quote!(|parts, _| parts.into_handler())
364 };
365
366 Ok(quote! {
367 fn new_handler(
368 params: wowlab_engine_ports::HandlerParams<'_>,
369 ) -> Result<Box<dyn SpecHandler>, wowlab_engine_ports::EngineError> {
370 let handler = crate::composition::compose_handler(
371 build_combat_system_draft,
372 ¶ms,
373 #finish,
374 )?;
375
376 Ok(Box::new(handler))
377 }
378 })
379}
380
381#[cfg(test)]
382#[path = "gen_build/tests.rs"]
383mod tests;