Skip to main content

forge/
talent_attribute_summary.rs

1// #t(file: rust_clone_in_loop) the bounded CLI table owns stable spec labels.
2// #t(file: rust_alloc_in_loop) the bounded CLI aggregation owns stable spec labels.
3
4//! Filtered cross-spec spell-attribute incidence reporting.
5
6use 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}