Skip to main content

wowlab_engine_application/audit/data_values/
periodic.rs

1//! Periodic aura and damage-multiplier override comparisons.
2
3use wowlab_engine_ports::{DataResolver, DynDataResolver, SpellId};
4use wowlab_manifest_schema::{ManifestAuraDef, ScalarField, ScalarRef};
5use wowlab_types::constants::HUNDRED;
6
7use super::{
8    AuditSink, DATA_EPS,
9    scalar::{resolve_scalar_ref, scalar_field_value},
10};
11
12pub(super) struct PeriodicCoefficientCheck<'a> {
13    pub name: &'a str,
14    pub spell_id: u32,
15    pub field_name: &'a str,
16    pub value: Option<&'a ScalarRef>,
17    pub field: ScalarField,
18}
19
20pub(super) struct AuraScalarCheck<'a> {
21    pub name: &'a str,
22    pub aura_id: u32,
23    pub field_name: &'a str,
24    pub value: &'a ScalarRef,
25    pub field: ScalarField,
26}
27
28pub(super) async fn check_periodic_aura_values(
29    resolver: &DynDataResolver<'_>,
30    name: &str,
31    aura: &ManifestAuraDef,
32    sink: &mut AuditSink,
33) {
34    let periodic_scalars = [
35        aura.periodic_hook
36            .as_ref()
37            .map(|value| ("periodic_hook.tick_ms", &value.tick_ms, ScalarField::Period)),
38        aura.periodic_residual_damage.as_ref().map(|value| {
39            (
40                "periodic_residual_damage.tick_ms",
41                &value.tick_ms,
42                ScalarField::Period,
43            )
44        }),
45        aura.periodic_resource.as_ref().map(|value| {
46            (
47                "periodic_resource.tick_ms",
48                &value.tick_ms,
49                ScalarField::Period,
50            )
51        }),
52        aura.periodic_resource_drain.as_ref().map(|value| {
53            (
54                "periodic_resource_drain.tick_ms",
55                &value.tick_ms,
56                ScalarField::Period,
57            )
58        }),
59        aura.periodic_apply_aura.as_ref().map(|value| {
60            (
61                "periodic_apply_aura.tick_ms",
62                &value.tick_ms,
63                ScalarField::Period,
64            )
65        }),
66        aura.periodic_remove_stack.as_ref().map(|value| {
67            (
68                "periodic_remove_stack.tick_ms",
69                &value.tick_ms,
70                ScalarField::Period,
71            )
72        }),
73    ];
74
75    for (field_name, value, field) in periodic_scalars.into_iter().flatten() {
76        check_redundant_aura_scalar(
77            resolver,
78            AuraScalarCheck {
79                name,
80                aura_id: aura.id,
81                field_name,
82                value,
83                field,
84            },
85            sink,
86        )
87        .await;
88    }
89
90    let amounts = [
91        aura.periodic_resource
92            .as_ref()
93            .map(|periodic| ("periodic_resource.amount", &periodic.amount)),
94        aura.periodic_resource_drain
95            .as_ref()
96            .map(|periodic| ("periodic_resource_drain.amount", &periodic.amount)),
97    ];
98
99    for (field_name, value) in amounts.into_iter().flatten() {
100        check_redundant_aura_scalar(
101            resolver,
102            AuraScalarCheck {
103                name,
104                aura_id: aura.id,
105                field_name,
106                value,
107                field: ScalarField::BasePoints,
108            },
109            sink,
110        )
111        .await;
112    }
113}
114
115pub(super) async fn check_redundant_aura_scalar(
116    resolver: &DynDataResolver<'_>,
117    check: AuraScalarCheck<'_>,
118    sink: &mut AuditSink,
119) {
120    let ScalarRef::Literal(literal) = check.value else {
121        return;
122    };
123    let effects = resolver
124        .get_spell_effects(SpellId::new(wowlab_types::numeric::u32_to_i32_saturating(
125            check.aura_id,
126        )))
127        .await
128        .unwrap_or_default();
129
130    if effects.iter().any(|effect| {
131        let resolved = scalar_field_value(effect, check.field);
132
133        resolved.abs() > DATA_EPS && (*literal - resolved).abs() <= DATA_EPS
134    }) {
135        sink.warning(format!(
136            "aura {} {}={literal} is a redundant override; use a ScalarRef",
137            check.name, check.field_name
138        ));
139    }
140}
141
142pub(super) async fn check_redundant_damage_mult(
143    resolver: &DynDataResolver<'_>,
144    name: &str,
145    aura_id: u32,
146    value: Option<&ScalarRef>,
147    sink: &mut AuditSink,
148) {
149    check_resolved_scalar(resolver, name, "damage_mult", value, sink).await;
150    let Some(ScalarRef::Literal(literal)) = value else {
151        return;
152    };
153    let effects = resolver
154        .get_spell_effects(SpellId::new(wowlab_types::numeric::u32_to_i32_saturating(
155            aura_id,
156        )))
157        .await
158        .unwrap_or_default();
159
160    if effects.iter().any(|effect| {
161        effect.base_points.abs() > DATA_EPS
162            && (*literal - (1.0 + effect.base_points / HUNDRED)).abs() <= DATA_EPS
163    }) {
164        sink.warning(format!(
165            "aura {name} damage_mult={literal} is a redundant override; use an affine ScalarRef"
166        ));
167    }
168}
169
170pub(super) async fn check_redundant_periodic_coefficient(
171    resolver: &DynDataResolver<'_>,
172    check: PeriodicCoefficientCheck<'_>,
173    sink: &mut AuditSink,
174) {
175    check_resolved_scalar(resolver, check.name, check.field_name, check.value, sink).await;
176    let Some(ScalarRef::Literal(literal)) = check.value else {
177        return;
178    };
179    let effects = resolver
180        .get_spell_effects(SpellId::new(wowlab_types::numeric::u32_to_i32_saturating(
181            check.spell_id,
182        )))
183        .await
184        .unwrap_or_default();
185
186    if effects.iter().any(|effect| {
187        let resolved = scalar_field_value(effect, check.field);
188
189        resolved.abs() > DATA_EPS && (*literal - resolved).abs() <= DATA_EPS
190    }) {
191        sink.warning(format!(
192            "aura {} periodic_damage.{}={literal} is a redundant override; use a ScalarRef",
193            check.name, check.field_name
194        ));
195    }
196}
197
198async fn check_resolved_scalar(
199    resolver: &DynDataResolver<'_>,
200    name: &str,
201    field_name: &str,
202    value: Option<&ScalarRef>,
203    sink: &mut AuditSink,
204) {
205    let resolved = resolve_scalar_ref(resolver, value).await;
206
207    if resolved.is_some_and(|resolved| !resolved.is_finite()) {
208        sink.warning(format!(
209            "aura {name} {field_name} resolves to a non-finite value"
210        ));
211    }
212}