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