Skip to main content

codegen/
gen_build.rs

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(&params.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                &params.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, &params.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, &params.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                &params,
370                #finish,
371            )?;
372
373            Ok(Box::new(handler))
374        }
375    })
376}
377
378#[cfg(test)]
379#[path = "gen_build/tests.rs"]
380mod tests;