1use std::collections::{BTreeMap, BTreeSet};
4
5use clap::ValueEnum;
6#[cfg(test)]
7use googletest::{Result as GtestResult, prelude::*};
8use wowlab_common::output;
9use wowlab_engine_domain::targeting::{TargetPlanAxis, TargetPlanGap};
10use wowlab_types::game::SpecId;
11
12#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
13pub(crate) enum TargetPlanAxisFilter {
14 Object,
15 Reference,
16 Algorithm,
17 Check,
18 Direction,
19 EffectFallback,
20}
21
22impl TargetPlanAxisFilter {
23 const fn matches(self, axis: TargetPlanAxis) -> bool {
24 matches!(
25 (self, axis),
26 (Self::Object, TargetPlanAxis::Object)
27 | (Self::Reference, TargetPlanAxis::Reference)
28 | (Self::Algorithm, TargetPlanAxis::Algorithm)
29 | (Self::Check, TargetPlanAxis::Check)
30 | (Self::Direction, TargetPlanAxis::Direction)
31 | (Self::EffectFallback, TargetPlanAxis::EffectFallback)
32 )
33 }
34}
35
36#[derive(Default)]
37pub(super) struct TargetPlanSummaries(BTreeMap<(i32, i32), TargetPlanSummary>);
38
39#[derive(Debug, Default)]
40struct TargetPlanSummary {
41 spell_name: String,
42 target_a: i32,
43 target_b: i32,
44 selector: Option<i32>,
45 axis: Option<TargetPlanAxis>,
46 value: &'static str,
47 specs: BTreeSet<String>,
48}
49
50#[derive(Clone, Copy)]
51pub(super) struct TargetPlanObservation<'a> {
52 pub spec: SpecId,
53 pub spell_id: i32,
54 pub spell_name: &'a str,
55 pub effect_index: i32,
56 pub target_a: i32,
57 pub target_b: i32,
58}
59
60#[derive(tabled::Tabled)]
61struct TargetPlanSummaryRow {
62 #[tabled(rename = "Spell")]
63 spell: String,
64 #[tabled(rename = "Effect")]
65 effect: i32,
66 #[tabled(rename = "TargetA / TargetB")]
67 selectors: String,
68 #[tabled(rename = "Gap selector")]
69 gap_selector: String,
70 #[tabled(rename = "Axis")]
71 axis: String,
72 #[tabled(rename = "Value")]
73 value: &'static str,
74 #[tabled(rename = "Specs")]
75 specs: usize,
76 #[tabled(rename = "Spec keys")]
77 spec_keys: String,
78}
79
80impl TargetPlanSummaries {
81 pub(super) fn observe(
82 &mut self,
83 observation: TargetPlanObservation<'_>,
84 gap: TargetPlanGap,
85 filter: &[TargetPlanAxisFilter],
86 ) {
87 if !filter.iter().any(|candidate| candidate.matches(gap.axis)) {
88 return;
89 }
90
91 let summary = self
92 .0
93 .entry((observation.spell_id, observation.effect_index))
94 .or_default();
95
96 summary.spell_name = observation.spell_name.to_string();
97 summary.target_a = observation.target_a;
98 summary.target_b = observation.target_b;
99 summary.selector = gap.selector;
100 summary.axis = Some(gap.axis);
101 summary.value = gap.value;
102 summary.specs.insert(observation.spec.slug().to_string());
103 }
104
105 pub(super) fn print(&self) {
106 output::blank();
107 output::header("Cross-spec unsupported target-plan summary");
108 output::table(self.0.iter().map(|(&(spell_id, effect), summary)| {
109 TargetPlanSummaryRow {
110 spell: format!("{} ({spell_id})", summary.spell_name),
111 effect,
112 selectors: format!("{} / {}", summary.target_a, summary.target_b),
113 gap_selector: summary
114 .selector
115 .map_or_else(|| "effect fallback".to_string(), |value| value.to_string()),
116 axis: format!("{:?}", summary.axis.expect("observed gap has an axis")),
117 value: summary.value,
118 specs: summary.specs.len(),
119 spec_keys: summary.specs.iter().cloned().collect::<Vec<_>>().join(","),
120 }
121 }));
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[gtest]
130 fn filtered_observation_retains_gap_coordinate_and_spec_provenance() -> GtestResult<()> {
131 let mut summaries = TargetPlanSummaries::default();
132
133 summaries.observe(
134 TargetPlanObservation {
135 spec: SpecId::Devastation,
136 spell_id: 100,
137 spell_name: "Directional spell",
138 effect_index: 2,
139 target_a: 78,
140 target_b: 16,
141 },
142 TargetPlanGap {
143 selector: Some(78),
144 axis: TargetPlanAxis::Direction,
145 value: "front",
146 },
147 &[TargetPlanAxisFilter::Direction],
148 );
149
150 let summary = &summaries.0[&(100, 2)];
151
152 verify_that!(
153 summary,
154 matches_pattern!(TargetPlanSummary {
155 target_a: eq(&78),
156 selector: eq(&Some(78)),
157 axis: eq(&Some(TargetPlanAxis::Direction)),
158 ..
159 })
160 )?;
161 verify_true!(summary.specs.contains("devastation_evoker"))?;
162
163 Ok(())
164 }
165}