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, "e!(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("e!(¶ms.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}