1use std::collections::{BTreeMap, BTreeSet};
7
8#[cfg(test)]
9use googletest::{Result as GtestResult, prelude::*};
10use wowlab_common::output;
11use wowlab_types::{data::SpellDataFlat, game::SpecId};
12
13#[derive(Default)]
14pub(super) struct EffectSummaries {
15 rows: BTreeMap<(i32, i32), EffectSummary>,
16 spells: BTreeMap<i32, SpellShape>,
17}
18
19#[derive(Default)]
20struct EffectSummary {
21 spell_name: String,
22 effect_type: i32,
23 aura_subtype: i32,
24 base_points: f64,
25 misc_value_0: i32,
26 trigger_spell: i32,
27 specs: BTreeSet<String>,
28}
29
30#[derive(Default)]
31struct SpellShape {
32 name: String,
33 operations: String,
34}
35
36#[derive(tabled::Tabled)]
37struct EffectSummaryRow {
38 #[tabled(rename = "Parent")]
39 parent: String,
40 #[tabled(rename = "Index")]
41 effect_index: i32,
42 #[tabled(rename = "SE")]
43 effect_type: i32,
44 #[tabled(rename = "AU")]
45 aura_subtype: i32,
46 #[tabled(rename = "Base")]
47 base_points: f64,
48 #[tabled(rename = "Misc 0")]
49 misc_value_0: i32,
50 #[tabled(rename = "Child")]
51 child: String,
52 #[tabled(rename = "Child operations")]
53 child_operations: String,
54 #[tabled(rename = "Specs")]
55 specs: usize,
56}
57
58#[derive(tabled::Tabled)]
59struct EffectCoverageRow {
61 #[tabled(rename = "Unique rows")]
62 unique_rows: usize,
63 #[tabled(rename = "Observations")]
64 observations: usize,
65 #[tabled(rename = "Specs")]
66 specs: usize,
67}
68
69#[derive(tabled::Tabled)]
70struct EffectSpecSummaryRow {
72 #[tabled(rename = "Spec")]
73 spec: String,
74 #[tabled(rename = "Unique rows")]
75 rows: usize,
76}
77
78impl EffectSummaries {
79 pub(super) fn observe(&mut self, spec: SpecId, spell: &SpellDataFlat, filter: &[i32]) {
80 self.spells.insert(
81 spell.id,
82 SpellShape {
83 name: spell.name.to_string(),
84 operations: spell
85 .effects
86 .iter()
87 .map(|effect| {
88 format!(
89 "{}:SE{}:AU{}:base{}:misc{}",
90 effect.index + 1,
91 effect.effect,
92 effect.aura,
93 effect.base_points,
94 effect.misc_value_0
95 )
96 })
97 .collect::<Vec<_>>()
98 .join(","),
99 },
100 );
101
102 for effect in &spell.effects {
103 if !filter.contains(&effect.effect) {
104 continue;
105 }
106
107 let effect_index = effect.index + 1;
108 let summary = self.rows.entry((spell.id, effect_index)).or_default();
109
110 summary.spell_name.clone_from(&spell.name.to_string());
111 summary.effect_type = effect.effect;
112 summary.aura_subtype = effect.aura;
113 summary.base_points = effect.base_points;
114 summary.misc_value_0 = effect.misc_value_0;
115 summary.trigger_spell = effect.trigger_spell;
116 summary.specs.insert(spec.slug().to_string());
117 }
118 }
119
120 pub(super) fn print(&self, filter: &[i32]) {
121 output::blank();
122 output::header(&format!(
123 "Cross-spec spell-effect summary ({})",
124 filter
125 .iter()
126 .map(i32::to_string)
127 .collect::<Vec<_>>()
128 .join(",")
129 ));
130 let observations = self.rows.values().map(|summary| summary.specs.len()).sum();
131 let specs = self
132 .rows
133 .values()
134 .flat_map(|summary| summary.specs.iter())
135 .collect::<BTreeSet<_>>()
136 .len();
137
138 output::table([EffectCoverageRow {
139 unique_rows: self.rows.len(),
140 observations,
141 specs,
142 }]);
143 output::blank();
144 output::table(
145 self.rows
146 .iter()
147 .map(|(&(spell_id, effect_index), summary)| {
148 let child = self.spells.get(&summary.trigger_spell);
149
150 EffectSummaryRow {
151 parent: format!("{} ({spell_id})", summary.spell_name),
152 effect_index,
153 effect_type: summary.effect_type,
154 aura_subtype: summary.aura_subtype,
155 base_points: summary.base_points,
156 misc_value_0: summary.misc_value_0,
157 child: child.map_or_else(
158 || summary.trigger_spell.to_string(),
159 |shape| format!("{} ({})", shape.name, summary.trigger_spell),
160 ),
161 child_operations: child
162 .map_or_else(String::new, |shape| shape.operations.clone()),
163 specs: summary.specs.len(),
164 }
165 }),
166 );
167
168 let mut spec_rows = BTreeMap::new();
169
170 for summary in self.rows.values() {
171 for spec in &summary.specs {
172 *spec_rows.entry(spec.clone()).or_default() += 1;
173 }
174 }
175
176 let mut spec_rows: Vec<_> = spec_rows
177 .into_iter()
178 .map(|(spec, rows)| EffectSpecSummaryRow { spec, rows })
179 .collect();
180
181 spec_rows.sort_by(|left, right| {
182 right
183 .rows
184 .cmp(&left.rows)
185 .then_with(|| left.spec.cmp(&right.spec))
186 });
187 output::blank();
188 output::header("Filtered spell-effect incidence by spec");
189 output::table(spec_rows);
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use wowlab_types::data::SpellEffect;
196
197 use super::*;
198
199 #[gtest]
200 fn filtered_observation_joins_child_operation_shape_and_provenance() -> GtestResult<()> {
201 let parent = SpellDataFlat {
202 id: 100,
203 name: "Parent".into(),
204 effects: vec![SpellEffect {
205 index: 0,
206 effect: 64,
207 trigger_spell: 200,
208 ..SpellEffect::default()
209 }],
210 ..SpellDataFlat::default()
211 };
212 let child = SpellDataFlat {
213 id: 200,
214 name: "Child".into(),
215 effects: vec![SpellEffect {
216 index: 0,
217 effect: 30,
218 base_points: 8.0,
219 misc_value_0: 3,
220 ..SpellEffect::default()
221 }],
222 ..SpellDataFlat::default()
223 };
224 let mut summaries = EffectSummaries::default();
225
226 summaries.observe(SpecId::Arms, &parent, &[64]);
227 summaries.observe(SpecId::Arms, &child, &[64]);
228
229 let summary = summaries.rows.get(&(100, 1)).or_fail()?;
230
231 verify_that!(summary.trigger_spell, eq(200))?;
232 verify_true!(summary.specs.contains("arms_warrior"))?;
233 verify_that!(
234 summaries.spells[&200].operations,
235 eq("1:SE30:AU0:base8:misc3")
236 )?;
237
238 Ok(())
239 }
240}