Skip to main content

forge/manifest_ledger/collect/
aura.rs

1// #t(file: rust_alloc_in_loop) ledger rows intentionally own stable manifest coordinates and evidence.
2
3use 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}