1use wowlab_manifest_schema::{ManifestAuraDef, PeriodicDamage};
4
5use super::{
6 base_row, effect_coordinate, event_effect, hook_placeholder, scalar_option, scalar_row,
7};
8use crate::manifest_ledger::types::{LedgerDisposition, LedgerRow, OperationCategory};
9
10pub(super) fn collect_aura(
11 rows: &mut Vec<LedgerRow>,
12 owner: &str,
13 path: &str,
14 name: &str,
15 aura: &ManifestAuraDef,
16) {
17 let root = format!("auras.{name}");
18
19 if aura.on != "player" {
20 rows.push(
21 base_row(
22 owner,
23 path,
24 format!("{root}.on"),
25 OperationCategory::ActorOrPet,
26 LedgerDisposition::GenericGap,
27 "authored aura actor scope",
28 )
29 .owner(aura.id),
30 );
31 }
32
33 scalar_option(
34 rows,
35 owner,
36 path,
37 &format!("{root}.duration_ms"),
38 aura.id,
39 aura.duration_ms.as_ref(),
40 OperationCategory::AuraApply,
41 true,
42 );
43 scalar_option(
44 rows,
45 owner,
46 path,
47 &format!("{root}.max_stacks"),
48 aura.id,
49 aura.max_stacks.as_ref(),
50 OperationCategory::AuraApply,
51 true,
52 );
53 scalar_fields(rows, owner, path, &root, aura);
54 aura_flags(rows, owner, path, &root, aura);
55 collect_periodics(rows, owner, path, &root, aura);
56 collect_scoped_modifiers(rows, owner, path, &root, aura);
57
58 for (index, effect) in aura.on_consume.iter().enumerate() {
59 event_effect(
60 rows,
61 owner,
62 path,
63 &format!("{root}.on_consume[{index}]"),
64 aura.id,
65 effect,
66 );
67 }
68
69 if aura.expire_hook.is_some() {
70 hook_placeholder(
71 rows,
72 owner,
73 path,
74 &format!("{root}.expire_hook"),
75 aura.id,
76 OperationCategory::AuraRemove,
77 );
78 }
79
80 if aura.tick_hook.is_some() {
81 hook_placeholder(
82 rows,
83 owner,
84 path,
85 &format!("{root}.tick_hook"),
86 aura.id,
87 OperationCategory::AuraTick,
88 );
89 }
90}
91
92fn scalar_fields(
93 rows: &mut Vec<LedgerRow>,
94 owner: &str,
95 path: &str,
96 root: &str,
97 aura: &ManifestAuraDef,
98) {
99 let fields = [
100 (
101 "damage_mult",
102 aura.damage_mult.as_ref(),
103 OperationCategory::Damage,
104 ),
105 (
106 "pet_damage_mult",
107 aura.pet_damage_mult.as_ref(),
108 OperationCategory::ActorOrPet,
109 ),
110 (
111 "haste_per_stack",
112 aura.haste_per_stack.as_ref(),
113 OperationCategory::AuraApply,
114 ),
115 (
116 "attack_speed_per_stack",
117 aura.attack_speed_per_stack.as_ref(),
118 OperationCategory::ActorOrPet,
119 ),
120 (
121 "auto_attack_damage_per_stack",
122 aura.auto_attack_damage_per_stack.as_ref(),
123 OperationCategory::Damage,
124 ),
125 (
126 "resource_gain_mult_per_stack",
127 aura.resource_gain_mult_per_stack.as_ref(),
128 OperationCategory::Resource,
129 ),
130 (
131 "regen_percent_per_stack",
132 aura.regen_percent_per_stack.as_ref(),
133 OperationCategory::Resource,
134 ),
135 (
136 "max_resource_per_stack",
137 aura.max_resource_per_stack.as_ref(),
138 OperationCategory::Resource,
139 ),
140 (
141 "crit_per_stack",
142 aura.crit_per_stack.as_ref(),
143 OperationCategory::Damage,
144 ),
145 (
146 "mastery_per_stack",
147 aura.mastery_per_stack.as_ref(),
148 OperationCategory::Mastery,
149 ),
150 (
151 "primary_stat_percent_per_stack",
152 aura.primary_stat_percent_per_stack.as_ref(),
153 OperationCategory::Damage,
154 ),
155 (
156 "attack_power_percent_per_stack",
157 aura.attack_power_percent_per_stack.as_ref(),
158 OperationCategory::Damage,
159 ),
160 (
161 "crit_rating_per_stack",
162 aura.crit_rating_per_stack.as_ref(),
163 OperationCategory::Damage,
164 ),
165 (
166 "haste_rating_per_stack",
167 aura.haste_rating_per_stack.as_ref(),
168 OperationCategory::Damage,
169 ),
170 (
171 "mastery_rating_per_stack",
172 aura.mastery_rating_per_stack.as_ref(),
173 OperationCategory::Mastery,
174 ),
175 (
176 "versatility_rating_per_stack",
177 aura.versatility_rating_per_stack.as_ref(),
178 OperationCategory::Damage,
179 ),
180 (
181 "primary_stat_per_stack",
182 aura.primary_stat_per_stack.as_ref(),
183 OperationCategory::Damage,
184 ),
185 ];
186
187 for (field, scalar, category) in fields {
188 scalar_option(
189 rows,
190 owner,
191 path,
192 &format!("{root}.{field}"),
193 aura.id,
194 scalar,
195 category,
196 false,
197 );
198 }
199
200 if let Some(stacking) = &aura.damage_mult_stacking {
201 scalar_option(
202 rows,
203 owner,
204 path,
205 &format!("{root}.damage_mult_stacking.initial"),
206 aura.id,
207 Some(&stacking.initial),
208 OperationCategory::Damage,
209 false,
210 );
211 scalar_option(
212 rows,
213 owner,
214 path,
215 &format!("{root}.damage_mult_stacking.per_stack"),
216 aura.id,
217 Some(&stacking.per_stack),
218 OperationCategory::Damage,
219 false,
220 );
221 }
222}
223
224fn aura_flags(
225 rows: &mut Vec<LedgerRow>,
226 owner: &str,
227 path: &str,
228 root: &str,
229 aura: &ManifestAuraDef,
230) {
231 let flags = [
232 (
233 "no_pandemic",
234 aura.no_pandemic,
235 OperationCategory::AuraApply,
236 ),
237 (
238 "split_across_stacks",
239 aura.split_across_stacks,
240 OperationCategory::AuraApply,
241 ),
242 (
243 "apply_at_max_stacks",
244 aura.apply_at_max_stacks,
245 OperationCategory::AuraApply,
246 ),
247 (
248 "async_stacks",
249 aura.async_stacks,
250 OperationCategory::AuraApply,
251 ),
252 (
253 "periodic_damage_scales_with_stacks",
254 aura.periodic_damage_scales_with_stacks,
255 OperationCategory::Damage,
256 ),
257 (
258 "reapply_on_expire",
259 aura.reapply_on_expire,
260 OperationCategory::AuraApply,
261 ),
262 (
263 "periodic_partial_tick",
264 aura.periodic_partial_tick,
265 OperationCategory::AuraTick,
266 ),
267 ("precombat", aura.precombat, OperationCategory::AuraApply),
268 ("snapshot", aura.snapshot, OperationCategory::Damage),
269 ];
270
271 for (field, enabled, category) in flags {
272 if enabled {
273 rows.push(
274 base_row(
275 owner,
276 path,
277 format!("{root}.{field}"),
278 category,
279 LedgerDisposition::ContentOrdering,
280 "authored aura lifecycle policy",
281 )
282 .owner(aura.id),
283 );
284 }
285 }
286
287 if aura.periodic_hasted.is_some() {
288 rows.push(
289 base_row(
290 owner,
291 path,
292 format!("{root}.periodic_hasted"),
293 OperationCategory::AuraTick,
294 LedgerDisposition::Unresolved,
295 "authored periodic haste policy",
296 )
297 .owner(aura.id),
298 );
299 }
300}
301
302fn collect_periodics(
303 rows: &mut Vec<LedgerRow>,
304 owner: &str,
305 path: &str,
306 root: &str,
307 aura: &ManifestAuraDef,
308) {
309 if let Some(periodic) = &aura.periodic_damage {
310 collect_periodic_damage(rows, owner, path, root, aura.id, periodic);
311 }
312
313 if let Some(periodic) = &aura.periodic_residual_damage {
314 rows.push(scalar_row(
315 owner,
316 path,
317 format!("{root}.periodic_residual_damage"),
318 aura.id,
319 &periodic.tick_ms,
320 OperationCategory::Damage,
321 LedgerDisposition::ContentOrdering,
322 "residual damage requires ordered post-mitigation pooling",
323 ));
324 }
325
326 if let Some(periodic) = &aura.periodic_resource {
327 rows.push(scalar_row(
328 owner,
329 path,
330 format!("{root}.periodic_resource.tick_ms"),
331 aura.id,
332 &periodic.tick_ms,
333 OperationCategory::Resource,
334 LedgerDisposition::GenericGap,
335 "DBC-backed periodic resource cadence",
336 ));
337 rows.push(scalar_row(
338 owner,
339 path,
340 format!("{root}.periodic_resource.amount"),
341 aura.id,
342 &periodic.amount,
343 OperationCategory::Resource,
344 LedgerDisposition::GenericGap,
345 "DBC-backed periodic resource amount",
346 ));
347 }
348
349 if let Some(periodic) = &aura.periodic_resource_drain {
350 rows.push(scalar_row(
351 owner,
352 path,
353 format!("{root}.periodic_resource_drain.tick_ms"),
354 aura.id,
355 &periodic.tick_ms,
356 OperationCategory::Resource,
357 LedgerDisposition::Unresolved,
358 "periodic drain requires actor/resource evidence",
359 ));
360 rows.push(scalar_row(
361 owner,
362 path,
363 format!("{root}.periodic_resource_drain.amount"),
364 aura.id,
365 &periodic.amount,
366 OperationCategory::Resource,
367 LedgerDisposition::Unresolved,
368 "periodic drain requires actor/resource evidence",
369 ));
370 }
371
372 if let Some(periodic) = &aura.periodic_apply_aura {
373 rows.push(
374 scalar_row(
375 owner,
376 path,
377 format!("{root}.periodic_apply_aura"),
378 aura.id,
379 &periodic.tick_ms,
380 OperationCategory::AuraApply,
381 LedgerDisposition::GenericGap,
382 "typed periodic child-aura operation",
383 )
384 .child(aura.id),
385 );
386 }
387
388 if let Some(periodic) = &aura.periodic_remove_stack {
389 rows.push(scalar_row(
390 owner,
391 path,
392 format!("{root}.periodic_remove_stack"),
393 aura.id,
394 &periodic.tick_ms,
395 OperationCategory::AuraRemove,
396 LedgerDisposition::GenericGap,
397 "typed periodic stack-removal operation",
398 ));
399 }
400
401 if let Some(periodic) = &aura.periodic_hook {
402 rows.push(scalar_row(
403 owner,
404 path,
405 format!("{root}.periodic_hook"),
406 aura.id,
407 &periodic.tick_ms,
408 OperationCategory::AuraTick,
409 LedgerDisposition::ContentOrdering,
410 "hook-only ordered periodic callback",
411 ));
412 }
413}
414
415fn collect_periodic_damage(
416 rows: &mut Vec<LedgerRow>,
417 owner: &str,
418 path: &str,
419 root: &str,
420 aura_id: u32,
421 periodic: &PeriodicDamage,
422) {
423 let disposition = if periodic.spell_id.is_some()
424 || periodic.ap_coef.is_some()
425 || periodic.sp_coef.is_some()
426 {
427 LedgerDisposition::GenericGap
428 } else {
429 LedgerDisposition::Unresolved
430 };
431 let mut row = scalar_row(
432 owner,
433 path,
434 format!("{root}.periodic_damage"),
435 aura_id,
436 &periodic.tick_ms,
437 OperationCategory::Damage,
438 disposition,
439 "authored periodic damage program",
440 );
441
442 if let Some(spell_id) = periodic.spell_id {
443 row = row.child(spell_id);
444 }
445
446 if let Some(scalar) = periodic.ap_coef.as_ref().or(periodic.sp_coef.as_ref()) {
447 if let Some((spell_id, effect)) = effect_coordinate(scalar) {
448 row = row.effect(spell_id, effect);
449 }
450 }
451
452 rows.push(row);
453
454 for (field, scalar) in [
455 ("resource_gain", periodic.resource_gain.as_ref()),
456 ("crit_resource_gain", periodic.crit_resource_gain.as_ref()),
457 (
458 "crit_resource_chance",
459 periodic.crit_resource_chance.as_ref(),
460 ),
461 ] {
462 scalar_option(
463 rows,
464 owner,
465 path,
466 &format!("{root}.periodic_damage.{field}"),
467 aura_id,
468 scalar,
469 OperationCategory::Resource,
470 false,
471 );
472 }
473}
474
475fn collect_scoped_modifiers(
476 rows: &mut Vec<LedgerRow>,
477 owner: &str,
478 path: &str,
479 root: &str,
480 aura: &ManifestAuraDef,
481) {
482 let pct_fields = [
483 (
484 "crit_chance_spells",
485 aura.crit_chance_spells.as_ref(),
486 OperationCategory::Damage,
487 ),
488 (
489 "crit_damage_spells",
490 aura.crit_damage_spells.as_ref(),
491 OperationCategory::Damage,
492 ),
493 (
494 "damage_percent_spells",
495 aura.damage_percent_spells.as_ref(),
496 OperationCategory::Damage,
497 ),
498 (
499 "damage_percent_spells_linear",
500 aura.damage_percent_spells_linear.as_ref(),
501 OperationCategory::Damage,
502 ),
503 (
504 "cast_time_percent",
505 aura.cast_time_percent.as_ref(),
506 OperationCategory::Gate,
507 ),
508 (
509 "cast_time_percent_per_stack",
510 aura.cast_time_percent_per_stack.as_ref(),
511 OperationCategory::Gate,
512 ),
513 (
514 "gcd_percent",
515 aura.gcd_percent.as_ref(),
516 OperationCategory::Gate,
517 ),
518 ];
519
520 for (field, value, category) in pct_fields {
521 if let Some(value) = value {
522 rows.push(scalar_row(
523 owner,
524 path,
525 format!("{root}.{field}"),
526 aura.id,
527 &value.pct,
528 category,
529 LedgerDisposition::GenericGap,
530 "authored affected-spell modifier duplicates a DBC modifier candidate",
531 ));
532 }
533 }
534
535 let value_fields = [
536 (
537 "spell_cost_flat",
538 aura.spell_cost_flat.as_ref(),
539 OperationCategory::Resource,
540 ),
541 (
542 "spell_cost_percent",
543 aura.spell_cost_percent.as_ref(),
544 OperationCategory::Resource,
545 ),
546 (
547 "secondary_spell_cost_percent",
548 aura.secondary_spell_cost_percent.as_ref(),
549 OperationCategory::Resource,
550 ),
551 (
552 "secondary_spell_cost_flat",
553 aura.secondary_spell_cost_flat.as_ref(),
554 OperationCategory::Resource,
555 ),
556 ];
557
558 for (field, value, category) in value_fields {
559 if let Some(value) = value {
560 rows.push(scalar_row(
561 owner,
562 path,
563 format!("{root}.{field}"),
564 aura.id,
565 &value.value,
566 category,
567 LedgerDisposition::GenericGap,
568 "authored affected-spell resource modifier",
569 ));
570 }
571 }
572
573 for (index, value) in aura.recharge_rate_percent.iter().enumerate() {
574 rows.push(scalar_row(
575 owner,
576 path,
577 format!("{root}.recharge_rate_percent[{index}]"),
578 aura.id,
579 &value.value,
580 OperationCategory::Cooldown,
581 LedgerDisposition::GenericGap,
582 "authored recharge-rate aura",
583 ));
584 }
585}