forge/
talent_attribute_summary.rs1use std::collections::BTreeMap;
7
8#[cfg(test)]
9use googletest::{Result as GtestResult, prelude::*};
10use wowlab_common::output;
11use wowlab_engine_domain::dbc::{SemanticSupport, spell_attribute_semantic};
12use wowlab_types::game::SpecId;
13
14#[derive(Default)]
15struct AttributeSummary {
16 observations: usize,
17 specs: BTreeMap<String, usize>,
18 spells: BTreeMap<(i32, String), BTreeMap<String, usize>>,
19}
20
21#[derive(Default)]
22pub(super) struct AttributeSummaries(BTreeMap<u16, AttributeSummary>);
23
24#[derive(tabled::Tabled)]
25struct AttributeSummaryRow {
26 #[tabled(rename = "Attribute")]
27 attribute: u16,
28 #[tabled(rename = "Name")]
29 name: &'static str,
30 #[tabled(rename = "Coverage")]
31 coverage: &'static str,
32 #[tabled(rename = "Observations")]
33 observations: usize,
34 #[tabled(rename = "Specs")]
35 specs: usize,
36 #[tabled(rename = "Handler / gap")]
37 handler: &'static str,
38}
39
40#[derive(tabled::Tabled)]
41struct AttributeSpecSummaryRow {
42 #[tabled(rename = "Spec")]
43 spec: String,
44 #[tabled(rename = "Observations")]
45 observations: usize,
46}
47
48#[derive(tabled::Tabled)]
49struct AttributeSpellSummaryRow {
50 #[tabled(rename = "Attribute")]
51 attribute: u16,
52 #[tabled(rename = "Spell")]
53 spell: String,
54 #[tabled(rename = "Spell ID")]
55 spell_id: i32,
56 #[tabled(rename = "Observations")]
57 observations: usize,
58 #[tabled(rename = "Specs")]
59 specs: usize,
60}
61
62impl AttributeSummaries {
63 pub(super) fn observe(
64 &mut self,
65 spec: SpecId,
66 spell_id: i32,
67 spell_name: &str,
68 attributes: impl IntoIterator<Item = u16>,
69 filter: &[u16],
70 ) {
71 for attribute in attributes {
72 if !filter.contains(&attribute) {
73 continue;
74 }
75
76 let summary = self.0.entry(attribute).or_default();
77
78 summary.observations += 1;
79 let spec = spec.slug().to_string();
80
81 *summary.specs.entry(spec.clone()).or_default() += 1;
82 *summary
83 .spells
84 .entry((spell_id, spell_name.to_string()))
85 .or_default()
86 .entry(spec)
87 .or_default() += 1;
88 }
89 }
90
91 pub(super) fn print(&self, filter: &[u16]) {
92 let rows = filter.iter().map(|attribute| self.row(*attribute));
93
94 output::blank();
95 output::header("Cross-spec spell-attribute summary");
96 output::table(rows);
97
98 let mut spec_observations = BTreeMap::new();
99
100 for summary in self.0.values() {
101 for (spec, observations) in &summary.specs {
102 *spec_observations.entry(spec.clone()).or_default() += observations;
103 }
104 }
105
106 let mut spec_rows: Vec<_> = spec_observations
107 .into_iter()
108 .map(|(spec, observations)| AttributeSpecSummaryRow { spec, observations })
109 .collect();
110
111 spec_rows.sort_by(|left, right| {
112 right
113 .observations
114 .cmp(&left.observations)
115 .then_with(|| left.spec.cmp(&right.spec))
116 });
117 output::blank();
118 output::header("Filtered attribute incidence by spec");
119 output::table(spec_rows);
120
121 let mut spell_rows = Vec::new();
122
123 for attribute in filter {
124 let Some(summary) = self.0.get(attribute) else {
125 continue;
126 };
127
128 for ((spell_id, spell), specs) in &summary.spells {
129 spell_rows.push(AttributeSpellSummaryRow {
130 attribute: *attribute,
131 spell: spell.clone(),
132 spell_id: *spell_id,
133 observations: specs.values().sum(),
134 specs: specs.len(),
135 });
136 }
137 }
138
139 spell_rows.sort_by(|left, right| {
140 right
141 .observations
142 .cmp(&left.observations)
143 .then_with(|| left.attribute.cmp(&right.attribute))
144 .then_with(|| left.spell_id.cmp(&right.spell_id))
145 });
146 output::blank();
147 output::header("Filtered attribute incidence by owning spell");
148 output::table(spell_rows);
149 }
150
151 fn row(&self, attribute: u16) -> AttributeSummaryRow {
152 let semantic = spell_attribute_semantic(i32::from(attribute));
153 let (name, coverage, handler) = semantic.map_or(
154 (
155 "Unregistered",
156 "unsupported",
157 "spell attribute is absent from registry",
158 ),
159 |semantic| {
160 let coverage = match semantic.support {
161 SemanticSupport::Generic => "generic",
162 SemanticSupport::Partial | SemanticSupport::ContentRequired => "partial",
163 SemanticSupport::Ignored => "ignored",
164 _ => "unsupported",
165 };
166
167 (semantic.name, coverage, semantic.handler)
168 },
169 );
170 let summary = self.0.get(&attribute);
171
172 AttributeSummaryRow {
173 attribute,
174 name,
175 coverage,
176 observations: summary.map_or(0, |summary| summary.observations),
177 specs: summary.map_or(0, |summary| summary.specs.len()),
178 handler,
179 }
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[gtest]
188 fn filters_and_counts_observations_separately_from_specs() -> GtestResult<()> {
189 let mut summaries = AttributeSummaries::default();
190
191 summaries.observe(SpecId::Fire, 123, "Example", [112, 112, 113], &[112]);
192
193 let summary = &summaries.0[&112];
194
195 verify_that!(summary.observations, eq(2))?;
196 verify_that!(summary.specs.len(), eq(1))?;
197 verify_that!(summary.specs["fire_mage"], eq(2))?;
198 verify_that!(
199 summary.spells[&(123, "Example".to_string())]["fire_mage"],
200 eq(2)
201 )?;
202 verify_true!(!summaries.0.contains_key(&113))?;
203
204 Ok(())
205 }
206}