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) -> anyhow::Result<TokenStream> {
102 let pre = pre_items_expression(manifest, spec_variant)?;
103
104 Ok(quote! {
105 pub(crate) fn build_combat_system(
106 params: &wowlab_engine_ports::HandlerParams<'_>,
107 ) -> Result<BuiltCombatSystem, wowlab_engine_ports::EngineError> {
108 let builder = #pre;
109
110 let builder = crate::generated::items::register_equipped(
111 builder,
112 ¶ms.game_data,
113 params.equipped_items,
114 )?;
115
116 builder
117 .build(params.rotation)
118 .map_err(wowlab_engine_ports::EngineError::rotation_compile)
119 }
120 })
121}
122
123fn power_or_data(value: Option<f64>, method: &str, type_id: u8) -> TokenStream {
124 value.map_or_else(
125 || {
126 let method = rust_ident(method).expect("fixed power accessor is a Rust identifier");
127 let type_id = lit_int(type_id);
128 let message = lit_str(&format!(
129 "missing power-type {method} for type_id {type_id}"
130 ));
131
132 quote! {
133 params.game_data.#method(#type_id).ok_or_else(|| {
134 wowlab_engine_ports::EngineError::spec_construction(#message)
135 })?
136 }
137 },
138 |value| {
139 let value = lit_f64(value);
140
141 quote!(#value)
142 },
143 )
144}
145
146fn pre_items_expression(manifest: &Manifest, spec_variant: &str) -> anyhow::Result<TokenStream> {
147 let type_id = lit_int(manifest.resource.type_id);
148 let max = power_or_data(
149 manifest.resource.max,
150 "power_max",
151 manifest.resource.type_id,
152 );
153 let regen = manifest.resource.regen.map_or_else(
154 || {
155 let message = lit_str(&format!(
156 "missing power-type power_regen_for_max for type_id {}",
157 manifest.resource.type_id
158 ));
159
160 quote! {
161 params.game_data
162 .power_regen_for_max(#type_id, #max)
163 .ok_or_else(|| {
164 wowlab_engine_ports::EngineError::spec_construction(#message)
165 })?
166 }
167 },
168 |regen| {
169 let regen = lit_f64(regen);
170
171 quote!(#regen)
172 },
173 );
174 let starts_at = power_or_data(
175 manifest.resource.starts_at,
176 "power_default",
177 manifest.resource.type_id,
178 );
179 let spec_variant = rust_ident(spec_variant)?;
180 let mastery_spell = lit_int(manifest.mastery.spell_id);
181 let mastery_hook = rust_path(&manifest.mastery.hook)?;
182 let resource_name = lit_str(&manifest.resource.name);
183 let mut expression = quote! {
184 BuiltCombatSystem::builder(params.stats.clone())
185 .spec_id(SpecId::#spec_variant)
186 .hero_talent_trees(DECLARED_METADATA.hero_talent_trees)
187 .mastery_spell(#mastery_spell)
188 .mastery_hook(#mastery_hook)
189 .resource(#resource_name, #max, #regen)
190 .resource_type_id(#type_id)
191 .resource_start(#starts_at)
192 };
193
194 if let Some(hook) = &manifest.mastery.crit_damage_hook {
195 let hook = rust_path(hook)?;
196
197 expression = quote!(#expression.mastery_crit_damage_hook(#hook));
198 }
199
200 if manifest.spec.has_pet {
201 expression = quote!(#expression.has_pet(true));
202 }
203
204 if let Some(resource) = &manifest.secondary_resource {
205 let max = power_or_data(resource.max, "power_max", resource.type_id);
206 let name = lit_str(&resource.name);
207 let type_id = lit_int(resource.type_id);
208
209 expression = quote! {
210 #expression
211 .secondary_resource(#name, #max)
212 .secondary_resource_type_id(#type_id)
213 };
214 }
215
216 if let Some(auras) = &manifest.spec.precombat_auras {
217 for name in auras {
218 let aura = format_ident!("AURA_{name}");
219
220 expression = quote!(#expression.precombat_aura(#aura));
221 }
222 }
223
224 if let Some(aura) = &manifest.spec.stealth_aura {
225 let aura = rust_ident(aura)?;
226
227 expression = quote!(#expression.stealth_aura(AURA::#aura));
228 }
229
230 expression = register_definitions(expression, manifest)?;
231
232 expression = quote!(#expression.game_data(params.game_data.clone()));
233
234 if !manifest.talents.is_empty() {
235 expression = quote!(#expression.talent_spell_ids(TALENTS));
236 }
237
238 expression = quote!(#expression.talent_selections(params.talent_selections));
239
240 expression = register_talent_content(expression, manifest)?;
241
242 Ok(quote! {
243 #expression
244 .set_bonus_auras(params.set_bonus_auras)
245 .encounter(params.encounter.clone())
246 })
247}
248
249fn register_definitions(
250 mut expression: TokenStream,
251 manifest: &Manifest,
252) -> anyhow::Result<TokenStream> {
253 for name in manifest.auras.keys() {
254 let display = lit_str(&to_rotation_key(name));
255 let aura = rust_ident(name)?;
256 let function = format_ident!("aura_{}", to_snake(name));
257
258 expression = quote! {
259 #expression.aura(
260 #display,
261 AURA::#aura,
262 |a| #function(a, ¶ms.game_data),
263 )
264 };
265 }
266
267 for name in manifest.spells.keys() {
268 let display = lit_str(&to_rotation_key(name));
269 let spell = rust_ident(name)?;
270 let function = format_ident!("spell_{}", to_snake(name));
271
272 expression = quote! {
273 #expression.spell(
274 #display,
275 SPELL::#spell,
276 |s| #function(s, ¶ms.game_data),
277 )
278 };
279 }
280
281 for auto_attack in manifest.auto_attacks.values() {
282 let auto_attack = auto_attack_expression(auto_attack)?;
283
284 expression = quote!(#expression.auto_attack(|a| #auto_attack));
285 }
286
287 Ok(expression)
288}
289
290fn register_talent_content(
291 mut expression: TokenStream,
292 manifest: &Manifest,
293) -> anyhow::Result<TokenStream> {
294 for gate in &manifest.talent_gated_aura_effects {
295 let source_aura = manifest
296 .auras
297 .iter()
298 .find_map(|(name, aura)| (aura.id == gate.source.spell_id).then_some(name))
299 .ok_or_else(|| {
300 anyhow::anyhow!(
301 "talent-gated effect source spell {} is not a declared aura",
302 gate.source.spell_id
303 )
304 })?;
305 let target_aura = gate.target_aura.as_ref().unwrap_or(source_aura);
306 let talent = rust_ident(&gate.talent)?;
307 let target_aura = format_ident!("AURA_{target_aura}");
308 let source_spell = lit_int(gate.source.spell_id);
309 let source_effect = lit_int(gate.source.effect);
310
311 expression = quote! {
312 #expression.talent_gated_aura_effect(
313 TALENT::#talent,
314 #target_aura,
315 #source_spell,
316 #source_effect,
317 )
318 };
319 }
320
321 for (talent, auras) in &manifest.talent_companion_auras {
322 let talent = rust_ident(talent)?;
323
324 for aura in auras {
325 let aura = format_ident!("AURA_{aura}");
326
327 expression = quote!(#expression.talent_companion_aura(TALENT::#talent, #aura));
328 }
329 }
330
331 for proc in &manifest.impact_procs {
332 let builder = impact_proc_builder(proc)?;
333
334 expression = if let Some(talent) = &proc.talent {
335 let talent = rust_ident(talent)?;
336
337 quote! {
338 #expression.register_impact_effect_proc_if_talent(TALENT::#talent, #builder)
339 }
340 } else {
341 quote!(#expression.register_impact_effect_proc(#builder))
342 };
343 }
344
345 Ok(expression)
346}
347
348pub(crate) fn handler_factory(manifest: &Manifest) -> anyhow::Result<TokenStream> {
349 let finish = if let Some(custom) = &manifest.spec.custom_handler {
350 if custom.ends_with("::try_new") {
351 let custom = rust_path(custom)?;
352
353 quote!(#custom)
354 } else {
355 let custom = rust_path(custom)?;
356
357 quote!(|parts, params| Ok(#custom(parts, params)))
358 }
359 } else {
360 quote!(|parts, _| Ok(parts.into_handler()))
361 };
362
363 Ok(quote! {
364 fn new_handler(
365 params: wowlab_engine_ports::HandlerParams<'_>,
366 ) -> Result<Box<dyn SpecHandler>, wowlab_engine_ports::EngineError> {
367 let handler = crate::composition::compose_handler(
368 build_combat_system,
369 ¶ms,
370 #finish,
371 )?;
372
373 Ok(Box::new(handler))
374 }
375 })
376}
377
378#[cfg(test)]
379#[path = "gen_build/tests.rs"]
380mod tests;