1use anyhow::bail;
4use proc_macro2::TokenStream;
5use quote::{format_ident, quote};
6use wowlab_manifest_schema::{
7 DamageSchoolKind, ManifestAuraDef, PeriodicDamage, PeriodicDamageMode, ScalarField, ScalarRef,
8 SpellScalarField,
9};
10
11use crate::{
12 gen_aura::AuraEmitOptions,
13 gen_emit::{damage_flags, effect_lookup, scalar_ref_call, validated_milliseconds_call},
14 rust_source::{lit_f64, lit_int, rust_path},
15};
16
17type DerivedPeriodicData = (u32, u8, Option<f64>, Option<f64>, Option<u8>);
18
19#[derive(Clone, Copy)]
20enum PeriodicCoefficient {
21 AttackPower,
22 SpellPower,
23}
24
25fn derived_periodic_data(
26 scalar: &ScalarRef,
27 coefficient: PeriodicCoefficient,
28 has_explicit_school: bool,
29 attribution_spell_id: Option<u32>,
30) -> Option<DerivedPeriodicData> {
31 let ScalarRef::EffectRef {
32 spell_id,
33 effect,
34 field,
35 multiplier,
36 offset,
37 round_decimals,
38 } = scalar
39 else {
40 return None;
41 };
42 let field_matches = matches!(
43 (field, coefficient),
44 (ScalarField::ApCoef, PeriodicCoefficient::AttackPower)
45 | (ScalarField::SpCoef, PeriodicCoefficient::SpellPower)
46 );
47
48 (field_matches
49 && !has_explicit_school
50 && attribution_spell_id.is_none_or(|attribution| attribution == *spell_id))
51 .then_some((*spell_id, *effect, *multiplier, *offset, *round_decimals))
52}
53
54fn option_u8(value: Option<u8>) -> TokenStream {
55 value.map_or_else(
56 || quote!(None),
57 |value| {
58 let value = lit_int(value);
59
60 quote!(Some(#value))
61 },
62 )
63}
64
65fn item_scaled_effect_call(scalar: &ScalarRef) -> anyhow::Result<TokenStream> {
66 let ScalarRef::EffectRef {
67 spell_id,
68 effect,
69 field,
70 multiplier,
71 offset,
72 round_decimals,
73 } = scalar
74 else {
75 bail!("item_damage_total must reference an item's scaling effect");
76 };
77
78 if !matches!(field, ScalarField::Coefficient) {
79 bail!("item_damage_total must reference the effect coefficient");
80 }
81
82 let lookup = effect_lookup("e!(data), *spell_id, *effect);
83 let raw_spell_id = lit_int(spell_id);
84 let effect_index = lit_int(effect);
85 let mut value = quote! {
86 wowlab_engine_domain::dbc::item_scaled_effect_value(#lookup, budget.item_level)
87 .or_else(|| data.is_empty().then_some(0.0))
88 .ok_or_else(|| {
89 wowlab_engine_combat::BuilderError::missing_item_scaling_data(
90 #raw_spell_id,
91 #effect_index,
92 budget.item_level,
93 )
94 })?
95 };
96
97 if let Some(multiplier) = multiplier {
98 let multiplier = lit_f64(*multiplier);
99
100 value = quote!((#value) * #multiplier);
101 }
102
103 if let Some(offset) = offset {
104 let offset = lit_f64(*offset);
105
106 value = quote!((#value) + #offset);
107 }
108
109 if let Some(decimals) = round_decimals {
110 let scale = lit_f64(10_f64.powi(i32::from(*decimals)));
111
112 value = quote!(((#value) * #scale).round() / #scale);
113 }
114
115 Ok(value)
116}
117
118pub(super) fn apply_periodic_hook_source(
119 mut expression: TokenStream,
120 def: &ManifestAuraDef,
121) -> anyhow::Result<TokenStream> {
122 let id = def.id;
123
124 if let Some(periodic_hook) = &def.periodic_hook {
125 if def.tick_hook.is_none() {
126 bail!("periodic_hook on aura {id} requires a tick_hook");
127 }
128
129 let tick_ms =
130 validated_milliseconds_call(&periodic_hook.tick_ms, id, "periodic_hook.tick_ms")?;
131
132 expression = quote!(#expression.periodic_hook(#tick_ms));
133 }
134
135 Ok(expression)
136}
137
138pub(super) fn apply_periodic_hooks(
139 mut expression: TokenStream,
140 def: &ManifestAuraDef,
141 options: &AuraEmitOptions<'_>,
142) -> anyhow::Result<TokenStream> {
143 let id = def.id;
144
145 match def.periodic_hasted {
146 Some(true) => expression = quote!(#expression.hasted_periodic()),
147 Some(false) => expression = quote!(#expression.unhasted_periodic()),
148 None => {}
149 }
150
151 if def.periodic_partial_tick {
152 if def.tick_hook.is_none() {
153 bail!("periodic_partial_tick on aura {id} requires a tick_hook");
154 }
155
156 expression = quote!(#expression.partial_tick());
157 }
158
159 if let Some(hook) = &def.expire_hook {
160 let module = rust_path(options.hook_module)?;
161 let hook = format_ident!("{hook}{}", options.hook_suffix);
162
163 expression = quote!(#expression.on_expire(crate::hooks::#module::#hook));
164 }
165
166 if let Some(hook) = &def.tick_hook {
167 let module = rust_path(options.hook_module)?;
168 let hook = format_ident!("{hook}{}", options.hook_suffix);
169
170 expression = quote!(#expression.tick_hook(crate::hooks::#module::#hook));
171 }
172
173 Ok(expression)
174}
175
176pub(super) fn apply_periodic_damage(
177 mut expression: TokenStream,
178 def: &ManifestAuraDef,
179 id: u32,
180 has_item_budget: bool,
181) -> anyhow::Result<TokenStream> {
182 if let Some(periodic) = &def.periodic_damage {
183 expression = apply_periodic_payload(expression, periodic, id, has_item_budget)?;
184 }
185
186 if let Some(periodic) = &def.periodic_residual_damage {
187 let tick =
188 validated_milliseconds_call(&periodic.tick_ms, id, "periodic_residual_damage.tick_ms")?;
189
190 expression = quote!(#expression.periodic_residual_damage(#tick));
191 }
192
193 Ok(expression)
194}
195
196fn apply_periodic_payload(
197 mut expression: TokenStream,
198 periodic: &PeriodicDamage,
199 id: u32,
200 has_item_budget: bool,
201) -> anyhow::Result<TokenStream> {
202 let tick = validated_milliseconds_call(&periodic.tick_ms, id, "periodic_damage.tick_ms")?;
203 let payload_count = [
204 periodic.ap_coef.is_some(),
205 periodic.sp_coef.is_some(),
206 periodic.item_damage_total.is_some(),
207 ]
208 .into_iter()
209 .filter(|present| *present)
210 .count();
211
212 if payload_count != 1 {
213 bail!(
214 "periodic_damage for spell {id} must have exactly one of ap_coef, sp_coef, or item_damage_total"
215 );
216 }
217
218 if let Some(total) = &periodic.item_damage_total {
219 if !has_item_budget {
220 bail!("periodic_damage.item_damage_total is items-manifest-only (aura {id})");
221 }
222
223 let spell_id = lit_int(periodic.spell_id.unwrap_or(id));
224 let total = item_scaled_effect_call(total)?;
225
226 expression = quote! {
227 #expression.periodic_total_flat_damage_from_data(
228 data,
229 #tick,
230 #spell_id,
231 #total,
232 )?
233 };
234 } else if let Some(sp) = &periodic.sp_coef {
235 let derived = derived_periodic_data(
236 sp,
237 PeriodicCoefficient::SpellPower,
238 periodic.school.is_some(),
239 periodic.spell_id,
240 );
241
242 expression = if let Some((spell_id, effect, multiplier, offset, round_decimals)) = derived {
243 let spell_id = lit_int(spell_id);
244 let effect = lit_int(effect);
245 let multiplier = lit_f64(multiplier.unwrap_or(1.0));
246 let offset = lit_f64(offset.unwrap_or(0.0));
247 let round_decimals = option_u8(round_decimals);
248
249 quote! {
250 #expression.periodic_damage_sp_from_data(
251 data,
252 wowlab_engine_combat::PeriodicDamageDataInput {
253 tick_ms: #tick,
254 spell_id: #spell_id,
255 effect_index: #effect,
256 multiplier: #multiplier,
257 offset: #offset,
258 round_decimals: #round_decimals,
259 },
260 )?
261 }
262 } else if let Some(spell_id) = periodic.spell_id {
263 let school = periodic.school.ok_or_else(|| {
264 anyhow::anyhow!(
265 "periodic_damage for aura {id} needs school when its coefficient is not derived from its attribution spell"
266 )
267 })?;
268 let flags = damage_flags(school);
269 let spell_id = lit_int(spell_id);
270 let coefficient = scalar_ref_call(sp)?;
271
272 quote! {
273 #expression.periodic_damage_sp_from_spell(
274 #tick,
275 wowlab_engine_combat::DamagePayload::new(
276 #spell_id,
277 #coefficient,
278 #flags,
279 ),
280 )
281 }
282 } else {
283 let flags = damage_flags(periodic.school.unwrap_or(DamageSchoolKind::Magic));
284 let coefficient = scalar_ref_call(sp)?;
285
286 quote!(#expression.periodic_damage_sp(#tick, #coefficient, #flags))
287 };
288 } else if let Some(ap) = &periodic.ap_coef {
289 expression = apply_periodic_ap(&expression, periodic, ap, &tick, id)?;
290 }
291
292 if let Some(resource_gain) = &periodic.resource_gain {
293 let resource_gain = scalar_ref_call(resource_gain)?;
294
295 expression = quote!(#expression.periodic_damage_resource_gain(#resource_gain));
296 }
297
298 if let Some(crit_resource_gain) = &periodic.crit_resource_gain {
299 let chance = periodic.crit_resource_chance.as_ref().ok_or_else(|| {
300 anyhow::anyhow!(
301 "periodic_damage for aura {id} with crit_resource_gain requires crit_resource_chance"
302 )
303 })?;
304 let chance = scalar_ref_call(chance)?;
305 let gain = scalar_ref_call(crit_resource_gain)?;
306
307 expression = quote!(#expression.periodic_damage_crit_resource_gain(#chance, #gain));
308 }
309
310 if periodic.direct {
311 expression = quote!(#expression.periodic_tick_direct_damage());
312 }
313
314 Ok(expression)
315}
316
317fn apply_periodic_ap(
318 expression: &TokenStream,
319 periodic: &PeriodicDamage,
320 coefficient: &ScalarRef,
321 tick: &TokenStream,
322 id: u32,
323) -> anyhow::Result<TokenStream> {
324 let derived = derived_periodic_data(
325 coefficient,
326 PeriodicCoefficient::AttackPower,
327 periodic.school.is_some(),
328 periodic.spell_id,
329 );
330
331 if let Some((spell_id, effect, multiplier, offset, round_decimals)) = derived {
332 let spell_id = lit_int(spell_id);
333 let effect = lit_int(effect);
334 let multiplier = lit_f64(multiplier.unwrap_or(1.0));
335 let offset = lit_f64(offset.unwrap_or(0.0));
336 let round_decimals = option_u8(round_decimals);
337 let mode = match periodic.mode {
338 PeriodicDamageMode::Standard => {
339 quote!(wowlab_engine_combat::PeriodicDamageMode::Standard)
340 }
341 PeriodicDamageMode::Rolling => {
342 quote!(wowlab_engine_combat::PeriodicDamageMode::Rolling)
343 }
344 };
345
346 return Ok(quote! {
347 #expression.periodic_damage_ap_from_data(
348 data,
349 wowlab_engine_combat::PeriodicDamageDataInput {
350 tick_ms: #tick,
351 spell_id: #spell_id,
352 effect_index: #effect,
353 multiplier: #multiplier,
354 offset: #offset,
355 round_decimals: #round_decimals,
356 },
357 #mode,
358 )?
359 });
360 }
361
362 if let Some(spell_id) = periodic.spell_id {
363 let school = periodic.school.ok_or_else(|| {
364 anyhow::anyhow!(
365 "periodic_damage for aura {id} needs school when its coefficient is not derived from its attribution spell"
366 )
367 })?;
368 let flags = damage_flags(school);
369 let spell_id = lit_int(spell_id);
370 let coefficient = scalar_ref_call(coefficient)?;
371
372 return Ok(quote! {
373 #expression.periodic_damage_from_spell(
374 #tick,
375 wowlab_engine_combat::DamagePayload::new(
376 #spell_id,
377 #coefficient,
378 #flags,
379 ),
380 )
381 });
382 }
383
384 let flags = damage_flags(periodic.school.unwrap_or(DamageSchoolKind::Magic));
385 let coefficient = scalar_ref_call(coefficient)?;
386
387 Ok(match periodic.mode {
388 PeriodicDamageMode::Standard => {
389 quote!(#expression.periodic_damage(#tick, #coefficient, #flags))
390 }
391 PeriodicDamageMode::Rolling => {
392 quote!(#expression.periodic_rolling_damage(#tick, #coefficient, #flags))
393 }
394 })
395}
396
397pub(super) fn apply_aura_duration(
398 mut expression: TokenStream,
399 def: &ManifestAuraDef,
400) -> anyhow::Result<TokenStream> {
401 let id = lit_int(def.id);
402
403 match &def.duration_ms {
404 Some(duration) => {
405 let source_spell_id = duration_spell_id(duration);
406 let duration_expr = validated_milliseconds_call(duration, def.id, "aura.duration_ms")?;
407
408 expression = quote!(#expression.duration_ms(#duration_expr));
409 expression = quote!(#expression.apply_periodic_effect_from_data(data, #id)?);
410
411 if let Some(source_spell_id) = source_spell_id {
412 let source_spell_id = lit_int(source_spell_id);
413
414 expression = quote! {
415 #expression.apply_duration_behavior_from_data(data, #source_spell_id)?
416 };
417 expression = quote!(#expression.apply_timing_modifiers_from_data(data, #id)?);
418 }
419 }
420 None => expression = quote!(#expression.apply_base_from_data(data, #id)?),
421 }
422
423 Ok(expression)
424}
425
426const fn duration_spell_id(value: &ScalarRef) -> Option<u32> {
427 match value {
428 ScalarRef::SpellRef {
429 spell_id,
430 field: SpellScalarField::DurationMs,
431 ..
432 } => Some(*spell_id),
433 _ => None,
434 }
435}
436
437pub(super) fn periodic_behavior_spell_id(def: &ManifestAuraDef) -> Option<u32> {
438 let direct_damage = def
439 .periodic_damage
440 .as_ref()
441 .and_then(|periodic| periodic_behavior_source(&periodic.tick_ms));
442 let direct_resource = def
443 .periodic_resource
444 .as_ref()
445 .and_then(|periodic| periodic_behavior_source(&periodic.tick_ms));
446 let residual_damage = def
447 .periodic_residual_damage
448 .as_ref()
449 .and_then(|periodic| periodic_behavior_source(&periodic.tick_ms));
450 let direct_damage_or_resource = direct_damage.or(direct_resource).or(residual_damage);
451 let drained_or_applied = def
452 .periodic_resource_drain
453 .as_ref()
454 .and_then(|periodic| periodic_behavior_source(&periodic.tick_ms))
455 .or_else(|| {
456 def.periodic_apply_aura
457 .as_ref()
458 .and_then(|periodic| periodic_behavior_source(&periodic.tick_ms))
459 });
460 let hook = def
461 .periodic_hook
462 .as_ref()
463 .and_then(|periodic| periodic_behavior_source(&periodic.tick_ms));
464 let periodic_source = direct_damage_or_resource.or(drained_or_applied).or(hook);
465
466 periodic_source
467 .or_else(|| def.duration_ms.as_ref().and_then(duration_spell_id))
468 .or_else(|| {
469 (def.periodic_damage.is_some()
470 || def.periodic_resource.is_some()
471 || def.periodic_residual_damage.is_some()
472 || def.periodic_resource_drain.is_some()
473 || def.periodic_apply_aura.is_some()
474 || def.periodic_hook.is_some())
475 .then_some(def.id)
476 })
477}
478
479const fn periodic_behavior_source(value: &ScalarRef) -> Option<u32> {
480 match value {
481 ScalarRef::EffectRef { spell_id, .. }
482 | ScalarRef::SpellRef {
483 spell_id,
484 field: SpellScalarField::DurationMs,
485 ..
486 } => Some(*spell_id),
487 ScalarRef::SpellRef { .. } | ScalarRef::Literal(_) => None,
488 }
489}