forge/
talent_modifier_summary.rs1use std::collections::{BTreeMap, BTreeSet};
7
8#[cfg(test)]
9use googletest::{Result as GtestResult, prelude::*};
10use wowlab_common::output;
11use wowlab_engine_domain::dbc::aura_subtype_semantic;
12use wowlab_types::{data::SpellDataFlat, game::SpecId};
13
14const EFFECT_CLASS_MASK_WORDS: usize = 4;
15
16#[derive(Default)]
17pub(super) struct ModifierSummaries(BTreeMap<(i32, i32), ModifierSummary>);
18
19#[derive(Debug, Default)]
20struct ModifierSummary {
21 spell_name: String,
22 aura_subtype: i32,
23 property: i32,
24 base_points: f64,
25 label: i32,
26 class_mask: [i32; EFFECT_CLASS_MASK_WORDS],
27 is_passive: bool,
28 specs: BTreeSet<String>,
29}
30
31#[derive(tabled::Tabled)]
32struct ModifierSummaryRow {
33 #[tabled(rename = "Owner")]
34 owner: String,
35 #[tabled(rename = "Effect")]
36 effect: i32,
37 #[tabled(rename = "Aura")]
38 aura_subtype: i32,
39 #[tabled(rename = "Property")]
40 property: i32,
41 #[tabled(rename = "Base")]
42 base_points: f64,
43 #[tabled(rename = "Label")]
44 label: i32,
45 #[tabled(rename = "Class mask")]
46 class_mask: String,
47 #[tabled(rename = "Passive")]
48 is_passive: bool,
49 #[tabled(rename = "Specs")]
50 specs: usize,
51 #[tabled(rename = "Spec keys")]
52 spec_keys: String,
53}
54
55#[derive(tabled::Tabled)]
56struct ModifierCoverageRow {
58 #[tabled(rename = "Unique rows")]
59 unique_rows: usize,
60 #[tabled(rename = "Observations")]
61 observations: usize,
62 #[tabled(rename = "Specs")]
63 specs: usize,
64}
65
66#[derive(tabled::Tabled)]
67struct ModifierSpecSummaryRow {
69 #[tabled(rename = "Spec")]
70 spec: String,
71 #[tabled(rename = "Unique rows")]
72 rows: usize,
73}
74
75impl ModifierSummaries {
76 pub(super) fn observe(
77 &mut self,
78 spec: SpecId,
79 spell: &SpellDataFlat,
80 property_filter: &[i32],
81 aura_filter: &[i32],
82 ) {
83 for effect in &spell.effects {
84 if !property_filter.contains(&effect.misc_value_0)
85 || (!aura_filter.is_empty() && !aura_filter.contains(&effect.aura))
86 || aura_subtype_semantic(effect.aura)
87 .and_then(|semantic| semantic.modifier)
88 .is_none()
89 {
90 continue;
91 }
92
93 let effect_index = effect.index + 1;
94 let summary = self.0.entry((spell.id, effect_index)).or_default();
95
96 summary.spell_name.clone_from(&spell.name.to_string());
97 summary.aura_subtype = effect.aura;
98 summary.property = effect.misc_value_0;
99 summary.base_points = effect.base_points;
100 summary.label = effect.misc_value_1;
101 summary.class_mask = [
102 effect.effect_class_mask_1,
103 effect.effect_class_mask_2,
104 effect.effect_class_mask_3,
105 effect.effect_class_mask_4,
106 ];
107 summary.is_passive = spell.is_passive;
108 summary.specs.insert(spec.slug().to_string());
109 }
110 }
111
112 pub(super) fn print(&self, filter: &[i32]) {
113 output::blank();
114 output::header(&format!(
115 "Cross-spec modifier-property summary ({})",
116 filter
117 .iter()
118 .map(i32::to_string)
119 .collect::<Vec<_>>()
120 .join(",")
121 ));
122 let observations = self.0.values().map(|summary| summary.specs.len()).sum();
123 let specs = self
124 .0
125 .values()
126 .flat_map(|summary| summary.specs.iter())
127 .collect::<BTreeSet<_>>()
128 .len();
129
130 output::table([ModifierCoverageRow {
131 unique_rows: self.0.len(),
132 observations,
133 specs,
134 }]);
135 output::blank();
136 output::table(self.0.iter().map(|(&(spell_id, effect), summary)| {
137 ModifierSummaryRow {
138 owner: format!("{} ({spell_id})", summary.spell_name),
139 effect,
140 aura_subtype: summary.aura_subtype,
141 property: summary.property,
142 base_points: summary.base_points,
143 label: summary.label,
144 class_mask: summary
145 .class_mask
146 .iter()
147 .map(i32::to_string)
148 .collect::<Vec<_>>()
149 .join("/"),
150 is_passive: summary.is_passive,
151 specs: summary.specs.len(),
152 spec_keys: summary.specs.iter().cloned().collect::<Vec<_>>().join(","),
153 }
154 }));
155
156 let mut spec_rows = BTreeMap::new();
157
158 for summary in self.0.values() {
159 for spec in &summary.specs {
160 *spec_rows.entry(spec.clone()).or_default() += 1;
161 }
162 }
163
164 let mut spec_rows: Vec<_> = spec_rows
165 .into_iter()
166 .map(|(spec, rows)| ModifierSpecSummaryRow { spec, rows })
167 .collect();
168
169 spec_rows.sort_by(|left, right| {
170 right
171 .rows
172 .cmp(&left.rows)
173 .then_with(|| left.spec.cmp(&right.spec))
174 });
175 output::blank();
176 output::header("Filtered modifier-property incidence by spec");
177 output::table(spec_rows);
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use wowlab_types::data::SpellEffect;
184
185 use super::*;
186
187 #[gtest]
188 fn filtered_observation_retains_operation_filter_and_spec_provenance() -> GtestResult<()> {
189 let spell = SpellDataFlat {
190 id: 100,
191 name: "Flat label damage".into(),
192 is_passive: true,
193 effects: vec![SpellEffect {
194 index: 2,
195 aura: 219,
196 base_points: 15.0,
197 misc_value_0: 0,
198 misc_value_1: 4137,
199 ..SpellEffect::default()
200 }],
201 ..SpellDataFlat::default()
202 };
203 let mut summaries = ModifierSummaries::default();
204
205 summaries.observe(SpecId::Arcane, &spell, &[0], &[219]);
206
207 let summary = summaries.0.get(&(100, 3)).or_fail()?;
208
209 verify_that!(
210 summary,
211 matches_pattern!(ModifierSummary {
212 aura_subtype: eq(&219),
213 base_points: near(15.0, f64::EPSILON),
214 label: eq(&4137),
215 is_passive: eq(&true),
216 ..
217 })
218 )?;
219 verify_true!(summary.specs.contains("arcane_mage"))?;
220
221 Ok(())
222 }
223}