Skip to main content

codegen/
gen_proc.rs

1use proc_macro2::TokenStream;
2use quote::{format_ident, quote};
3use wowlab_manifest_schema::{
4    EventEffectDef, ImpactProcDef, ImpactProcHitDef, ImpactProcPhaseDef, ImpactProcRngDef,
5    ScalarRef,
6};
7
8use crate::{
9    gen_emit::{effect_lookup, scalar_ref_call_with_data, spell_idx},
10    rust_source::{lit_f64, lit_int, lit_str, rust_ident, rust_path},
11};
12
13fn build_scalar_ref(scalar: &ScalarRef) -> anyhow::Result<TokenStream> {
14    scalar_ref_call_with_data(scalar, &quote!(params.game_data))
15}
16
17fn dbc_proc_driver(proc: &ImpactProcDef) -> anyhow::Result<Option<(u32, u8)>> {
18    match (proc.proc_type_mask_spell_id, proc.proc_type_mask_effect) {
19        (Some(spell_id), Some(effect_index)) if effect_index > 0 => {
20            Ok(Some((spell_id, effect_index)))
21        }
22        (None, None) => Ok(None),
23        _ => anyhow::bail!(
24            "proc_type_mask_spell_id requires a positive proc_type_mask_effect and vice versa"
25        ),
26    }
27}
28
29fn impact_source(
30    proc: &ImpactProcDef,
31    dbc_driver: Option<(u32, u8)>,
32) -> anyhow::Result<TokenStream> {
33    match (&proc.spell, &proc.auto_attack_hand, dbc_driver) {
34        (Some(spell), None, None) => {
35            let spell = rust_ident(spell)?;
36
37            Ok(quote!(
38                wowlab_engine_combat::ImpactSource::Spell(SPELL::#spell)
39            ))
40        }
41        (None, Some(hand), None) => {
42            let variant = match hand.as_str() {
43                "mainhand" => format_ident!("MainHand"),
44                "offhand" => format_ident!("OffHand"),
45                other => anyhow::bail!("unknown impact proc auto_attack_hand: {other}"),
46            };
47
48            Ok(quote! {
49                wowlab_engine_combat::ImpactSource::AutoAttack(
50                    wowlab_engine_combat::SwingHand::#variant,
51                )
52            })
53        }
54        (None, None, Some((driver_spell_id, _))) => {
55            let driver_spell_id = spell_idx(driver_spell_id);
56
57            Ok(quote! {
58                wowlab_engine_combat::ImpactSource::ProcTypeMask(
59                    params.game_data.require_proc_type_mask(#driver_spell_id)?,
60                )
61            })
62        }
63        _ => anyhow::bail!("impact proc requires exactly one source"),
64    }
65}
66
67pub(crate) fn impact_proc_builder(proc: &ImpactProcDef) -> anyhow::Result<TokenStream> {
68    let dbc_driver = dbc_proc_driver(proc)?;
69    let source = impact_source(proc, dbc_driver)?;
70    let chance = match &proc.chance_pct {
71        ScalarRef::Literal(value) => {
72            let value = lit_f64(*value / 100.0);
73
74            quote!(#value)
75        }
76        scalar => {
77            let value = build_scalar_ref(scalar)?;
78
79            quote!((#value) / 100.0)
80        }
81    };
82    let mut expression = quote! {
83        wowlab_engine_combat::ImpactEffectProcDefinition::new(#source)
84    };
85
86    if let Some((driver_spell_id, effect_index)) = dbc_driver {
87        let lookup = effect_lookup(&quote!(&params.game_data), driver_spell_id, effect_index);
88
89        expression = quote!(#expression.dbc_driver_from_data(#lookup));
90    }
91
92    expression = quote!(#expression.chance(#chance));
93
94    match proc.rng_model {
95        ImpactProcRngDef::Fixed => {}
96        ImpactProcRngDef::WeaponSpeed { baseline_ms } => {
97            let baseline_ms = lit_int(baseline_ms);
98
99            expression = quote!(#expression.weapon_speed_scaled(#baseline_ms));
100        }
101        ImpactProcRngDef::Shuffled {
102            success_entries,
103            total_entries,
104        } => {
105            if total_entries == 0 || success_entries > total_entries {
106                anyhow::bail!(
107                    "shuffled impact proc requires a non-empty deck with success_entries <= total_entries"
108                );
109            }
110
111            let success_entries = lit_int(success_entries);
112            let total_entries = lit_int(total_entries);
113
114            expression = quote!(#expression.shuffled(#success_entries, #total_entries));
115        }
116        ImpactProcRngDef::Accumulated { cap, initial_count } => {
117            let cap = lit_int(cap);
118            let initial_count = lit_int(initial_count);
119
120            expression = quote!(#expression.accumulated(#cap, #initial_count));
121        }
122        _ => anyhow::bail!("unsupported impact proc RNG model"),
123    }
124
125    if let Some(icd_ms) = &proc.icd_ms {
126        let owner_id = lit_int(proc.proc_type_mask_spell_id.unwrap_or_default());
127        let icd_ms = build_scalar_ref(icd_ms)?;
128        let field = lit_str("impact_proc.icd_ms");
129
130        expression = quote! {
131            #expression.icd_ms(
132                wowlab_engine_combat::validated_milliseconds(#icd_ms, #owner_id, #field)
133                    .map_err(|error| {
134                        wowlab_engine_ports::EngineError::spec_construction(error.to_string())
135                    })?,
136            )
137        };
138    }
139
140    if proc.target_icd_ms > 0 {
141        let target_icd_ms = lit_int(proc.target_icd_ms);
142
143        expression = quote!(#expression.target_icd_ms(#target_icd_ms));
144    }
145
146    expression = apply_masks(expression, proc)?;
147
148    if proc.charges > 0 {
149        let charges = lit_int(proc.charges);
150
151        expression = quote!(#expression.charges(#charges));
152    }
153
154    if proc.periodic_only {
155        expression = quote!(#expression.periodic_only());
156    }
157
158    if proc.skip_periodic {
159        expression = quote!(#expression.skip_periodic());
160    }
161
162    if proc.physical_only {
163        expression = quote!(#expression.physical_only());
164    }
165
166    if let Some(hook) = &proc.hook {
167        let hook = rust_path(hook)?;
168
169        expression = quote!(#expression.fire(#hook));
170    }
171
172    for effect in &proc.effects {
173        match effect {
174            EventEffectDef::Energize { amount, secondary } => {
175                let amount = build_scalar_ref(amount)?;
176
177                expression = if *secondary {
178                    quote!(#expression.secondary_gain(#amount))
179                } else {
180                    quote!(#expression.gain(#amount))
181                };
182            }
183            _ => anyhow::bail!("unsupported impact proc effect"),
184        }
185    }
186
187    Ok(expression)
188}
189
190fn apply_masks(mut expression: TokenStream, proc: &ImpactProcDef) -> anyhow::Result<TokenStream> {
191    if !proc.phase.is_empty() {
192        let values = proc
193            .phase
194            .iter()
195            .map(|phase| {
196                let variant = match phase {
197                    ImpactProcPhaseDef::Cast => format_ident!("CAST"),
198                    ImpactProcPhaseDef::Hit => format_ident!("HIT"),
199                    ImpactProcPhaseDef::Finish => format_ident!("FINISH"),
200                    _ => anyhow::bail!("unsupported impact proc phase"),
201                };
202
203                Ok::<_, anyhow::Error>(quote!(wowlab_engine_combat::ProcPhaseMask::#variant))
204            })
205            .collect::<anyhow::Result<Vec<_>>>()?;
206        let values = bit_or(values).expect("non-empty phase list");
207
208        expression = quote!(#expression.phase_mask(#values));
209    }
210
211    if !proc.hit_mask.is_empty() {
212        let values = proc
213            .hit_mask
214            .iter()
215            .map(|hit| {
216                let variant = match hit {
217                    ImpactProcHitDef::Normal => format_ident!("NORMAL"),
218                    ImpactProcHitDef::Critical => format_ident!("CRITICAL"),
219                    ImpactProcHitDef::Miss => format_ident!("MISS"),
220                    ImpactProcHitDef::Dodge => format_ident!("DODGE"),
221                    ImpactProcHitDef::Parry => format_ident!("PARRY"),
222                    ImpactProcHitDef::Glance => format_ident!("GLANCE"),
223                    ImpactProcHitDef::Block => format_ident!("BLOCK"),
224                    ImpactProcHitDef::Absorb => format_ident!("ABSORB"),
225                    ImpactProcHitDef::Landed => format_ident!("LANDED"),
226                    _ => anyhow::bail!("unsupported impact proc hit mask"),
227                };
228
229                Ok::<_, anyhow::Error>(quote!(wowlab_engine_combat::ProcHitMask::#variant))
230            })
231            .collect::<anyhow::Result<Vec<_>>>()?;
232        let values = bit_or(values).expect("non-empty hit-mask list");
233
234        expression = quote!(#expression.hit_mask(#values));
235    }
236
237    Ok(expression)
238}
239
240fn bit_or(values: Vec<TokenStream>) -> Option<TokenStream> {
241    let mut values = values.into_iter();
242    let first = values.next()?;
243
244    Some(values.fold(first, |left, right| quote!(#left | #right)))
245}